From 8647509eab5d8322dd5b5f407dec6a647d847697 Mon Sep 17 00:00:00 2001 From: az Date: Sun, 2 Nov 2025 01:42:57 -0500 Subject: [PATCH 1/6] Add py gui --- tests/ipgGUI/Python/RS_GUI.py | 116 ---------- tests/ipgGUI/Python/sim_runner_ui.py | 313 +++++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 116 deletions(-) delete mode 100644 tests/ipgGUI/Python/RS_GUI.py create mode 100644 tests/ipgGUI/Python/sim_runner_ui.py diff --git a/tests/ipgGUI/Python/RS_GUI.py b/tests/ipgGUI/Python/RS_GUI.py deleted file mode 100644 index 43f82f7e..00000000 --- a/tests/ipgGUI/Python/RS_GUI.py +++ /dev/null @@ -1,116 +0,0 @@ -# Create the step-1 GUI script and a tiny requirements file -from pathlib import Path - -import json -import os -from pathlib import Path -import PySimpleGUI as sg - -APP_TITLE = "Simulink Runner – Step 1 (Path Picker)" -PRESET_FILE = Path.home() / ".sim_gui_preset.json" - -def validate_paths(model_path: str, input_dir: str, output_dir: str): - errors = [] - mp = Path(model_path).expanduser() - if not mp.is_file(): - errors.append(f"Model path does not exist: {mp}") - elif mp.suffix.lower() != ".slx": - errors.append(f"Model must be a .slx file: {mp.name}") - - ip = Path(input_dir).expanduser() - if not ip.exists() or not ip.is_dir(): - errors.append(f"Input folder is not a directory: {ip}") - - op = Path(output_dir).expanduser() - if not op.exists(): - try: - op.mkdir(parents=True, exist_ok=True) - except Exception as e: - errors.append(f"Output folder could not be created: {op} ({e})") - elif not op.is_dir(): - errors.append(f"Output path is not a directory: {op}") - - return errors - -def save_preset(values): - data = { - "model": values.get("-MODEL-", ""), - "in_dir": values.get("-IN-", ""), - "out_dir": values.get("-OUT-", ""), - } - try: - with open(PRESET_FILE, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - return True, f"Preset saved to {PRESET_FILE}" - except Exception as e: - return False, f"Failed to save preset: {e}" - -def load_preset(window): - if not PRESET_FILE.exists(): - return False, f"No preset file found at {PRESET_FILE}" - try: - with open(PRESET_FILE, "r", encoding="utf-8") as f: - data = json.load(f) - window["-MODEL-"].update(data.get("model","")) - window["-IN-"].update(data.get("in_dir","")) - window["-OUT-"].update(data.get("out_dir","")) - return True, f"Preset loaded from {PRESET_FILE}" - except Exception as e: - return False, f"Failed to load preset: {e}" - -def main(): - sg.theme("SystemDefault") - layout = [ - [sg.Text("Simulink model (.slx)", size=(22,1)), - sg.Input(key="-MODEL-", expand_x=True, tooltip="Full path to your .slx model"), - sg.FileBrowse(file_types=(("Simulink Model","*.slx"),), tooltip="Browse for a .slx file")], - [sg.Text("Input folder", size=(22,1)), - sg.Input(key="-IN-", expand_x=True, tooltip="Folder containing your model inputs"), - sg.FolderBrowse(tooltip="Browse for input directory")], - [sg.Text("Output folder", size=(22,1)), - sg.Input(key="-OUT-", expand_x=True, tooltip="Folder where results/logs will be saved"), - sg.FolderBrowse(tooltip="Browse for output directory")], - [sg.Button("Validate", key="-VALIDATE-"), - sg.Button("Save Preset", key="-SAVE-"), - sg.Button("Load Preset", key="-LOAD-"), - sg.Push(), - sg.Button("Run (disabled in Step 1)", key="-RUN-", disabled=True), - sg.Button("Quit")], - [sg.Multiline(size=(100,12), key="-LOG-", autoscroll=True, write_only=True, expand_x=True, expand_y=True)] - ] - - window = sg.Window(APP_TITLE, layout, resizable=True) - - while True: - event, values = window.read() - if event in (sg.WIN_CLOSED, "Quit"): - break - - if event == "-VALIDATE-": - errs = validate_paths(values["-MODEL-"], values["-IN-"], values["-OUT-"]) - if errs: - for e in errs: - window["-LOG-"].print("❌", e) - else: - window["-LOG-"].print("✅ Paths look good. (MATLAB run will be wired in Step 2)") - - elif event == "-SAVE-": - errs = validate_paths(values["-MODEL-"], values["-IN-"], values["-OUT-"]) - if errs: - for e in errs: - window["-LOG-"].print("❌", e) - ok, msg = save_preset(values) - window["-LOG-"].print(("✅ " if ok else "❌ ") + msg) - - elif event == "-LOAD-": - ok, msg = load_preset(window) - window["-LOG-"].print(("✅ " if ok else "❌ ") + msg) - - elif event == "-RUN-": - # Disabled in Step 1. Placeholder for Step 2 wiring to MATLAB. - window["-LOG-"].print("Run clicked (no-op in Step 1).") - - window.close() - -if __name__ == "__main__": - main() diff --git a/tests/ipgGUI/Python/sim_runner_ui.py b/tests/ipgGUI/Python/sim_runner_ui.py new file mode 100644 index 00000000..1471950a --- /dev/null +++ b/tests/ipgGUI/Python/sim_runner_ui.py @@ -0,0 +1,313 @@ +# sim_runner_ui.py +# Packages: Python 3.9+, PyQt6 +from __future__ import annotations +import sys +import os +import time +import subprocess +from dataclasses import dataclass +from typing import List, Optional + +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QFileDialog, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QTextEdit, QMessageBox, QGridLayout +) + +# -------- MATLAB engine detection -------- +# Note that here for 2024a, it can support Python 3.9 up to 3.11 +# For more details, please refer to https://pypi.org/project/matlabengine/ +MATLAB_AVAILABLE = False +try: + import matlab.engine # type: ignore + MATLAB_AVAILABLE = True +except Exception: + MATLAB_AVAILABLE = False + + +# ---------------- State ---------------- +@dataclass +class UIState: + modelPath: str = "" + inputDir: str = "" # actually the SUMO .sumocfg file path + ipgDir: str = "" # IPG testrun file path + outputDir: str = "" # output folder + + +# ---------------- GUI Worker ---------------- +class RunnerWorker(QThread): + message = pyqtSignal(str) + error = pyqtSignal(str) + finished_ok = pyqtSignal() + + def __init__(self, state: UIState, parent=None): + super().__init__(parent) + self.state = state + + def log(self, msg: str): + self.message.emit(msg) + + def run(self): + try: + self.log("Starting simulation…") + run_simulink_job( + self.state.modelPath, + self.state.inputDir, # SUMO .sumocfg path + self.state.ipgDir, # IPG testrun file + self.state.outputDir, + log_cb=self.log + ) + self.log(f"✅ Done.\nResults saved to: {self.state.outputDir}") + self.finished_ok.emit() + except Exception as e: + self.error.emit(f"❌ Error:\n{e}") + + +# ---------------- GUI Main Window ---------------- +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Real-Sim GUI v0.0") + self.setMinimumSize(QSize(720, 520)) + self.state = UIState() + self.worker: Optional[RunnerWorker] = None + self._build_ui() + + def _build_ui(self): + central = QWidget(self) + self.setCentralWidget(central) + + lblModel = QLabel("Simulink model (.slx or .mdl)") + self.edModel = QLineEdit(); self.edModel.setReadOnly(True) + + lblTest = QLabel("IPG Testrun file (.txt)") + self.edTest = QLineEdit(); self.edTest.setReadOnly(True) + + lblIn = QLabel("Input SUMO configuration file (.sumocfg)") + self.edIn = QLineEdit(); self.edIn.setReadOnly(True) + + lblOut = QLabel("Output folder") + self.edOut = QLineEdit(); self.edOut.setReadOnly(True) + + btnPickModel = QPushButton("Browse simulink model"); btnPickModel.clicked.connect(self.onPickModel) + btnPickTest = QPushButton("Browse IPG Testrun"); btnPickTest.clicked.connect(self.onPickTestRun) + btnPickIn = QPushButton("Browse…"); btnPickIn.clicked.connect(self.onPickFileIn) + btnPickOut = QPushButton("Browse…"); btnPickOut.clicked.connect(self.onPickOut) + + self.btnValidate = QPushButton("Validate"); self.btnValidate.clicked.connect(self.onValidate) + self.btnRun = QPushButton("Run"); self.btnRun.clicked.connect(self.onRun) + self.btnQuit = QPushButton("Quit"); self.btnQuit.clicked.connect(self.close) + + self.txtLog = QTextEdit(); self.txtLog.setReadOnly(True) + + grid = QGridLayout() + grid.addWidget(lblModel, 0, 0); grid.addWidget(self.edModel, 1, 0, 1, 3); grid.addWidget(btnPickModel, 1, 3) + grid.addWidget(lblTest, 2, 0); grid.addWidget(self.edTest, 3, 0, 1, 3); grid.addWidget(btnPickTest, 3, 3) + grid.addWidget(lblIn, 4, 0); grid.addWidget(self.edIn, 5, 0, 1, 3); grid.addWidget(btnPickIn, 5, 3) + grid.addWidget(lblOut, 6, 0); grid.addWidget(self.edOut, 7, 0, 1, 3); grid.addWidget(btnPickOut, 7, 3) + + row = QHBoxLayout() + row.addWidget(self.btnValidate); row.addWidget(self.btnRun); row.addWidget(self.btnQuit); row.addStretch(1) + + root = QVBoxLayout(central) + root.addLayout(grid) + root.addLayout(row) + root.addWidget(self.txtLog) + + def log(self, msg: str): + self.txtLog.append(msg) + self.txtLog.moveCursor(self.txtLog.textCursor().MoveOperation.End) + + # --- Pickers --- + def onPickModel(self): + path, _ = QFileDialog.getOpenFileName(self, "Select Simulink model", "", "Simulink Models (*.slx *.mdl);;All Files (*)") + if not path: return + self.state.modelPath = path; self.edModel.setText(path); self.log(f"Selected model: {path}") + + def onPickTestRun(self): + path, _ = QFileDialog.getOpenFileName(self, "Select IPG testrun", "", "All Files (*)") + if not path: return + self.state.ipgDir = path; self.edTest.setText(path); self.log(f"Selected testrun: {path}") + + def onPickFileIn(self): + path, _ = QFileDialog.getOpenFileName(self, "Select SUMO config", "", "SUMO Config (*.sumocfg);;All Files (*)") + if not path: return + self.state.inputDir = path; self.edIn.setText(path); self.log(f"Selected input: {path}") + + def onPickOut(self): + path = QFileDialog.getExistingDirectory(self, "Select output folder", "") + if not path: return + self.state.outputDir = path; self.edOut.setText(path); self.log(f"Selected output: {path}") + + # --- Validation --- + def validate(self, s: UIState) -> List[str]: + errs: List[str] = [] + if not s.modelPath or not os.path.isfile(s.modelPath): + errs.append("Model path does not exist.") + else: + low = s.modelPath.lower() + if not (low.endswith(".slx") or low.endswith(".mdl")): + errs.append("Model must be a .slx or .mdl file.") + if not s.inputDir: + errs.append("Input folder is invalid.") + if not s.outputDir: + errs.append("Output folder is empty.") + else: + if not os.path.isdir(s.outputDir): + try: os.makedirs(s.outputDir, exist_ok=True) + except Exception as e: errs.append(f"Cannot create output folder: {e}") + return errs + + def onValidate(self): + errs = self.validate(self.state) + if not errs: self.log("✅ Paths look good.") + else: + for e in errs: self.log(f"❌ {e}") + + # --- Run --- + def set_controls_enabled(self, enabled: bool): + self.btnRun.setEnabled(enabled); self.btnValidate.setEnabled(enabled) + + def onRun(self): + errs = self.validate(self.state) + if errs: + for e in errs: self.log(f"❌ {e}") + return + self.set_controls_enabled(False) + self.worker = RunnerWorker(self.state) + self.worker.message.connect(self.log) + self.worker.error.connect(self._on_run_error) + self.worker.finished_ok.connect(self._on_run_ok) + self.worker.finished.connect(lambda: self.set_controls_enabled(True)) + self.worker.start() + + def _on_run_ok(self): + pass + + def _on_run_error(self, msg: str): + self.log(msg) + QMessageBox.critical(self, "Run Error", msg) + + +# ---------------- MATLAB-backed runner (port of your MATLAB function) ---------------- +def run_simulink_job(model_path: str, input_dir: str, ipg_dir: str, output_dir: str, log_cb=None): + """ + Python port of your MATLAB 'run_simulink_job' for Windows. + - Launches SUMO GUI and TrafficLayer.exe + - Opens Simulink model, starts TM_Simulink, loads IPG testrun (cmguicmd) + - Runs sim(model_path), assigns base vars, saves simout.mat + - Attempts cleanup similar to your taskkill-based approach + + log_cb: optional callable(str) for UI logging + """ + def log(msg: str): + if log_cb: log_cb(msg) + else: print(msg) + + if os.name != "nt": + raise RuntimeError("This runner currently targets Windows (cmd/start/taskkill).") + + if not MATLAB_AVAILABLE: + raise RuntimeError("MATLAB Engine for Python not found. Please install and ensure MATLAB is on this machine.") + + # Ensure output dir exists + os.makedirs(output_dir, exist_ok=True) + + # --- Launch SUMO & Traffic Layer (as in your MATLAB code) --- + ######### The following dir can be replace with any test directory ######### + i81_tests_dir = r"C:\CM_Projects\CVXTEST\tests\UAtest" + if not os.path.isdir(i81_tests_dir): + log(f"⚠️ Warning: Directory not found: {i81_tests_dir} (continuing anyway)") + + # SUMO GUI command: start "" sumo-gui -c "" ... --start + sumo_cmd = [ + "cmd", "/c", "start", "", "sumo-gui", + "-c", input_dir, + "--remote-port", "1337", + #"--time-to-teleport", "-1", + #"--time-to-teleport.remove", "false", + #"--max-depart-delay", "-1", + "--step-length", "0.1", + "--start" + ] + log("Launching SUMO GUI…") + try: + subprocess.Popen(sumo_cmd, cwd=i81_tests_dir) + except Exception as e: + log(f"⚠️ Could not start SUMO GUI: {e}") + + # TrafficLayer.exe + exe_path = os.path.normpath(os.path.join(i81_tests_dir, "..", "..", "TrafficLayer.exe")) + config_filename = r"C:\CM_Projects\CVXTEST\tests\UAtest\RS_config_release.yaml" + tl_cmd = ["cmd", "/c", "start", "", exe_path, "-f", config_filename] + log("Launching TrafficLayer.exe…") + try: + subprocess.Popen(tl_cmd, cwd=i81_tests_dir) + except Exception as e: + log(f"⚠️ Could not start TrafficLayer.exe: {e}") + + time.sleep(3) # let them come up + + # --- MATLAB Engine sequence --- + sim_src_dir = r"C:\CM_Projects\CVXTEST\CM13_proj\src_cm4sl" + if not os.path.isdir(sim_src_dir): + log(f"⚠️ Warning: Directory not found: {sim_src_dir} (continuing anyway)") + + log("Starting MATLAB Engine…") + eng = matlab.engine.start_matlab("-desktop") + #eng = matlab.engine.start_matlab() + eng.open_system(model_path, nargout=0) + eng.CM_Simulink(nargout=0) + print(f"LoadTestRun , {ipg_dir}") + eng.cmguicmd(f"LoadTestRun {ipg_dir}", nargout=0) + + try: + # Run simulation + log("Running Simulink simulation…") + simOut = eng.sim(model_path, nargout=1) + + log("Simulation finished successfully.") + + # Provide variables to base workspace + eng.assignin('base', 'INPUT_DIR', input_dir, nargout=0) + eng.assignin('base', 'OUTPUT_DIR', output_dir, nargout=0) + eng.assignin('base', 'SIM_OUT', simOut, nargout=0) + + # Save simOut.mat + out_mat = os.path.join(output_dir, "simout.mat") + # put simOut into MATLAB workspace, then save from MATLAB side + eng.workspace['simOut'] = simOut + log(f'Saving "{out_mat}" …') + eng.save(out_mat, 'simOut', nargout=0) + + except matlab.engine.MatlabExecutionError as mee: + raise RuntimeError(f"MATLAB error: {mee}") + finally: + try: + eng.quit() + #print("Simulation completes!") + except Exception: + pass + + # --- Cleanup (mirrors your taskkill approach; ⚠️ kills all matching processes) --- + # If you'd prefer to only kill what *we* started, I can rework this to track PIDs. + log("Cleaning up helper terminals (cmd.exe / WindowsTerminal.exe)…") + try: + subprocess.run(["taskkill", "/F", "/IM", "cmd.exe"], capture_output=True, text=True) + except Exception as e: + log(f"⚠️ taskkill cmd.exe failed: {e}") + try: + subprocess.run(["taskkill", "/F", "/IM", "WindowsTerminal.exe"], capture_output=True, text=True) + except Exception as e: + log(f"⚠️ taskkill WindowsTerminal.exe failed: {e}") + + +# ---------------- Entrypoint ---------------- +def main(): + app = QApplication(sys.argv) + win = MainWindow() + win.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() From e87948b4daf760894dc4687c27ee57bd2675033b Mon Sep 17 00:00:00 2001 From: az Date: Sun, 2 Nov 2025 18:29:38 -0500 Subject: [PATCH 2/6] Add automated path finding for py GUI --- tests/ipgGUI/Python/sim_runner_ui.py | 38 +++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/ipgGUI/Python/sim_runner_ui.py b/tests/ipgGUI/Python/sim_runner_ui.py index 1471950a..79fd4624 100644 --- a/tests/ipgGUI/Python/sim_runner_ui.py +++ b/tests/ipgGUI/Python/sim_runner_ui.py @@ -214,56 +214,58 @@ def log(msg: str): os.makedirs(output_dir, exist_ok=True) # --- Launch SUMO & Traffic Layer (as in your MATLAB code) --- - ######### The following dir can be replace with any test directory ######### - i81_tests_dir = r"C:\CM_Projects\CVXTEST\tests\UAtest" - if not os.path.isdir(i81_tests_dir): - log(f"⚠️ Warning: Directory not found: {i81_tests_dir} (continuing anyway)") + # Go to the SUMO file directory + test_dir = os.path.dirname(input_dir) + if not os.path.isdir(test_dir): + log(f"⚠️ Warning: Directory not found: {test_dir} (continuing anyway)") # SUMO GUI command: start "" sumo-gui -c "" ... --start sumo_cmd = [ "cmd", "/c", "start", "", "sumo-gui", "-c", input_dir, "--remote-port", "1337", - #"--time-to-teleport", "-1", - #"--time-to-teleport.remove", "false", - #"--max-depart-delay", "-1", + "--time-to-teleport", "-1", + "--time-to-teleport.remove", "false", + "--max-depart-delay", "-1", "--step-length", "0.1", "--start" ] - log("Launching SUMO GUI…") + log("⚙️ Launching SUMO GUI…") try: - subprocess.Popen(sumo_cmd, cwd=i81_tests_dir) + subprocess.Popen(sumo_cmd, cwd=test_dir) except Exception as e: log(f"⚠️ Could not start SUMO GUI: {e}") - # TrafficLayer.exe - exe_path = os.path.normpath(os.path.join(i81_tests_dir, "..", "..", "TrafficLayer.exe")) - config_filename = r"C:\CM_Projects\CVXTEST\tests\UAtest\RS_config_release.yaml" + # Go to get TrafficLayer.exe and the relevant config.yaml file + exe_path = os.path.normpath(os.path.join(test_dir, "..", "..", "TrafficLayer.exe")) + config_filename = os.path.join(test_dir, "RS_config_release.yaml") + tl_cmd = ["cmd", "/c", "start", "", exe_path, "-f", config_filename] - log("Launching TrafficLayer.exe…") + log("⚙️ Launching TrafficLayer.exe…") try: - subprocess.Popen(tl_cmd, cwd=i81_tests_dir) + subprocess.Popen(tl_cmd, cwd=test_dir) except Exception as e: log(f"⚠️ Could not start TrafficLayer.exe: {e}") time.sleep(3) # let them come up # --- MATLAB Engine sequence --- - sim_src_dir = r"C:\CM_Projects\CVXTEST\CM13_proj\src_cm4sl" + # Go to the Simulink model directory + sim_src_dir = os.path.dirname(model_path) if not os.path.isdir(sim_src_dir): log(f"⚠️ Warning: Directory not found: {sim_src_dir} (continuing anyway)") - log("Starting MATLAB Engine…") + log("⚒️ Starting MATLAB Engine…") eng = matlab.engine.start_matlab("-desktop") #eng = matlab.engine.start_matlab() eng.open_system(model_path, nargout=0) eng.CM_Simulink(nargout=0) - print(f"LoadTestRun , {ipg_dir}") + log(f"LoadTestRun , {ipg_dir}") eng.cmguicmd(f"LoadTestRun {ipg_dir}", nargout=0) try: # Run simulation - log("Running Simulink simulation…") + log("⚙️ Running Simulink simulation…") simOut = eng.sim(model_path, nargout=1) log("Simulation finished successfully.") From 74eb4b15eb30b667c34a3162204b6ca60da8f4db Mon Sep 17 00:00:00 2001 From: az Date: Sun, 2 Nov 2025 23:33:27 -0500 Subject: [PATCH 3/6] GUI for CM-Simulink-Sumo and CM-Sumo --- tests/ipgGUI/Python/GUI_CM-Sumo.py | 275 ++++++++++++++++++ ...m_runner_ui.py => GUI_Simulink-CM-Sumo.py} | 0 2 files changed, 275 insertions(+) create mode 100644 tests/ipgGUI/Python/GUI_CM-Sumo.py rename tests/ipgGUI/Python/{sim_runner_ui.py => GUI_Simulink-CM-Sumo.py} (100%) diff --git a/tests/ipgGUI/Python/GUI_CM-Sumo.py b/tests/ipgGUI/Python/GUI_CM-Sumo.py new file mode 100644 index 00000000..7e4929b1 --- /dev/null +++ b/tests/ipgGUI/Python/GUI_CM-Sumo.py @@ -0,0 +1,275 @@ +# Packages: Python 3.9+, PyQt6 +from __future__ import annotations +import sys +import os +import time +import subprocess +from dataclasses import dataclass +from typing import List, Optional + +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QFileDialog, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QTextEdit, QMessageBox, QGridLayout +) + +# ---------------- State ---------------- +@dataclass +class UIState: + modelPath: str = "" # TrafficLayer.exe file path + inputDir: str = "" # actually the SUMO .sumocfg file path + ipgDir: str = "" # IPG testrun file path + outputDir: str = "" # output folder + + +# ---------------- GUI Worker ---------------- +class RunnerWorker(QThread): + message = pyqtSignal(str) + error = pyqtSignal(str) + finished_ok = pyqtSignal() + + def __init__(self, state: UIState, parent=None): + super().__init__(parent) + self.state = state + + def log(self, msg: str): + self.message.emit(msg) + + def run(self): + try: + self.log("Starting simulation…") + run_cm_sumo_job( + self.state.modelPath, + self.state.inputDir, # SUMO .sumocfg path + self.state.ipgDir, # IPG testrun file + self.state.outputDir, + log_cb=self.log + ) + self.log(f"✅ Done.\nResults saved to: {self.state.outputDir}") + self.finished_ok.emit() + except Exception as e: + self.error.emit(f"❌ Error:\n{e}") + + +# ---------------- GUI Main Window ---------------- +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Real-Sim GUI v0.0") + self.setMinimumSize(QSize(720, 520)) + self.state = UIState() + self.worker: Optional[RunnerWorker] = None + self._build_ui() + + def _build_ui(self): + central = QWidget(self) + self.setCentralWidget(central) + + lblModel = QLabel("Real-Sim TrafficLayer (.exe)") + self.edModel = QLineEdit(); self.edModel.setReadOnly(True) + + lblTest = QLabel("IPG Testrun file (.txt)") + self.edTest = QLineEdit(); self.edTest.setReadOnly(True) + + lblIn = QLabel("Input SUMO configuration file (.sumocfg)") + self.edIn = QLineEdit(); self.edIn.setReadOnly(True) + + lblOut = QLabel("Output folder") + self.edOut = QLineEdit(); self.edOut.setReadOnly(True) + + btnPickModel = QPushButton("Browse TrafficLayer"); btnPickModel.clicked.connect(self.onPickModel) + btnPickTest = QPushButton("Browse IPG Testrun"); btnPickTest.clicked.connect(self.onPickTestRun) + btnPickIn = QPushButton("Browse…"); btnPickIn.clicked.connect(self.onPickFileIn) + btnPickOut = QPushButton("Browse…"); btnPickOut.clicked.connect(self.onPickOut) + + self.btnValidate = QPushButton("Validate"); self.btnValidate.clicked.connect(self.onValidate) + self.btnRun = QPushButton("Run"); self.btnRun.clicked.connect(self.onRun) + self.btnQuit = QPushButton("Quit"); self.btnQuit.clicked.connect(self.close) + + self.txtLog = QTextEdit(); self.txtLog.setReadOnly(True) + + grid = QGridLayout() + grid.addWidget(lblModel, 0, 0); grid.addWidget(self.edModel, 1, 0, 1, 3); grid.addWidget(btnPickModel, 1, 3) + grid.addWidget(lblTest, 2, 0); grid.addWidget(self.edTest, 3, 0, 1, 3); grid.addWidget(btnPickTest, 3, 3) + grid.addWidget(lblIn, 4, 0); grid.addWidget(self.edIn, 5, 0, 1, 3); grid.addWidget(btnPickIn, 5, 3) + grid.addWidget(lblOut, 6, 0); grid.addWidget(self.edOut, 7, 0, 1, 3); grid.addWidget(btnPickOut, 7, 3) + + row = QHBoxLayout() + row.addWidget(self.btnValidate); row.addWidget(self.btnRun); row.addWidget(self.btnQuit); row.addStretch(1) + + root = QVBoxLayout(central) + root.addLayout(grid) + root.addLayout(row) + root.addWidget(self.txtLog) + + def log(self, msg: str): + self.txtLog.append(msg) + self.txtLog.moveCursor(self.txtLog.textCursor().MoveOperation.End) + + # --- Pickers --- + def onPickModel(self): + path, _ = QFileDialog.getOpenFileName(self, "Select TrafficLayer", "", "TrafficLayer file(*.exe);; All Files (*)") + if not path: return + self.state.modelPath = path; self.edModel.setText(path); self.log(f"Selected model: {path}") + + def onPickTestRun(self): + path, _ = QFileDialog.getOpenFileName(self, "Select IPG testrun", "", "All Files (*)") + if not path: return + self.state.ipgDir = path; self.edTest.setText(path); self.log(f"Selected testrun: {path}") + + def onPickFileIn(self): + path, _ = QFileDialog.getOpenFileName(self, "Select SUMO config", "", "SUMO Config (*.sumocfg);;All Files (*)") + if not path: return + self.state.inputDir = path; self.edIn.setText(path); self.log(f"Selected input: {path}") + + def onPickOut(self): + path = QFileDialog.getExistingDirectory(self, "Select output folder", "") + if not path: return + self.state.outputDir = path; self.edOut.setText(path); self.log(f"Selected output: {path}") + + # --- Validation --- + def validate(self, s: UIState) -> List[str]: + errs: List[str] = [] + if not s.modelPath or not os.path.isfile(s.modelPath): + errs.append("Model path does not exist.") + else: + low = s.modelPath.lower() + if not (low.endswith(".exe")): + errs.append("Model must be a .exe file.") + if not s.inputDir: + errs.append("Input folder is invalid.") + if not s.outputDir: + errs.append("Output folder is empty.") + else: + if not os.path.isdir(s.outputDir): + try: os.makedirs(s.outputDir, exist_ok=True) + except Exception as e: errs.append(f"Cannot create output folder: {e}") + return errs + + def onValidate(self): + errs = self.validate(self.state) + if not errs: self.log("✅ Paths look good.") + else: + for e in errs: self.log(f"❌ {e}") + + # --- Run --- + def set_controls_enabled(self, enabled: bool): + self.btnRun.setEnabled(enabled); self.btnValidate.setEnabled(enabled) + + def onRun(self): + errs = self.validate(self.state) + if errs: + for e in errs: self.log(f"❌ {e}") + return + self.set_controls_enabled(False) + self.worker = RunnerWorker(self.state) + self.worker.message.connect(self.log) + self.worker.error.connect(self._on_run_error) + self.worker.finished_ok.connect(self._on_run_ok) + self.worker.finished.connect(lambda: self.set_controls_enabled(True)) + self.worker.start() + + def _on_run_ok(self): + pass + + def _on_run_error(self, msg: str): + self.log(msg) + QMessageBox.critical(self, "Run Error", msg) + + +# ---------------- MATLAB-backed runner (port of your MATLAB function) ---------------- +def run_cm_sumo_job(model_path: str, input_dir: str, ipg_dir: str, output_dir: str, log_cb=None): + """ + Python port to run Real-Sim Carmaker-Sumo co-sim on Windows. + - Launches SUMO GUI and TrafficLayer.exe + - Opens CarMaker, loads IPG testrun (cmguicmd) + - Runs co-sim and save results + - Attempts cleanup similar to your taskkill-based approach + + log_cb: optional callable(str) for UI logging + """ + def log(msg: str): + if log_cb: log_cb(msg) + else: print(msg) + + if os.name != "nt": + raise RuntimeError("This runner currently targets Windows (cmd/start/taskkill).") + + # Ensure output dir exists + os.makedirs(output_dir, exist_ok=True) + + # --- Launch SUMO & Traffic Layer (as in your MATLAB code) --- + # Go to the SUMO file directory + test_dir = os.path.dirname(input_dir) + if not os.path.isdir(test_dir): + log(f"⚠️ Warning: Directory not found: {test_dir} (continuing anyway)") + + # SUMO GUI command: start "" sumo-gui -c "" ... --start + sumo_cmd = [ + "cmd", "/c", "start", "", "sumo-gui", + "-c", input_dir, + "--remote-port", "1337", + "--time-to-teleport", "-1", + "--time-to-teleport.remove", "false", + "--max-depart-delay", "-1", + "--step-length", "0.1", + "--start" + ] + log("⚙️ Launching SUMO GUI…") + try: + subprocess.Popen(sumo_cmd, cwd=test_dir) + except Exception as e: + log(f"⚠️ Could not start SUMO GUI: {e}") + + # Go to get TrafficLayer.exe and the relevant config.yaml file + config_filename = os.path.join(test_dir, "RS_config_release.yaml") + + tl_cmd = ["cmd", "/c", "start", "", model_path, "-f", config_filename] + log("⚙️ Launching TrafficLayer.exe…") + try: + subprocess.Popen(tl_cmd, cwd=test_dir) + except Exception as e: + log(f"⚠️ Could not start TrafficLayer.exe: {e}") + + time.sleep(5) # let them come up + + # --- IPG CarMaker starting sequence --- + # Check CarMaker root directory, load testrun, then start the simulation + cm_root = r"C:\IPG\carmaker\win64-13.1.2\bin\CM_Office.exe" + cm_cmd = [cm_root, "-cmd", f"LoadTestRun {ipg_dir}; StartSim"] + try: + log("⚒️ Starting CarMaker…") + log(f"LoadTestRun , {ipg_dir}") + log("⚙️ Running Carmaker simulation…") + subprocess.Popen(cm_cmd, cwd=test_dir) + log("Simulation finished successfully.") + except Exception as e: + log(f"⚠️ Could not start CarMaker: {e}") + log(f"⚠️ Check directory: {cm_root} and {ipg_dir}") + + ### TODO ### + # Extract information as output json file in the output directory + out_json = os.path.join(output_dir, "output.json") + + # --- Cleanup (mirrors your taskkill approach; ⚠️ kills all matching processes) --- + # If you'd prefer to only kill what *we* started, I can rework this to track PIDs. + log("Cleaning up helper terminals (cmd.exe / WindowsTerminal.exe)…") + try: + subprocess.run(["taskkill", "/F", "/IM", "cmd.exe"], capture_output=True, text=True) + except Exception as e: + log(f"⚠️ taskkill cmd.exe failed: {e}") + try: + subprocess.run(["taskkill", "/F", "/IM", "WindowsTerminal.exe"], capture_output=True, text=True) + except Exception as e: + log(f"⚠️ taskkill WindowsTerminal.exe failed: {e}") + + +# ---------------- Entrypoint ---------------- +def main(): + app = QApplication(sys.argv) + win = MainWindow() + win.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/tests/ipgGUI/Python/sim_runner_ui.py b/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py similarity index 100% rename from tests/ipgGUI/Python/sim_runner_ui.py rename to tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py From 59ba30b4159d620189812c8e3a1f2319bc474a59 Mon Sep 17 00:00:00 2001 From: az Date: Mon, 3 Nov 2025 13:32:05 -0500 Subject: [PATCH 4/6] Include a looping feature to run multiple testruns and sumocfgs --- tests/ipgGUI/Python/GUI_CM-Sumo.py | 6 +- tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py | 7 +- .../Python/GUI_Simulink-CM-Sumo_loop.py | 323 ++++++++++++++++++ tests/ipgGUI/Python/guiUtils.py | 90 +++++ 4 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 tests/ipgGUI/Python/GUI_Simulink-CM-Sumo_loop.py create mode 100644 tests/ipgGUI/Python/guiUtils.py diff --git a/tests/ipgGUI/Python/GUI_CM-Sumo.py b/tests/ipgGUI/Python/GUI_CM-Sumo.py index 7e4929b1..af1bbbe5 100644 --- a/tests/ipgGUI/Python/GUI_CM-Sumo.py +++ b/tests/ipgGUI/Python/GUI_CM-Sumo.py @@ -1,3 +1,7 @@ +################################################################################ +### ---------- A GUI to run Real-Sim CarMaker-Sumo co-simulation ----------- ### +################################################################################ + # Packages: Python 3.9+, PyQt6 from __future__ import annotations import sys @@ -177,7 +181,7 @@ def _on_run_error(self, msg: str): QMessageBox.critical(self, "Run Error", msg) -# ---------------- MATLAB-backed runner (port of your MATLAB function) ---------------- +# ---------------- cosim-backed runner ---------------- def run_cm_sumo_job(model_path: str, input_dir: str, ipg_dir: str, output_dir: str, log_cb=None): """ Python port to run Real-Sim Carmaker-Sumo co-sim on Windows. diff --git a/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py b/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py index 79fd4624..ba7ca666 100644 --- a/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py +++ b/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo.py @@ -1,4 +1,7 @@ -# sim_runner_ui.py +################################################################################ +# -------- A GUI to run Real-Sim CarMaker-Simulink-Sumo co-simulation -------- # +################################################################################ + # Packages: Python 3.9+, PyQt6 from __future__ import annotations import sys @@ -213,7 +216,7 @@ def log(msg: str): # Ensure output dir exists os.makedirs(output_dir, exist_ok=True) - # --- Launch SUMO & Traffic Layer (as in your MATLAB code) --- + # --- Launch SUMO & Traffic Layer --- # Go to the SUMO file directory test_dir = os.path.dirname(input_dir) if not os.path.isdir(test_dir): diff --git a/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo_loop.py b/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo_loop.py new file mode 100644 index 00000000..47495c71 --- /dev/null +++ b/tests/ipgGUI/Python/GUI_Simulink-CM-Sumo_loop.py @@ -0,0 +1,323 @@ +################################################################################ +# -------- A GUI to run Real-Sim CarMaker-Simulink-Sumo co-simulation -------- # +# ---------- looping through a series of IPG testruns and sumo files --------- # +################################################################################ + +# Packages: Python 3.9+, PyQt6 +from __future__ import annotations +import sys +import os +import time +import subprocess +from dataclasses import dataclass +from typing import List, Optional + +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QFileDialog, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QTextEdit, QMessageBox, QGridLayout +) + +from guiUtils import enumerate_sumocfg, enumerate_ipgtestruns + +# -------- MATLAB engine detection -------- +# Note that here for 2024a, it can support Python 3.9 up to 3.11 +# For more details, please refer to https://pypi.org/project/matlabengine/ +MATLAB_AVAILABLE = False +try: + import matlab.engine # type: ignore + MATLAB_AVAILABLE = True +except Exception: + MATLAB_AVAILABLE = False + + +# ---------------- State ---------------- +@dataclass +class UIState: + modelPath: str = "" # Simulink model path + inputDir: str = "" # actually the SUMO .sumocfg file path + ipgDir: str = "" # IPG testrun file path + outputDir: str = "" # output folder + + +# ---------------- GUI Worker ---------------- +class RunnerWorker(QThread): + message = pyqtSignal(str) + error = pyqtSignal(str) + finished_ok = pyqtSignal() + + def __init__(self, state: UIState, parent=None): + super().__init__(parent) + self.state = state + + def log(self, msg: str): + self.message.emit(msg) + + def run(self): + try: + self.log("Starting simulation…") + run_simulink_job( + self.state.modelPath, + self.state.inputDir, # SUMO .sumocfg path + self.state.ipgDir, # IPG testrun file + self.state.outputDir, + log_cb=self.log + ) + self.log(f"✅ Done.\nResults saved to: {self.state.outputDir}") + self.finished_ok.emit() + except Exception as e: + self.error.emit(f"❌ Error:\n{e}") + + +# ---------------- GUI Main Window ---------------- +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Real-Sim GUI v0.0") + self.setMinimumSize(QSize(720, 520)) + self.state = UIState() + self.worker: Optional[RunnerWorker] = None + self._build_ui() + + def _build_ui(self): + central = QWidget(self) + self.setCentralWidget(central) + + lblModel = QLabel("Simulink model (.slx or .mdl)") + self.edModel = QLineEdit(); self.edModel.setReadOnly(True) + + lblTest = QLabel("Directory of IPG Testrun files") + self.edTest = QLineEdit(); self.edTest.setReadOnly(True) + + lblIn = QLabel("Directory of input SUMO configuration files") + self.edIn = QLineEdit(); self.edIn.setReadOnly(True) + + lblOut = QLabel("Output folder") + self.edOut = QLineEdit(); self.edOut.setReadOnly(True) + + btnPickModel = QPushButton("Browse Simulink model"); btnPickModel.clicked.connect(self.onPickModel) + btnPickTest = QPushButton("Browse IPG Testrun folder"); btnPickTest.clicked.connect(self.onPickTestRun) + btnPickIn = QPushButton("Browse SUMO Configuration folder"); btnPickIn.clicked.connect(self.onPickFileIn) + btnPickOut = QPushButton("Browse output folder"); btnPickOut.clicked.connect(self.onPickOut) + + self.btnValidate = QPushButton("Validate"); self.btnValidate.clicked.connect(self.onValidate) + self.btnRun = QPushButton("Run"); self.btnRun.clicked.connect(self.onRun) + self.btnQuit = QPushButton("Quit"); self.btnQuit.clicked.connect(self.close) + + self.txtLog = QTextEdit(); self.txtLog.setReadOnly(True) + + grid = QGridLayout() + grid.addWidget(lblModel, 0, 0); grid.addWidget(self.edModel, 1, 0, 1, 3); grid.addWidget(btnPickModel, 1, 3) + grid.addWidget(lblTest, 2, 0); grid.addWidget(self.edTest, 3, 0, 1, 3); grid.addWidget(btnPickTest, 3, 3) + grid.addWidget(lblIn, 4, 0); grid.addWidget(self.edIn, 5, 0, 1, 3); grid.addWidget(btnPickIn, 5, 3) + grid.addWidget(lblOut, 6, 0); grid.addWidget(self.edOut, 7, 0, 1, 3); grid.addWidget(btnPickOut, 7, 3) + + row = QHBoxLayout() + row.addWidget(self.btnValidate); row.addWidget(self.btnRun); row.addWidget(self.btnQuit); row.addStretch(1) + + root = QVBoxLayout(central) + root.addLayout(grid) + root.addLayout(row) + root.addWidget(self.txtLog) + + def log(self, msg: str): + self.txtLog.append(msg) + self.txtLog.moveCursor(self.txtLog.textCursor().MoveOperation.End) + + # --- Pickers --- + def onPickModel(self): + path = QFileDialog.getExistingDirectory(self, "Select Simulink model folder", "") + if not path: return + self.state.modelPath = path; self.edModel.setText(path); self.log(f"Selected model: {path}") + + def onPickTestRun(self): + path = QFileDialog.getExistingDirectory(self, "Select IPG testrun model folder", "") + if not path: return + self.state.ipgDir = path; self.edTest.setText(path); self.log(f"Selected testrun: {path}") + + def onPickFileIn(self): + path = QFileDialog.getExistingDirectory(self, "Select SUMO configuration folder", "") + if not path: return + self.state.inputDir = path; self.edIn.setText(path); self.log(f"Selected input: {path}") + + def onPickOut(self): + path = QFileDialog.getExistingDirectory(self, "Select output folder", "") + if not path: return + self.state.outputDir = path; self.edOut.setText(path); self.log(f"Selected output: {path}") + + # --- Validation --- + def validate(self, s: UIState) -> List[str]: + errs: List[str] = [] + if not s.modelPath or not os.path.isfile(s.modelPath): + errs.append("Model path does not exist.") + else: + low = s.modelPath.lower() + if not (low.endswith(".slx") or low.endswith(".mdl")): + errs.append("Model must be a .slx or .mdl file.") + if not s.inputDir: + errs.append("Input folder is invalid.") + if not s.outputDir: + errs.append("Output folder is empty.") + else: + if not os.path.isdir(s.outputDir): + try: os.makedirs(s.outputDir, exist_ok=True) + except Exception as e: errs.append(f"Cannot create output folder: {e}") + return errs + + def onValidate(self): + errs = self.validate(self.state) + if not errs: self.log("✅ Paths look good.") + else: + for e in errs: self.log(f"❌ {e}") + + # --- Run --- + def set_controls_enabled(self, enabled: bool): + self.btnRun.setEnabled(enabled); self.btnValidate.setEnabled(enabled) + + def onRun(self): + errs = self.validate(self.state) + if errs: + for e in errs: self.log(f"❌ {e}") + return + self.set_controls_enabled(False) + self.worker = RunnerWorker(self.state) + self.worker.message.connect(self.log) + self.worker.error.connect(self._on_run_error) + self.worker.finished_ok.connect(self._on_run_ok) + self.worker.finished.connect(lambda: self.set_controls_enabled(True)) + self.worker.start() + + def _on_run_ok(self): + pass + + def _on_run_error(self, msg: str): + self.log(msg) + QMessageBox.critical(self, "Run Error", msg) + + +# ---------------- MATLAB-backed runner (port of your MATLAB function) ---------------- +def run_simulink_job(model_path: str, input_dir: str, ipg_dir: str, output_dir: str, log_cb=None): + """ + Python port of your MATLAB 'run_simulink_job' for Windows. + - Launches SUMO GUI and TrafficLayer.exe + - Opens Simulink model, starts TM_Simulink, loads IPG testrun (cmguicmd) + - Runs sim(model_path), assigns base vars, saves simout.mat + - Attempts cleanup similar to your taskkill-based approach + + log_cb: optional callable(str) for UI logging + """ + def log(msg: str): + if log_cb: log_cb(msg) + else: print(msg) + + if os.name != "nt": + raise RuntimeError("This runner currently targets Windows (cmd/start/taskkill).") + + if not MATLAB_AVAILABLE: + raise RuntimeError("MATLAB Engine for Python not found. Please install and ensure MATLAB is on this machine.") + + # Ensure output dir exists + os.makedirs(output_dir, exist_ok=True) + + # --- MATLAB Engine sequence --- + # Go to the Simulink model directory + sim_src_dir = os.path.dirname(model_path) + if not os.path.isdir(sim_src_dir): + log(f"⚠️ Warning: Directory not found: {sim_src_dir} (continuing anyway)") + log("⚒️ Starting MATLAB Engine…") + eng = matlab.engine.start_matlab("-desktop") + #eng = matlab.engine.start_matlab() + + eng.open_system(model_path, nargout=0) + eng.CM_Simulink(nargout=0) + + # --- Launch SUMO & Traffic Layer --- + # Go to the SUMO file directory + if not os.path.isdir(input_dir): + log(f"⚠️ Warning: Directory not found: {input_dir} (continuing anyway)") + + sumo_files = enumerate_sumocfg(input_dir) + test_dir = os.path.dirname(input_dir) + + for sumo_run in sumo_files: + # SUMO GUI command: start "" sumo-gui -c "" ... --start + sumo_cmd = [ + "cmd", "/c", "start", "", "sumo-gui", + "-c", sumo_run, + "--remote-port", "1337", + "--time-to-teleport", "-1", + "--time-to-teleport.remove", "false", + "--max-depart-delay", "-1", + "--step-length", "0.1", + "--start" + ] + log("⚙️ Launching SUMO GUI…") + try: + subprocess.Popen(sumo_cmd, cwd=test_dir) + except Exception as e: + log(f"⚠️ Could not start SUMO GUI: {e}") + + # TrafficLayer.exe + # Go to get TrafficLayer.exe and the relevant config.yaml file + exe_path = os.path.normpath(os.path.join(test_dir, "..", "..", "TrafficLayer.exe")) + config_filename = os.path.join(test_dir, "RS_config_release.yaml") + + tl_cmd = ["cmd", "/c", "start", "", exe_path, "-f", config_filename] + log("⚙️ Launching TrafficLayer.exe…") + try: + subprocess.Popen(tl_cmd, cwd=test_dir) + except Exception as e: + log(f"⚠️ Could not start TrafficLayer.exe: {e}") + + time.sleep(3) # let them come up + + testrun_names = enumerate_ipgtestruns(ipg_dir) + for testrun in testrun_names: + log(f"LoadTestRun , {testrun}") + eng.cmguicmd(f"LoadTestRun {testrun}", nargout=0) + + try: + # Run simulation + log("⚙️ Running Simulink simulation…") + simOut = eng.sim(model_path, nargout=1) + + log("Simulation finished successfully.") + + # Provide variables to base workspace + eng.assignin('base', 'INPUT_DIR', input_dir, nargout=0) + eng.assignin('base', 'OUTPUT_DIR', output_dir, nargout=0) + eng.assignin('base', 'SIM_OUT', simOut, nargout=0) + + # Save simOut.mat + file_name = f"simout_{sumo_run}_{testrun}.mat" + out_mat = os.path.join(output_dir, file_name) + # put simOut into MATLAB workspace, then save from MATLAB side + eng.workspace['simOut'] = simOut + log(f'Saving "{out_mat}" …') + eng.save(out_mat, 'simOut', nargout=0) + + except matlab.engine.MatlabExecutionError as mee: + raise RuntimeError(f"MATLAB error: {mee}") + + # --- Cleanup (mirrors your taskkill approach; ⚠️ kills all matching processes) --- + # If you'd prefer to only kill what *we* started, I can rework this to track PIDs. + log("Cleaning up helper terminals (cmd.exe / WindowsTerminal.exe)…") + try: + subprocess.run(["taskkill", "/F", "/IM", "cmd.exe"], capture_output=True, text=True) + except Exception as e: + log(f"⚠️ taskkill cmd.exe failed: {e}") + try: + subprocess.run(["taskkill", "/F", "/IM", "WindowsTerminal.exe"], capture_output=True, text=True) + except Exception as e: + log(f"⚠️ taskkill WindowsTerminal.exe failed: {e}") + + +# ---------------- Entrypoint ---------------- +def main(): + app = QApplication(sys.argv) + win = MainWindow() + win.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/tests/ipgGUI/Python/guiUtils.py b/tests/ipgGUI/Python/guiUtils.py new file mode 100644 index 00000000..a9fc0426 --- /dev/null +++ b/tests/ipgGUI/Python/guiUtils.py @@ -0,0 +1,90 @@ +## Helper functions for GUI +from pathlib import Path +def enumerate_sumocfg(input_dir: str, recursive: bool = False) -> list[str]: + """ + Return a sorted list of absolute paths to .sumocfg files found under `input_dir`. + + Behavior: + - If `input_dir` points to an existing file and has a .sumocfg suffix, return that file. + - If `input_dir` points to a directory, list files matching `*.sumocfg`. + If `recursive` is True, search subdirectories as well. + - Raises FileNotFoundError if the path does not exist. + """ + p = Path(input_dir) + + # If a file was passed in directly + if p.exists() and p.is_file(): + if p.suffix.lower() == ".sumocfg": + return [str(p.resolve())] + # it's a file but not .sumocfg -> treat as parent dir below + + # Must be a directory for listing + if not p.exists(): + raise FileNotFoundError(f"Input path does not exist: {input_dir}") + + if not p.is_dir(): + # if it wasn't a .sumocfg file above and is not a directory, return empty + return [] + + if recursive: + candidates = p.rglob("*.sumocfg") + else: + candidates = p.glob("*.sumocfg") + + files = [str(f.resolve()) for f in candidates if f.is_file() and f.suffix.lower() == ".sumocfg"] + files.sort() + return files + +def enumerate_ipgtestruns(ipg_dir: str, recursive: bool = False) -> list[str]: + """ + Enumerate candidate IPG testruns in `ipg_dir`. + + Returns a sorted list of absolute paths. The function is forgiving and will + return either files or directories that look like testruns. + + Heuristics used: + - If `ipg_dir` is an existing file, return that file. + - If `ipg_dir` is a directory, return (sorted): + - any subdirectory (first-class candidate), and + - any file matching common testrun filename extensions (case-insensitive): + .txt, .testrun, .tr + Use `recursive=True` to search subdirectories as well. + + Raises FileNotFoundError if the given path doesn't exist. + """ + p = Path(ipg_dir) + + # If a file was provided directly + if p.exists() and p.is_file(): + return [str(p.resolve())] + + if not p.exists(): + raise FileNotFoundError(f"IPG testrun path does not exist: {ipg_dir}") + + if not p.is_dir(): + return [] + + # Collect directories first (these often represent named testruns) + results: list[str] = [] + if recursive: + dir_iter = (d for d in p.rglob("*") if d.is_dir()) + else: + dir_iter = (d for d in p.iterdir() if d.is_dir()) + + for d in dir_iter: + results.append(str(d.resolve())) + + # Common file extensions used for testrun definitions + exts = (".txt", ".testrun", ".tr") + if recursive: + file_iter = p.rglob("*") + else: + file_iter = p.iterdir() + + for f in file_iter: + if f.is_file() and f.suffix.lower() in exts: + results.append(str(f.resolve())) + + # Deduplicate and sort for deterministic order + unique = sorted(dict.fromkeys(results)) + return unique \ No newline at end of file From 0de56768b13bdd01407da253e360820df2a4c844 Mon Sep 17 00:00:00 2001 From: az Date: Tue, 4 Nov 2025 11:34:43 -0500 Subject: [PATCH 5/6] Add a simple readme file and environment yml --- tests/ipgGUI/README.md | 33 +++++++++++++++++++++++++++++++++ tests/ipgGUI/environment.yml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/ipgGUI/README.md create mode 100644 tests/ipgGUI/environment.yml diff --git a/tests/ipgGUI/README.md b/tests/ipgGUI/README.md new file mode 100644 index 00000000..d0dbecd0 --- /dev/null +++ b/tests/ipgGUI/README.md @@ -0,0 +1,33 @@ +# GUI for Real-Sim FIXS +Real-Sim GUI for non-developer users to run co-simulation involving IPG CarMaker/TruckMaker. + +## 🚀 Features + - Currently support: **CarMaker-Sumo** and **CarMaker-Simulink-Sumo** co-simulations + - Can be run using Python or Matlab (Simulink users) scripts + +## 🛠️ Installation +Can be installed with conda command: `conda env create -f environment.yml` + +## 📁 Project Structure +#### As a good practice, a Real-Sim project (when involving IPG) follows the following structure: +Project_root \ +├── tests/ \ +│ ├── SUMO_test_name/ \ +│ │ ├── .sumocfg \ +│ │ └── RS_config_release.yaml \ +│ │ └── SUMO simulation files/ \ +│ │ │ ├── Routes/ \ +│ │ │ └── Network/ \ +│ │ │ └── Additional files/ \ +├── CM_proj/ \ +│ ├── src_cm4sl/ \ +│ │ ├── Simulink file \ +│ │ └── **Matlab-based GUI** \ +│ │ └── ... other matlab/simulink files \ +│ ├── Data/ \ +│ │ ├── TestRun/ \ +│ │ └── ... other folders for CM runs (roads, vehicles, tires, etc.) \ +│ ├── ... other folders for this CM project \ +├── CarMaker/ \ +├── CommonLib/ \ +├── TrafficLayer.exe diff --git a/tests/ipgGUI/environment.yml b/tests/ipgGUI/environment.yml new file mode 100644 index 00000000..f39f5e41 --- /dev/null +++ b/tests/ipgGUI/environment.yml @@ -0,0 +1,32 @@ +name: gui2024a +channels: + - defaults + - https://repo.anaconda.com/pkgs/main + - https://repo.anaconda.com/pkgs/r + - https://repo.anaconda.com/pkgs/msys2 +dependencies: + - bzip2=1.0.8=h2bbff1b_6 + - ca-certificates=2025.9.9=haa95532_0 + - expat=2.7.1=h8ddb27b_0 + - libffi=3.4.4=hd77b12b_1 + - libzlib=1.3.1=h02ab6af_0 + - openssl=3.0.18=h543e019_0 + - pip=25.2=pyhc872135_1 + - python=3.11.13=h981015d_0 + - setuptools=80.9.0=py311haa95532_0 + - sqlite=3.50.2=hda9a48d_1 + - tk=8.6.15=hf199647_0 + - tzdata=2025b=h04d1e81_0 + - ucrt=10.0.22621.0=haa95532_0 + - vc=14.3=h2df5915_10 + - vc14_runtime=14.44.35208=h4927774_10 + - vs2015_runtime=14.44.35208=ha6b5a95_10 + - wheel=0.45.1=py311haa95532_0 + - xz=5.6.4=h4754444_1 + - zlib=1.3.1=h02ab6af_0 + - pip: + - matlabengine==24.1.4 + - pyqt6==6.9.1 + - pyqt6-qt6==6.9.2 + - pyqt6-sip==13.10.2 +prefix: C:\Users\dev\miniconda3\envs\gui2024a From acc4f6ee0cc204d60cb3c8bf7a08f1701b3fdf5c Mon Sep 17 00:00:00 2001 From: az Date: Thu, 6 Nov 2025 16:05:43 -0500 Subject: [PATCH 6/6] Improve the readme with project structure --- tests/ipgGUI/README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/ipgGUI/README.md b/tests/ipgGUI/README.md index d0dbecd0..47f7fc83 100644 --- a/tests/ipgGUI/README.md +++ b/tests/ipgGUI/README.md @@ -1,5 +1,5 @@ # GUI for Real-Sim FIXS -Real-Sim GUI for non-developer users to run co-simulation involving IPG CarMaker/TruckMaker. +Real-Sim GUI for non-developer users to run co-simulation involving IPG CarMaker/TruckMaker. The GUI allows users to select IPG testrun file, Sumo simulation configuration, Simulink model, and directory to store simulation output data. ## 🚀 Features - Currently support: **CarMaker-Sumo** and **CarMaker-Simulink-Sumo** co-simulations @@ -9,9 +9,10 @@ Real-Sim GUI for non-developer users to run co-simulation involving IPG CarMaker Can be installed with conda command: `conda env create -f environment.yml` ## 📁 Project Structure -#### As a good practice, a Real-Sim project (when involving IPG) follows the following structure: +#### As a good practice, a Real-Sim project (when involving IPG) can be organized following the structure: Project_root \ ├── tests/ \ +│ ├── **GUI scripts** \ │ ├── SUMO_test_name/ \ │ │ ├── .sumocfg \ │ │ └── RS_config_release.yaml \ @@ -21,8 +22,8 @@ Project_root \ │ │ │ └── Additional files/ \ ├── CM_proj/ \ │ ├── src_cm4sl/ \ -│ │ ├── Simulink file \ -│ │ └── **Matlab-based GUI** \ +│ │ ├── Simulink model (.slx or .mdl) \ +│ │ └── cmenv.m \ │ │ └── ... other matlab/simulink files \ │ ├── Data/ \ │ │ ├── TestRun/ \ @@ -31,3 +32,7 @@ Project_root \ ├── CarMaker/ \ ├── CommonLib/ \ ├── TrafficLayer.exe + +### ⚙️ Launching the GUI +**Matlab user:** run `RS_init.m` to initialize the workspace and Real-Sim config, then run `run_sim_gui.m` to launch the GUI. \ +**Python user:** depending on your co-sim combination, run the corresponding `.py` file. Note that if you wanna involve Simulink, you can put the codes of `RS_init.m` in the `init` callback function of the Simulink file (can be open via model explorer). \ No newline at end of file