physicsworks 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: physicsworks
3
+ Version: 1.0.0
4
+ Summary: PhysicsWorks Python Runner - Simulation execution and post-processing framework
5
+ Author: PhysicsWorks
6
+ Author-email: contact@physicsworks.io
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.6
11
+ Requires-Dist: setuptools
12
+ Requires-Dist: packaging
13
+ Requires-Dist: GPUtil
14
+ Requires-Dist: numpy
15
+ Requires-Dist: psutil
16
+ Requires-Dist: python-slugify
17
+ Requires-Dist: PyYAML
18
+ Requires-Dist: requests
19
+ Requires-Dist: text-unidecode
20
+ Requires-Dist: urllib3
21
+ Requires-Dist: watchdog
22
+ Requires-Dist: Pillow
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
@@ -0,0 +1,28 @@
1
+ physicsworks_python/__init__.py,sha256=DbUh10nyKqpqaezaWeWiC8CfzeIV7zXly5bWgpYKgm0,28
2
+ physicsworks_python/greet.py,sha256=Mcwty1YrbkMK7pyBUEW1ql3DZil6-SOCCZKvVzMU2jQ,61
3
+ physicsworks_python/threejs.py,sha256=3d-lZ_tc_thWz8Wg6V7um5nwru18B_poTkzIWCAKcmM,366
4
+ physicsworks_python/events/Geometry.py,sha256=PgkCSrprcaZcwCr7oiYUS28IaaC3pBp--Uw-oivqMGU,330
5
+ physicsworks_python/events/Mesh.py,sha256=AO5_DtRFcfpapkyo0qQUfPLfosMvCdZvfmYRUuwPs_Q,116
6
+ physicsworks_python/events/__init__.py,sha256=EtCn2KXIpTMShKtPHZcphZ4VBnEt_H_018RM9z4yJuU,226
7
+ physicsworks_python/nats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ physicsworks_python/nats/listener.py,sha256=npnN8kyZ1LcgWT1GcWl_ug48OJJ50GS6eY8fg2Xgkd4,904
9
+ physicsworks_python/nats/publisher.py,sha256=SlB4jqF3k1iMkzta_YIDAwGRogkebQK7QuyFxPvip4c,438
10
+ physicsworks_python/runner/__init__.py,sha256=e902zSwHyukJPws8kqMnLxDb3kP9zewGylLnQTJghW8,1111
11
+ physicsworks_python/runner/config.py,sha256=6XeEP79prjUOZ4f-lC0-bUXS4rSJ0zxY0ay8wB4FtEw,1663
12
+ physicsworks_python/runner/core.py,sha256=YsuMEw4fq5ro2Srb-CiMr3DWL6paeo87oRAppok8k0E,13235
13
+ physicsworks_python/runner/executor.py,sha256=epYUU-AWltC_QIBTZla9ySw-zbSyD-9rmI9YVbv8BRc,22904
14
+ physicsworks_python/runner/interface.py,sha256=3KrU6lqWayMkI8QK4ECEuG9nt2TjmXPZnGnqMg-W2m8,1158
15
+ physicsworks_python/runner/logger.py,sha256=i8GRWB1d3kSjnBGa0pmg0CAqhS2wDbjYMpDD4oc1bcM,1000
16
+ physicsworks_python/runner/server.py,sha256=2Lu_c0Mo3yl8eo98xL3lrpJcWjbv1VbemfvYcnkUTtc,10309
17
+ physicsworks_python/runner/template.py,sha256=C6L5SJkxtIroHjcifdZkTH4LKlAWEFt9eV4zuG0nK2M,12180
18
+ physicsworks_python/runner/utils.py,sha256=jSfSbjFZI6MhR7zMXfDVKfTWsmNDxBQAZBnmd6INkRY,8467
19
+ physicsworks_python/runner/watcher.py,sha256=cXbRYg47V8TIvR5zz2IOTe12auSfvfqhoKskTlOD2O4,14417
20
+ physicsworks_python/wrappers/MongoClientWrapper.py,sha256=5UGiZzQXz-urp_LplRToYLbmtCXmYgrvc_A8O2yMGyg,547
21
+ physicsworks_python/wrappers/NatsClientWrapper.py,sha256=KfybP9-Baana3j4ESOCSou691kWNIXbcUR6HVFL0Nkc,1452
22
+ physicsworks_python/wrappers/SocketIOClientWrapper.py,sha256=P-UQt_8jAhgHm5mx9RxiincPSK_WwNUd2nEXb8Rw3BA,349
23
+ physicsworks_python/wrappers/SocketIOServerWrapper.py,sha256=o86pVfwOleK-e5lhNHVRAwiWeG5l2Gq9xOsNstaDhko,241
24
+ physicsworks_python/wrappers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ physicsworks-1.0.0.dist-info/METADATA,sha256=LYKaDp7HOtjqIBdF_T2824vUR39qqtvTuMd_xpBjPKo,815
26
+ physicsworks-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ physicsworks-1.0.0.dist-info/top_level.txt,sha256=CXso4CWH4hDi-4UARANfVAUrfYc5kRAYlppQIJbr2Aw,20
28
+ physicsworks-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ physicsworks_python
@@ -0,0 +1 @@
1
+ from .greet import SayHello
@@ -0,0 +1,15 @@
1
+ from enum import Enum
2
+
3
+
4
+ class GeometrySubjects(Enum):
5
+ GeometryUploaded = 'geometry:uploaded'
6
+ GeometryParsed = 'geometry:parsed'
7
+ GeometryRemoved = 'geometry:removed'
8
+
9
+
10
+ # class GeometryUploaded(Event):
11
+ # sub
12
+
13
+ # GeometryUploaded = Event()
14
+ # GeometryUploaded.subject = GeometrySubjects.GeometryUploaded
15
+ # GeometryUploaded.data
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MeshSubjects(Enum):
5
+ MeshGenerated = 'mesh:generated'
6
+ MeshDeleted = 'mesh:deleted'
@@ -0,0 +1,11 @@
1
+ # from enum import Enum
2
+
3
+ # from .Geometry import GeometrySubjects
4
+ # from .Mesh import MeshSubjects
5
+
6
+ # class Subjects(GeometrySubjects, MeshSubjects, Enum):
7
+ # '''Test'''
8
+
9
+ # class Event:
10
+ # subject: Subjects
11
+ # data: object
@@ -0,0 +1,3 @@
1
+ def SayHello():
2
+ print("Hello from MyPyPiPackage")
3
+ return
File without changes
@@ -0,0 +1,34 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from stan.aio.client import Client as STAN
4
+ from stan.aio.client import Msg
5
+
6
+
7
+ class Listener(ABC):
8
+
9
+ def __init__(self, stan: STAN, subject: str, queueGroupName: str) -> None:
10
+ self.__client = stan
11
+
12
+ self._subject = subject
13
+ self._queueGroupName = queueGroupName
14
+
15
+ @abstractmethod
16
+ async def onMessage(self, msg: Msg) -> None:
17
+ print("Received a message (seq={}): {}".format(msg.seq, msg.data))
18
+
19
+ await self.__client.ack(msg)
20
+
21
+ # async def onError(ex):
22
+ # print("NATS: An error occured", ex)
23
+
24
+ async def listen(self, ack_wait: int = 30) -> None:
25
+ await self.__client.subscribe(
26
+ subject=self._subject,
27
+ queue=self._queueGroupName,
28
+ deliver_all_available=True,
29
+ manual_acks=True,
30
+ ack_wait=ack_wait,
31
+ durable_name=self._queueGroupName,
32
+ cb=self.onMessage,
33
+ # error_cb=self.onError
34
+ )
@@ -0,0 +1,15 @@
1
+ import json
2
+ from abc import ABC
3
+ from stan.aio.client import Client as STAN
4
+
5
+
6
+ class Publisher(ABC):
7
+
8
+ def __init__(self, client: STAN, subject: str) -> None:
9
+ self._client = client
10
+ self._subject = subject
11
+
12
+ async def publish(self, data):
13
+ bytes_data = json.dumps(data).encode('utf-8')
14
+ await self._client.publish(subject=self._subject, payload=bytes_data)
15
+ print(f'[EVENT ->] Event published to subject {self._subject}')
@@ -0,0 +1,44 @@
1
+ """
2
+ SPRIME Runner Package
3
+
4
+ A comprehensive package for running physics simulations with remote and native support.
5
+ Provides a clean, modular architecture for solver execution with debugging capabilities.
6
+ """
7
+
8
+ from .core import UnifiedRunner
9
+ from .template import (
10
+ load_run_inputs,
11
+ reconstruct_tree_from_run_inputs,
12
+ get_input_value_by_slug,
13
+ get_input_values_by_slug,
14
+ get_input_value_by_slugs,
15
+ get_input_values_by_slugs,
16
+ update_solver_progress,
17
+ add_to_simulation_results,
18
+ replace_placeholders_in_file,
19
+ pre_remote,
20
+ remote,
21
+ post_remote,
22
+ run_solver
23
+ )
24
+
25
+ __version__ = "1.0.0"
26
+ __all__ = [
27
+ # Main runner class
28
+ "UnifiedRunner",
29
+
30
+ # Template functions for solver scripts (all exported)
31
+ "load_run_inputs",
32
+ "reconstruct_tree_from_run_inputs",
33
+ "get_input_value_by_slug",
34
+ "get_input_values_by_slug",
35
+ "get_input_value_by_slugs",
36
+ "get_input_values_by_slugs",
37
+ "update_solver_progress",
38
+ "add_to_simulation_results",
39
+ "replace_placeholders_in_file",
40
+ "pre_remote",
41
+ "remote",
42
+ "post_remote",
43
+ "run_solver"
44
+ ]
@@ -0,0 +1,68 @@
1
+ """
2
+ Configuration and enums for the runner package.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Dict, List, Optional, Any
8
+
9
+
10
+ class ExecutionMode(Enum):
11
+ """Execution mode for the solver"""
12
+ REMOTE = "remote"
13
+ NATIVE = "native"
14
+
15
+
16
+ class Stage(Enum):
17
+ """Available execution stages"""
18
+ START = "start"
19
+ INPUT = "input"
20
+ DOWNLOAD = "download"
21
+ UPLOAD = "upload"
22
+ POSTPROCESS = "postprocess"
23
+
24
+
25
+ @dataclass
26
+ class Configuration:
27
+ """Configuration container for the wrapper"""
28
+ config_path: str
29
+ work_dir: str
30
+ inputs_path: str
31
+ outputs_path: str
32
+ downloads_path: str
33
+ raw_path: Optional[str] = None
34
+ debug_path: Optional[str] = None
35
+ scripts_path: Optional[str] = None
36
+ starting_stage: Stage = Stage.START
37
+ skip_stages: List[Stage] = None
38
+ debug_mode: bool = False
39
+ execution_mode: ExecutionMode = ExecutionMode.NATIVE
40
+ config: Dict[str, Any] = None
41
+
42
+ def __post_init__(self):
43
+ if self.skip_stages is None:
44
+ self.skip_stages = []
45
+
46
+
47
+ class RuntimeAttributes:
48
+ """Runtime state tracking"""
49
+ def __init__(self):
50
+ self.status = ""
51
+ self.progress = 0
52
+ self.status_label = ""
53
+ self.log_paths = []
54
+ self.logs_status = {}
55
+ self.logs = {}
56
+ self.plots_paths = []
57
+ self.plots = {}
58
+ self.media_paths = []
59
+ self.media = {}
60
+ self.fields = {}
61
+ self.point_clouds = {}
62
+ self.output_files = []
63
+ self.filenames = {}
64
+ self.run_id = ""
65
+ self.compression = {"withCompression": False}
66
+ self.current_uploads = []
67
+ self.run_succeeded = False
68
+ self.aborted = False # Flag to indicate abort was requested
@@ -0,0 +1,357 @@
1
+ """
2
+ Core runner implementation for the SPRIME runner package.
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ import time
10
+ import traceback
11
+
12
+ from .config import Configuration, RuntimeAttributes, Stage
13
+ from .logger import DebugLogger
14
+ from .server import ServerCommunicator
15
+ from .watcher import FileSystemWatcher
16
+ from .executor import StageExecutor
17
+ from .interface import append_status, StepStatus
18
+
19
+
20
+ class UnifiedRunner:
21
+ """Main runner class that orchestrates the execution"""
22
+
23
+ def __init__(self, on_abort_callback=None):
24
+ self.config = None
25
+ self.runtime_attrs = RuntimeAttributes()
26
+ self.logger = DebugLogger()
27
+ self.server_comm = ServerCommunicator(self.runtime_attrs, self.logger)
28
+ self.watcher = None # Will be initialized after config is loaded
29
+ self.executor = None
30
+ self.on_abort_callback = on_abort_callback
31
+ self.main_process = None # Reference to running subprocess for abort handling
32
+
33
+ def _parse_arguments(self, args=None) -> Configuration:
34
+ """Parse command line arguments"""
35
+ parser = argparse.ArgumentParser(
36
+ description="Unified PhysicsWorks Solver Template Runner",
37
+ formatter_class=argparse.RawDescriptionHelpFormatter,
38
+ epilog="""
39
+ Examples:
40
+ # Normal execution
41
+ python runner.py --configPath=config.json
42
+
43
+ # Start from download stage
44
+ python runner.py --configPath=config.json --startingStage=download
45
+
46
+ # Skip stages for debugging
47
+ python runner.py --configPath=config.json --skipStages=watch,upload
48
+
49
+ # Debug mode with verbose logging
50
+ python runner.py --configPath=config.json --debugMode
51
+ """
52
+ )
53
+
54
+ parser.add_argument(
55
+ "--configPath",
56
+ required=True,
57
+ help="Path to the configuration JSON file"
58
+ )
59
+
60
+ parser.add_argument(
61
+ "--startingStage",
62
+ choices=[stage.value for stage in Stage],
63
+ default=Stage.START.value,
64
+ help="Starting stage for execution (default: start)"
65
+ )
66
+
67
+ parser.add_argument(
68
+ "--skipStages",
69
+ help="Comma-separated list of stages to skip (e.g., watch,upload)"
70
+ )
71
+
72
+ parser.add_argument(
73
+ "--debugMode",
74
+ action='store_true',
75
+ help="Enable debug mode with verbose logging"
76
+ )
77
+
78
+ parsed_args = parser.parse_args(args)
79
+
80
+ # Parse configuration file
81
+ try:
82
+ with open(parsed_args.configPath) as config_file:
83
+ config_dict = json.load(config_file)
84
+ except Exception as e:
85
+ print(f"Error loading configuration file: {e}")
86
+ sys.exit(1)
87
+
88
+ # Parse skip stages
89
+ skip_stages = []
90
+ if parsed_args.skipStages:
91
+ skip_stage_names = [s.strip() for s in parsed_args.skipStages.split(',')]
92
+ for stage_name in skip_stage_names:
93
+ try:
94
+ skip_stages.append(Stage(stage_name))
95
+ except ValueError:
96
+ print(f"Warning: Unknown stage '{stage_name}' in skipStages")
97
+
98
+ # Create configuration
99
+ work_dir = config_dict['workDir']
100
+
101
+ config = Configuration(
102
+ config_path=parsed_args.configPath,
103
+ work_dir=work_dir,
104
+ inputs_path=os.path.join(work_dir, "inputs"),
105
+ outputs_path=os.path.join(work_dir, "outputs"),
106
+ downloads_path=os.path.join(work_dir, "inputs"),
107
+ raw_path=os.path.join(work_dir, "raw"),
108
+ debug_path=os.path.join(work_dir, "debug"),
109
+ scripts_path=os.path.join(work_dir, "scripts"),
110
+ starting_stage=Stage(parsed_args.startingStage),
111
+ skip_stages=skip_stages,
112
+ debug_mode=parsed_args.debugMode,
113
+ config=config_dict
114
+ )
115
+
116
+ return config
117
+
118
+ def run(self, args=None):
119
+ """Main execution method"""
120
+ try:
121
+ # 1. Initialize (parse arguments and create directories)
122
+ self.config = self._parse_arguments(args)
123
+ self._create_directories()
124
+
125
+ self.logger = DebugLogger(self.config.debug_mode)
126
+ self.server_comm = ServerCommunicator(self.runtime_attrs, self.logger)
127
+ # Create watcher with reference to get main process for abort handling
128
+ self.watcher = FileSystemWatcher(
129
+ self.runtime_attrs,
130
+ self.server_comm,
131
+ self.logger,
132
+ on_abort_callback=self.on_abort_callback,
133
+ main_process_ref=lambda: self.main_process
134
+ )
135
+ self.executor = StageExecutor(
136
+ self.config, self.runtime_attrs, self.server_comm, self.watcher, self.logger
137
+ )
138
+
139
+ self.logger.info(f"Starting unified runner in {self.config.starting_stage.value} mode")
140
+ self.logger.debug(f"Work directory: {self.config.work_dir}")
141
+
142
+ if self.config.skip_stages:
143
+ self.logger.info(f"Skipping stages: {[s.value for s in self.config.skip_stages]}")
144
+
145
+ # Initialize status file
146
+ append_status(self.config.outputs_path, 0, "Initializing simulation", "initialization", StepStatus.RUNNING)
147
+
148
+ # 2. Download inputs from server (0-10%)
149
+ append_status(self.config.outputs_path, 0, "Starting preprocessing", "preprocessing", StepStatus.RUNNING)
150
+
151
+ if not self.executor.execute_stage(Stage.DOWNLOAD):
152
+ self.logger.error("Download stage failed")
153
+ self.server_comm.set_status("error", 0, "Failed at download")
154
+ append_status(self.config.outputs_path, 0, "Error in preprocessing: Download stage failed", "preprocessing", StepStatus.FAILED)
155
+ return False
156
+
157
+ append_status(self.config.outputs_path, 10, "Preprocessing complete", "preprocessing", StepStatus.FINISHED)
158
+
159
+ # 3. Start file watcher on a separate thread
160
+ self._start_file_watcher()
161
+
162
+ # 4. Run the main.py script under scripts (10-90%)
163
+ append_status(self.config.outputs_path, 10, "Starting solver execution", "solver", StepStatus.RUNNING)
164
+
165
+ if not self._run_main_script():
166
+ self.logger.error("Main script execution failed")
167
+ self.server_comm.set_status("error", 0, "Failed at script execution")
168
+ append_status(self.config.outputs_path, 0, "Error in solver: Main script execution failed", "solver", StepStatus.FAILED)
169
+ return False
170
+
171
+ append_status(self.config.outputs_path, 90, "Solver execution complete", "solver", StepStatus.FINISHED)
172
+
173
+ # Check if aborted before post-processing
174
+ if self.runtime_attrs.aborted:
175
+ self.logger.error("Execution aborted by user")
176
+ return False
177
+
178
+ # 5. Post processing (90-100%)
179
+ append_status(self.config.outputs_path, 90, "Starting postprocessing", "postprocessing", StepStatus.RUNNING)
180
+
181
+ if not self.executor.execute_stage(Stage.POSTPROCESS):
182
+ self.logger.error("Post-processing stage failed")
183
+ self.server_comm.set_status("error", 0, "Failed at post-processing")
184
+ append_status(self.config.outputs_path, 0, "Error in postprocessing: Post-processing stage failed", "postprocessing", StepStatus.FAILED)
185
+ return False
186
+
187
+ append_status(self.config.outputs_path, 100, "Simulation completed successfully", "postprocessing", StepStatus.FINISHED)
188
+
189
+ # Final status
190
+ append_status(self.config.outputs_path, 100, "Simulation completed successfully")
191
+
192
+ self.logger.info("Runner execution completed successfully")
193
+ return True
194
+
195
+ except KeyboardInterrupt:
196
+ self.logger.info("Execution interrupted by user")
197
+ self.server_comm.set_status("error", 0, "Interrupted by user")
198
+ return False
199
+ except Exception as e:
200
+ self.logger.error(f"Runner execution failed: {e}")
201
+ if self.config and self.config.debug_mode:
202
+ traceback.print_exc()
203
+ self.server_comm.set_status("error", 0, "error")
204
+ return False
205
+ finally:
206
+ # Stop file watcher
207
+ if hasattr(self, 'watcher'):
208
+ self.watcher.stop_watching()
209
+
210
+ def _create_directories(self):
211
+ """Create required directories: debug, inputs, outputs, raw, scripts and outputs subdirectories"""
212
+ directories = [
213
+ self.config.debug_path,
214
+ self.config.inputs_path,
215
+ self.config.outputs_path,
216
+ self.config.raw_path,
217
+ self.config.scripts_path
218
+ ]
219
+
220
+ # Create output subdirectories
221
+ output_subdirs = ["files", "graphics", "logs", "media", "plots"]
222
+ for subdir in output_subdirs:
223
+ directories.append(os.path.join(self.config.outputs_path, subdir))
224
+
225
+ for directory in directories:
226
+ try:
227
+ os.makedirs(directory, exist_ok=True)
228
+ self.logger.debug(f"Created directory: {directory}")
229
+ except Exception as e:
230
+ self.logger.error(f"Failed to create directory {directory}: {e}")
231
+ raise
232
+
233
+ def _start_file_watcher(self):
234
+ """Start file watcher on a separate thread for outputs monitoring and config.json abort signals"""
235
+ try:
236
+ # Watch outputs directory for state.txt changes and file uploads
237
+ watch_paths = [
238
+ self.config.outputs_path,
239
+ os.path.join(self.config.outputs_path, "logs"),
240
+ os.path.join(self.config.outputs_path, "media"),
241
+ os.path.join(self.config.outputs_path, "plots")
242
+ ]
243
+
244
+ # Also watch config directory for config.json changes (abort signals)
245
+ config_dir = os.path.dirname(self.config.config_path)
246
+ if config_dir and os.path.isdir(config_dir):
247
+ watch_paths.append(config_dir)
248
+ self.logger.debug(f"Watching config directory for abort signals: {config_dir}")
249
+
250
+ self.watcher.start_watching(watch_paths)
251
+ self.logger.info("File watcher started on separate thread")
252
+
253
+ except Exception as e:
254
+ self.logger.error(f"Failed to start file watcher: {e}")
255
+ raise
256
+
257
+ def _run_main_script(self):
258
+ """Run the main.py script under scripts directory"""
259
+ try:
260
+ main_script_path = os.path.join(self.config.scripts_path, "main.py")
261
+
262
+ if not os.path.exists(main_script_path):
263
+ self.logger.error(f"Main script not found at {main_script_path}")
264
+ return False
265
+
266
+ self.logger.info(f"Executing main script: {main_script_path}")
267
+
268
+ # Set environment variables for the script
269
+ # Convert all paths to use forward slashes for cross-platform compatibility
270
+ script_env = os.environ.copy()
271
+ script_env["PHYSICSWORKS_INPUTS_DIR"] = self.config.inputs_path.replace('\\', '/')
272
+ script_env["PHYSICSWORKS_OUTPUTS_DIR"] = self.config.outputs_path.replace('\\', '/')
273
+ script_env["PHYSICSWORKS_RAW_DIR"] = self.config.raw_path.replace('\\', '/')
274
+ script_env["PHYSICSWORKS_WORK_DIR"] = self.config.work_dir.replace('\\', '/')
275
+ script_env["PHYSICSWORKS_RUN_ID"] = self.runtime_attrs.run_id
276
+
277
+ # Build command to run main.py with proper arguments
278
+ command = [
279
+ "python",
280
+ main_script_path,
281
+ f"--inputsPath={self.config.inputs_path}",
282
+ f"--outputsPath={self.config.outputs_path}",
283
+ f"--rawPath={self.config.raw_path}"
284
+ ]
285
+
286
+ # Execute the main script with environment variables
287
+ import subprocess
288
+ self.main_process = subprocess.Popen(
289
+ command,
290
+ cwd=self.config.scripts_path,
291
+ stdout=subprocess.PIPE,
292
+ stderr=subprocess.PIPE,
293
+ text=True,
294
+ env=script_env
295
+ )
296
+
297
+ # Poll the process instead of blocking, checking for abort
298
+ while self.main_process.poll() is None:
299
+ # Check if abort was requested
300
+ if self.runtime_attrs.aborted:
301
+ self.logger.error("Main script execution was aborted")
302
+ self.main_process = None
303
+ return False
304
+ time.sleep(0.5) # Check every 500ms
305
+
306
+ # Process completed, get output
307
+ stdout, stderr = self.main_process.communicate()
308
+ result_code = self.main_process.returncode
309
+ self.main_process = None
310
+
311
+ # Check if aborted during execution (redundant but safe)
312
+ if self.runtime_attrs.aborted:
313
+ self.logger.error("Main script execution was aborted")
314
+ return False
315
+
316
+ # Create result object for compatibility
317
+ class Result:
318
+ def __init__(self, stdout, stderr, returncode):
319
+ self.stdout = stdout
320
+ self.stderr = stderr
321
+ self.returncode = returncode
322
+
323
+ result = Result(stdout, stderr, result_code)
324
+
325
+ # Write logs to debug/main.txt
326
+ log_file_path = os.path.join(self.config.debug_path, "main.txt")
327
+ try:
328
+ with open(log_file_path, 'w') as log_file:
329
+ log_file.write("=== STDOUT ===\n")
330
+ log_file.write(result.stdout)
331
+ log_file.write("\n=== STDERR ===\n")
332
+ log_file.write(result.stderr)
333
+ self.logger.debug(f"Main script logs written to {log_file_path}")
334
+ except Exception as log_error:
335
+ self.logger.error(f"Failed to write main script logs: {log_error}")
336
+
337
+ if result.returncode == 0:
338
+ self.logger.info("Main script executed successfully")
339
+ self.runtime_attrs.run_succeeded = True
340
+ return True
341
+ else:
342
+ self.logger.error(f"Main script failed with exit code: {result.returncode}")
343
+ self.runtime_attrs.run_succeeded = False
344
+ return False
345
+
346
+ except Exception as e:
347
+ self.logger.error(f"Error executing main script: {e}")
348
+ return False
349
+
350
+ def main():
351
+ """Entry point for the runner"""
352
+ runner = UnifiedRunner()
353
+ success = runner.run()
354
+ sys.exit(0 if success else 1)
355
+
356
+ if __name__ == "__main__":
357
+ main()