dataeval-flow 0.1.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 (94) hide show
  1. dataeval_flow/__init__.py +93 -0
  2. dataeval_flow/__main__.py +149 -0
  3. dataeval_flow/_app/__init__.py +5 -0
  4. dataeval_flow/_app/_model/__init__.py +5 -0
  5. dataeval_flow/_app/_model/_coerce.py +126 -0
  6. dataeval_flow/_app/_model/_discover.py +171 -0
  7. dataeval_flow/_app/_model/_execution.py +108 -0
  8. dataeval_flow/_app/_model/_introspect.py +280 -0
  9. dataeval_flow/_app/_model/_item.py +213 -0
  10. dataeval_flow/_app/_model/_registry.py +255 -0
  11. dataeval_flow/_app/_model/_state.py +322 -0
  12. dataeval_flow/_app/_model/_undo.py +61 -0
  13. dataeval_flow/_app/_panes/__init__.py +35 -0
  14. dataeval_flow/_app/_panes/_config_pane.py +173 -0
  15. dataeval_flow/_app/_panes/_result_pane.py +125 -0
  16. dataeval_flow/_app/_panes/_task_pane.py +91 -0
  17. dataeval_flow/_app/_panes/_widgets.py +111 -0
  18. dataeval_flow/_app/_screens/__init__.py +25 -0
  19. dataeval_flow/_app/_screens/_base.py +242 -0
  20. dataeval_flow/_app/_screens/_detail.py +333 -0
  21. dataeval_flow/_app/_screens/_model.py +102 -0
  22. dataeval_flow/_app/_screens/_params.py +80 -0
  23. dataeval_flow/_app/_screens/_pathpicker.py +68 -0
  24. dataeval_flow/_app/_screens/_section.py +621 -0
  25. dataeval_flow/_app/_screens/_settings.py +183 -0
  26. dataeval_flow/_app/_viewmodel/__init__.py +15 -0
  27. dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
  28. dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
  29. dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
  30. dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
  31. dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
  32. dataeval_flow/_app/app.py +742 -0
  33. dataeval_flow/_app/cli.py +592 -0
  34. dataeval_flow/_logging.py +102 -0
  35. dataeval_flow/cache.py +1355 -0
  36. dataeval_flow/config/__init__.py +80 -0
  37. dataeval_flow/config/_loader.py +79 -0
  38. dataeval_flow/config/_merge.py +92 -0
  39. dataeval_flow/config/_models.py +115 -0
  40. dataeval_flow/config/_paths.py +85 -0
  41. dataeval_flow/config/schemas/__init__.py +112 -0
  42. dataeval_flow/config/schemas/_dataset.py +111 -0
  43. dataeval_flow/config/schemas/_extractor.py +119 -0
  44. dataeval_flow/config/schemas/_metadata.py +28 -0
  45. dataeval_flow/config/schemas/_preprocessor.py +18 -0
  46. dataeval_flow/config/schemas/_selection.py +100 -0
  47. dataeval_flow/config/schemas/_task.py +89 -0
  48. dataeval_flow/config/schemas/_workflow.py +135 -0
  49. dataeval_flow/dataset.py +635 -0
  50. dataeval_flow/embeddings.py +135 -0
  51. dataeval_flow/metadata.py +48 -0
  52. dataeval_flow/preprocessing.py +141 -0
  53. dataeval_flow/py.typed +0 -0
  54. dataeval_flow/runner.py +118 -0
  55. dataeval_flow/selection.py +50 -0
  56. dataeval_flow/workflow/__init__.py +328 -0
  57. dataeval_flow/workflow/_text_report.py +511 -0
  58. dataeval_flow/workflow/base.py +69 -0
  59. dataeval_flow/workflow/orchestrator.py +454 -0
  60. dataeval_flow/workflows/__init__.py +1 -0
  61. dataeval_flow/workflows/analysis/__init__.py +38 -0
  62. dataeval_flow/workflows/analysis/outputs.py +202 -0
  63. dataeval_flow/workflows/analysis/params.py +114 -0
  64. dataeval_flow/workflows/analysis/workflow.py +1313 -0
  65. dataeval_flow/workflows/cleaning/__init__.py +23 -0
  66. dataeval_flow/workflows/cleaning/outputs.py +200 -0
  67. dataeval_flow/workflows/cleaning/params.py +160 -0
  68. dataeval_flow/workflows/cleaning/report.py +304 -0
  69. dataeval_flow/workflows/cleaning/workflow.py +794 -0
  70. dataeval_flow/workflows/drift/__init__.py +1 -0
  71. dataeval_flow/workflows/drift/outputs.py +144 -0
  72. dataeval_flow/workflows/drift/params.py +332 -0
  73. dataeval_flow/workflows/drift/report.py +201 -0
  74. dataeval_flow/workflows/drift/workflow.py +647 -0
  75. dataeval_flow/workflows/ood/__init__.py +1 -0
  76. dataeval_flow/workflows/ood/outputs.py +134 -0
  77. dataeval_flow/workflows/ood/params.py +161 -0
  78. dataeval_flow/workflows/ood/report.py +311 -0
  79. dataeval_flow/workflows/ood/workflow.py +728 -0
  80. dataeval_flow/workflows/prioritization/__init__.py +1 -0
  81. dataeval_flow/workflows/prioritization/outputs.py +122 -0
  82. dataeval_flow/workflows/prioritization/params.py +124 -0
  83. dataeval_flow/workflows/prioritization/report.py +117 -0
  84. dataeval_flow/workflows/prioritization/workflow.py +587 -0
  85. dataeval_flow/workflows/splitting/__init__.py +25 -0
  86. dataeval_flow/workflows/splitting/outputs.py +101 -0
  87. dataeval_flow/workflows/splitting/params.py +61 -0
  88. dataeval_flow/workflows/splitting/report.py +485 -0
  89. dataeval_flow/workflows/splitting/workflow.py +371 -0
  90. dataeval_flow-0.1.0.dist-info/METADATA +305 -0
  91. dataeval_flow-0.1.0.dist-info/RECORD +94 -0
  92. dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
  93. dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
  94. dataeval_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,371 @@
1
+ """Dataset splitting workflow implementation."""
2
+
3
+ import logging
4
+ import traceback
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+ from pydantic import BaseModel
9
+
10
+ from dataeval_flow.workflow import WorkflowContext, WorkflowResult
11
+ from dataeval_flow.workflows.splitting.outputs import (
12
+ DataSplittingMetadata,
13
+ DataSplittingOutputs,
14
+ DataSplittingRawOutputs,
15
+ DataSplittingReport,
16
+ SplitInfo,
17
+ )
18
+ from dataeval_flow.workflows.splitting.params import DataSplittingParameters
19
+ from dataeval_flow.workflows.splitting.report import build_findings
20
+
21
+ __all__ = ["DataSplittingWorkflow"]
22
+
23
+ logger: logging.Logger = logging.getLogger(__name__)
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Serialization helpers
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def _serialize_label_stats(stats: Any) -> dict[str, Any]:
32
+ """Convert a LabelStatsResult to a plain dict."""
33
+ if stats is None:
34
+ return {}
35
+ result: dict[str, Any] = {}
36
+ for key in (
37
+ "label_counts_per_class",
38
+ "image_counts_per_class",
39
+ "class_count",
40
+ "label_count",
41
+ "image_count",
42
+ "index2label",
43
+ ):
44
+ val = stats.get(key, None) if hasattr(stats, "get") else getattr(stats, key, None)
45
+ if val is not None:
46
+ if hasattr(val, "tolist"):
47
+ val = val.tolist()
48
+ result[key] = val
49
+ return result
50
+
51
+
52
+ def _serialize_balance(output: Any) -> dict[str, Any]:
53
+ """Convert BalanceOutput to a plain dict."""
54
+ result: dict[str, Any] = {}
55
+ for attr in ("balance", "factors", "classwise"):
56
+ df = getattr(output, attr, None)
57
+ if df is not None:
58
+ result[attr] = df.to_dicts() if hasattr(df, "to_dicts") else str(df)
59
+ return result
60
+
61
+
62
+ def _serialize_diversity(output: Any) -> dict[str, Any]:
63
+ """Convert DiversityOutput to a plain dict."""
64
+ result: dict[str, Any] = {}
65
+ for attr in ("factors", "classwise"):
66
+ df = getattr(output, attr, None)
67
+ if df is not None:
68
+ result[attr] = df.to_dicts() if hasattr(df, "to_dicts") else str(df)
69
+ return result
70
+
71
+
72
+ def _serialize_coverage(coverage_result: Any) -> dict[str, Any]:
73
+ """Convert CoverageResult to a plain dict."""
74
+ result: dict[str, Any] = {}
75
+ for key in ("uncovered_indices", "critical_value_radii", "coverage_radius"):
76
+ val = coverage_result.get(key, None) if hasattr(coverage_result, "get") else getattr(coverage_result, key, None)
77
+ if val is not None:
78
+ if hasattr(val, "tolist"):
79
+ val = val.tolist()
80
+ result[key] = val
81
+ return result
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Embedding-space coverage helper (step 7)
86
+ # Extracted from _execute to satisfy C901 complexity limit.
87
+ # "Coverage" here refers to dataeval.core.coverage_adaptive(), not test coverage.
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ def _run_coverage(
92
+ ds_ctx: Any,
93
+ dataset: Any,
94
+ fold_infos: list[SplitInfo],
95
+ test_indices: list[int],
96
+ params: DataSplittingParameters,
97
+ ) -> dict[str, Any] | None:
98
+ """Run per-split coverage assessment if model is provided."""
99
+ if ds_ctx.extractor is None:
100
+ logger.info("Step 7: Skipping coverage (no model provided)")
101
+ return None
102
+
103
+ from dataeval.core import coverage_adaptive
104
+
105
+ from dataeval_flow.embeddings import build_embeddings
106
+
107
+ logger.info("Step 7: Per-split coverage assessment")
108
+ embeddings_obj = build_embeddings(
109
+ dataset,
110
+ ds_ctx.extractor,
111
+ transforms=ds_ctx.transforms,
112
+ batch_size=ds_ctx.batch_size,
113
+ )
114
+ all_embeddings = np.array(embeddings_obj)
115
+
116
+ # Normalize to [0, 1]
117
+ emb_min = all_embeddings.min(axis=0, keepdims=True)
118
+ emb_max = all_embeddings.max(axis=0, keepdims=True)
119
+ emb_range = emb_max - emb_min
120
+ emb_range[emb_range == 0] = 1.0
121
+ all_embeddings = (all_embeddings - emb_min) / emb_range
122
+
123
+ for fold_info in fold_infos:
124
+ train_embs = all_embeddings[fold_info.train_indices]
125
+ val_embs = all_embeddings[fold_info.val_indices]
126
+ fold_info.coverage_train = _serialize_coverage(
127
+ coverage_adaptive(train_embs, params.num_observations, params.coverage_percent)
128
+ )
129
+ fold_info.coverage_val = _serialize_coverage(
130
+ coverage_adaptive(val_embs, params.num_observations, params.coverage_percent)
131
+ )
132
+
133
+ coverage_test_data: dict[str, Any] | None = None
134
+ if test_indices:
135
+ test_embs = all_embeddings[test_indices]
136
+ coverage_test_data = _serialize_coverage(
137
+ coverage_adaptive(test_embs, params.num_observations, params.coverage_percent)
138
+ )
139
+ return coverage_test_data
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Workflow class
144
+ # ---------------------------------------------------------------------------
145
+
146
+
147
+ class DataSplittingWorkflow:
148
+ """Dataset splitting workflow.
149
+
150
+ Assesses dataset balance/diversity, produces stratified train/val/test
151
+ splits, and optionally rebalances the train split.
152
+ """
153
+
154
+ @property
155
+ def name(self) -> str:
156
+ """Workflow identifier."""
157
+ return "data-splitting"
158
+
159
+ @property
160
+ def description(self) -> str:
161
+ """Human-readable description."""
162
+ return (
163
+ "Assess dataset balance/diversity, produce stratified train/val/test "
164
+ "splits, and optionally rebalance the train split."
165
+ )
166
+
167
+ @property
168
+ def params_schema(self) -> type[BaseModel]:
169
+ """Pydantic model for workflow parameters."""
170
+ return DataSplittingParameters
171
+
172
+ @property
173
+ def output_schema(self) -> type[BaseModel]:
174
+ """Pydantic model for workflow output."""
175
+ return DataSplittingOutputs
176
+
177
+ def execute(
178
+ self,
179
+ context: WorkflowContext,
180
+ params: BaseModel | None = None,
181
+ ) -> WorkflowResult[DataSplittingMetadata, DataSplittingOutputs]:
182
+ """Execute the splitting workflow."""
183
+ if not isinstance(context, WorkflowContext):
184
+ msg = f"Expected WorkflowContext, got {type(context).__name__}"
185
+ raise TypeError(msg)
186
+
187
+ if params is not None and not isinstance(params, DataSplittingParameters):
188
+ msg = f"Expected DataSplittingParameters, got {type(params).__name__}"
189
+ raise TypeError(msg)
190
+
191
+ p = params or DataSplittingParameters()
192
+
193
+ try:
194
+ return self._execute(context, p)
195
+ except Exception:
196
+ logger.exception("Splitting workflow failed")
197
+ return WorkflowResult(
198
+ name=self.name,
199
+ success=False,
200
+ data=_empty_outputs(),
201
+ errors=[traceback.format_exc()],
202
+ metadata=DataSplittingMetadata(),
203
+ )
204
+
205
+ def _execute(
206
+ self,
207
+ context: WorkflowContext,
208
+ params: DataSplittingParameters,
209
+ ) -> WorkflowResult[DataSplittingMetadata, DataSplittingOutputs]:
210
+ from dataeval.bias import Balance, Diversity
211
+ from dataeval.core import label_stats
212
+ from dataeval.utils.data import split_dataset
213
+
214
+ from dataeval_flow.metadata import build_metadata
215
+ from dataeval_flow.selection import build_selection
216
+
217
+ # --- Resolve single dataset ---
218
+ if not context.dataset_contexts:
219
+ msg = "No datasets provided"
220
+ raise ValueError(msg)
221
+
222
+ ds_name = next(iter(context.dataset_contexts))
223
+ ds_ctx = context.dataset_contexts[ds_name]
224
+ dataset: Any = ds_ctx.dataset
225
+
226
+ # Apply selection if configured
227
+ if ds_ctx.selection_steps:
228
+ dataset = build_selection(dataset, list(ds_ctx.selection_steps))
229
+
230
+ dataset_size = len(dataset)
231
+ logger.info("Step 1: Building metadata for %s (%d items)", ds_name, dataset_size)
232
+
233
+ # --- Step 1: Build Metadata ---
234
+ metadata = build_metadata(dataset)
235
+
236
+ # --- Step 2: Pre-split bias assessment ---
237
+ logger.info("Step 2: Pre-split bias assessment")
238
+ balance_output = Balance().evaluate(metadata)
239
+ diversity_output = Diversity().evaluate(metadata)
240
+
241
+ pre_split_balance = _serialize_balance(balance_output)
242
+ pre_split_diversity = _serialize_diversity(diversity_output)
243
+
244
+ # --- Step 3: Full-dataset label stats ---
245
+ logger.info("Step 3: Label statistics (full dataset)")
246
+ class_labels = metadata.class_labels
247
+ index2label = metadata.index2label if hasattr(metadata, "index2label") else None
248
+ full_stats = label_stats(class_labels, index2label=index2label)
249
+ label_stats_full = _serialize_label_stats(full_stats)
250
+
251
+ # --- Step 4: Split ---
252
+ logger.info(
253
+ "Step 4: Splitting dataset (num_folds=%d, stratify=%s, test_frac=%s, val_frac=%s)",
254
+ params.num_folds,
255
+ params.stratify,
256
+ params.test_frac,
257
+ params.val_frac,
258
+ )
259
+ # Use metadata if split_on is specified, otherwise use dataset directly
260
+ split_input: Any = metadata if params.split_on else dataset
261
+ # val_frac is only valid for single-fold; multi-fold uses 1/num_folds automatically
262
+ val_frac = params.val_frac if params.num_folds == 1 else 0.0
263
+ splits = split_dataset(
264
+ split_input,
265
+ num_folds=params.num_folds,
266
+ stratify=params.stratify,
267
+ split_on=params.split_on,
268
+ test_frac=params.test_frac,
269
+ val_frac=val_frac,
270
+ )
271
+
272
+ test_indices = splits.test.tolist()
273
+
274
+ # --- Step 5: Optional rebalancing ---
275
+ # ClassBalance is a selection step that operates on a Select-wrapped dataset.
276
+ # For now we store the raw indices; rebalancing modifies train indices.
277
+ fold_infos: list[SplitInfo] = []
278
+ for i, fold in enumerate(splits.folds):
279
+ train_idx = fold.train.tolist()
280
+ val_idx = fold.val.tolist()
281
+
282
+ if params.rebalance_method is not None:
283
+ logger.info("Step 5: Rebalancing fold %d train split (method=%s)", i, params.rebalance_method)
284
+ from dataeval.selection import ClassBalance, Indices, Select
285
+
286
+ train_selected = Select(dataset, Indices(train_idx))
287
+ ClassBalance(method=params.rebalance_method)(train_selected)
288
+ train_idx = train_selected.resolve_indices()
289
+
290
+ fold_infos.append(
291
+ SplitInfo(
292
+ fold=i,
293
+ train_indices=train_idx,
294
+ val_indices=val_idx,
295
+ )
296
+ )
297
+
298
+ # --- Step 6: Per-split label stats ---
299
+ logger.info("Step 6: Per-split label statistics")
300
+ for fold_info in fold_infos:
301
+ train_labels = class_labels[fold_info.train_indices]
302
+ val_labels = class_labels[fold_info.val_indices]
303
+ fold_info.label_stats_train = _serialize_label_stats(label_stats(train_labels, index2label=index2label))
304
+ fold_info.label_stats_val = _serialize_label_stats(label_stats(val_labels, index2label=index2label))
305
+
306
+ test_labels = class_labels[test_indices] if test_indices else np.array([], dtype=np.intp)
307
+ label_stats_test = (
308
+ _serialize_label_stats(label_stats(test_labels, index2label=index2label)) if len(test_labels) > 0 else {}
309
+ )
310
+
311
+ # --- Step 7: Per-split coverage (if model provided) ---
312
+ coverage_test_data = _run_coverage(
313
+ ds_ctx,
314
+ dataset,
315
+ fold_infos,
316
+ test_indices,
317
+ params,
318
+ )
319
+
320
+ # --- Build raw outputs ---
321
+ raw = DataSplittingRawOutputs(
322
+ dataset_size=dataset_size,
323
+ pre_split_balance=pre_split_balance,
324
+ pre_split_diversity=pre_split_diversity,
325
+ label_stats_full=label_stats_full,
326
+ test_indices=test_indices,
327
+ label_stats_test=label_stats_test,
328
+ coverage_test=coverage_test_data,
329
+ folds=fold_infos,
330
+ )
331
+
332
+ # --- Build findings ---
333
+ findings = build_findings(raw)
334
+
335
+ # --- Build split sizes for metadata ---
336
+ fold0 = fold_infos[0] if fold_infos else None
337
+ split_sizes = {
338
+ "train": len(fold0.train_indices) if fold0 else 0,
339
+ "val": len(fold0.val_indices) if fold0 else 0,
340
+ "test": len(test_indices),
341
+ }
342
+
343
+ report = DataSplittingReport(
344
+ summary=f"Dataset splitting: {dataset_size} items → {len(fold_infos)} fold(s)",
345
+ findings=findings,
346
+ )
347
+
348
+ outputs = DataSplittingOutputs(raw=raw, report=report)
349
+
350
+ metadata = DataSplittingMetadata(
351
+ num_folds=params.num_folds,
352
+ stratified=params.stratify,
353
+ split_on=params.split_on,
354
+ rebalance_method=params.rebalance_method,
355
+ split_sizes=split_sizes,
356
+ )
357
+
358
+ return WorkflowResult(
359
+ name=self.name,
360
+ success=True,
361
+ data=outputs,
362
+ metadata=metadata,
363
+ )
364
+
365
+
366
+ def _empty_outputs() -> DataSplittingOutputs:
367
+ """Create empty outputs for error cases."""
368
+ return DataSplittingOutputs(
369
+ raw=DataSplittingRawOutputs(dataset_size=0),
370
+ report=DataSplittingReport(summary="Splitting workflow failed"),
371
+ )
@@ -0,0 +1,305 @@
1
+ Metadata-Version: 2.4
2
+ Name: dataeval-flow
3
+ Version: 0.1.0
4
+ Summary: DataEval Workflows container for data evaluation
5
+ Project-URL: Repository, https://gitlab.jatic.net/jatic/aria/dataeval-flow
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: <3.14,>=3.10
19
+ Requires-Dist: dataeval==1.0.4
20
+ Requires-Dist: datasets>=4.0.0
21
+ Requires-Dist: maite-datasets>=0.0.12
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Provides-Extra: all-cpu
25
+ Requires-Dist: onnx>=1.15; extra == 'all-cpu'
26
+ Requires-Dist: onnxruntime>=1.20; extra == 'all-cpu'
27
+ Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cpu'
28
+ Requires-Dist: textual>=3.0; extra == 'all-cpu'
29
+ Requires-Dist: torch>=2.2.0; extra == 'all-cpu'
30
+ Requires-Dist: torchvision>=0.17.0; extra == 'all-cpu'
31
+ Provides-Extra: all-cu118
32
+ Requires-Dist: onnx>=1.15; extra == 'all-cu118'
33
+ Requires-Dist: onnxruntime-gpu>=1.20; extra == 'all-cu118'
34
+ Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cu118'
35
+ Requires-Dist: textual>=3.0; extra == 'all-cu118'
36
+ Requires-Dist: torch>=2.2.0; extra == 'all-cu118'
37
+ Requires-Dist: torchvision>=0.17.0; extra == 'all-cu118'
38
+ Provides-Extra: all-cu124
39
+ Requires-Dist: onnx>=1.15; extra == 'all-cu124'
40
+ Requires-Dist: onnxruntime-gpu>=1.20; extra == 'all-cu124'
41
+ Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cu124'
42
+ Requires-Dist: textual>=3.0; extra == 'all-cu124'
43
+ Requires-Dist: torch>=2.2.0; extra == 'all-cu124'
44
+ Requires-Dist: torchvision>=0.17.0; extra == 'all-cu124'
45
+ Provides-Extra: all-cu128
46
+ Requires-Dist: onnx>=1.15; extra == 'all-cu128'
47
+ Requires-Dist: onnxruntime-gpu>=1.23.2; extra == 'all-cu128'
48
+ Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cu128'
49
+ Requires-Dist: textual>=3.0; extra == 'all-cu128'
50
+ Requires-Dist: torch>=2.2.0; extra == 'all-cu128'
51
+ Requires-Dist: torchvision>=0.17.0; extra == 'all-cu128'
52
+ Provides-Extra: app
53
+ Requires-Dist: textual>=3.0; extra == 'app'
54
+ Provides-Extra: cpu
55
+ Requires-Dist: torch>=2.2.0; extra == 'cpu'
56
+ Requires-Dist: torchvision>=0.17.0; extra == 'cpu'
57
+ Provides-Extra: cu118
58
+ Requires-Dist: torch>=2.2.0; extra == 'cu118'
59
+ Requires-Dist: torchvision>=0.17.0; extra == 'cu118'
60
+ Provides-Extra: cu124
61
+ Requires-Dist: torch>=2.2.0; extra == 'cu124'
62
+ Requires-Dist: torchvision>=0.17.0; extra == 'cu124'
63
+ Provides-Extra: cu128
64
+ Requires-Dist: torch>=2.2.0; extra == 'cu128'
65
+ Requires-Dist: torchvision>=0.17.0; extra == 'cu128'
66
+ Provides-Extra: onnx
67
+ Requires-Dist: onnx>=1.15; extra == 'onnx'
68
+ Requires-Dist: onnxruntime>=1.20; extra == 'onnx'
69
+ Provides-Extra: onnx-gpu
70
+ Requires-Dist: onnx>=1.15; extra == 'onnx-gpu'
71
+ Requires-Dist: onnxruntime-gpu>=1.20; extra == 'onnx-gpu'
72
+ Provides-Extra: opencv
73
+ Requires-Dist: opencv-python-headless>=4.8.0; extra == 'opencv'
74
+ Description-Content-Type: text/markdown
75
+
76
+ # DataEval Workflows
77
+
78
+ Workflow orchestration for DataEval with GPU support.
79
+
80
+ ## Quick Start
81
+
82
+ ```bash
83
+ # 1. Build CUDA 11.8 container
84
+ docker build -f docker/Dockerfile.cu118 -t dataeval:cu118 .
85
+
86
+ # 2. Show help
87
+ docker run dataeval:cu118
88
+
89
+ # 3. Run with data and output
90
+ docker run --gpus all \
91
+ --mount type=bind,source=/path/to/data,target=/dataeval,readonly \
92
+ --mount type=bind,source=/path/to/output,target=/output \
93
+ dataeval:cu118
94
+ ```
95
+
96
+ ## Requirements
97
+
98
+ | Requirement | Version |
99
+ |-------------|---------|
100
+ | Docker | >= 20.10 |
101
+ | NVIDIA GPU | Any (for GPU mode) |
102
+ | NVIDIA Driver | >= 520 (for GPU mode) |
103
+ | CUDA | 11.8.0 (for GPU mode) |
104
+
105
+ ### Verify GPU Access
106
+
107
+ ```bash
108
+ docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu22.04 nvidia-smi
109
+ ```
110
+
111
+ ## Volume Mounts
112
+
113
+ | Path | Mode | Purpose |
114
+ |------|------|---------|
115
+ | `/dataeval` | ro | Data directory — datasets, models, configs (required) |
116
+ | `/output` | rw | Results (required) |
117
+ | `/cache` | rw | Computation cache (optional) |
118
+
119
+ ### File Permissions
120
+
121
+ The container runs as a non-root user (`dataeval`, UID 1000). Mounted directories for `/output` and `/cache` must be writable by the container process. There are two approaches:
122
+
123
+ #### Option 1: Pass your host UID (recommended)
124
+
125
+ Use `--user` to run the container as your host user, so mounted directories are naturally writable:
126
+
127
+ ```bash
128
+ docker run --gpus all \
129
+ --user "$(id -u):$(id -g)" \
130
+ --mount type=bind,source=/path/to/data,target=/dataeval,readonly \
131
+ --mount type=bind,source=/path/to/output,target=/output \
132
+ dataeval:cu118
133
+ ```
134
+
135
+ #### Option 2: Open directory permissions
136
+
137
+ Make the output and cache directories world-writable on the host:
138
+
139
+ ```bash
140
+ chmod 777 /path/to/output /path/to/cache
141
+ ```
142
+
143
+ Then run without `--user`. This is simpler but less secure.
144
+
145
+ ### Custom Data Root
146
+
147
+ The data root path can be overridden via the `DATAEVAL_DATA` environment variable:
148
+
149
+ ```bash
150
+ docker run --gpus all \
151
+ -e DATAEVAL_DATA=/data \
152
+ --mount type=bind,source=/path/to/data,target=/data,readonly \
153
+ --mount type=bind,source=/path/to/output,target=/output \
154
+ dataeval:cu118
155
+ ```
156
+
157
+ ## Configuration
158
+
159
+ Config files (YAML or JSON) can be placed anywhere in your data directory. By default, all YAML/JSON files at the root of the data mount are auto-discovered and merged.
160
+
161
+ To specify a config path explicitly:
162
+
163
+ ```bash
164
+ # Config folder within data directory
165
+ docker run --gpus all \
166
+ --mount type=bind,source=/path/to/data,target=/dataeval,readonly \
167
+ --mount type=bind,source=/path/to/output,target=/output \
168
+ dataeval:cu118 --config config/
169
+
170
+ # Single config file
171
+ docker run --gpus all \
172
+ --mount type=bind,source=/path/to/data,target=/dataeval,readonly \
173
+ --mount type=bind,source=/path/to/output,target=/output \
174
+ dataeval:cu118 --config params.yaml
175
+ ```
176
+
177
+ Dataset and model paths in config files are resolved relative to the data root (`/dataeval` by default).
178
+
179
+ ## Dataset Formats
180
+
181
+ Currently supported dataset structures:
182
+
183
+ | Format | Structure | Example |
184
+ |--------|-----------|---------|
185
+ | **Dataset** | Single split, used directly | `cifar10_test/` |
186
+ | **DatasetDict** | Multiple splits (dict), configured via config YAML | `cifar10_full/` |
187
+
188
+ ## CPU Fallback
189
+
190
+ For machines without NVIDIA GPU:
191
+
192
+ ```bash
193
+ docker build -f docker/Dockerfile.cpu -t dataeval:cpu .
194
+ docker run dataeval:cpu # Shows help
195
+ docker run \
196
+ --mount type=bind,source=/path/to/data,target=/dataeval,readonly \
197
+ --mount type=bind,source=/path/to/output,target=/output \
198
+ dataeval:cpu
199
+ ```
200
+
201
+ ## CLI Modes
202
+
203
+ DataEval Flow has three modes:
204
+
205
+ | Command | Purpose |
206
+ | ---------------------- | ---------------------------------------------------------------- |
207
+ | `dataeval-flow [opts]` | Headless execution — for automation and CI/CD pipelines |
208
+ | `dataeval-flow app` | Interactive TUI dashboard — configure, execute, and view results |
209
+ | `dataeval-flow config` | Simple CLI config builder — create/edit configs without the TUI |
210
+
211
+ ### Interactive TUI (`app`)
212
+
213
+ **Installation:**
214
+
215
+ ```bash
216
+ uv sync --extra app # or: pip install dataeval-flow[app]
217
+ ```
218
+
219
+ **Usage:**
220
+
221
+ ```bash
222
+ # Launch with a blank config
223
+ python -m dataeval_flow app
224
+
225
+ # Load an existing config for editing
226
+ python -m dataeval_flow app --config /path/to/params.yaml
227
+ ```
228
+
229
+ The TUI provides a three-pane dashboard for config editing, task execution, and result viewing. It auto-discovers available torchvision transforms, dataeval selection classes, and workflow types, generating dynamic parameter forms from their schemas.
230
+
231
+ ### Simple CLI Config Builder (`config`)
232
+
233
+ For environments without the TUI dependency:
234
+
235
+ ```bash
236
+ python -m dataeval_flow config
237
+ python -m dataeval_flow config --config /path/to/params.yaml
238
+ ```
239
+
240
+ Configs can be saved as YAML or JSON.
241
+
242
+ ## Dependencies
243
+
244
+ - `dataeval` - Core evaluation library
245
+ - `datasets` - Huggingface library
246
+ - `maite-datasets` - MAITE protocol adapter
247
+ - `maite` - MAITE protocol library
248
+ - `pydantic` - Structural typing and schema validation
249
+
250
+ ## Troubleshooting
251
+
252
+ ### Build appears stuck at `uv sync`
253
+
254
+ The Docker build may appear frozen during the `uv sync` step:
255
+
256
+ ```
257
+ => [builder 7/7] RUN uv sync --frozen --no-dev --no-install-project 1139.3s
258
+ ```
259
+
260
+ **This is normal.** The step downloads ~2GB of dependencies (PyTorch, scipy, etc.) with no progress indicator.
261
+
262
+ | Network Speed | Expected Build Time |
263
+ |---------------|---------------------|
264
+ | 100 Mbps | ~10 minutes |
265
+ | 30 Mbps | ~20 minutes |
266
+ | 10 Mbps | ~45 minutes |
267
+
268
+ **Tip:** First build is slow; subsequent builds use Docker cache and complete in seconds.
269
+
270
+ ## Running Without Container
271
+
272
+ The `dataeval_flow` package can be used standalone without Docker.
273
+
274
+ **Installation:**
275
+ ```bash
276
+ git clone https://gitlab.jatic.net/jatic/aria/dataeval-flow.git
277
+ cd dataeval-flow
278
+ uv sync
279
+ ```
280
+
281
+ **CLI Usage:**
282
+ ```bash
283
+ python -m dataeval_flow --config /path/to/config --output /path/to/output
284
+ python -m dataeval_flow --data /path/to/data --output /path/to/output
285
+ ```
286
+
287
+ **Python API Usage:**
288
+ ```python
289
+ from pathlib import Path
290
+ from dataeval_flow import load_config, run_tasks
291
+
292
+ config = load_config(Path("/path/to/data/config.yaml"))
293
+ results = run_tasks(config, data_dir=Path("/path/to/data"))
294
+ print(results[0].report())
295
+ ```
296
+
297
+ **Development:**
298
+ ```bash
299
+ uv sync --group dev
300
+ nox
301
+ ```
302
+
303
+ ## License
304
+
305
+ MIT