From e8c9efef965432a950d1407568c3f8f8ebac1a65 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Feb 2026 15:10:48 +0100 Subject: [PATCH] Rename pathview_server to pathview and add convert API --- .gitignore | 2 +- README.md | 5 +- package.json | 2 +- {pathview_server => pathview}/__init__.py | 4 +- pathview/__main__.py | 6 + {pathview_server => pathview}/app.py | 0 {pathview_server => pathview}/cli.py | 4 +- pathview/converter.py | 550 +++++++++++++++++++ pathview/data/registry.json | 609 ++++++++++++++++++++++ {pathview_server => pathview}/worker.py | 0 pathview_server/__main__.py | 6 - pyproject.toml | 6 +- scripts/build_package.py | 6 +- scripts/extract.py | 12 +- scripts/pvm2py.py | 515 +----------------- tests/conftest.py | 2 +- tests/test_worker.py | 2 +- 17 files changed, 1195 insertions(+), 536 deletions(-) rename {pathview_server => pathview}/__init__.py (60%) create mode 100644 pathview/__main__.py rename {pathview_server => pathview}/app.py (100%) rename {pathview_server => pathview}/cli.py (96%) create mode 100644 pathview/converter.py create mode 100644 pathview/data/registry.json rename {pathview_server => pathview}/worker.py (100%) delete mode 100644 pathview_server/__main__.py diff --git a/.gitignore b/.gitignore index 7ae8466c..daca3259 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,7 @@ tmpclaude-* __pycache__/ *.egg-info/ dist/ -pathview_server/static/ +pathview/static/ # Generated screenshots static/examples/screenshots/ diff --git a/README.md b/README.md index c80d582d..99896922 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,13 @@ src/ ├── routes/ # SvelteKit pages └── app.css # Global styles with CSS variables -pathview_server/ # Python package (pip install pathview) +pathview/ # Python package (pip install pathview) ├── app.py # Flask server (subprocess management, HTTP routes) ├── worker.py # REPL worker subprocess (Python execution) ├── cli.py # CLI entry point (pathview serve) +├── converter.py # PVM to Python converter (public API) +├── data/ # Bundled data files +│ └── registry.json # Block/event registry for converter └── static/ # Bundled frontend (generated at build time) diff --git a/package.json b/package.json index 44294871..84fef55f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "eslint .", "format": "prettier --write .", - "server": "python -m pathview_server.app", + "server": "python -m pathview.app", "build:package": "python scripts/build_package.py" }, "devDependencies": { diff --git a/pathview_server/__init__.py b/pathview/__init__.py similarity index 60% rename from pathview_server/__init__.py rename to pathview/__init__.py index 1431ca56..a9a03133 100644 --- a/pathview_server/__init__.py +++ b/pathview/__init__.py @@ -1,7 +1,9 @@ -"""PathView Server — local Flask backend for PathView.""" +"""PathView — local Flask backend and PVM converter for PathView.""" try: from importlib.metadata import version __version__ = version("pathview") except Exception: __version__ = "0.5.0" # fallback for editable installs / dev + +from pathview.converter import convert diff --git a/pathview/__main__.py b/pathview/__main__.py new file mode 100644 index 00000000..ebf233f1 --- /dev/null +++ b/pathview/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for: python -m pathview""" + +from pathview.cli import main + +if __name__ == "__main__": + main() diff --git a/pathview_server/app.py b/pathview/app.py similarity index 100% rename from pathview_server/app.py rename to pathview/app.py diff --git a/pathview_server/cli.py b/pathview/cli.py similarity index 96% rename from pathview_server/cli.py rename to pathview/cli.py index fa81466e..99839d49 100644 --- a/pathview_server/cli.py +++ b/pathview/cli.py @@ -6,7 +6,7 @@ import time import webbrowser -from pathview_server import __version__ +from pathview import __version__ def main(): @@ -29,7 +29,7 @@ def main(): args = parser.parse_args() - from pathview_server.app import create_app + from pathview.app import create_app app = create_app(serve_static=not args.debug) diff --git a/pathview/converter.py b/pathview/converter.py new file mode 100644 index 00000000..b5d77194 --- /dev/null +++ b/pathview/converter.py @@ -0,0 +1,550 @@ +""" +PVM to Python Converter + +Converts PathView .pvm/.json files to standalone PathSim Python scripts. + +Public API: + from pathview import convert + python_code = convert("model.pvm") +""" + +import json +import re +import sys +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +# ============================================================================= +# Registry +# ============================================================================= + +_BUNDLED_REGISTRY = Path(__file__).parent / "data" / "registry.json" + + +def load_registry(registry_path: Path) -> dict: + """Load the JSON registry generated by extract.py.""" + if not registry_path.exists(): + print(f"Error: Registry not found at {registry_path}", file=sys.stderr) + print("Run 'npm run extract' or 'python scripts/extract.py' first.", file=sys.stderr) + sys.exit(1) + with open(registry_path, encoding="utf-8") as f: + return json.load(f) + + +# ============================================================================= +# Code builder utilities (ported from codeBuilder.ts) +# ============================================================================= + +def sanitize_name(name: str) -> str: + """Sanitize a name for use as a Python variable.""" + if not name: + return "" + sanitized = "" + for char in name: + if re.match(r"[a-zA-Z0-9_]", char): + sanitized += char + elif char == " ": + sanitized += "_" + if sanitized and sanitized[0].isdigit(): + sanitized = "n_" + sanitized + return sanitized.lower() + + +def generate_param_string(params: dict, valid_params: set[str], multi_line: bool = False) -> str: + """Generate parameter string for Python constructor calls.""" + parts = [] + for name, value in params.items(): + if value is None or value == "": + continue + if name.startswith("_"): + continue + if name not in valid_params: + continue + parts.append(f"{name}={value}") + + if multi_line and parts: + indent = " " + return "\n" + ",\n".join(indent + p for p in parts) + "\n" + return ", ".join(parts) + + +def group_connections_by_source( + connections: list[dict], node_vars: dict[str, str] +) -> list[dict]: + """Group connections by source for multi-target Connection syntax.""" + groups: dict[str, dict] = {} + for conn in connections: + source_var = node_vars.get(conn["sourceNodeId"]) + target_var = node_vars.get(conn["targetNodeId"]) + if not source_var or not target_var: + continue + key = f"{conn['sourceNodeId']}:{conn['sourcePortIndex']}" + if key not in groups: + groups[key] = { + "sourceVar": source_var, + "sourcePort": conn["sourcePortIndex"], + "targets": [], + } + groups[key]["targets"].append({ + "varName": target_var, + "port": conn["targetPortIndex"], + }) + return list(groups.values()) + + +def generate_connection_lines( + connections: list[dict], node_vars: dict[str, str], indent: str = " " +) -> list[str]: + """Generate Connection() lines from connections.""" + lines = [] + for group in group_connections_by_source(connections, node_vars): + source = f"{group['sourceVar']}[{group['sourcePort']}]" + targets = ", ".join( + f"{t['varName']}[{t['port']}]" for t in group["targets"] + ) + lines.append(f"{indent}Connection({source}, {targets}),") + return lines + + +# ============================================================================= +# Default simulation settings (fallback when .pvm has empty strings) +# ============================================================================= + +DEFAULT_SETTINGS = { + "duration": "10.0", + "dt": "0.01", + "solver": "SSPRK22", + "atol": "1e-6", + "rtol": "1e-3", + "ftol": "1e-12", + "dt_min": "1e-12", + "dt_max": None, +} + + +def get_setting(settings: dict, key: str) -> str | None: + """Get setting value, falling back to defaults for empty strings.""" + value = settings.get(key, "") + if value == "" or value is None: + return DEFAULT_SETTINGS.get(key) + return str(value) + + +# ============================================================================= +# Node collection and import resolution +# ============================================================================= + +def collect_all_nodes(nodes: list[dict]) -> list[dict]: + """Recursively collect all nodes including those inside subsystems.""" + result = [] + for node in nodes: + result.append(node) + if node.get("type") == "Subsystem" and node.get("graph"): + result.extend(collect_all_nodes(node["graph"].get("nodes", []))) + return result + + +def collect_all_events(events: list[dict], nodes: list[dict]) -> list[dict]: + """Collect all events including those inside subsystems.""" + result = list(events) + for node in nodes: + if node.get("type") == "Subsystem" and node.get("graph"): + sub_events = node["graph"].get("events", []) + result.extend(sub_events) + result.extend( + collect_all_events([], node["graph"].get("nodes", [])) + ) + return result + + +def resolve_block_imports( + nodes: list[dict], registry: dict +) -> dict[str, list[str]]: + """Group block classes by import path. Returns {importPath: [className, ...]}.""" + all_nodes = collect_all_nodes(nodes) + imports: dict[str, set[str]] = defaultdict(set) + for node in all_nodes: + node_type = node.get("type", "") + if node_type in ("Subsystem", "Interface"): + continue + block_info = registry["blocks"].get(node_type) + if block_info: + imports[block_info["importPath"]].add(block_info["blockClass"]) + return {path: sorted(classes) for path, classes in imports.items()} + + +def resolve_event_imports( + events: list[dict], nodes: list[dict], registry: dict +) -> dict[str, list[str]]: + """Group event classes by import path.""" + all_events = collect_all_events(events, nodes) + imports: dict[str, set[str]] = defaultdict(set) + for event in all_events: + event_type = event.get("type", "") + # Event type is like "pathsim.events.ZeroCrossing" — extract class name + event_name = event_type.rsplit(".", 1)[-1] if "." in event_type else event_type + event_info = registry["events"].get(event_name) + if event_info: + imports[event_info["importPath"]].add(event_info["eventClass"]) + return {path: sorted(classes) for path, classes in imports.items()} + + +# ============================================================================= +# Subsystem code generation +# ============================================================================= + +def generate_subsystem_code( + node: dict, + node_vars: dict[str, str], + var_names: list[str], + lines: list[str], + registry: dict, + prefix: str = "", +) -> str: + """Generate code for a subsystem and its contents. Returns variable name.""" + graph = node.get("graph", {}) + child_nodes = graph.get("nodes", []) + child_connections = graph.get("connections", []) + child_events = graph.get("events", []) + + # Generate subsystem variable name + var_name = sanitize_name(node["name"]) + if not var_name or var_name in var_names: + var_name = f"subsystem_{len(var_names)}" + var_names.append(var_name) + node_vars[node["id"]] = var_name + + sub_prefix = prefix + var_name + "_" + + # Separate interface nodes from internal blocks + interface_nodes = [n for n in child_nodes if n.get("type") == "Interface"] + internal_blocks = [n for n in child_nodes if n.get("type") != "Interface"] + + internal_var_names: list[str] = [] + internal_node_vars: dict[str, str] = {} + + lines.append("") + lines.append(f"# Subsystem: {node['name']}") + + # Generate Interface blocks + for iface in interface_nodes: + iface_var = sub_prefix + "interface" + internal_var_names.append(iface_var) + internal_node_vars[iface["id"]] = iface_var + lines.append(f"{iface_var} = Interface()") + + # Generate internal blocks + for child in internal_blocks: + if child.get("type") == "Subsystem": + generate_subsystem_code( + child, internal_node_vars, internal_var_names, + lines, registry, sub_prefix, + ) + else: + block_info = registry["blocks"].get(child.get("type", "")) + if not block_info: + continue + child_var = sub_prefix + sanitize_name(child["name"]) + if not child_var or child_var in internal_var_names: + child_var = f"{sub_prefix}block_{len(internal_var_names)}" + internal_var_names.append(child_var) + internal_node_vars[child["id"]] = child_var + + valid_params = set(block_info["params"]) + params = generate_param_string( + child.get("params", {}), valid_params, multi_line=True + ) + if params: + lines.append(f"{child_var} = {block_info['blockClass']}({params})") + else: + lines.append(f"{child_var} = {block_info['blockClass']}()") + + # Propagate internal node vars to parent + for nid, nvar in internal_node_vars.items(): + node_vars[nid] = nvar + + # Generate internal events + event_var_names: list[str] = [] + for event in child_events: + event_type = event.get("type", "") + event_name = event_type.rsplit(".", 1)[-1] if "." in event_type else event_type + event_info = registry["events"].get(event_name) + if not event_info: + continue + evt_var = sub_prefix + sanitize_name(event["name"]) + if not evt_var or evt_var in var_names or evt_var in event_var_names: + evt_var = f"{sub_prefix}event_{len(event_var_names)}" + event_var_names.append(evt_var) + + valid_params = set(event_info["params"]) + params = generate_param_string(event.get("params", {}), valid_params, multi_line=True) + if params: + lines.append(f"{evt_var} = {event_info['eventClass']}({params})") + else: + lines.append(f"{evt_var} = {event_info['eventClass']}()") + + # Subsystem constructor + lines.append(f"{var_name} = Subsystem(") + lines.append(" blocks=[") + for v in internal_var_names: + lines.append(f" {v},") + lines.append(" ],") + lines.append(" connections=[") + for line in generate_connection_lines(child_connections, internal_node_vars, " "): + lines.append(line) + lines.append(" ],") + if child_events: + lines.append(" events=[") + for evt_var in event_var_names: + lines.append(f" {evt_var},") + lines.append(" ],") + lines.append(")") + + return var_name + + +# ============================================================================= +# Main code generation +# ============================================================================= + +def generate_python(pvm: dict, registry: dict, source_name: str = "") -> str: + """Generate formatted standalone Python code from a .pvm file.""" + lines: list[str] = [] + divider = "# " + "\u2500" * 76 + + graph = pvm.get("graph", {}) + nodes = graph.get("nodes", []) + connections = graph.get("connections", []) + events = pvm.get("events", []) + code_context = pvm.get("codeContext", {}).get("code", "") + settings = pvm.get("simulationSettings", {}) + + has_subsystems = any(n.get("type") == "Subsystem" for n in nodes) + block_imports = resolve_block_imports(nodes, registry) + event_imports = resolve_event_imports(events, nodes, registry) + + # Header + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + lines.append("#!/usr/bin/env python3") + lines.append("# -*- coding: utf-8 -*-") + lines.append('"""') + lines.append("PathSim Simulation") + lines.append("==================") + lines.append("") + if source_name: + lines.append(f"Converted from: {source_name}") + lines.append(f"Generated by pvm2py on {timestamp}") + lines.append("https://view.pathsim.org") + lines.append("") + lines.append("PathSim documentation: https://docs.pathsim.org") + lines.append('"""') + lines.append("") + + # Imports + lines.append(divider) + lines.append("# IMPORTS") + lines.append(divider) + lines.append("") + lines.append("import numpy as np") + lines.append("import matplotlib.pyplot as plt") + lines.append("") + + if has_subsystems: + lines.append("from pathsim import Simulation, Connection, Subsystem, Interface") + else: + lines.append("from pathsim import Simulation, Connection") + + # Block imports grouped by import path + for import_path, classes in sorted(block_imports.items()): + lines.append(f"from {import_path} import (") + for i, cls in enumerate(classes): + comma = "," if i < len(classes) - 1 else "" + lines.append(f" {cls}{comma}") + lines.append(")") + + solver = get_setting(settings, "solver") or "SSPRK22" + lines.append(f"from pathsim.solvers import {solver}") + + # Event imports grouped by import path + for import_path, classes in sorted(event_imports.items()): + lines.append(f"from {import_path} import {', '.join(classes)}") + + lines.append("") + + # Code context + if code_context.strip(): + lines.append(divider) + lines.append("# USER-DEFINED CODE") + lines.append(divider) + lines.append("") + lines.append(code_context.strip()) + lines.append("") + + # Blocks + lines.append(divider) + lines.append("# BLOCKS") + lines.append(divider) + + node_vars: dict[str, str] = {} + var_names: list[str] = [] + + # Generate subsystems first + for node in nodes: + if node.get("type") == "Subsystem": + generate_subsystem_code(node, node_vars, var_names, lines, registry) + + # Generate regular blocks (excluding subsystems and interfaces) + regular_nodes = [ + n for n in nodes + if n.get("type") not in ("Subsystem", "Interface") + ] + + if regular_nodes: + lines.append("") + + for i, node in enumerate(regular_nodes): + block_info = registry["blocks"].get(node.get("type", "")) + if not block_info: + continue + + var_name = sanitize_name(node["name"]) + if not var_name or var_name in var_names: + var_name = f"block_{i}" + var_names.append(var_name) + node_vars[node["id"]] = var_name + + valid_params = set(block_info["params"]) + params = generate_param_string(node.get("params", {}), valid_params, multi_line=True) + if params: + lines.append(f"{var_name} = {block_info['blockClass']}({params})") + else: + lines.append(f"{var_name} = {block_info['blockClass']}()") + + # Blocks list + lines.append("") + lines.append("blocks = [") + for v in var_names: + lines.append(f" {v},") + lines.append("]") + lines.append("") + + # Connections + lines.append(divider) + lines.append("# CONNECTIONS") + lines.append(divider) + lines.append("") + if not connections: + lines.append("connections = []") + else: + lines.append("connections = [") + for line in generate_connection_lines(connections, node_vars, " "): + lines.append(line) + lines.append("]") + lines.append("") + + # Events + if events: + lines.append(divider) + lines.append("# EVENTS") + lines.append(divider) + lines.append("") + + event_var_names: list[str] = [] + for event in events: + event_type = event.get("type", "") + event_name = event_type.rsplit(".", 1)[-1] if "." in event_type else event_type + event_info = registry["events"].get(event_name) + if not event_info: + continue + + evt_var = sanitize_name(event["name"]) + if not evt_var or evt_var in var_names or evt_var in event_var_names: + evt_var = f"event_{len(event_var_names)}" + event_var_names.append(evt_var) + + valid_params = set(event_info["params"]) + params = generate_param_string(event.get("params", {}), valid_params, multi_line=True) + if params: + lines.append(f"{evt_var} = {event_info['eventClass']}({params})") + else: + lines.append(f"{evt_var} = {event_info['eventClass']}()") + + lines.append("") + lines.append("events = [") + for v in event_var_names: + lines.append(f" {v},") + lines.append("]") + lines.append("") + + # Simulation + lines.append(divider) + lines.append("# SIMULATION") + lines.append(divider) + lines.append("") + + has_events = bool(events) + lines.append("sim = Simulation(") + lines.append(" blocks,") + lines.append(" connections,") + if has_events: + lines.append(" events,") + lines.append(f" Solver={solver},") + lines.append(f" dt={get_setting(settings, 'dt')},") + lines.append(f" dt_min={get_setting(settings, 'dt_min')},") + + dt_max = get_setting(settings, "dt_max") + if dt_max: + lines.append(f" dt_max={dt_max},") + + lines.append(f" tolerance_lte_rel={get_setting(settings, 'rtol')},") + lines.append(f" tolerance_lte_abs={get_setting(settings, 'atol')},") + lines.append(f" tolerance_fpi={get_setting(settings, 'ftol')},") + lines.append(")") + lines.append("") + + # Main block + lines.append(divider) + lines.append("# MAIN") + lines.append(divider) + lines.append("") + lines.append("if __name__ == '__main__':") + lines.append("") + lines.append(" # Run simulation") + lines.append(f" sim.run(duration={get_setting(settings, 'duration')})") + lines.append("") + lines.append(" # Plot results") + lines.append(" sim.plot()") + lines.append(" plt.show()") + lines.append("") + + return "\n".join(lines) + + +# ============================================================================= +# Public API +# ============================================================================= + +def convert(path: str | Path, *, registry_path: Path | None = None) -> str: + """Convert a .pvm file to a standalone PathSim Python script. + + Args: + path: Path to a .pvm or .json file. + registry_path: Optional custom registry.json path. + Defaults to the bundled registry. + + Returns: + Generated Python script as a string. + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"Input file not found: {path}") + + registry = load_registry(registry_path or _BUNDLED_REGISTRY) + + with open(path, encoding="utf-8") as f: + pvm = json.load(f) + + return generate_python(pvm, registry, source_name=path.name) diff --git a/pathview/data/registry.json b/pathview/data/registry.json new file mode 100644 index 00000000..cf3fb206 --- /dev/null +++ b/pathview/data/registry.json @@ -0,0 +1,609 @@ +{ + "blocks": { + "Constant": { + "blockClass": "Constant", + "importPath": "pathsim.blocks", + "params": [ + "value" + ] + }, + "Source": { + "blockClass": "Source", + "importPath": "pathsim.blocks", + "params": [ + "func" + ] + }, + "SinusoidalSource": { + "blockClass": "SinusoidalSource", + "importPath": "pathsim.blocks", + "params": [ + "frequency", + "amplitude", + "phase" + ] + }, + "StepSource": { + "blockClass": "StepSource", + "importPath": "pathsim.blocks", + "params": [ + "amplitude", + "tau" + ] + }, + "PulseSource": { + "blockClass": "PulseSource", + "importPath": "pathsim.blocks", + "params": [ + "amplitude", + "T", + "t_rise", + "t_fall", + "tau", + "duty" + ] + }, + "TriangleWaveSource": { + "blockClass": "TriangleWaveSource", + "importPath": "pathsim.blocks", + "params": [ + "frequency", + "amplitude", + "phase" + ] + }, + "SquareWaveSource": { + "blockClass": "SquareWaveSource", + "importPath": "pathsim.blocks", + "params": [ + "amplitude", + "frequency", + "phase" + ] + }, + "GaussianPulseSource": { + "blockClass": "GaussianPulseSource", + "importPath": "pathsim.blocks", + "params": [ + "amplitude", + "f_max", + "tau" + ] + }, + "ChirpPhaseNoiseSource": { + "blockClass": "ChirpPhaseNoiseSource", + "importPath": "pathsim.blocks", + "params": [ + "amplitude", + "f0", + "BW", + "T", + "phase", + "sig_cum", + "sig_white", + "sampling_period" + ] + }, + "ClockSource": { + "blockClass": "ClockSource", + "importPath": "pathsim.blocks", + "params": [ + "T", + "tau" + ] + }, + "WhiteNoise": { + "blockClass": "WhiteNoise", + "importPath": "pathsim.blocks", + "params": [ + "standard_deviation", + "spectral_density", + "sampling_period", + "seed" + ] + }, + "PinkNoise": { + "blockClass": "PinkNoise", + "importPath": "pathsim.blocks", + "params": [ + "standard_deviation", + "spectral_density", + "num_octaves", + "sampling_period", + "seed" + ] + }, + "RandomNumberGenerator": { + "blockClass": "RandomNumberGenerator", + "importPath": "pathsim.blocks", + "params": [ + "sampling_period" + ] + }, + "Integrator": { + "blockClass": "Integrator", + "importPath": "pathsim.blocks", + "params": [ + "initial_value" + ] + }, + "Differentiator": { + "blockClass": "Differentiator", + "importPath": "pathsim.blocks", + "params": [ + "f_max" + ] + }, + "Delay": { + "blockClass": "Delay", + "importPath": "pathsim.blocks", + "params": [ + "tau" + ] + }, + "ODE": { + "blockClass": "ODE", + "importPath": "pathsim.blocks", + "params": [ + "func", + "initial_value", + "jac" + ] + }, + "DynamicalSystem": { + "blockClass": "DynamicalSystem", + "importPath": "pathsim.blocks", + "params": [ + "func_dyn", + "func_alg", + "initial_value", + "jac_dyn" + ] + }, + "StateSpace": { + "blockClass": "StateSpace", + "importPath": "pathsim.blocks", + "params": [ + "A", + "B", + "C", + "D", + "initial_value" + ] + }, + "PT1": { + "blockClass": "PT1", + "importPath": "pathsim.blocks", + "params": [ + "K", + "T" + ] + }, + "PT2": { + "blockClass": "PT2", + "importPath": "pathsim.blocks", + "params": [ + "K", + "T", + "d" + ] + }, + "LeadLag": { + "blockClass": "LeadLag", + "importPath": "pathsim.blocks", + "params": [ + "K", + "T1", + "T2" + ] + }, + "PID": { + "blockClass": "PID", + "importPath": "pathsim.blocks", + "params": [ + "Kp", + "Ki", + "Kd", + "f_max" + ] + }, + "AntiWindupPID": { + "blockClass": "AntiWindupPID", + "importPath": "pathsim.blocks", + "params": [ + "Kp", + "Ki", + "Kd", + "f_max", + "Ks", + "limits" + ] + }, + "RateLimiter": { + "blockClass": "RateLimiter", + "importPath": "pathsim.blocks", + "params": [ + "rate", + "f_max" + ] + }, + "Backlash": { + "blockClass": "Backlash", + "importPath": "pathsim.blocks", + "params": [ + "width", + "f_max" + ] + }, + "Deadband": { + "blockClass": "Deadband", + "importPath": "pathsim.blocks", + "params": [ + "lower", + "upper" + ] + }, + "TransferFunctionNumDen": { + "blockClass": "TransferFunctionNumDen", + "importPath": "pathsim.blocks", + "params": [ + "Num", + "Den" + ] + }, + "TransferFunctionZPG": { + "blockClass": "TransferFunctionZPG", + "importPath": "pathsim.blocks", + "params": [ + "Zeros", + "Poles", + "Gain" + ] + }, + "ButterworthLowpassFilter": { + "blockClass": "ButterworthLowpassFilter", + "importPath": "pathsim.blocks", + "params": [ + "Fc", + "n" + ] + }, + "ButterworthHighpassFilter": { + "blockClass": "ButterworthHighpassFilter", + "importPath": "pathsim.blocks", + "params": [ + "Fc", + "n" + ] + }, + "ButterworthBandpassFilter": { + "blockClass": "ButterworthBandpassFilter", + "importPath": "pathsim.blocks", + "params": [ + "Fc", + "n" + ] + }, + "ButterworthBandstopFilter": { + "blockClass": "ButterworthBandstopFilter", + "importPath": "pathsim.blocks", + "params": [ + "Fc", + "n" + ] + }, + "Adder": { + "blockClass": "Adder", + "importPath": "pathsim.blocks", + "params": [ + "operations" + ] + }, + "Multiplier": { + "blockClass": "Multiplier", + "importPath": "pathsim.blocks", + "params": [] + }, + "Amplifier": { + "blockClass": "Amplifier", + "importPath": "pathsim.blocks", + "params": [ + "gain" + ] + }, + "Function": { + "blockClass": "Function", + "importPath": "pathsim.blocks", + "params": [ + "func" + ] + }, + "Sin": { + "blockClass": "Sin", + "importPath": "pathsim.blocks", + "params": [] + }, + "Cos": { + "blockClass": "Cos", + "importPath": "pathsim.blocks", + "params": [] + }, + "Tan": { + "blockClass": "Tan", + "importPath": "pathsim.blocks", + "params": [] + }, + "Tanh": { + "blockClass": "Tanh", + "importPath": "pathsim.blocks", + "params": [] + }, + "Abs": { + "blockClass": "Abs", + "importPath": "pathsim.blocks", + "params": [] + }, + "Sqrt": { + "blockClass": "Sqrt", + "importPath": "pathsim.blocks", + "params": [] + }, + "Exp": { + "blockClass": "Exp", + "importPath": "pathsim.blocks", + "params": [] + }, + "Log": { + "blockClass": "Log", + "importPath": "pathsim.blocks", + "params": [] + }, + "Log10": { + "blockClass": "Log10", + "importPath": "pathsim.blocks", + "params": [] + }, + "Mod": { + "blockClass": "Mod", + "importPath": "pathsim.blocks", + "params": [ + "modulus" + ] + }, + "Clip": { + "blockClass": "Clip", + "importPath": "pathsim.blocks", + "params": [ + "min_val", + "max_val" + ] + }, + "Pow": { + "blockClass": "Pow", + "importPath": "pathsim.blocks", + "params": [ + "exponent" + ] + }, + "Switch": { + "blockClass": "Switch", + "importPath": "pathsim.blocks", + "params": [ + "state" + ] + }, + "LUT": { + "blockClass": "LUT", + "importPath": "pathsim.blocks", + "params": [ + "points", + "values" + ] + }, + "LUT1D": { + "blockClass": "LUT1D", + "importPath": "pathsim.blocks", + "params": [ + "points", + "values", + "fill_value" + ] + }, + "SampleHold": { + "blockClass": "SampleHold", + "importPath": "pathsim.blocks", + "params": [ + "T", + "tau" + ] + }, + "FIR": { + "blockClass": "FIR", + "importPath": "pathsim.blocks", + "params": [ + "coeffs", + "T", + "tau" + ] + }, + "ADC": { + "blockClass": "ADC", + "importPath": "pathsim.blocks", + "params": [ + "n_bits", + "span", + "T", + "tau" + ] + }, + "DAC": { + "blockClass": "DAC", + "importPath": "pathsim.blocks", + "params": [ + "n_bits", + "span", + "T", + "tau" + ] + }, + "Counter": { + "blockClass": "Counter", + "importPath": "pathsim.blocks", + "params": [ + "start", + "threshold" + ] + }, + "CounterUp": { + "blockClass": "CounterUp", + "importPath": "pathsim.blocks", + "params": [ + "start", + "threshold" + ] + }, + "CounterDown": { + "blockClass": "CounterDown", + "importPath": "pathsim.blocks", + "params": [ + "start", + "threshold" + ] + }, + "Relay": { + "blockClass": "Relay", + "importPath": "pathsim.blocks", + "params": [ + "threshold_up", + "threshold_down", + "value_up", + "value_down" + ] + }, + "Scope": { + "blockClass": "Scope", + "importPath": "pathsim.blocks", + "params": [ + "sampling_period", + "t_wait", + "labels" + ] + }, + "Spectrum": { + "blockClass": "Spectrum", + "importPath": "pathsim.blocks", + "params": [ + "freq", + "t_wait", + "alpha", + "labels" + ] + }, + "Subsystem": { + "blockClass": "Subsystem", + "importPath": "pathsim.blocks", + "params": [] + }, + "Interface": { + "blockClass": "Interface", + "importPath": "pathsim.blocks", + "params": [] + }, + "Process": { + "blockClass": "Process", + "importPath": "pathsim_chem.tritium", + "params": [ + "tau", + "initial_value", + "source_term" + ] + }, + "Bubbler4": { + "blockClass": "Bubbler4", + "importPath": "pathsim_chem.tritium", + "params": [ + "conversion_efficiency", + "vial_efficiency", + "replacement_times" + ] + }, + "Splitter": { + "blockClass": "Splitter", + "importPath": "pathsim_chem.tritium", + "params": [ + "fractions" + ] + }, + "GLC": { + "blockClass": "GLC", + "importPath": "pathsim_chem.tritium", + "params": [ + "P_in", + "L", + "D", + "T", + "BCs", + "g", + "initial_nb_of_elements" + ] + } + }, + "events": { + "ZeroCrossing": { + "eventClass": "ZeroCrossing", + "importPath": "pathsim.events", + "params": [ + "func_evt", + "func_act", + "tolerance" + ] + }, + "ZeroCrossingUp": { + "eventClass": "ZeroCrossingUp", + "importPath": "pathsim.events", + "params": [ + "func_evt", + "func_act", + "tolerance" + ] + }, + "ZeroCrossingDown": { + "eventClass": "ZeroCrossingDown", + "importPath": "pathsim.events", + "params": [ + "func_evt", + "func_act", + "tolerance" + ] + }, + "Schedule": { + "eventClass": "Schedule", + "importPath": "pathsim.events", + "params": [ + "t_start", + "t_end", + "t_period", + "func_act", + "tolerance" + ] + }, + "ScheduleList": { + "eventClass": "ScheduleList", + "importPath": "pathsim.events", + "params": [ + "times_evt", + "func_act", + "tolerance" + ] + }, + "Condition": { + "eventClass": "Condition", + "importPath": "pathsim.events", + "params": [ + "func_evt", + "func_act", + "tolerance" + ] + } + } +} diff --git a/pathview_server/worker.py b/pathview/worker.py similarity index 100% rename from pathview_server/worker.py rename to pathview/worker.py diff --git a/pathview_server/__main__.py b/pathview_server/__main__.py deleted file mode 100644 index 4082f5cc..00000000 --- a/pathview_server/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Entry point for: python -m pathview_server""" - -from pathview_server.cli import main - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 77d2293c..0d81673b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,10 +34,10 @@ Homepage = "https://view.pathsim.org" Repository = "https://github.com/pathsim/pathview" [project.scripts] -pathview = "pathview_server.cli:main" +pathview = "pathview.cli:main" [tool.setuptools] -packages = ["pathview_server"] +packages = ["pathview"] [tool.setuptools.package-data] -pathview_server = ["static/**/*"] +pathview = ["static/**/*", "data/*.json"] diff --git a/scripts/build_package.py b/scripts/build_package.py index 68eb4adf..c6cc5ed2 100644 --- a/scripts/build_package.py +++ b/scripts/build_package.py @@ -3,7 +3,7 @@ Build script for the PathView PyPI package. 1. Builds the SvelteKit frontend (vite build) -2. Copies build/ output to pathview_server/static/ +2. Copies build/ output to pathview/static/ 3. Builds the Python wheel """ @@ -17,7 +17,7 @@ REPO_ROOT = Path(__file__).parent.parent BUILD_DIR = REPO_ROOT / "build" -STATIC_DIR = REPO_ROOT / "pathview_server" / "static" +STATIC_DIR = REPO_ROOT / "pathview" / "static" def _find_npx(): @@ -81,7 +81,7 @@ def main(): print("ERROR: build/index.html not found") sys.exit(1) - print("[3/4] Copying frontend to pathview_server/static/...") + print("[3/4] Copying frontend to pathview/static/...") shutil.copytree(BUILD_DIR, STATIC_DIR) print(f" Copied {sum(1 for _ in STATIC_DIR.rglob('*') if _.is_file())} files") diff --git a/scripts/extract.py b/scripts/extract.py index f486d2cf..bc473337 100644 --- a/scripts/extract.py +++ b/scripts/extract.py @@ -827,12 +827,16 @@ def write_registry(self, blocks: dict, block_import_paths: dict[str, str], output_path = self.output_dir / "generated" / "registry.json" output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text( - json.dumps(registry, indent=2, ensure_ascii=False) + "\n", - encoding="utf-8" - ) + registry_json = json.dumps(registry, indent=2, ensure_ascii=False) + "\n" + output_path.write_text(registry_json, encoding="utf-8") print(f"Generated: {output_path}") + # Also write to pathview/data/ for the bundled converter API + bundled_path = self.output_dir.parent / "pathview" / "data" / "registry.json" + bundled_path.parent.mkdir(parents=True, exist_ok=True) + bundled_path.write_text(registry_json, encoding="utf-8") + print(f"Generated: {bundled_path}") + class TypeScriptGenerator: """Generate TypeScript output files.""" diff --git a/scripts/pvm2py.py b/scripts/pvm2py.py index d1452f0e..d0657bf6 100644 --- a/scripts/pvm2py.py +++ b/scripts/pvm2py.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -PVM to Python Converter +PVM to Python Converter (CLI wrapper) Converts PathView .pvm/.json files to standalone PathSim Python scripts. @@ -11,521 +11,11 @@ """ import argparse -import json -import re import sys -from collections import defaultdict -from datetime import datetime, timezone from pathlib import Path -from typing import Any +from pathview.converter import generate_python, load_registry -# ============================================================================= -# Registry -# ============================================================================= - -def load_registry(registry_path: Path) -> dict: - """Load the JSON registry generated by extract.py.""" - if not registry_path.exists(): - print(f"Error: Registry not found at {registry_path}", file=sys.stderr) - print("Run 'npm run extract' or 'python scripts/extract.py' first.", file=sys.stderr) - sys.exit(1) - with open(registry_path, encoding="utf-8") as f: - return json.load(f) - - -# ============================================================================= -# Code builder utilities (ported from codeBuilder.ts) -# ============================================================================= - -def sanitize_name(name: str) -> str: - """Sanitize a name for use as a Python variable.""" - if not name: - return "" - sanitized = "" - for char in name: - if re.match(r"[a-zA-Z0-9_]", char): - sanitized += char - elif char == " ": - sanitized += "_" - if sanitized and sanitized[0].isdigit(): - sanitized = "n_" + sanitized - return sanitized.lower() - - -def generate_param_string(params: dict, valid_params: set[str], multi_line: bool = False) -> str: - """Generate parameter string for Python constructor calls.""" - parts = [] - for name, value in params.items(): - if value is None or value == "": - continue - if name.startswith("_"): - continue - if name not in valid_params: - continue - parts.append(f"{name}={value}") - - if multi_line and parts: - indent = " " - return "\n" + ",\n".join(indent + p for p in parts) + "\n" - return ", ".join(parts) - - -def group_connections_by_source( - connections: list[dict], node_vars: dict[str, str] -) -> list[dict]: - """Group connections by source for multi-target Connection syntax.""" - groups: dict[str, dict] = {} - for conn in connections: - source_var = node_vars.get(conn["sourceNodeId"]) - target_var = node_vars.get(conn["targetNodeId"]) - if not source_var or not target_var: - continue - key = f"{conn['sourceNodeId']}:{conn['sourcePortIndex']}" - if key not in groups: - groups[key] = { - "sourceVar": source_var, - "sourcePort": conn["sourcePortIndex"], - "targets": [], - } - groups[key]["targets"].append({ - "varName": target_var, - "port": conn["targetPortIndex"], - }) - return list(groups.values()) - - -def generate_connection_lines( - connections: list[dict], node_vars: dict[str, str], indent: str = " " -) -> list[str]: - """Generate Connection() lines from connections.""" - lines = [] - for group in group_connections_by_source(connections, node_vars): - source = f"{group['sourceVar']}[{group['sourcePort']}]" - targets = ", ".join( - f"{t['varName']}[{t['port']}]" for t in group["targets"] - ) - lines.append(f"{indent}Connection({source}, {targets}),") - return lines - - -# ============================================================================= -# Default simulation settings (fallback when .pvm has empty strings) -# ============================================================================= - -DEFAULT_SETTINGS = { - "duration": "10.0", - "dt": "0.01", - "solver": "SSPRK22", - "atol": "1e-6", - "rtol": "1e-3", - "ftol": "1e-12", - "dt_min": "1e-12", - "dt_max": None, -} - - -def get_setting(settings: dict, key: str) -> str | None: - """Get setting value, falling back to defaults for empty strings.""" - value = settings.get(key, "") - if value == "" or value is None: - return DEFAULT_SETTINGS.get(key) - return str(value) - - -# ============================================================================= -# Node collection and import resolution -# ============================================================================= - -def collect_all_nodes(nodes: list[dict]) -> list[dict]: - """Recursively collect all nodes including those inside subsystems.""" - result = [] - for node in nodes: - result.append(node) - if node.get("type") == "Subsystem" and node.get("graph"): - result.extend(collect_all_nodes(node["graph"].get("nodes", []))) - return result - - -def collect_all_events(events: list[dict], nodes: list[dict]) -> list[dict]: - """Collect all events including those inside subsystems.""" - result = list(events) - for node in nodes: - if node.get("type") == "Subsystem" and node.get("graph"): - sub_events = node["graph"].get("events", []) - result.extend(sub_events) - result.extend( - collect_all_events([], node["graph"].get("nodes", [])) - ) - return result - - -def resolve_block_imports( - nodes: list[dict], registry: dict -) -> dict[str, list[str]]: - """Group block classes by import path. Returns {importPath: [className, ...]}.""" - all_nodes = collect_all_nodes(nodes) - imports: dict[str, set[str]] = defaultdict(set) - for node in all_nodes: - node_type = node.get("type", "") - if node_type in ("Subsystem", "Interface"): - continue - block_info = registry["blocks"].get(node_type) - if block_info: - imports[block_info["importPath"]].add(block_info["blockClass"]) - return {path: sorted(classes) for path, classes in imports.items()} - - -def resolve_event_imports( - events: list[dict], nodes: list[dict], registry: dict -) -> dict[str, list[str]]: - """Group event classes by import path.""" - all_events = collect_all_events(events, nodes) - imports: dict[str, set[str]] = defaultdict(set) - for event in all_events: - event_type = event.get("type", "") - # Event type is like "pathsim.events.ZeroCrossing" — extract class name - event_name = event_type.rsplit(".", 1)[-1] if "." in event_type else event_type - event_info = registry["events"].get(event_name) - if event_info: - imports[event_info["importPath"]].add(event_info["eventClass"]) - return {path: sorted(classes) for path, classes in imports.items()} - - -# ============================================================================= -# Subsystem code generation -# ============================================================================= - -def generate_subsystem_code( - node: dict, - node_vars: dict[str, str], - var_names: list[str], - lines: list[str], - registry: dict, - prefix: str = "", -) -> str: - """Generate code for a subsystem and its contents. Returns variable name.""" - graph = node.get("graph", {}) - child_nodes = graph.get("nodes", []) - child_connections = graph.get("connections", []) - child_events = graph.get("events", []) - - # Generate subsystem variable name - var_name = sanitize_name(node["name"]) - if not var_name or var_name in var_names: - var_name = f"subsystem_{len(var_names)}" - var_names.append(var_name) - node_vars[node["id"]] = var_name - - sub_prefix = prefix + var_name + "_" - - # Separate interface nodes from internal blocks - interface_nodes = [n for n in child_nodes if n.get("type") == "Interface"] - internal_blocks = [n for n in child_nodes if n.get("type") != "Interface"] - - internal_var_names: list[str] = [] - internal_node_vars: dict[str, str] = {} - - lines.append("") - lines.append(f"# Subsystem: {node['name']}") - - # Generate Interface blocks - for iface in interface_nodes: - iface_var = sub_prefix + "interface" - internal_var_names.append(iface_var) - internal_node_vars[iface["id"]] = iface_var - lines.append(f"{iface_var} = Interface()") - - # Generate internal blocks - for child in internal_blocks: - if child.get("type") == "Subsystem": - generate_subsystem_code( - child, internal_node_vars, internal_var_names, - lines, registry, sub_prefix, - ) - else: - block_info = registry["blocks"].get(child.get("type", "")) - if not block_info: - continue - child_var = sub_prefix + sanitize_name(child["name"]) - if not child_var or child_var in internal_var_names: - child_var = f"{sub_prefix}block_{len(internal_var_names)}" - internal_var_names.append(child_var) - internal_node_vars[child["id"]] = child_var - - valid_params = set(block_info["params"]) - params = generate_param_string( - child.get("params", {}), valid_params, multi_line=True - ) - if params: - lines.append(f"{child_var} = {block_info['blockClass']}({params})") - else: - lines.append(f"{child_var} = {block_info['blockClass']}()") - - # Propagate internal node vars to parent - for nid, nvar in internal_node_vars.items(): - node_vars[nid] = nvar - - # Generate internal events - event_var_names: list[str] = [] - for event in child_events: - event_type = event.get("type", "") - event_name = event_type.rsplit(".", 1)[-1] if "." in event_type else event_type - event_info = registry["events"].get(event_name) - if not event_info: - continue - evt_var = sub_prefix + sanitize_name(event["name"]) - if not evt_var or evt_var in var_names or evt_var in event_var_names: - evt_var = f"{sub_prefix}event_{len(event_var_names)}" - event_var_names.append(evt_var) - - valid_params = set(event_info["params"]) - params = generate_param_string(event.get("params", {}), valid_params, multi_line=True) - if params: - lines.append(f"{evt_var} = {event_info['eventClass']}({params})") - else: - lines.append(f"{evt_var} = {event_info['eventClass']}()") - - # Subsystem constructor - lines.append(f"{var_name} = Subsystem(") - lines.append(" blocks=[") - for v in internal_var_names: - lines.append(f" {v},") - lines.append(" ],") - lines.append(" connections=[") - for line in generate_connection_lines(child_connections, internal_node_vars, " "): - lines.append(line) - lines.append(" ],") - if child_events: - lines.append(" events=[") - for evt_var in event_var_names: - lines.append(f" {evt_var},") - lines.append(" ],") - lines.append(")") - - return var_name - - -# ============================================================================= -# Main code generation -# ============================================================================= - -def generate_python(pvm: dict, registry: dict, source_name: str = "") -> str: - """Generate formatted standalone Python code from a .pvm file.""" - lines: list[str] = [] - divider = "# " + "\u2500" * 76 - - graph = pvm.get("graph", {}) - nodes = graph.get("nodes", []) - connections = graph.get("connections", []) - events = pvm.get("events", []) - code_context = pvm.get("codeContext", {}).get("code", "") - settings = pvm.get("simulationSettings", {}) - - has_subsystems = any(n.get("type") == "Subsystem" for n in nodes) - block_imports = resolve_block_imports(nodes, registry) - event_imports = resolve_event_imports(events, nodes, registry) - - # Header - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - lines.append("#!/usr/bin/env python3") - lines.append("# -*- coding: utf-8 -*-") - lines.append('"""') - lines.append("PathSim Simulation") - lines.append("==================") - lines.append("") - if source_name: - lines.append(f"Converted from: {source_name}") - lines.append(f"Generated by pvm2py on {timestamp}") - lines.append("https://view.pathsim.org") - lines.append("") - lines.append("PathSim documentation: https://docs.pathsim.org") - lines.append('"""') - lines.append("") - - # Imports - lines.append(divider) - lines.append("# IMPORTS") - lines.append(divider) - lines.append("") - lines.append("import numpy as np") - lines.append("import matplotlib.pyplot as plt") - lines.append("") - - if has_subsystems: - lines.append("from pathsim import Simulation, Connection, Subsystem, Interface") - else: - lines.append("from pathsim import Simulation, Connection") - - # Block imports grouped by import path - for import_path, classes in sorted(block_imports.items()): - lines.append(f"from {import_path} import (") - for i, cls in enumerate(classes): - comma = "," if i < len(classes) - 1 else "" - lines.append(f" {cls}{comma}") - lines.append(")") - - solver = get_setting(settings, "solver") or "SSPRK22" - lines.append(f"from pathsim.solvers import {solver}") - - # Event imports grouped by import path - for import_path, classes in sorted(event_imports.items()): - lines.append(f"from {import_path} import {', '.join(classes)}") - - lines.append("") - - # Code context - if code_context.strip(): - lines.append(divider) - lines.append("# USER-DEFINED CODE") - lines.append(divider) - lines.append("") - lines.append(code_context.strip()) - lines.append("") - - # Blocks - lines.append(divider) - lines.append("# BLOCKS") - lines.append(divider) - - node_vars: dict[str, str] = {} - var_names: list[str] = [] - - # Generate subsystems first - for node in nodes: - if node.get("type") == "Subsystem": - generate_subsystem_code(node, node_vars, var_names, lines, registry) - - # Generate regular blocks (excluding subsystems and interfaces) - regular_nodes = [ - n for n in nodes - if n.get("type") not in ("Subsystem", "Interface") - ] - - if regular_nodes: - lines.append("") - - for i, node in enumerate(regular_nodes): - block_info = registry["blocks"].get(node.get("type", "")) - if not block_info: - continue - - var_name = sanitize_name(node["name"]) - if not var_name or var_name in var_names: - var_name = f"block_{i}" - var_names.append(var_name) - node_vars[node["id"]] = var_name - - valid_params = set(block_info["params"]) - params = generate_param_string(node.get("params", {}), valid_params, multi_line=True) - if params: - lines.append(f"{var_name} = {block_info['blockClass']}({params})") - else: - lines.append(f"{var_name} = {block_info['blockClass']}()") - - # Blocks list - lines.append("") - lines.append("blocks = [") - for v in var_names: - lines.append(f" {v},") - lines.append("]") - lines.append("") - - # Connections - lines.append(divider) - lines.append("# CONNECTIONS") - lines.append(divider) - lines.append("") - if not connections: - lines.append("connections = []") - else: - lines.append("connections = [") - for line in generate_connection_lines(connections, node_vars, " "): - lines.append(line) - lines.append("]") - lines.append("") - - # Events - if events: - lines.append(divider) - lines.append("# EVENTS") - lines.append(divider) - lines.append("") - - event_var_names: list[str] = [] - for event in events: - event_type = event.get("type", "") - event_name = event_type.rsplit(".", 1)[-1] if "." in event_type else event_type - event_info = registry["events"].get(event_name) - if not event_info: - continue - - evt_var = sanitize_name(event["name"]) - if not evt_var or evt_var in var_names or evt_var in event_var_names: - evt_var = f"event_{len(event_var_names)}" - event_var_names.append(evt_var) - - valid_params = set(event_info["params"]) - params = generate_param_string(event.get("params", {}), valid_params, multi_line=True) - if params: - lines.append(f"{evt_var} = {event_info['eventClass']}({params})") - else: - lines.append(f"{evt_var} = {event_info['eventClass']}()") - - lines.append("") - lines.append("events = [") - for v in event_var_names: - lines.append(f" {v},") - lines.append("]") - lines.append("") - - # Simulation - lines.append(divider) - lines.append("# SIMULATION") - lines.append(divider) - lines.append("") - - has_events = bool(events) - lines.append("sim = Simulation(") - lines.append(" blocks,") - lines.append(" connections,") - if has_events: - lines.append(" events,") - lines.append(f" Solver={solver},") - lines.append(f" dt={get_setting(settings, 'dt')},") - lines.append(f" dt_min={get_setting(settings, 'dt_min')},") - - dt_max = get_setting(settings, "dt_max") - if dt_max: - lines.append(f" dt_max={dt_max},") - - lines.append(f" tolerance_lte_rel={get_setting(settings, 'rtol')},") - lines.append(f" tolerance_lte_abs={get_setting(settings, 'atol')},") - lines.append(f" tolerance_fpi={get_setting(settings, 'ftol')},") - lines.append(")") - lines.append("") - - # Main block - lines.append(divider) - lines.append("# MAIN") - lines.append(divider) - lines.append("") - lines.append("if __name__ == '__main__':") - lines.append("") - lines.append(" # Run simulation") - lines.append(f" sim.run(duration={get_setting(settings, 'duration')})") - lines.append("") - lines.append(" # Plot results") - lines.append(" sim.plot()") - lines.append(" plt.show()") - lines.append("") - - return "\n".join(lines) - - -# ============================================================================= -# CLI -# ============================================================================= def main(): parser = argparse.ArgumentParser( @@ -560,6 +50,7 @@ def main(): registry = load_registry(registry_path) # Load .pvm file + import json with open(input_path, encoding="utf-8") as f: pvm = json.load(f) diff --git a/tests/conftest.py b/tests/conftest.py index e5a0b75e..5623060f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import pytest -from pathview_server.app import create_app, _sessions, _sessions_lock +from pathview.app import create_app, _sessions, _sessions_lock @pytest.fixture() diff --git a/tests/test_worker.py b/tests/test_worker.py index 8397720a..5d048dd4 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -5,7 +5,7 @@ import sys from pathlib import Path -WORKER_SCRIPT = str(Path(__file__).parent.parent / "pathview_server" / "worker.py") +WORKER_SCRIPT = str(Path(__file__).parent.parent / "pathview" / "worker.py") def _start_worker():