scitex 2.16.2__py3-none-any.whl → 2.17.3__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.
- scitex/_dev/__init__.py +122 -0
- scitex/_dev/_config.py +391 -0
- scitex/_dev/_dashboard/__init__.py +11 -0
- scitex/_dev/_dashboard/_app.py +89 -0
- scitex/_dev/_dashboard/_routes.py +169 -0
- scitex/_dev/_dashboard/_scripts.py +301 -0
- scitex/_dev/_dashboard/_styles.py +205 -0
- scitex/_dev/_dashboard/_templates.py +117 -0
- scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
- scitex/_dev/_ecosystem.py +109 -0
- scitex/_dev/_github.py +360 -0
- scitex/_dev/_mcp/__init__.py +11 -0
- scitex/_dev/_mcp/handlers.py +182 -0
- scitex/_dev/_ssh.py +332 -0
- scitex/_dev/_versions.py +272 -0
- scitex/_mcp_resources/_cheatsheet.py +1 -1
- scitex/_mcp_resources/_modules.py +1 -1
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/dev.py +186 -0
- scitex/_mcp_tools/verify.py +256 -0
- scitex/audio/_audio_check.py +84 -41
- scitex/cli/capture.py +45 -22
- scitex/cli/dev.py +494 -0
- scitex/cli/main.py +4 -0
- scitex/cli/stats.py +48 -20
- scitex/cli/verify.py +473 -0
- scitex/dev/plt/__init__.py +1 -1
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/io/_load.py +8 -1
- scitex/io/_save.py +12 -0
- scitex/plt/__init__.py +16 -6
- scitex/session/README.md +2 -2
- scitex/session/__init__.py +1 -0
- scitex/session/_decorator.py +57 -33
- scitex/session/_lifecycle/__init__.py +23 -0
- scitex/session/_lifecycle/_close.py +225 -0
- scitex/session/_lifecycle/_config.py +112 -0
- scitex/session/_lifecycle/_matplotlib.py +83 -0
- scitex/session/_lifecycle/_start.py +246 -0
- scitex/session/_lifecycle/_utils.py +186 -0
- scitex/session/_manager.py +40 -3
- scitex/session/template.py +1 -1
- scitex/template/__init__.py +18 -1
- scitex/template/_templates/plt.py +1 -1
- scitex/template/_templates/session.py +1 -1
- scitex/template/clone_research_minimal.py +111 -0
- scitex/verify/README.md +300 -0
- scitex/verify/__init__.py +208 -0
- scitex/verify/_chain.py +369 -0
- scitex/verify/_db.py +600 -0
- scitex/verify/_hash.py +187 -0
- scitex/verify/_integration.py +127 -0
- scitex/verify/_rerun.py +253 -0
- scitex/verify/_tracker.py +330 -0
- scitex/verify/_visualize.py +44 -0
- scitex/verify/_viz/__init__.py +38 -0
- scitex/verify/_viz/_colors.py +84 -0
- scitex/verify/_viz/_format.py +302 -0
- scitex/verify/_viz/_json.py +192 -0
- scitex/verify/_viz/_mermaid.py +440 -0
- scitex/verify/_viz/_templates.py +246 -0
- scitex/verify/_viz/_utils.py +56 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
- scitex/session/_lifecycle.py +0 -827
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/licenses/LICENSE +0 -0
scitex/session/_lifecycle.py
DELETED
|
@@ -1,827 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-10-16 20:25:05 (ywatanabe)"
|
|
4
|
-
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/session/_lifecycle.py
|
|
5
|
-
# ----------------------------------------
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
import os
|
|
8
|
-
|
|
9
|
-
__FILE__ = "./src/scitex/session/_lifecycle.py"
|
|
10
|
-
__DIR__ = os.path.dirname(__FILE__)
|
|
11
|
-
# ----------------------------------------
|
|
12
|
-
|
|
13
|
-
__FILE__ = __file__
|
|
14
|
-
|
|
15
|
-
"""Session lifecycle management for SciTeX experiments.
|
|
16
|
-
|
|
17
|
-
This module contains the start() and close() functions that replace
|
|
18
|
-
scitex.session.start() and scitex.session.close() with enhanced session management.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import inspect
|
|
22
|
-
import os as _os
|
|
23
|
-
import re
|
|
24
|
-
import shutil
|
|
25
|
-
import sys as sys_module
|
|
26
|
-
import time
|
|
27
|
-
from datetime import datetime
|
|
28
|
-
from glob import glob as _glob
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
from pprint import pprint
|
|
31
|
-
from time import sleep
|
|
32
|
-
from typing import Any, Dict, Optional, Tuple, Union
|
|
33
|
-
|
|
34
|
-
from scitex.logging import getLogger
|
|
35
|
-
|
|
36
|
-
logger = getLogger(__name__)
|
|
37
|
-
|
|
38
|
-
import matplotlib
|
|
39
|
-
|
|
40
|
-
# CRITICAL: Set backend before importing pyplot to avoid tkinter issues in headless/WSL environments
|
|
41
|
-
# Check if we're in a headless environment (no DISPLAY) or WSL
|
|
42
|
-
import os
|
|
43
|
-
import sys
|
|
44
|
-
import platform
|
|
45
|
-
|
|
46
|
-
# Detect headless/WSL environments
|
|
47
|
-
is_headless = False
|
|
48
|
-
try:
|
|
49
|
-
# Check for WSL
|
|
50
|
-
if "microsoft" in platform.uname().release.lower() or "WSL" in os.environ.get(
|
|
51
|
-
"WSL_DISTRO_NAME", ""
|
|
52
|
-
):
|
|
53
|
-
is_headless = True
|
|
54
|
-
# Check for no X11 display
|
|
55
|
-
elif not os.environ.get("DISPLAY"):
|
|
56
|
-
is_headless = True
|
|
57
|
-
except Exception:
|
|
58
|
-
# Fallback: if on Linux without DISPLAY, assume headless
|
|
59
|
-
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
|
|
60
|
-
is_headless = True
|
|
61
|
-
|
|
62
|
-
if is_headless:
|
|
63
|
-
matplotlib.use("Agg")
|
|
64
|
-
|
|
65
|
-
import matplotlib.pyplot as plt_module
|
|
66
|
-
|
|
67
|
-
from scitex.dict import DotDict
|
|
68
|
-
|
|
69
|
-
# Lazy imports moved to functions to avoid circular dependency
|
|
70
|
-
# from scitex.io._flush import flush
|
|
71
|
-
# from scitex.io._save import save as scitex_io_save
|
|
72
|
-
# from scitex.io._load import load
|
|
73
|
-
# from scitex.io._load_configs import load_configs
|
|
74
|
-
from scitex.plt.utils._configure_mpl import configure_mpl
|
|
75
|
-
from scitex.repro._gen_ID import gen_ID
|
|
76
|
-
from scitex.repro import RandomStateManager
|
|
77
|
-
from scitex.str._clean_path import clean_path
|
|
78
|
-
from scitex.str._printc import printc as _printc
|
|
79
|
-
from scitex.utils._notify import notify as scitex_utils_notify
|
|
80
|
-
from ._manager import get_global_session_manager
|
|
81
|
-
|
|
82
|
-
# For development code flow analysis
|
|
83
|
-
try:
|
|
84
|
-
from scitex.dev._analyze_code_flow import analyze_code_flow
|
|
85
|
-
except ImportError:
|
|
86
|
-
|
|
87
|
-
def analyze_code_flow(file):
|
|
88
|
-
return "Code flow analysis not available"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _print_header(
|
|
92
|
-
ID: str,
|
|
93
|
-
PID: int,
|
|
94
|
-
file: str,
|
|
95
|
-
args: Any,
|
|
96
|
-
configs: Dict[str, Any],
|
|
97
|
-
verbose: bool = True,
|
|
98
|
-
) -> None:
|
|
99
|
-
"""Prints formatted header with scitex version, ID, and PID information.
|
|
100
|
-
|
|
101
|
-
Parameters
|
|
102
|
-
----------
|
|
103
|
-
ID : str
|
|
104
|
-
Unique identifier for the current run
|
|
105
|
-
PID : int
|
|
106
|
-
Process ID of the current Python process
|
|
107
|
-
file : str
|
|
108
|
-
File path of the calling script
|
|
109
|
-
args : Any
|
|
110
|
-
Command line arguments or configuration object
|
|
111
|
-
configs : Dict[str, Any]
|
|
112
|
-
Configuration dictionary to display
|
|
113
|
-
verbose : bool, optional
|
|
114
|
-
Whether to print detailed information, by default True
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
if args is not None and hasattr(args, "_get_kwargs"):
|
|
118
|
-
args_str = "Arguments:"
|
|
119
|
-
for arg, value in args._get_kwargs():
|
|
120
|
-
args_str += f"\n {arg}: {value}"
|
|
121
|
-
else:
|
|
122
|
-
args_str = "Arguments: None"
|
|
123
|
-
|
|
124
|
-
_printc(
|
|
125
|
-
(
|
|
126
|
-
f"SciTeX v{_get_scitex_version()} | {ID} (PID: {PID})\n\n"
|
|
127
|
-
f"{file}\n\n"
|
|
128
|
-
f"{args_str}"
|
|
129
|
-
),
|
|
130
|
-
char="=",
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
sleep(1)
|
|
134
|
-
if verbose:
|
|
135
|
-
from pprint import pformat
|
|
136
|
-
config_str = pformat(configs.to_dict())
|
|
137
|
-
logger.info(f"\n{'-' * 40}\n\n{config_str}\n\n{'-' * 40}\n")
|
|
138
|
-
sleep(1)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _initialize_env(IS_DEBUG: bool) -> Tuple[str, int]:
|
|
142
|
-
"""Initialize environment with ID and PID.
|
|
143
|
-
|
|
144
|
-
Parameters
|
|
145
|
-
----------
|
|
146
|
-
IS_DEBUG : bool
|
|
147
|
-
Debug mode flag
|
|
148
|
-
|
|
149
|
-
Returns
|
|
150
|
-
-------
|
|
151
|
-
tuple
|
|
152
|
-
(ID, PID) - Unique identifier and Process ID
|
|
153
|
-
"""
|
|
154
|
-
ID = gen_ID(N=4) if not IS_DEBUG else "DEBUG_" + gen_ID(N=4)
|
|
155
|
-
PID = _os.getpid()
|
|
156
|
-
return ID, PID
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _setup_configs(
|
|
160
|
-
IS_DEBUG: bool,
|
|
161
|
-
ID: str,
|
|
162
|
-
PID: int,
|
|
163
|
-
file: str,
|
|
164
|
-
sdir: str,
|
|
165
|
-
relative_sdir: str,
|
|
166
|
-
verbose: bool,
|
|
167
|
-
) -> Dict[str, Any]:
|
|
168
|
-
"""Setup configuration dictionary with basic parameters.
|
|
169
|
-
|
|
170
|
-
Parameters
|
|
171
|
-
----------
|
|
172
|
-
IS_DEBUG : bool
|
|
173
|
-
Debug mode flag
|
|
174
|
-
ID : str
|
|
175
|
-
Unique identifier
|
|
176
|
-
PID : int
|
|
177
|
-
Process ID
|
|
178
|
-
file : str
|
|
179
|
-
File path
|
|
180
|
-
sdir : str
|
|
181
|
-
Save directory path
|
|
182
|
-
relative_sdir : str
|
|
183
|
-
Relative save directory path
|
|
184
|
-
verbose : bool
|
|
185
|
-
Verbosity flag
|
|
186
|
-
|
|
187
|
-
Returns
|
|
188
|
-
-------
|
|
189
|
-
dict
|
|
190
|
-
Configuration dictionary
|
|
191
|
-
"""
|
|
192
|
-
# Calculate SDIR_OUT (base output directory)
|
|
193
|
-
# sdir format: /path/to/script_out/RUNNING/ID/
|
|
194
|
-
sdir_path = Path(sdir) if sdir else None
|
|
195
|
-
if sdir_path:
|
|
196
|
-
# Remove /RUNNING/ID/ to get base output dir
|
|
197
|
-
parts = sdir_path.parts
|
|
198
|
-
if "RUNNING" in parts:
|
|
199
|
-
running_idx = parts.index("RUNNING")
|
|
200
|
-
sdir_out = Path(*parts[:running_idx])
|
|
201
|
-
else:
|
|
202
|
-
sdir_out = sdir_path.parent
|
|
203
|
-
else:
|
|
204
|
-
sdir_out = None
|
|
205
|
-
|
|
206
|
-
# Load YAML configs from ./config/*.yaml
|
|
207
|
-
from scitex.io._load_configs import load_configs
|
|
208
|
-
|
|
209
|
-
CONFIGS = load_configs(IS_DEBUG).to_dict()
|
|
210
|
-
|
|
211
|
-
# Add session-specific config with clean structure (Path objects only)
|
|
212
|
-
CONFIGS.update(
|
|
213
|
-
{
|
|
214
|
-
"ID": ID,
|
|
215
|
-
"PID": PID,
|
|
216
|
-
"START_DATETIME": datetime.now(),
|
|
217
|
-
"FILE": Path(file) if file else None,
|
|
218
|
-
"SDIR_OUT": sdir_out,
|
|
219
|
-
"SDIR_RUN": sdir_path,
|
|
220
|
-
}
|
|
221
|
-
)
|
|
222
|
-
return CONFIGS
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _setup_matplotlib(
|
|
226
|
-
plt: plt_module = None, agg: bool = False, **mpl_kwargs: Any
|
|
227
|
-
) -> Tuple[Any, Optional[Dict[str, Any]]]:
|
|
228
|
-
"""Configure matplotlib settings.
|
|
229
|
-
|
|
230
|
-
Parameters
|
|
231
|
-
----------
|
|
232
|
-
plt : module
|
|
233
|
-
Matplotlib.pyplot module (will be replaced with scitex.plt)
|
|
234
|
-
agg : bool
|
|
235
|
-
Whether to use Agg backend
|
|
236
|
-
**mpl_kwargs : dict
|
|
237
|
-
Additional matplotlib configuration parameters
|
|
238
|
-
|
|
239
|
-
Returns
|
|
240
|
-
-------
|
|
241
|
-
tuple
|
|
242
|
-
(plt, COLORS) - Configured scitex.plt module and color cycle
|
|
243
|
-
"""
|
|
244
|
-
if plt is not None:
|
|
245
|
-
plt.close("all")
|
|
246
|
-
_, COLORS = configure_mpl(plt, **mpl_kwargs)
|
|
247
|
-
COLORS["gray"] = COLORS["grey"]
|
|
248
|
-
|
|
249
|
-
# Note: Backend is now set early in module initialization (line 50)
|
|
250
|
-
# to avoid tkinter threading issues in headless/WSL environments.
|
|
251
|
-
# The 'agg' parameter is kept for backwards compatibility but has
|
|
252
|
-
# no effect since backend must be set before pyplot import.
|
|
253
|
-
if agg and not is_headless:
|
|
254
|
-
logger.warning(
|
|
255
|
-
"agg=True specified but backend was already set to Agg "
|
|
256
|
-
"during module initialization for headless environment"
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
# Replace matplotlib.pyplot with scitex.plt to get wrapped functions
|
|
260
|
-
import scitex.plt as stx_plt
|
|
261
|
-
|
|
262
|
-
return stx_plt, COLORS
|
|
263
|
-
return plt, None
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _simplify_relative_path(sdir: str) -> str:
|
|
267
|
-
"""
|
|
268
|
-
Simplify the relative path by removing specific patterns.
|
|
269
|
-
|
|
270
|
-
Example
|
|
271
|
-
-------
|
|
272
|
-
sdir = '/home/user/scripts/memory-load/distance_between_gs_stats/RUNNING/2024Y-09M-12D-02h44m40s_GlBZ'
|
|
273
|
-
simplified_path = simplify_relative_path(sdir)
|
|
274
|
-
print(simplified_path)
|
|
275
|
-
# Output: './memory-load/distance_between_gs_stats/'
|
|
276
|
-
|
|
277
|
-
Parameters
|
|
278
|
-
----------
|
|
279
|
-
sdir : str
|
|
280
|
-
The directory path to simplify
|
|
281
|
-
|
|
282
|
-
Returns
|
|
283
|
-
-------
|
|
284
|
-
str
|
|
285
|
-
Simplified relative path
|
|
286
|
-
"""
|
|
287
|
-
base_path = _os.getcwd()
|
|
288
|
-
relative_sdir = _os.path.relpath(sdir, base_path) if base_path else sdir
|
|
289
|
-
simplified_path = relative_sdir.replace("scripts/", "./scripts/").replace(
|
|
290
|
-
"RUNNING/", ""
|
|
291
|
-
)
|
|
292
|
-
# Remove date-time pattern and random string
|
|
293
|
-
simplified_path = re.sub(
|
|
294
|
-
r"\d{4}Y-\d{2}M-\d{2}D-\d{2}h\d{2}m\d{2}s_\w+/?$", "", simplified_path
|
|
295
|
-
)
|
|
296
|
-
return simplified_path
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _get_debug_mode() -> bool:
|
|
300
|
-
"""Get debug mode from configuration."""
|
|
301
|
-
try:
|
|
302
|
-
from scitex.io._load import load
|
|
303
|
-
|
|
304
|
-
IS_DEBUG_PATH = "./config/IS_DEBUG.yaml"
|
|
305
|
-
if _os.path.exists(IS_DEBUG_PATH):
|
|
306
|
-
IS_DEBUG = load(IS_DEBUG_PATH).get("IS_DEBUG", False)
|
|
307
|
-
if IS_DEBUG == "true":
|
|
308
|
-
IS_DEBUG = True
|
|
309
|
-
else:
|
|
310
|
-
IS_DEBUG = False
|
|
311
|
-
|
|
312
|
-
except Exception as e:
|
|
313
|
-
print(e)
|
|
314
|
-
IS_DEBUG = False
|
|
315
|
-
return IS_DEBUG
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def _clear_python_log_dir(log_dir: str) -> None:
|
|
319
|
-
"""Clear Python log directory."""
|
|
320
|
-
try:
|
|
321
|
-
if _os.path.exists(log_dir):
|
|
322
|
-
_os.system(f"rm -rf {log_dir}")
|
|
323
|
-
except Exception as e:
|
|
324
|
-
print(f"Failed to clear directory {log_dir}: {e}")
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def _get_scitex_version() -> str:
|
|
328
|
-
"""Gets scitex version"""
|
|
329
|
-
try:
|
|
330
|
-
import scitex
|
|
331
|
-
|
|
332
|
-
return scitex.__version__
|
|
333
|
-
except Exception as e:
|
|
334
|
-
print(e)
|
|
335
|
-
return "(not found)"
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def start(
|
|
339
|
-
sys: sys_module = None,
|
|
340
|
-
plt: plt_module = None,
|
|
341
|
-
file: Optional[str] = None,
|
|
342
|
-
sdir: Optional[Union[str, Path]] = None,
|
|
343
|
-
sdir_suffix: Optional[str] = None,
|
|
344
|
-
args: Optional[Any] = None,
|
|
345
|
-
os: Optional[Any] = None,
|
|
346
|
-
random: Optional[Any] = None,
|
|
347
|
-
np: Optional[Any] = None,
|
|
348
|
-
torch: Optional[Any] = None,
|
|
349
|
-
seed: int = 42,
|
|
350
|
-
agg: bool = False,
|
|
351
|
-
fig_size_mm: Tuple[int, int] = (160, 100),
|
|
352
|
-
fig_scale: float = 1.0,
|
|
353
|
-
dpi_display: int = 100,
|
|
354
|
-
dpi_save: int = 300,
|
|
355
|
-
fontsize="small",
|
|
356
|
-
autolayout=True,
|
|
357
|
-
show_execution_flow=False,
|
|
358
|
-
hide_top_right_spines: bool = True,
|
|
359
|
-
alpha: float = 0.9,
|
|
360
|
-
line_width: float = 1.0,
|
|
361
|
-
clear_logs: bool = False,
|
|
362
|
-
verbose: bool = True,
|
|
363
|
-
) -> Tuple[DotDict, Any, Any, Any, Optional[Dict[str, Any]], Any]:
|
|
364
|
-
"""Initialize experiment session with reproducibility settings.
|
|
365
|
-
|
|
366
|
-
This function replaces scitex.session.start() with enhanced session management.
|
|
367
|
-
|
|
368
|
-
Parameters
|
|
369
|
-
----------
|
|
370
|
-
sys : module, optional
|
|
371
|
-
Python sys module for I/O redirection
|
|
372
|
-
plt : module, optional
|
|
373
|
-
Matplotlib pyplot module for plotting configuration
|
|
374
|
-
file : str, optional
|
|
375
|
-
Script file path. If None, automatically detected
|
|
376
|
-
sdir : Union[str, Path], optional
|
|
377
|
-
Save directory path. Can be a string or pathlib.Path object. If None, automatically generated
|
|
378
|
-
sdir_suffix : str, optional
|
|
379
|
-
Suffix to append to save directory
|
|
380
|
-
args : object, optional
|
|
381
|
-
Command line arguments or configuration object
|
|
382
|
-
os, random, np, torch : modules, optional
|
|
383
|
-
Modules for random seed fixing
|
|
384
|
-
seed : int, default=42
|
|
385
|
-
Random seed for reproducibility
|
|
386
|
-
agg : bool, default=False
|
|
387
|
-
Whether to use matplotlib Agg backend
|
|
388
|
-
fig_size_mm : tuple, default=(160, 100)
|
|
389
|
-
Figure size in millimeters
|
|
390
|
-
fig_scale : float, default=1.0
|
|
391
|
-
Scale factor for figure size
|
|
392
|
-
dpi_display, dpi_save : int
|
|
393
|
-
DPI for display and saving
|
|
394
|
-
fontsize : str, default='small'
|
|
395
|
-
Font size setting
|
|
396
|
-
autolayout : bool, default=True
|
|
397
|
-
Enable matplotlib autolayout
|
|
398
|
-
show_execution_flow : bool, default=False
|
|
399
|
-
Show code execution flow analysis
|
|
400
|
-
hide_top_right_spines : bool, default=True
|
|
401
|
-
Whether to hide top and right spines
|
|
402
|
-
alpha : float, default=0.9
|
|
403
|
-
Default alpha value for plots
|
|
404
|
-
line_width : float, default=1.0
|
|
405
|
-
Default line width for plots
|
|
406
|
-
clear_logs : bool, default=False
|
|
407
|
-
Whether to clear existing log directory
|
|
408
|
-
verbose : bool, default=True
|
|
409
|
-
Whether to print detailed information
|
|
410
|
-
|
|
411
|
-
Returns
|
|
412
|
-
-------
|
|
413
|
-
tuple
|
|
414
|
-
(CONFIGS, stdout, stderr, plt, COLORS, rng)
|
|
415
|
-
- CONFIGS: Configuration dictionary
|
|
416
|
-
- stdout, stderr: Redirected output streams
|
|
417
|
-
- plt: Configured matplotlib.pyplot module
|
|
418
|
-
- COLORS: Color cycle dictionary
|
|
419
|
-
- rng: Global RandomStateManager instance for reproducible random generation
|
|
420
|
-
"""
|
|
421
|
-
IS_DEBUG = _get_debug_mode()
|
|
422
|
-
ID, PID = _initialize_env(IS_DEBUG)
|
|
423
|
-
|
|
424
|
-
# Convert Path objects to strings for internal processing
|
|
425
|
-
if sdir is not None and isinstance(sdir, Path):
|
|
426
|
-
sdir = str(sdir)
|
|
427
|
-
|
|
428
|
-
########################################
|
|
429
|
-
# Defines SDIR (DO NOT MODIFY THIS SECTION)
|
|
430
|
-
########################################
|
|
431
|
-
if sdir is None:
|
|
432
|
-
# Define __file__
|
|
433
|
-
if file:
|
|
434
|
-
caller_file = file
|
|
435
|
-
else:
|
|
436
|
-
caller_file = inspect.stack()[1].filename
|
|
437
|
-
if "ipython" in __file__:
|
|
438
|
-
caller_file = f"/tmp/{_os.getenv('USER')}.py"
|
|
439
|
-
|
|
440
|
-
# Convert to absolute path if relative and resolve symlinks
|
|
441
|
-
if not _os.path.isabs(caller_file):
|
|
442
|
-
caller_file = _os.path.realpath(_os.path.abspath(caller_file))
|
|
443
|
-
else:
|
|
444
|
-
# Even if already absolute, resolve symlinks to get the real path
|
|
445
|
-
caller_file = _os.path.realpath(caller_file)
|
|
446
|
-
|
|
447
|
-
# Define sdir
|
|
448
|
-
sdir = clean_path(_os.path.splitext(caller_file)[0] + f"_out/RUNNING/{ID}/")
|
|
449
|
-
|
|
450
|
-
# Optional
|
|
451
|
-
if sdir_suffix:
|
|
452
|
-
sdir = sdir[:-1] + f"-{sdir_suffix}/"
|
|
453
|
-
|
|
454
|
-
if clear_logs:
|
|
455
|
-
_clear_python_log_dir(sdir + caller_file + "/")
|
|
456
|
-
_os.makedirs(sdir, exist_ok=True)
|
|
457
|
-
relative_sdir = _simplify_relative_path(sdir)
|
|
458
|
-
########################################
|
|
459
|
-
|
|
460
|
-
# Setup configs after having all necessary parameters
|
|
461
|
-
CONFIGS = _setup_configs(IS_DEBUG, ID, PID, file, sdir, relative_sdir, verbose)
|
|
462
|
-
|
|
463
|
-
# Logging
|
|
464
|
-
if sys is not None:
|
|
465
|
-
from scitex.io._flush import flush
|
|
466
|
-
|
|
467
|
-
flush(sys)
|
|
468
|
-
# Lazy import to avoid circular dependency
|
|
469
|
-
from scitex.logging._Tee import tee
|
|
470
|
-
|
|
471
|
-
sys.stdout, sys.stderr = tee(sys, sdir=sdir, verbose=verbose)
|
|
472
|
-
CONFIGS["_sys"] = sys # Private key, won't show in user-facing pprint
|
|
473
|
-
|
|
474
|
-
# Redirect logging handlers to use the tee-wrapped streams
|
|
475
|
-
# This ensures that logger output is captured in the log files
|
|
476
|
-
import logging
|
|
477
|
-
|
|
478
|
-
# Update all existing StreamHandler instances to use our wrapped streams
|
|
479
|
-
for logger_name in list(logging.Logger.manager.loggerDict.keys()):
|
|
480
|
-
try:
|
|
481
|
-
logger = logging.getLogger(logger_name)
|
|
482
|
-
for handler in logger.handlers:
|
|
483
|
-
if isinstance(handler, logging.StreamHandler):
|
|
484
|
-
# StreamHandler typically uses stderr by default
|
|
485
|
-
if not hasattr(handler, "stream"):
|
|
486
|
-
continue
|
|
487
|
-
# Check if handler is using the original stderr or stdout
|
|
488
|
-
if handler.stream in (sys.__stderr__, sys.__stdout__):
|
|
489
|
-
# Replace with our tee-wrapped stream
|
|
490
|
-
handler.stream = (
|
|
491
|
-
sys.stderr
|
|
492
|
-
if handler.stream == sys.__stderr__
|
|
493
|
-
else sys.stdout
|
|
494
|
-
)
|
|
495
|
-
except Exception:
|
|
496
|
-
# Silently skip any logger that can't be updated
|
|
497
|
-
pass
|
|
498
|
-
|
|
499
|
-
# Also update the root logger handlers
|
|
500
|
-
try:
|
|
501
|
-
root_logger = logging.getLogger()
|
|
502
|
-
for handler in root_logger.handlers:
|
|
503
|
-
if isinstance(handler, logging.StreamHandler):
|
|
504
|
-
if not hasattr(handler, "stream"):
|
|
505
|
-
continue
|
|
506
|
-
# Check if handler is using the original stderr or stdout
|
|
507
|
-
if handler.stream in (sys.__stderr__, sys.__stdout__):
|
|
508
|
-
# Replace with our tee-wrapped stream
|
|
509
|
-
handler.stream = (
|
|
510
|
-
sys.stderr
|
|
511
|
-
if handler.stream == sys.__stderr__
|
|
512
|
-
else sys.stdout
|
|
513
|
-
)
|
|
514
|
-
except Exception:
|
|
515
|
-
# Silently skip if root logger can't be updated
|
|
516
|
-
pass
|
|
517
|
-
|
|
518
|
-
# Initialize RandomStateManager (automatically fixes all seeds)
|
|
519
|
-
rng = RandomStateManager(seed=seed, verbose=verbose)
|
|
520
|
-
if verbose:
|
|
521
|
-
logger.info(f"Initialized RandomStateManager with seed {seed}")
|
|
522
|
-
|
|
523
|
-
# Matplotlib configurations
|
|
524
|
-
plt, COLORS = _setup_matplotlib(
|
|
525
|
-
plt,
|
|
526
|
-
agg,
|
|
527
|
-
fig_size_mm=fig_size_mm,
|
|
528
|
-
fig_scale=fig_scale,
|
|
529
|
-
dpi_display=dpi_display,
|
|
530
|
-
dpi_save=dpi_save,
|
|
531
|
-
hide_top_right_spines=hide_top_right_spines,
|
|
532
|
-
alpha=alpha,
|
|
533
|
-
line_width=line_width,
|
|
534
|
-
fontsize=fontsize,
|
|
535
|
-
autolayout=autolayout,
|
|
536
|
-
verbose=verbose,
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
# Adds argument-parsed variables
|
|
540
|
-
if args is not None:
|
|
541
|
-
CONFIGS["ARGS"] = vars(args) if hasattr(args, "__dict__") else args
|
|
542
|
-
|
|
543
|
-
CONFIGS = DotDict(CONFIGS)
|
|
544
|
-
|
|
545
|
-
# Register session
|
|
546
|
-
session_manager = get_global_session_manager()
|
|
547
|
-
session_manager.create_session(ID, CONFIGS)
|
|
548
|
-
|
|
549
|
-
_print_header(ID, PID, file, args, CONFIGS, verbose)
|
|
550
|
-
|
|
551
|
-
if show_execution_flow:
|
|
552
|
-
structure = analyze_code_flow(file)
|
|
553
|
-
_printc(structure)
|
|
554
|
-
|
|
555
|
-
# Return appropriate values based on whether sys was provided
|
|
556
|
-
if sys is not None:
|
|
557
|
-
return CONFIGS, sys.stdout, sys.stderr, plt, COLORS, rng
|
|
558
|
-
else:
|
|
559
|
-
return CONFIGS, None, None, plt, COLORS, rng
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def _format_diff_time(diff_time):
|
|
563
|
-
"""Format time difference as HH:MM:SS."""
|
|
564
|
-
total_seconds = int(diff_time.total_seconds())
|
|
565
|
-
hours = total_seconds // 3600
|
|
566
|
-
minutes = (total_seconds % 3600) // 60
|
|
567
|
-
seconds = total_seconds % 60
|
|
568
|
-
diff_time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
569
|
-
return diff_time_str
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
def _process_timestamp(CONFIG, verbose=True):
|
|
573
|
-
"""Process session timestamps."""
|
|
574
|
-
try:
|
|
575
|
-
CONFIG["END_DATETIME"] = datetime.now()
|
|
576
|
-
CONFIG["RUN_DURATION"] = _format_diff_time(
|
|
577
|
-
CONFIG["END_DATETIME"] - CONFIG["START_DATETIME"]
|
|
578
|
-
)
|
|
579
|
-
if verbose:
|
|
580
|
-
logger.info(
|
|
581
|
-
f"\nSTART TIME: {CONFIG['START_DATETIME']}\n"
|
|
582
|
-
f"END TIME: {CONFIG['END_DATETIME']}\n"
|
|
583
|
-
f"RUN DURATION: {CONFIG['RUN_DURATION']}\n"
|
|
584
|
-
)
|
|
585
|
-
|
|
586
|
-
except Exception as e:
|
|
587
|
-
print(e)
|
|
588
|
-
|
|
589
|
-
return CONFIG
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def _save_configs(CONFIG):
|
|
593
|
-
"""Save configuration to files."""
|
|
594
|
-
from scitex.io._save import save as scitex_io_save
|
|
595
|
-
|
|
596
|
-
# Convert to dict with all keys (including private ones) for saving
|
|
597
|
-
config_dict = (
|
|
598
|
-
CONFIG.to_dict(include_private=True) if hasattr(CONFIG, "to_dict") else CONFIG
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
scitex_io_save(
|
|
602
|
-
config_dict, str(CONFIG["SDIR_RUN"] / "CONFIGS/CONFIG.pkl"), verbose=False
|
|
603
|
-
)
|
|
604
|
-
scitex_io_save(
|
|
605
|
-
config_dict, str(CONFIG["SDIR_RUN"] / "CONFIGS/CONFIG.yaml"), verbose=False
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
def _escape_ansi_from_log_files(log_files):
|
|
610
|
-
"""Remove ANSI escape sequences from log files.
|
|
611
|
-
|
|
612
|
-
Parameters
|
|
613
|
-
----------
|
|
614
|
-
log_files : list
|
|
615
|
-
List of log file paths to clean
|
|
616
|
-
"""
|
|
617
|
-
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
618
|
-
|
|
619
|
-
# ANSI code escape
|
|
620
|
-
for f in log_files:
|
|
621
|
-
with open(f, "r", encoding="utf-8") as file:
|
|
622
|
-
content = file.read()
|
|
623
|
-
cleaned_content = ansi_escape.sub("", content)
|
|
624
|
-
with open(f, "w", encoding="utf-8") as file:
|
|
625
|
-
file.write(cleaned_content)
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
def _args_to_str(args_dict):
|
|
629
|
-
"""Convert args dictionary to formatted string."""
|
|
630
|
-
if args_dict:
|
|
631
|
-
max_key_length = max(len(str(k)) for k in args_dict.keys())
|
|
632
|
-
return "\n".join(
|
|
633
|
-
f"{str(k):<{max_key_length}} : {str(v)}"
|
|
634
|
-
for k, v in sorted(args_dict.items())
|
|
635
|
-
)
|
|
636
|
-
else:
|
|
637
|
-
return ""
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
def running2finished(CONFIG, exit_status=None, remove_src_dir=True, max_wait=60):
|
|
641
|
-
"""Move session from RUNNING to FINISHED directory.
|
|
642
|
-
|
|
643
|
-
Parameters
|
|
644
|
-
----------
|
|
645
|
-
CONFIG : dict
|
|
646
|
-
Session configuration dictionary
|
|
647
|
-
exit_status : int, optional
|
|
648
|
-
Exit status code (0=success, 1=error, None=finished)
|
|
649
|
-
remove_src_dir : bool, default=True
|
|
650
|
-
Whether to remove source directory after copy
|
|
651
|
-
max_wait : int, default=60
|
|
652
|
-
Maximum seconds to wait for copy operation
|
|
653
|
-
|
|
654
|
-
Returns
|
|
655
|
-
-------
|
|
656
|
-
dict
|
|
657
|
-
Updated configuration with new SDIR
|
|
658
|
-
"""
|
|
659
|
-
if exit_status == 0:
|
|
660
|
-
dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED_SUCCESS/")
|
|
661
|
-
elif exit_status == 1:
|
|
662
|
-
dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED_ERROR/")
|
|
663
|
-
else: # exit_status is None:
|
|
664
|
-
dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED/")
|
|
665
|
-
|
|
666
|
-
src_dir = str(CONFIG["SDIR_RUN"])
|
|
667
|
-
_os.makedirs(dest_dir, exist_ok=True)
|
|
668
|
-
try:
|
|
669
|
-
# Copy files individually
|
|
670
|
-
for item in _os.listdir(src_dir):
|
|
671
|
-
s = _os.path.join(src_dir, item)
|
|
672
|
-
d = _os.path.join(dest_dir, item)
|
|
673
|
-
if _os.path.isdir(s):
|
|
674
|
-
shutil.copytree(s, d)
|
|
675
|
-
else:
|
|
676
|
-
shutil.copy2(s, d)
|
|
677
|
-
|
|
678
|
-
start_time = time.time()
|
|
679
|
-
while not _os.path.exists(dest_dir) and time.time() - start_time < max_wait:
|
|
680
|
-
time.sleep(0.1)
|
|
681
|
-
if _os.path.exists(dest_dir):
|
|
682
|
-
print()
|
|
683
|
-
logger.success(
|
|
684
|
-
f"Congratulations! The script completed: {dest_dir}",
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
if remove_src_dir:
|
|
688
|
-
shutil.rmtree(src_dir)
|
|
689
|
-
|
|
690
|
-
# Cleanup RUNNING when empty
|
|
691
|
-
running_base = os.path.dirname(src_dir.rstrip("/"))
|
|
692
|
-
if os.path.basename(running_base) == "RUNNING":
|
|
693
|
-
try:
|
|
694
|
-
os.rmdir(running_base)
|
|
695
|
-
except OSError:
|
|
696
|
-
pass
|
|
697
|
-
|
|
698
|
-
else:
|
|
699
|
-
print(f"Copy operation timed out after {max_wait} seconds")
|
|
700
|
-
|
|
701
|
-
CONFIG["SDIR_RUN"] = Path(dest_dir)
|
|
702
|
-
except Exception as e:
|
|
703
|
-
print(e)
|
|
704
|
-
|
|
705
|
-
finally:
|
|
706
|
-
return CONFIG
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def close(CONFIG, message=":)", notify=False, verbose=True, exit_status=None):
|
|
710
|
-
"""Close experiment session and finalize logging.
|
|
711
|
-
|
|
712
|
-
This function replaces scitex.session.close() with enhanced session management.
|
|
713
|
-
|
|
714
|
-
Parameters
|
|
715
|
-
----------
|
|
716
|
-
CONFIG : DotDict
|
|
717
|
-
Configuration dictionary from start()
|
|
718
|
-
message : str, default=':)'
|
|
719
|
-
Completion message
|
|
720
|
-
notify : bool, default=False
|
|
721
|
-
Whether to send notification
|
|
722
|
-
verbose : bool, default=True
|
|
723
|
-
Whether to print verbose output
|
|
724
|
-
exit_status : int, optional
|
|
725
|
-
Exit status code (0=success, 1=error, None=finished)
|
|
726
|
-
"""
|
|
727
|
-
sys = None # Initialize sys outside try block
|
|
728
|
-
try:
|
|
729
|
-
CONFIG.EXIT_STATUS = exit_status
|
|
730
|
-
CONFIG = CONFIG.to_dict()
|
|
731
|
-
CONFIG = _process_timestamp(CONFIG, verbose=verbose)
|
|
732
|
-
sys = CONFIG.pop("_sys", None) # Pop private sys reference
|
|
733
|
-
|
|
734
|
-
# CRITICAL: Close matplotlib BEFORE closing streams to prevent segfault
|
|
735
|
-
try:
|
|
736
|
-
import matplotlib
|
|
737
|
-
import matplotlib.pyplot as plt
|
|
738
|
-
import atexit
|
|
739
|
-
|
|
740
|
-
# Close all figures
|
|
741
|
-
plt.close("all")
|
|
742
|
-
|
|
743
|
-
# CRITICAL: Unregister matplotlib's atexit handlers to prevent segfault
|
|
744
|
-
# Matplotlib registers handlers that try to cleanup on exit,
|
|
745
|
-
# but we're closing streams first, causing segfault
|
|
746
|
-
try:
|
|
747
|
-
# Remove matplotlib-related atexit handlers
|
|
748
|
-
import weakref
|
|
749
|
-
|
|
750
|
-
# Matplotlib uses weakref for some cleanup
|
|
751
|
-
if hasattr(matplotlib, "_pylab_helpers"):
|
|
752
|
-
matplotlib._pylab_helpers.Gcf.destroy_all()
|
|
753
|
-
|
|
754
|
-
# Clear any pyplot state
|
|
755
|
-
if hasattr(plt, "get_fignums"):
|
|
756
|
-
for fignum in plt.get_fignums():
|
|
757
|
-
plt.close(fignum)
|
|
758
|
-
|
|
759
|
-
except Exception:
|
|
760
|
-
pass
|
|
761
|
-
|
|
762
|
-
# Force garbage collection to cleanup matplotlib resources
|
|
763
|
-
import gc
|
|
764
|
-
|
|
765
|
-
gc.collect()
|
|
766
|
-
|
|
767
|
-
if verbose:
|
|
768
|
-
logger.info("Matplotlib cleanup completed")
|
|
769
|
-
|
|
770
|
-
except Exception as e:
|
|
771
|
-
if verbose:
|
|
772
|
-
logger.warning(f"Could not close matplotlib: {e}")
|
|
773
|
-
|
|
774
|
-
_save_configs(CONFIG)
|
|
775
|
-
|
|
776
|
-
# RUNNING to FINISHED
|
|
777
|
-
CONFIG = running2finished(CONFIG, exit_status=exit_status)
|
|
778
|
-
|
|
779
|
-
# ANSI code escape
|
|
780
|
-
log_files = _glob(str(CONFIG["SDIR_RUN"]) + "logs/*.log")
|
|
781
|
-
_escape_ansi_from_log_files(log_files)
|
|
782
|
-
|
|
783
|
-
if CONFIG.get("ARGS"):
|
|
784
|
-
message += f"\n{_args_to_str(CONFIG.get('ARGS'))}"
|
|
785
|
-
|
|
786
|
-
if notify:
|
|
787
|
-
try:
|
|
788
|
-
message = (
|
|
789
|
-
f"[DEBUG]\n" + str(message)
|
|
790
|
-
if CONFIG.get("DEBUG", False)
|
|
791
|
-
else str(message)
|
|
792
|
-
)
|
|
793
|
-
scitex_utils_notify(
|
|
794
|
-
message=message,
|
|
795
|
-
ID=CONFIG["ID"],
|
|
796
|
-
file=CONFIG.get("FILE"),
|
|
797
|
-
attachment_paths=log_files,
|
|
798
|
-
verbose=verbose,
|
|
799
|
-
)
|
|
800
|
-
except Exception as e:
|
|
801
|
-
print(e)
|
|
802
|
-
|
|
803
|
-
# Close session
|
|
804
|
-
session_manager = get_global_session_manager()
|
|
805
|
-
session_manager.close_session(CONFIG["ID"])
|
|
806
|
-
|
|
807
|
-
finally:
|
|
808
|
-
# Only close if they're custom file objects (Tee objects)
|
|
809
|
-
if sys:
|
|
810
|
-
try:
|
|
811
|
-
# First, flush all outputs
|
|
812
|
-
if hasattr(sys, "stdout") and hasattr(sys.stdout, "flush"):
|
|
813
|
-
sys.stdout.flush()
|
|
814
|
-
if hasattr(sys, "stderr") and hasattr(sys.stderr, "flush"):
|
|
815
|
-
sys.stderr.flush()
|
|
816
|
-
|
|
817
|
-
# Then close Tee objects
|
|
818
|
-
if hasattr(sys, "stdout") and hasattr(sys.stdout, "_log_file"):
|
|
819
|
-
sys.stdout.close()
|
|
820
|
-
if hasattr(sys, "stderr") and hasattr(sys.stderr, "_log_file"):
|
|
821
|
-
sys.stderr.close()
|
|
822
|
-
except Exception:
|
|
823
|
-
# Silent fail to ensure logs are saved even if there's an error
|
|
824
|
-
pass
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
# EOF
|