workbench 0.8.234__py3-none-any.whl → 0.8.239__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 (44) hide show
  1. workbench/algorithms/dataframe/smart_aggregator.py +17 -12
  2. workbench/api/endpoint.py +13 -4
  3. workbench/api/model.py +2 -2
  4. workbench/cached/cached_model.py +2 -2
  5. workbench/core/artifacts/athena_source.py +5 -3
  6. workbench/core/artifacts/endpoint_core.py +30 -5
  7. workbench/core/cloud_platform/aws/aws_meta.py +2 -1
  8. workbench/core/transforms/model_to_endpoint/model_to_endpoint.py +27 -14
  9. workbench/model_script_utils/model_script_utils.py +225 -0
  10. workbench/model_script_utils/uq_harness.py +39 -21
  11. workbench/model_scripts/chemprop/chemprop.template +30 -15
  12. workbench/model_scripts/chemprop/generated_model_script.py +35 -18
  13. workbench/model_scripts/chemprop/model_script_utils.py +225 -0
  14. workbench/model_scripts/pytorch_model/generated_model_script.py +29 -15
  15. workbench/model_scripts/pytorch_model/model_script_utils.py +225 -0
  16. workbench/model_scripts/pytorch_model/pytorch.template +28 -14
  17. workbench/model_scripts/pytorch_model/uq_harness.py +39 -21
  18. workbench/model_scripts/xgb_model/generated_model_script.py +35 -22
  19. workbench/model_scripts/xgb_model/model_script_utils.py +225 -0
  20. workbench/model_scripts/xgb_model/uq_harness.py +39 -21
  21. workbench/model_scripts/xgb_model/xgb_model.template +29 -18
  22. workbench/scripts/ml_pipeline_batch.py +47 -2
  23. workbench/scripts/ml_pipeline_launcher.py +410 -0
  24. workbench/scripts/ml_pipeline_sqs.py +22 -2
  25. workbench/themes/dark/custom.css +29 -0
  26. workbench/themes/light/custom.css +29 -0
  27. workbench/themes/midnight_blue/custom.css +28 -0
  28. workbench/utils/model_utils.py +9 -0
  29. workbench/utils/theme_manager.py +95 -0
  30. workbench/web_interface/components/component_interface.py +3 -0
  31. workbench/web_interface/components/plugin_interface.py +26 -0
  32. workbench/web_interface/components/plugins/ag_table.py +4 -11
  33. workbench/web_interface/components/plugins/confusion_matrix.py +14 -8
  34. workbench/web_interface/components/plugins/model_plot.py +156 -0
  35. workbench/web_interface/components/plugins/scatter_plot.py +9 -2
  36. workbench/web_interface/components/plugins/shap_summary_plot.py +12 -4
  37. workbench/web_interface/components/settings_menu.py +10 -49
  38. {workbench-0.8.234.dist-info → workbench-0.8.239.dist-info}/METADATA +2 -2
  39. {workbench-0.8.234.dist-info → workbench-0.8.239.dist-info}/RECORD +43 -42
  40. {workbench-0.8.234.dist-info → workbench-0.8.239.dist-info}/WHEEL +1 -1
  41. {workbench-0.8.234.dist-info → workbench-0.8.239.dist-info}/entry_points.txt +1 -0
  42. workbench/web_interface/components/model_plot.py +0 -75
  43. {workbench-0.8.234.dist-info → workbench-0.8.239.dist-info}/licenses/LICENSE +0 -0
  44. {workbench-0.8.234.dist-info → workbench-0.8.239.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from sklearn.metrics import (
16
16
  r2_score,
17
17
  root_mean_squared_error,
18
18
  )
19
+ from sklearn.model_selection import GroupKFold, GroupShuffleSplit
19
20
  from scipy.stats import spearmanr
20
21
 
21
22
 
@@ -367,3 +368,227 @@ def print_confusion_matrix(y_true: np.ndarray, y_pred: np.ndarray, label_names:
367
368
  for j, col_name in enumerate(label_names):
368
369
  value = conf_mtx[i, j]
369
370
  print(f"ConfusionMatrix:{row_name}:{col_name} {value}")
371
+
372
+
373
+ # =============================================================================
374
+ # Dataset Splitting Utilities for Molecular Data
375
+ # =============================================================================
376
+ def get_scaffold(smiles: str) -> str:
377
+ """Extract Bemis-Murcko scaffold from a SMILES string.
378
+
379
+ Args:
380
+ smiles: SMILES string of the molecule
381
+
382
+ Returns:
383
+ SMILES string of the scaffold, or empty string if molecule is invalid
384
+ """
385
+ from rdkit import Chem
386
+ from rdkit.Chem.Scaffolds import MurckoScaffold
387
+
388
+ mol = Chem.MolFromSmiles(smiles)
389
+ if mol is None:
390
+ return ""
391
+ try:
392
+ scaffold = MurckoScaffold.GetScaffoldForMol(mol)
393
+ return Chem.MolToSmiles(scaffold)
394
+ except Exception:
395
+ return ""
396
+
397
+
398
+ def get_scaffold_groups(smiles_list: list[str]) -> np.ndarray:
399
+ """Assign each molecule to a scaffold group.
400
+
401
+ Args:
402
+ smiles_list: List of SMILES strings
403
+
404
+ Returns:
405
+ Array of group indices (same scaffold = same group)
406
+ """
407
+ scaffold_to_group = {}
408
+ groups = []
409
+
410
+ for smi in smiles_list:
411
+ scaffold = get_scaffold(smi)
412
+ if scaffold not in scaffold_to_group:
413
+ scaffold_to_group[scaffold] = len(scaffold_to_group)
414
+ groups.append(scaffold_to_group[scaffold])
415
+
416
+ n_scaffolds = len(scaffold_to_group)
417
+ print(f"Found {n_scaffolds} unique scaffolds from {len(smiles_list)} molecules")
418
+ return np.array(groups)
419
+
420
+
421
+ def get_butina_clusters(smiles_list: list[str], cutoff: float = 0.4) -> np.ndarray:
422
+ """Cluster molecules using Butina algorithm on Morgan fingerprints.
423
+
424
+ Uses RDKit's Butina clustering with Tanimoto distance on Morgan fingerprints.
425
+ This is Pat Walters' recommended approach for creating diverse train/test splits.
426
+
427
+ Args:
428
+ smiles_list: List of SMILES strings
429
+ cutoff: Tanimoto distance cutoff for clustering (default 0.4)
430
+ Lower values = more clusters = more similar molecules per cluster
431
+
432
+ Returns:
433
+ Array of cluster indices
434
+ """
435
+ from rdkit import Chem, DataStructs
436
+ from rdkit.Chem.rdFingerprintGenerator import GetMorganGenerator
437
+ from rdkit.ML.Cluster import Butina
438
+
439
+ # Create Morgan fingerprint generator
440
+ fp_gen = GetMorganGenerator(radius=2, fpSize=2048)
441
+
442
+ # Generate Morgan fingerprints
443
+ fps = []
444
+ valid_indices = []
445
+ for i, smi in enumerate(smiles_list):
446
+ mol = Chem.MolFromSmiles(smi)
447
+ if mol is not None:
448
+ fp = fp_gen.GetFingerprint(mol)
449
+ fps.append(fp)
450
+ valid_indices.append(i)
451
+
452
+ if len(fps) == 0:
453
+ raise ValueError("No valid molecules found for clustering")
454
+
455
+ # Compute distance matrix (upper triangle only for efficiency)
456
+ n = len(fps)
457
+ dists = []
458
+ for i in range(1, n):
459
+ sims = DataStructs.BulkTanimotoSimilarity(fps[i], fps[:i])
460
+ dists.extend([1 - s for s in sims])
461
+
462
+ # Butina clustering
463
+ clusters = Butina.ClusterData(dists, n, cutoff, isDistData=True)
464
+
465
+ # Map back to original indices
466
+ cluster_labels = np.zeros(len(smiles_list), dtype=int)
467
+ for cluster_idx, cluster in enumerate(clusters):
468
+ for mol_idx in cluster:
469
+ original_idx = valid_indices[mol_idx]
470
+ cluster_labels[original_idx] = cluster_idx
471
+
472
+ # Assign invalid molecules to their own clusters
473
+ next_cluster = len(clusters)
474
+ for i in range(len(smiles_list)):
475
+ if i not in valid_indices:
476
+ cluster_labels[i] = next_cluster
477
+ next_cluster += 1
478
+
479
+ n_clusters = len(set(cluster_labels))
480
+ print(f"Butina clustering: {n_clusters} clusters from {len(smiles_list)} molecules (cutoff={cutoff})")
481
+ return cluster_labels
482
+
483
+
484
+ def _find_smiles_column(columns: list[str]) -> str | None:
485
+ """Find SMILES column (case-insensitive match for 'smiles').
486
+
487
+ Args:
488
+ columns: List of column names
489
+
490
+ Returns:
491
+ The matching column name, or None if not found
492
+ """
493
+ return next((c for c in columns if c.lower() == "smiles"), None)
494
+
495
+
496
+ def get_split_indices(
497
+ df: pd.DataFrame,
498
+ n_splits: int = 5,
499
+ strategy: str = "random",
500
+ smiles_column: str | None = None,
501
+ target_column: str | None = None,
502
+ test_size: float = 0.2,
503
+ random_state: int = 42,
504
+ butina_cutoff: float = 0.4,
505
+ ) -> list[tuple[np.ndarray, np.ndarray]]:
506
+ """Get train/validation split indices using various strategies.
507
+
508
+ This is a unified interface for generating splits that can be used across
509
+ all model templates (XGBoost, PyTorch, ChemProp).
510
+
511
+ Args:
512
+ df: DataFrame containing the data
513
+ n_splits: Number of CV folds (1 = single train/val split)
514
+ strategy: Split strategy - one of:
515
+ - "random": Standard random split (default sklearn behavior)
516
+ - "scaffold": Bemis-Murcko scaffold-based grouping
517
+ - "butina": Morgan fingerprint clustering (recommended for ADMET)
518
+ smiles_column: Column containing SMILES. If None, auto-detects 'smiles' (case-insensitive)
519
+ target_column: Column containing target values (for stratification, optional)
520
+ test_size: Fraction for validation set when n_splits=1 (default 0.2)
521
+ random_state: Random seed for reproducibility
522
+ butina_cutoff: Tanimoto distance cutoff for Butina clustering (default 0.4)
523
+
524
+ Returns:
525
+ List of (train_indices, val_indices) tuples
526
+
527
+ Note:
528
+ If scaffold/butina strategy is requested but no SMILES column is found,
529
+ automatically falls back to random split with a warning message.
530
+
531
+ Example:
532
+ >>> folds = get_split_indices(df, n_splits=5, strategy="scaffold")
533
+ >>> for train_idx, val_idx in folds:
534
+ ... X_train, X_val = df.iloc[train_idx], df.iloc[val_idx]
535
+ """
536
+ from sklearn.model_selection import KFold, StratifiedKFold, train_test_split
537
+
538
+ n_samples = len(df)
539
+
540
+ # Random split (original behavior)
541
+ if strategy == "random":
542
+ if n_splits == 1:
543
+ indices = np.arange(n_samples)
544
+ train_idx, val_idx = train_test_split(indices, test_size=test_size, random_state=random_state)
545
+ return [(train_idx, val_idx)]
546
+ else:
547
+ if target_column and df[target_column].dtype in ["object", "category", "bool"]:
548
+ kfold = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
549
+ return list(kfold.split(df, df[target_column]))
550
+ else:
551
+ kfold = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
552
+ return list(kfold.split(df))
553
+
554
+ # Scaffold or Butina split requires SMILES - auto-detect if not provided
555
+ if smiles_column is None:
556
+ smiles_column = _find_smiles_column(df.columns.tolist())
557
+
558
+ # Fall back to random split if no SMILES column available
559
+ if smiles_column is None or smiles_column not in df.columns:
560
+ print(f"No 'smiles' column found for strategy='{strategy}', falling back to random split")
561
+ return get_split_indices(
562
+ df,
563
+ n_splits=n_splits,
564
+ strategy="random",
565
+ target_column=target_column,
566
+ test_size=test_size,
567
+ random_state=random_state,
568
+ )
569
+
570
+ smiles_list = df[smiles_column].tolist()
571
+
572
+ # Get group assignments
573
+ if strategy == "scaffold":
574
+ groups = get_scaffold_groups(smiles_list)
575
+ elif strategy == "butina":
576
+ groups = get_butina_clusters(smiles_list, cutoff=butina_cutoff)
577
+ else:
578
+ raise ValueError(f"Unknown strategy: {strategy}. Use 'random', 'scaffold', or 'butina'")
579
+
580
+ # Generate splits using GroupKFold or GroupShuffleSplit
581
+ if n_splits == 1:
582
+ # Single split: use GroupShuffleSplit
583
+ splitter = GroupShuffleSplit(n_splits=1, test_size=test_size, random_state=random_state)
584
+ return list(splitter.split(df, groups=groups))
585
+ else:
586
+ # K-fold: use GroupKFold (ensures no group appears in both train and val)
587
+ # Note: GroupKFold doesn't shuffle, so we shuffle group order first
588
+ unique_groups = np.unique(groups)
589
+ rng = np.random.default_rng(random_state)
590
+ shuffled_group_map = {g: i for i, g in enumerate(rng.permutation(unique_groups))}
591
+ shuffled_groups = np.array([shuffled_group_map[g] for g in groups])
592
+
593
+ gkf = GroupKFold(n_splits=n_splits)
594
+ return list(gkf.split(df, groups=shuffled_groups))
@@ -3,6 +3,25 @@
3
3
  This module provides a reusable UQ harness that can wrap any point predictor model
4
4
  (XGBoost, PyTorch, ChemProp, etc.) to provide calibrated prediction intervals.
5
5
 
6
+ Features:
7
+ - Conformalized Quantile Regression (CQR) for distribution-free coverage guarantees
8
+ - Multiple confidence levels (50%, 68%, 80%, 90%, 95%)
9
+ - Confidence scoring based on interval width
10
+
11
+ Why CQR without additional Z-scaling:
12
+ MAPIE's conformalization step already guarantees that prediction intervals achieve
13
+ their target coverage on the calibration set. For example, an 80% CI will contain
14
+ ~80% of true values. This is the core promise of conformal prediction.
15
+
16
+ Z-scaling (post-hoc interval adjustment) would only help if there's a distribution
17
+ shift between calibration and test data. However:
18
+ 1. We'd compute Z-scale on the same calibration set MAPIE uses, making it redundant
19
+ 2. Our cross-fold validation metrics confirm coverage is already well-calibrated
20
+ 3. Adding Z-scaling would "second-guess" MAPIE's principled conformalization
21
+
22
+ Empirically, our models achieve excellent coverage (e.g., 80% CI → 80.1% coverage),
23
+ validating that MAPIE's approach is sufficient without additional calibration.
24
+
6
25
  Usage:
7
26
  # Training
8
27
  uq_models, uq_metadata = train_uq_models(X_train, y_train, X_val, y_val)
@@ -240,38 +259,37 @@ def compute_confidence(
240
259
  median_interval_width: float,
241
260
  lower_q: str = "q_10",
242
261
  upper_q: str = "q_90",
243
- alpha: float = 1.0,
244
- beta: float = 1.0,
245
262
  ) -> pd.DataFrame:
246
263
  """Compute confidence scores (0.0 to 1.0) based on prediction interval width.
247
264
 
248
- Uses exponential decay based on:
249
- 1. Interval width relative to median (alpha weight)
250
- 2. Distance from median prediction (beta weight)
265
+ Confidence is derived from the 80% prediction interval (q_10 to q_90) width:
266
+ - Narrower intervals higher confidence (model is more certain)
267
+ - Wider intervals lower confidence (model is less certain)
268
+
269
+ Why 80% CI (q_10/q_90)?
270
+ - 68% CI is too narrow and sensitive to noise
271
+ - 95% CI is too wide and less discriminating between samples
272
+ - 80% provides a good balance for ranking prediction reliability
273
+
274
+ Formula: confidence = exp(-width / median_width)
275
+ - When width equals median, confidence ≈ 0.37
276
+ - When width is half median, confidence ≈ 0.61
277
+ - When width is double median, confidence ≈ 0.14
278
+
279
+ This exponential decay is a common choice for converting uncertainty to
280
+ confidence scores, providing a smooth mapping that appropriately penalizes
281
+ high-uncertainty predictions.
251
282
 
252
283
  Args:
253
- df: DataFrame with 'prediction', 'q_50', and quantile columns
284
+ df: DataFrame with quantile columns from predict_intervals()
254
285
  median_interval_width: Pre-computed median interval width from training data
255
286
  lower_q: Lower quantile column name (default: 'q_10')
256
287
  upper_q: Upper quantile column name (default: 'q_90')
257
- alpha: Weight for interval width term (default: 1.0)
258
- beta: Weight for distance from median term (default: 1.0)
259
288
 
260
289
  Returns:
261
- DataFrame with added 'confidence' column
290
+ DataFrame with added 'confidence' column (values between 0 and 1)
262
291
  """
263
- # Interval width
264
292
  interval_width = (df[upper_q] - df[lower_q]).abs()
265
-
266
- # Distance from median, normalized by interval width
267
- distance_from_median = (df["prediction"] - df["q_50"]).abs()
268
- normalized_distance = distance_from_median / (interval_width + 1e-6)
269
-
270
- # Cap the distance penalty at 1.0
271
- normalized_distance = np.minimum(normalized_distance, 1.0)
272
-
273
- # Confidence using exponential decay
274
- interval_term = interval_width / median_interval_width
275
- df["confidence"] = np.exp(-(alpha * interval_term + beta * normalized_distance))
293
+ df["confidence"] = np.exp(-interval_width / median_interval_width)
276
294
 
277
295
  return df
@@ -51,12 +51,18 @@ DEFAULT_HYPERPARAMETERS = {
51
51
  "gamma": 0.1,
52
52
  "reg_alpha": 0.1,
53
53
  "reg_lambda": 1.0,
54
+ # Split strategy: "random", "scaffold", or "butina"
55
+ # - random: Standard random split (default)
56
+ # - scaffold: Bemis-Murcko scaffold-based grouping (requires 'smiles' column in data)
57
+ # - butina: Morgan fingerprint clustering (requires 'smiles' column, recommended for ADMET)
58
+ "split_strategy": "random",
59
+ "butina_cutoff": 0.4, # Tanimoto distance cutoff for Butina clustering
54
60
  # Random seed
55
61
  "seed": 42,
56
62
  }
57
63
 
58
64
  # Workbench-specific parameters (not passed to XGBoost)
59
- WORKBENCH_PARAMS = {"n_folds"}
65
+ WORKBENCH_PARAMS = {"n_folds", "split_strategy", "butina_cutoff"}
60
66
 
61
67
  # Regression-only parameters (filtered out for classifiers)
62
68
  REGRESSION_ONLY_PARAMS = {"objective"}
@@ -198,6 +204,7 @@ if __name__ == "__main__":
198
204
  check_dataframe,
199
205
  compute_classification_metrics,
200
206
  compute_regression_metrics,
207
+ get_split_indices,
201
208
  print_classification_metrics,
202
209
  print_confusion_matrix,
203
210
  print_regression_metrics,
@@ -273,25 +280,29 @@ if __name__ == "__main__":
273
280
 
274
281
  print(f"XGBoost params: {xgb_params}")
275
282
 
276
- if n_folds == 1:
277
- # Single train/val split
278
- if "training" in all_df.columns:
279
- print("Using 'training' column for train/val split")
280
- train_idx = np.where(all_df["training"])[0]
281
- val_idx = np.where(~all_df["training"])[0]
282
- else:
283
- print("WARNING: No 'training' column found, using random 80/20 split")
284
- indices = np.arange(len(all_df))
285
- train_idx, val_idx = train_test_split(indices, test_size=0.2, random_state=42)
283
+ # Get split strategy parameters
284
+ split_strategy = hyperparameters.get("split_strategy", "random")
285
+ butina_cutoff = hyperparameters.get("butina_cutoff", 0.4)
286
+
287
+ # Check for pre-defined training column (overrides split strategy)
288
+ if n_folds == 1 and "training" in all_df.columns:
289
+ print("Using 'training' column for train/val split")
290
+ train_idx = np.where(all_df["training"])[0]
291
+ val_idx = np.where(~all_df["training"])[0]
286
292
  folds = [(train_idx, val_idx)]
287
293
  else:
288
- # K-fold cross-validation
289
- if model_type == "classifier":
290
- kfold = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
291
- folds = list(kfold.split(all_df, all_df[target]))
292
- else:
293
- kfold = KFold(n_splits=n_folds, shuffle=True, random_state=42)
294
- folds = list(kfold.split(all_df))
294
+ # Use unified split interface (auto-detects 'smiles' column for scaffold/butina)
295
+ target_col = target if model_type == "classifier" else None
296
+ folds = get_split_indices(
297
+ all_df,
298
+ n_splits=n_folds,
299
+ strategy=split_strategy,
300
+ target_column=target_col,
301
+ test_size=0.2,
302
+ random_state=42,
303
+ butina_cutoff=butina_cutoff,
304
+ )
305
+ print(f"Split strategy: {split_strategy}")
295
306
 
296
307
  print(f"Training {'single model' if n_folds == 1 else f'{n_folds}-fold ensemble'}...")
297
308
 
@@ -44,7 +44,14 @@ def _log_cloudwatch_link(job: dict, message_prefix: str = "View logs") -> None:
44
44
  log.info("Check AWS Batch console for logs")
45
45
 
46
46
 
47
- def run_batch_job(script_path: str, size: str = "small") -> int:
47
+ def run_batch_job(
48
+ script_path: str,
49
+ size: str = "small",
50
+ realtime: bool = False,
51
+ dt: bool = False,
52
+ promote: bool = False,
53
+ test_promote: bool = False,
54
+ ) -> int:
48
55
  """
49
56
  Submit and monitor an AWS Batch job for ML pipeline execution.
50
57
 
@@ -56,6 +63,10 @@ def run_batch_job(script_path: str, size: str = "small") -> int:
56
63
  - small: 2 vCPU, 4GB RAM for lightweight processing
57
64
  - medium: 4 vCPU, 8GB RAM for standard ML workloads
58
65
  - large: 8 vCPU, 16GB RAM for heavy training/inference
66
+ realtime: If True, sets serverless=False for real-time processing (default: False)
67
+ dt: If True, sets DT=True in environment (default: False)
68
+ promote: If True, sets PROMOTE=True in environment (default: False)
69
+ test_promote: If True, sets TEST_PROMOTE=True in environment (default: False)
59
70
 
60
71
  Returns:
61
72
  Exit code (0 for success/disconnected, non-zero for failure)
@@ -81,6 +92,10 @@ def run_batch_job(script_path: str, size: str = "small") -> int:
81
92
  "environment": [
82
93
  {"name": "ML_PIPELINE_S3_PATH", "value": s3_path},
83
94
  {"name": "WORKBENCH_BUCKET", "value": workbench_bucket},
95
+ {"name": "SERVERLESS", "value": "False" if realtime else "True"},
96
+ {"name": "DT", "value": str(dt)},
97
+ {"name": "PROMOTE", "value": str(promote)},
98
+ {"name": "TEST_PROMOTE", "value": str(test_promote)},
84
99
  ]
85
100
  },
86
101
  )
@@ -124,9 +139,39 @@ def main():
124
139
  """CLI entry point for running ML pipelines on AWS Batch."""
125
140
  parser = argparse.ArgumentParser(description="Run ML pipeline script on AWS Batch")
126
141
  parser.add_argument("script_file", help="Local path to ML pipeline script")
142
+ parser.add_argument(
143
+ "--size", default="small", choices=["small", "medium", "large"], help="Job size tier (default: small)"
144
+ )
145
+ parser.add_argument(
146
+ "--realtime",
147
+ action="store_true",
148
+ help="Create realtime endpoints (default is serverless)",
149
+ )
150
+ parser.add_argument(
151
+ "--dt",
152
+ action="store_true",
153
+ help="Set DT=True (models and endpoints will have '-dt' suffix)",
154
+ )
155
+ parser.add_argument(
156
+ "--promote",
157
+ action="store_true",
158
+ help="Set Promote=True (models and endpoints will use promoted naming)",
159
+ )
160
+ parser.add_argument(
161
+ "--test-promote",
162
+ action="store_true",
163
+ help="Set TEST_PROMOTE=True (creates test endpoint with '-test' suffix)",
164
+ )
127
165
  args = parser.parse_args()
128
166
  try:
129
- exit_code = run_batch_job(args.script_file)
167
+ exit_code = run_batch_job(
168
+ args.script_file,
169
+ size=args.size,
170
+ realtime=args.realtime,
171
+ dt=args.dt,
172
+ promote=args.promote,
173
+ test_promote=args.test_promote,
174
+ )
130
175
  exit(exit_code)
131
176
  except Exception as e:
132
177
  log.error(f"Error: {e}")