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.
- physicsworks-1.0.0.dist-info/METADATA +28 -0
- physicsworks-1.0.0.dist-info/RECORD +28 -0
- physicsworks-1.0.0.dist-info/WHEEL +5 -0
- physicsworks-1.0.0.dist-info/top_level.txt +1 -0
- physicsworks_python/__init__.py +1 -0
- physicsworks_python/events/Geometry.py +15 -0
- physicsworks_python/events/Mesh.py +6 -0
- physicsworks_python/events/__init__.py +11 -0
- physicsworks_python/greet.py +3 -0
- physicsworks_python/nats/__init__.py +0 -0
- physicsworks_python/nats/listener.py +34 -0
- physicsworks_python/nats/publisher.py +15 -0
- physicsworks_python/runner/__init__.py +44 -0
- physicsworks_python/runner/config.py +68 -0
- physicsworks_python/runner/core.py +357 -0
- physicsworks_python/runner/executor.py +606 -0
- physicsworks_python/runner/interface.py +39 -0
- physicsworks_python/runner/logger.py +37 -0
- physicsworks_python/runner/server.py +260 -0
- physicsworks_python/runner/template.py +402 -0
- physicsworks_python/runner/utils.py +234 -0
- physicsworks_python/runner/watcher.py +357 -0
- physicsworks_python/threejs.py +19 -0
- physicsworks_python/wrappers/MongoClientWrapper.py +29 -0
- physicsworks_python/wrappers/NatsClientWrapper.py +62 -0
- physicsworks_python/wrappers/SocketIOClientWrapper.py +23 -0
- physicsworks_python/wrappers/SocketIOServerWrapper.py +18 -0
- physicsworks_python/wrappers/__init__.py +0 -0
|
@@ -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 @@
|
|
|
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
|
|
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()
|