CodeEntropy 2.2.4__py3-none-any.whl → 2.4.1__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 (43) hide show
  1. CodeEntropy/__init__.py +1 -1
  2. CodeEntropy/config/argparse.py +232 -66
  3. CodeEntropy/config/runtime.py +18 -3
  4. CodeEntropy/core/dask_clusters.py +279 -0
  5. CodeEntropy/entropy/workflow.py +148 -89
  6. CodeEntropy/levels/axes.py +550 -55
  7. CodeEntropy/levels/dihedrals/__init__.py +0 -0
  8. CodeEntropy/levels/dihedrals/angle_observations.py +271 -0
  9. CodeEntropy/levels/dihedrals/conformational_state_builder.py +204 -0
  10. CodeEntropy/levels/dihedrals/kernels.py +116 -0
  11. CodeEntropy/levels/dihedrals/peak_detection.py +270 -0
  12. CodeEntropy/levels/dihedrals/state_assignment.py +281 -0
  13. CodeEntropy/levels/dihedrals/topology.py +233 -0
  14. CodeEntropy/levels/execution/__init__.py +0 -0
  15. CodeEntropy/levels/execution/chunks.py +28 -0
  16. CodeEntropy/levels/execution/policy.py +84 -0
  17. CodeEntropy/levels/execution/reducers.py +386 -0
  18. CodeEntropy/levels/execution/scheduler.py +306 -0
  19. CodeEntropy/levels/execution/tasks.py +330 -0
  20. CodeEntropy/levels/graph/__init__.py +0 -0
  21. CodeEntropy/levels/graph/conformation_dag.py +95 -0
  22. CodeEntropy/levels/graph/frame_dag.py +108 -0
  23. CodeEntropy/levels/graph/level_dag.py +206 -0
  24. CodeEntropy/levels/neighbors.py +144 -202
  25. CodeEntropy/levels/nodes/accumulators.py +13 -27
  26. CodeEntropy/levels/nodes/axes_topology.py +316 -0
  27. CodeEntropy/levels/nodes/covariance.py +240 -152
  28. CodeEntropy/levels/search.py +33 -37
  29. CodeEntropy/trajectory/__init__.py +0 -0
  30. CodeEntropy/trajectory/frames.py +134 -0
  31. CodeEntropy/{levels → trajectory}/mda.py +148 -92
  32. CodeEntropy/trajectory/source.py +49 -0
  33. {codeentropy-2.2.4.dist-info → codeentropy-2.4.1.dist-info}/METADATA +6 -5
  34. codeentropy-2.4.1.dist-info/RECORD +65 -0
  35. {codeentropy-2.2.4.dist-info → codeentropy-2.4.1.dist-info}/WHEEL +1 -1
  36. CodeEntropy/levels/dihedrals.py +0 -538
  37. CodeEntropy/levels/frame_dag.py +0 -116
  38. CodeEntropy/levels/level_dag.py +0 -331
  39. CodeEntropy/levels/nodes/conformations.py +0 -105
  40. CodeEntropy/levels/nodes/find_neighbors.py +0 -95
  41. codeentropy-2.2.4.dist-info/RECORD +0 -48
  42. {codeentropy-2.2.4.dist-info → codeentropy-2.4.1.dist-info}/entry_points.txt +0 -0
  43. {codeentropy-2.2.4.dist-info → codeentropy-2.4.1.dist-info}/licenses/LICENSE +0 -0
CodeEntropy/__init__.py CHANGED
@@ -8,4 +8,4 @@ entropy estimates, supporting a wide range of applications in molecular simulati
8
8
  and statistical mechanics.
9
9
  """
10
10
 
11
- __version__ = "2.2.4"
11
+ __version__ = "2.4.1"
@@ -7,8 +7,10 @@ This module provides:
7
7
 
8
8
  2) A `ConfigResolver` that:
9
9
 
10
- - loads YAML configuration (if present),
11
- - merges YAML values with CLI values (CLI wins),
10
+ - loads YAML configuration if present,
11
+ - merges YAML values with CLI values, with CLI values taking precedence,
12
+ - fills missing defaults,
13
+ - auto-detects conda/mamba settings for HPC runs,
12
14
  - adjusts logging verbosity,
13
15
  - validates a subset of runtime inputs against the trajectory.
14
16
 
@@ -29,6 +31,8 @@ from typing import Any
29
31
 
30
32
  import yaml
31
33
 
34
+ from CodeEntropy.core.dask_clusters import HPCDaskManager
35
+
32
36
  logger = logging.getLogger(__name__)
33
37
 
34
38
 
@@ -39,10 +43,9 @@ class ArgSpec:
39
43
  Attributes:
40
44
  help: Help text shown in CLI usage.
41
45
  default: Default value if not provided via CLI or YAML.
42
- type: Python type for parsing (e.g., int, float, str, bool). If bool,
43
- `ConfigResolver.str2bool` will be used.
44
- action: Optional argparse action (e.g., "store_true").
45
- nargs: Optional nargs spec (e.g., "+").
46
+ type: Python type for parsing, such as int, float, str, or bool.
47
+ action: Optional argparse action, such as "store_true".
48
+ nargs: Optional nargs spec, such as "+".
46
49
  """
47
50
 
48
51
  help: str
@@ -96,7 +99,7 @@ ARG_SPECS: dict[str, ArgSpec] = {
96
99
  "step": ArgSpec(
97
100
  type=int,
98
101
  default=1,
99
- help="Interval between two consecutive frames to be read index",
102
+ help="Interval between two consecutive frames to be read.",
100
103
  ),
101
104
  "bin_width": ArgSpec(
102
105
  type=int,
@@ -106,7 +109,7 @@ ARG_SPECS: dict[str, ArgSpec] = {
106
109
  "temperature": ArgSpec(
107
110
  type=float,
108
111
  default=298.0,
109
- help="Temperature for entropy calculation (K)",
112
+ help="Temperature for entropy calculation in K",
110
113
  ),
111
114
  "verbose": ArgSpec(
112
115
  action="store_true",
@@ -116,8 +119,7 @@ ARG_SPECS: dict[str, ArgSpec] = {
116
119
  type=str,
117
120
  default="output_file.json",
118
121
  help=(
119
- "Name of the output file to write results to (filename only). Defaults "
120
- "to output_file.json"
122
+ "Name of the output file to write results to. Defaults to output_file.json."
121
123
  ),
122
124
  ),
123
125
  "force_partitioning": ArgSpec(
@@ -138,35 +140,117 @@ ARG_SPECS: dict[str, ArgSpec] = {
138
140
  "combined_forcetorque": ArgSpec(
139
141
  type=bool,
140
142
  default=True,
141
- help="Use combined force-torque matrix for residue level vibrational entropies",
143
+ help="Use combined force-torque matrix for residue-level vibrational entropies",
142
144
  ),
143
145
  "customised_axes": ArgSpec(
144
146
  type=bool,
145
147
  default=True,
146
- help="Use bonded axes to rotate forces for UA level vibrational entropies",
148
+ help="Use bonded axes to rotate forces for united-atom vibrational entropies",
147
149
  ),
148
150
  "search_type": ArgSpec(
149
151
  type=str,
150
152
  default="RAD",
151
- help="Type of neighbor search to use."
152
- "Default RAD; grid search is also available",
153
+ help="Type of neighbour search to use. Default is RAD; grid search is also "
154
+ "available.",
155
+ ),
156
+ "parallel_frames": ArgSpec(
157
+ type=bool,
158
+ default=False,
159
+ help="Execute frame-local covariance calculations in parallel using Dask.",
160
+ ),
161
+ "use_dask": ArgSpec(
162
+ type=bool,
163
+ default=False,
164
+ help="Enable local Dask frame parallelism.",
165
+ ),
166
+ "dask_workers": ArgSpec(
167
+ type=int,
168
+ default=None,
169
+ help="Number of local Dask worker processes.",
170
+ ),
171
+ "dask_threads_per_worker": ArgSpec(
172
+ type=int,
173
+ default=1,
174
+ help="Threads per local Dask worker. Use 1 for MDAnalysis trajectory safety.",
175
+ ),
176
+ "hpc": ArgSpec(
177
+ type=bool,
178
+ default=False,
179
+ help="Use a SLURM-backed Dask cluster for parallel frame execution.",
180
+ ),
181
+ "hpc_account": ArgSpec(
182
+ type=str,
183
+ default=None,
184
+ help="SLURM account/project code.",
185
+ ),
186
+ "hpc_qos": ArgSpec(
187
+ type=str,
188
+ default=None,
189
+ help="Optional SLURM QoS.",
190
+ ),
191
+ "hpc_constraint": ArgSpec(
192
+ type=str,
193
+ default=None,
194
+ help="Optional SLURM node constraint.",
195
+ ),
196
+ "hpc_queue": ArgSpec(
197
+ type=str,
198
+ default=None,
199
+ help="SLURM partition/queue.",
200
+ ),
201
+ "hpc_cores": ArgSpec(
202
+ type=int,
203
+ default=1,
204
+ help="Number of CPU cores per Dask worker job.",
205
+ ),
206
+ "hpc_processes": ArgSpec(
207
+ type=int,
208
+ default=1,
209
+ help="Number of Dask worker processes per SLURM job.",
210
+ ),
211
+ "hpc_memory": ArgSpec(
212
+ type=str,
213
+ default="4GB",
214
+ help="Memory requested per Dask worker job.",
215
+ ),
216
+ "hpc_walltime": ArgSpec(
217
+ type=str,
218
+ default="01:00:00",
219
+ help="Walltime for each Dask worker job, formatted as HH:MM:SS.",
220
+ ),
221
+ "hpc_nodes": ArgSpec(
222
+ type=int,
223
+ default=1,
224
+ help="Number of SLURM Dask worker jobs to launch.",
225
+ ),
226
+ "submit": ArgSpec(
227
+ type=bool,
228
+ default=False,
229
+ help="Submit a master SLURM job instead of running immediately.",
230
+ ),
231
+ "conda_path": ArgSpec(
232
+ type=str,
233
+ default=None,
234
+ help="Path to conda executable used by the SLURM worker prologue.",
235
+ ),
236
+ "conda_exec": ArgSpec(
237
+ type=str,
238
+ default=None,
239
+ help="Conda-compatible executable to use, usually conda or mamba.",
240
+ ),
241
+ "conda_env": ArgSpec(
242
+ type=str,
243
+ default=None,
244
+ help="Conda environment name to activate on Dask workers.",
153
245
  ),
154
246
  }
155
247
 
156
248
 
157
249
  class ConfigResolver:
158
- """Load, merge, and validate CodeEntropy configuration.
159
-
160
- This class provides a consistent interface for:
161
- - YAML config discovery/loading
162
- - CLI parser construction
163
- - merging YAML values with CLI values (CLI wins)
164
- - setting logging verbosity
165
- - validating trajectory-related numeric parameters
166
- """
250
+ """Load, merge, and validate CodeEntropy configuration."""
167
251
 
168
252
  def __init__(self, arg_specs: dict[str, ArgSpec] | None = None) -> None:
169
- """Initialize the manager.
253
+ """Initialise the resolver.
170
254
 
171
255
  Args:
172
256
  arg_specs: Optional override for argument specs. If omitted, uses
@@ -177,16 +261,11 @@ class ConfigResolver:
177
261
  def load_config(self, directory_path: str) -> dict[str, Any]:
178
262
  """Load the first YAML config file found in a directory.
179
263
 
180
- The current behavior matches your existing workflow:
181
- - searches for ``*.yaml`` in `directory_path`,
182
- - loads the first match,
183
- - returns ``{"run1": {}}`` if none found or file is empty/invalid.
184
-
185
264
  Args:
186
265
  directory_path: Directory to search for YAML files.
187
266
 
188
267
  Returns:
189
- A configuration dictionary.
268
+ Configuration dictionary.
190
269
  """
191
270
  yaml_files = glob.glob(os.path.join(directory_path, "*.yaml"))
192
271
  if not yaml_files:
@@ -206,41 +285,38 @@ class ConfigResolver:
206
285
  def str2bool(value: Any) -> bool:
207
286
  """Convert a string or boolean input into a boolean.
208
287
 
209
- Accepts common string representations:
210
- - True values: "true", "t", "yes", "1"
211
- - False values: "false", "f", "no", "0"
212
-
213
- If the input is already a boolean, it is returned as-is.
214
-
215
288
  Args:
216
289
  value: Input value to convert.
217
290
 
218
291
  Returns:
219
- The corresponding boolean.
292
+ Corresponding boolean value.
220
293
 
221
294
  Raises:
222
295
  argparse.ArgumentTypeError: If the input cannot be interpreted as a boolean.
223
296
  """
224
297
  if isinstance(value, bool):
225
298
  return value
299
+
226
300
  if not isinstance(value, str):
227
- raise argparse.ArgumentTypeError("Boolean value expected (True/False).")
301
+ raise argparse.ArgumentTypeError("Boolean value expected: true or false.")
228
302
 
229
303
  lowered = value.lower()
230
- if lowered in {"true", "t", "yes", "1"}:
304
+ if lowered in {"true", "t", "yes", "y", "1"}:
231
305
  return True
232
- if lowered in {"false", "f", "no", "0"}:
306
+
307
+ if lowered in {"false", "f", "no", "n", "0"}:
233
308
  return False
234
- raise argparse.ArgumentTypeError("Boolean value expected (True/False).")
309
+
310
+ raise argparse.ArgumentTypeError("Boolean value expected: true or false.")
235
311
 
236
312
  def build_parser(self) -> argparse.ArgumentParser:
237
- """Build an ArgumentParser from argument specs.
313
+ """Build an ArgumentParser from the argument specs.
238
314
 
239
315
  Returns:
240
- An argparse.ArgumentParser configured with all supported flags.
316
+ Configured argparse.ArgumentParser.
241
317
  """
242
318
  parser = argparse.ArgumentParser(
243
- description="CodeEntropy: Entropy calculation with MCC method."
319
+ description="CodeEntropy: entropy calculation with the MCC method."
244
320
  )
245
321
 
246
322
  for name, spec in self._arg_specs.items():
@@ -260,10 +336,13 @@ class ConfigResolver:
260
336
  continue
261
337
 
262
338
  kwargs: dict[str, Any] = {}
339
+
263
340
  if spec.type is not None:
264
341
  kwargs["type"] = spec.type
342
+
265
343
  if spec.default is not None:
266
344
  kwargs["default"] = spec.default
345
+
267
346
  if spec.nargs is not None:
268
347
  kwargs["nargs"] = spec.nargs
269
348
 
@@ -272,27 +351,31 @@ class ConfigResolver:
272
351
  return parser
273
352
 
274
353
  def resolve(
275
- self, args: argparse.Namespace, run_config: dict[str, Any] | None
354
+ self,
355
+ args: argparse.Namespace,
356
+ run_config: dict[str, Any] | None,
276
357
  ) -> argparse.Namespace:
277
- """Merge CLI arguments with YAML configuration and adjust logging level.
358
+ """Merge CLI arguments with YAML configuration and fill defaults.
278
359
 
279
360
  Merge rule:
280
- - CLI explicitly-provided values take precedence.
281
- - YAML values fill in missing values.
361
+ - CLI explicitly provided values take precedence.
362
+ - YAML values fill in values not provided by CLI.
282
363
  - Defaults fill in anything still unset.
364
+ - HPC conda/mamba settings are auto-detected if missing.
283
365
 
284
366
  Args:
285
367
  args: Parsed CLI arguments.
286
368
  run_config: Dict of YAML values for a specific run, or None.
287
369
 
288
370
  Returns:
289
- The mutated argparse.Namespace with merged values.
371
+ Mutated argparse.Namespace with merged values.
290
372
 
291
373
  Raises:
292
374
  TypeError: If `run_config` is not a dict or None.
293
375
  """
294
376
  if run_config is None:
295
377
  run_config = {}
378
+
296
379
  if not isinstance(run_config, dict):
297
380
  raise TypeError("run_config must be a dictionary or None.")
298
381
 
@@ -306,15 +389,17 @@ class ConfigResolver:
306
389
 
307
390
  self._apply_yaml_defaults(args, run_config, cli_provided)
308
391
  self._ensure_defaults(args)
392
+ self._apply_hpc_conda_auto_detection(args)
309
393
  self._apply_logging_level(bool(getattr(args, "verbose", False)))
310
394
 
311
395
  return args
312
396
 
313
397
  @staticmethod
314
398
  def _detect_cli_overrides(
315
- args_dict: dict[str, Any], default_dict: dict[str, Any]
399
+ args_dict: dict[str, Any],
400
+ default_dict: dict[str, Any],
316
401
  ) -> set[str]:
317
- """Detect which args were explicitly overridden in the CLI.
402
+ """Detect which args were explicitly overridden on the CLI.
318
403
 
319
404
  Args:
320
405
  args_dict: Parsed arg values.
@@ -323,7 +408,9 @@ class ConfigResolver:
323
408
  Returns:
324
409
  Set of argument names that differ from defaults.
325
410
  """
326
- return {k for k, v in args_dict.items() if v != default_dict.get(k)}
411
+ return {
412
+ key for key, value in args_dict.items() if value != default_dict.get(key)
413
+ }
327
414
 
328
415
  def _apply_yaml_defaults(
329
416
  self,
@@ -334,27 +421,44 @@ class ConfigResolver:
334
421
  """Apply YAML values onto args for keys not provided by CLI.
335
422
 
336
423
  Args:
337
- args: Parsed CLI arguments (mutated in-place).
424
+ args: Parsed CLI arguments, mutated in place.
338
425
  run_config: YAML dict for this run.
339
426
  cli_provided: Keys explicitly set via CLI.
340
427
  """
341
428
  for key, yaml_value in run_config.items():
342
429
  if yaml_value is None or key in cli_provided:
343
430
  continue
431
+
344
432
  if key in self._arg_specs:
345
- logger.debug(f"Using YAML value for {key}: {yaml_value}")
433
+ logger.debug("Using YAML value for %s: %s", key, yaml_value)
346
434
  setattr(args, key, yaml_value)
347
435
 
348
436
  def _ensure_defaults(self, args: argparse.Namespace) -> None:
349
437
  """Ensure all known args have defaults if still unset.
350
438
 
351
439
  Args:
352
- args: Parsed arg namespace (mutated in-place).
440
+ args: Parsed arg namespace, mutated in place.
353
441
  """
354
442
  for key, spec in self._arg_specs.items():
355
443
  if getattr(args, key, None) is None:
356
444
  setattr(args, key, spec.default)
357
445
 
446
+ @staticmethod
447
+ def _apply_hpc_conda_auto_detection(args: argparse.Namespace) -> None:
448
+ """Auto-detect conda/mamba settings for HPC runs if not configured.
449
+
450
+ This runs during resolution, before validation, so strict validation can
451
+ still require conda_env, conda_exec, and conda_path after detection.
452
+ """
453
+ using_hpc = bool(getattr(args, "hpc", False))
454
+ submitting = bool(getattr(args, "submit", False))
455
+
456
+ if not using_hpc and not submitting:
457
+ return
458
+
459
+ manager = HPCDaskManager(args)
460
+ manager.resolve_conda_settings()
461
+
358
462
  @staticmethod
359
463
  def _apply_logging_level(verbose: bool) -> None:
360
464
  """Adjust logging levels for this module's logger and its handlers.
@@ -364,8 +468,10 @@ class ConfigResolver:
364
468
  """
365
469
  level = logging.DEBUG if verbose else logging.INFO
366
470
  logger.setLevel(level)
471
+
367
472
  for handler in logger.handlers:
368
473
  handler.setLevel(level)
474
+
369
475
  if verbose:
370
476
  logger.debug("Verbose mode enabled. Logger set to DEBUG level.")
371
477
 
@@ -373,8 +479,8 @@ class ConfigResolver:
373
479
  """Validate user inputs against sensible runtime constraints.
374
480
 
375
481
  Args:
376
- u: MDAnalysis universe (or compatible) with a `trajectory`.
377
- args: Parsed/merged arguments.
482
+ u: MDAnalysis Universe or compatible object with a trajectory.
483
+ args: Parsed and merged arguments.
378
484
 
379
485
  Raises:
380
486
  ValueError: If a parameter is invalid.
@@ -385,11 +491,13 @@ class ConfigResolver:
385
491
  self._check_input_bin_width(args)
386
492
  self._check_input_temperature(args)
387
493
  self._check_input_force_partitioning(args)
494
+ self._check_parallel_frame_options(args)
388
495
 
389
496
  @staticmethod
390
497
  def _check_input_start(u: Any, args: argparse.Namespace) -> None:
391
498
  """Check that the start index does not exceed the trajectory length."""
392
499
  traj_len = len(u.trajectory)
500
+
393
501
  if args.start > traj_len:
394
502
  raise ValueError(
395
503
  f"Invalid 'start' value: {args.start}. It exceeds the trajectory "
@@ -400,10 +508,11 @@ class ConfigResolver:
400
508
  def _check_input_end(u: Any, args: argparse.Namespace) -> None:
401
509
  """Check that the end index does not exceed the trajectory length."""
402
510
  traj_len = len(u.trajectory)
511
+
403
512
  if args.end > traj_len:
404
513
  raise ValueError(
405
- f"Invalid 'end' value: {args.end}. It exceeds the trajectory length of "
406
- f"{traj_len}."
514
+ f"Invalid 'end' value: {args.end}. It exceeds the trajectory length "
515
+ f"of {traj_len}."
407
516
  )
408
517
 
409
518
  @staticmethod
@@ -421,25 +530,82 @@ class ConfigResolver:
421
530
  """Check that the bin width is within the valid range [0, 360]."""
422
531
  if args.bin_width < 0 or args.bin_width > 360:
423
532
  raise ValueError(
424
- f"Invalid 'bin_width': {args.bin_width}. It must be between 0 and 360 "
425
- f"degrees."
533
+ f"Invalid 'bin_width': {args.bin_width}. It must be between "
534
+ f"0 and 360 degrees."
426
535
  )
427
536
 
428
537
  @staticmethod
429
538
  def _check_input_temperature(args: argparse.Namespace) -> None:
430
- """Check that the temperature is non-negative."""
431
- if args.temperature < 0:
539
+ """Check that the temperature is positive."""
540
+ if args.temperature <= 0:
432
541
  raise ValueError(
433
- f"Invalid 'temperature': {args.temperature}. Temperature cannot be "
434
- f"below 0."
542
+ f"Invalid 'temperature': {args.temperature}. Temperature must be "
543
+ f"greater than 0 K."
435
544
  )
436
545
 
437
546
  def _check_input_force_partitioning(self, args: argparse.Namespace) -> None:
438
- """Warn if force partitioning is not set to the default value."""
547
+ """Warn if force partitioning differs from the default value."""
439
548
  default_value = self._arg_specs["force_partitioning"].default
549
+
440
550
  if args.force_partitioning != default_value:
441
551
  logger.warning(
442
552
  "'force_partitioning' is set to %s, which differs from the default %s.",
443
553
  args.force_partitioning,
444
554
  default_value,
445
555
  )
556
+
557
+ @staticmethod
558
+ def _check_parallel_frame_options(args: argparse.Namespace) -> None:
559
+ """Validate local Dask, HPC Dask, and submit-related options."""
560
+ dask_workers = getattr(args, "dask_workers", None)
561
+ if dask_workers is not None and dask_workers < 1:
562
+ raise ValueError("'dask_workers' must be at least 1 if provided.")
563
+
564
+ dask_threads = getattr(args, "dask_threads_per_worker", 1)
565
+ if dask_threads < 1:
566
+ raise ValueError("'dask_threads_per_worker' must be at least 1.")
567
+
568
+ using_hpc = bool(getattr(args, "hpc", False))
569
+ submitting = bool(getattr(args, "submit", False))
570
+
571
+ if submitting and not using_hpc:
572
+ raise ValueError("'submit' requires 'hpc' to be enabled.")
573
+
574
+ if not using_hpc and not submitting:
575
+ return
576
+
577
+ if not getattr(args, "hpc_queue", None):
578
+ raise ValueError("'hpc_queue' must be provided when using HPC Dask.")
579
+
580
+ if getattr(args, "hpc_nodes", 1) < 1:
581
+ raise ValueError("'hpc_nodes' must be at least 1.")
582
+
583
+ if getattr(args, "hpc_cores", 1) < 1:
584
+ raise ValueError("'hpc_cores' must be at least 1.")
585
+
586
+ if getattr(args, "hpc_processes", 1) < 1:
587
+ raise ValueError("'hpc_processes' must be at least 1.")
588
+
589
+ if not getattr(args, "hpc_memory", None):
590
+ raise ValueError("'hpc_memory' must be provided when using HPC Dask.")
591
+
592
+ if not getattr(args, "hpc_walltime", None):
593
+ raise ValueError("'hpc_walltime' must be provided when using HPC Dask.")
594
+
595
+ if not getattr(args, "conda_env", None):
596
+ raise ValueError(
597
+ "'conda_env' must be provided when using HPC Dask, or be detectable "
598
+ "from the active conda/mamba environment."
599
+ )
600
+
601
+ if not getattr(args, "conda_path", None):
602
+ raise ValueError(
603
+ "'conda_path' must be provided when using HPC Dask, or be detectable "
604
+ "from the active conda/mamba environment."
605
+ )
606
+
607
+ if not getattr(args, "conda_exec", None):
608
+ raise ValueError(
609
+ "'conda_exec' must be provided when using HPC Dask, or be detectable "
610
+ "from the active conda/mamba environment."
611
+ )
@@ -33,12 +33,15 @@ from rich.table import Table
33
33
  from rich.text import Text
34
34
 
35
35
  from CodeEntropy.config.argparse import ConfigResolver
36
+ from CodeEntropy.core.dask_clusters import HPCDaskManager
36
37
  from CodeEntropy.core.logging import LoggingConfig
37
38
  from CodeEntropy.entropy.workflow import EntropyWorkflow
38
- from CodeEntropy.levels.dihedrals import ConformationStateBuilder
39
- from CodeEntropy.levels.mda import UniverseOperations
39
+ from CodeEntropy.levels.dihedrals.conformational_state_builder import (
40
+ ConformationStateBuilder,
41
+ )
40
42
  from CodeEntropy.molecules.grouping import MoleculeGrouper
41
43
  from CodeEntropy.results.reporter import ResultsReporter
44
+ from CodeEntropy.trajectory.mda import UniverseOperations
42
45
 
43
46
  logger = logging.getLogger(__name__)
44
47
  console = LoggingConfig.get_console()
@@ -223,8 +226,9 @@ class CodeEntropyRunner:
223
226
 
224
227
  This method:
225
228
  - Sets up logging and prints the splash screen
226
- - Loads YAML config from CWD and parses CLI args
229
+ - Loads YAML configuration from CWD and parses CLI args
227
230
  - Merges args with YAML per-run config
231
+ - Optionally submits a master SLURM job and exits
228
232
  - Builds the MDAnalysis Universe (with optional force merging)
229
233
  - Validates user parameters
230
234
  - Constructs dependencies and executes EntropyWorkflow
@@ -256,6 +260,16 @@ class CodeEntropyRunner:
256
260
 
257
261
  args = self._config_manager.resolve(args, run_config)
258
262
 
263
+ if getattr(args, "submit", False):
264
+ if os.environ.get("CODEENTROPY_SUBMITTED_JOB") == "1":
265
+ run_logger.info(
266
+ "Already running inside submitted SLURM job; "
267
+ "continuing workflow."
268
+ )
269
+ else:
270
+ HPCDaskManager(args).submit_master()
271
+ return
272
+
259
273
  log_level = (
260
274
  logging.DEBUG if getattr(args, "verbose", False) else logging.INFO
261
275
  )
@@ -298,6 +312,7 @@ class CodeEntropyRunner:
298
312
  except Exception:
299
313
  logger.error("Run arguments at failure could not be serialized")
300
314
 
315
+ logger.exception("Fatal error during entropy calculation")
301
316
  raise RuntimeError("CodeEntropyRunner encountered an error") from exc
302
317
 
303
318
  @staticmethod