scitex 2.16.2__py3-none-any.whl → 2.17.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.
Files changed (85) hide show
  1. scitex/_mcp_resources/_cheatsheet.py +1 -1
  2. scitex/_mcp_resources/_modules.py +1 -1
  3. scitex/_mcp_tools/__init__.py +2 -0
  4. scitex/_mcp_tools/verify.py +256 -0
  5. scitex/cli/main.py +2 -0
  6. scitex/cli/verify.py +476 -0
  7. scitex/dev/plt/__init__.py +1 -1
  8. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +90 -0
  9. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +1571 -0
  10. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +6262 -0
  11. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +1274 -0
  12. scitex/dev/plt/data/mpl/dir_ax.txt +459 -0
  13. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  14. scitex/dev/plt/mpl/get_signatures.py +1 -1
  15. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  16. scitex/io/_load.py +8 -1
  17. scitex/io/_save.py +12 -0
  18. scitex/scholar/data/.gitkeep +0 -0
  19. scitex/scholar/data/README.md +44 -0
  20. scitex/scholar/data/bib_files/bibliography.bib +1952 -0
  21. scitex/scholar/data/bib_files/neurovista.bib +277 -0
  22. scitex/scholar/data/bib_files/neurovista_enriched.bib +441 -0
  23. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +441 -0
  24. scitex/scholar/data/bib_files/neurovista_processed.bib +338 -0
  25. scitex/scholar/data/bib_files/openaccess.bib +89 -0
  26. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +2178 -0
  27. scitex/scholar/data/bib_files/pac.bib +698 -0
  28. scitex/scholar/data/bib_files/pac_enriched.bib +1061 -0
  29. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  30. scitex/scholar/data/bib_files/pac_titles.txt +75 -0
  31. scitex/scholar/data/bib_files/paywalled.bib +98 -0
  32. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +58 -0
  33. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +87 -0
  34. scitex/scholar/data/bib_files/seizure_prediction.bib +694 -0
  35. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  36. scitex/scholar/data/bib_files/test_complete_enriched.bib +437 -0
  37. scitex/scholar/data/bib_files/test_final_enriched.bib +437 -0
  38. scitex/scholar/data/bib_files/test_seizure.bib +46 -0
  39. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  40. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  41. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  42. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  43. scitex/scholar/data/impact_factor.db +0 -0
  44. scitex/session/README.md +2 -2
  45. scitex/session/__init__.py +1 -0
  46. scitex/session/_decorator.py +57 -33
  47. scitex/session/_lifecycle/__init__.py +23 -0
  48. scitex/session/_lifecycle/_close.py +225 -0
  49. scitex/session/_lifecycle/_config.py +112 -0
  50. scitex/session/_lifecycle/_matplotlib.py +83 -0
  51. scitex/session/_lifecycle/_start.py +246 -0
  52. scitex/session/_lifecycle/_utils.py +186 -0
  53. scitex/session/_manager.py +40 -3
  54. scitex/session/template.py +1 -1
  55. scitex/template/_templates/plt.py +1 -1
  56. scitex/template/_templates/session.py +1 -1
  57. scitex/verify/README.md +312 -0
  58. scitex/verify/__init__.py +212 -0
  59. scitex/verify/_chain.py +369 -0
  60. scitex/verify/_db.py +600 -0
  61. scitex/verify/_hash.py +187 -0
  62. scitex/verify/_integration.py +127 -0
  63. scitex/verify/_rerun.py +253 -0
  64. scitex/verify/_tracker.py +330 -0
  65. scitex/verify/_visualize.py +48 -0
  66. scitex/verify/_viz/__init__.py +56 -0
  67. scitex/verify/_viz/_colors.py +84 -0
  68. scitex/verify/_viz/_format.py +302 -0
  69. scitex/verify/_viz/_json.py +192 -0
  70. scitex/verify/_viz/_mermaid.py +440 -0
  71. scitex/verify/_viz/_plotly.py +193 -0
  72. scitex/verify/_viz/_templates.py +246 -0
  73. scitex/verify/_viz/_utils.py +56 -0
  74. {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/METADATA +1 -1
  75. {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/RECORD +78 -29
  76. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +0 -462
  77. scitex/scholar/url_finder/.tmp/open_url/README.md +0 -223
  78. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +0 -694
  79. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +0 -1160
  80. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +0 -344
  81. scitex/scholar/url_finder/.tmp/open_url/__init__.py +0 -24
  82. scitex/session/_lifecycle.py +0 -827
  83. {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/WHEEL +0 -0
  84. {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/entry_points.txt +0 -0
  85. {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -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