silver-torch 0.1.0__tar.gz

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.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: silver-torch
3
+ Version: 0.1.0
4
+ Summary: Inspectable Silver dataset preprocessing and PyTorch input pipelines.
5
+ License-Expression: Apache-2.0
6
+ Keywords: pytorch,preprocessing,datasets,machine-learning,research
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Provides-Extra: pytorch
10
+ Requires-Dist: torch>=1.9.0; extra == "pytorch"
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
13
+
14
+ # silver-torch
15
+
16
+ An optional PyTorch layer for Silver. It turns a small Silver preprocessing
17
+ program into a fitted, inspectable, reusable tensor and `DataLoader` pipeline.
18
+
19
+ ```bash
20
+ pip install silver-data
21
+ pip install 'silver-torch[pytorch]'
22
+ ```
23
+
24
+ ```python
25
+ from silver_data import Dataset
26
+ from silver_torch import compile_silver
27
+
28
+ program = """
29
+ pipeline ieee_inverse:
30
+ features voltage, current, phase, sensor
31
+ categorical sensor
32
+ label fault
33
+ architecture transformer
34
+ sequence_length 2
35
+ scaling standard
36
+ missing median
37
+ label_type classification
38
+ batch_size 128
39
+ num_workers 2
40
+ cache_dir .cache/ieee_inverse
41
+ """
42
+
43
+ dataset = Dataset.from_records("ieee", [
44
+ {"voltage": 1.0, "current": 2.0, "phase": 0.2, "fault": 0},
45
+ {"voltage": 1.2, "current": 2.1, "phase": 0.3, "fault": 1},
46
+ ])
47
+ splits = dataset.split(0.8, 0.1, 0.1)
48
+ pipeline = compile_silver(program).fit(splits.train.records())
49
+ loader = pipeline.dataloader(splits.validation.records(), device="cuda")
50
+ print(pipeline.plan(device="cuda").to_dict())
51
+ print(pipeline.benchmark(splits.validation.records(), steps=20))
52
+ ```
53
+
54
+ The compiler has explicit research-safety boundaries:
55
+
56
+ - statistics and vocabularies are fitted only on `splits.train`;
57
+ - missing columns, non-finite numbers, invalid labels, and incompatible
58
+ sequence lengths fail loudly;
59
+ - categorical vocabularies are sorted for reproducibility and reserve index 0
60
+ for unknown values;
61
+ - classification targets are `torch.long`; regression targets use the chosen
62
+ floating dtype;
63
+ - cache keys include the fitted-training fingerprint and transformed rows, and
64
+ cache writes are atomic;
65
+ - loaders are seeded and tune pinning, persistent workers, prefetching, and
66
+ `drop_last` based on the declared runtime.
67
+
68
+ The emitted shapes are `[batch, features]` for MLP, `[batch, 1, features]` for
69
+ CNN, and `[batch, sequence_length, features_per_step]` for RNN/Transformer.
70
+ These are layout contracts, not model implementations. Measure with
71
+ `benchmark()` on the target machine; input speedups depend on storage, CPU,
72
+ worker count, batch size, and accelerator.
@@ -0,0 +1,59 @@
1
+ # silver-torch
2
+
3
+ An optional PyTorch layer for Silver. It turns a small Silver preprocessing
4
+ program into a fitted, inspectable, reusable tensor and `DataLoader` pipeline.
5
+
6
+ ```bash
7
+ pip install silver-data
8
+ pip install 'silver-torch[pytorch]'
9
+ ```
10
+
11
+ ```python
12
+ from silver_data import Dataset
13
+ from silver_torch import compile_silver
14
+
15
+ program = """
16
+ pipeline ieee_inverse:
17
+ features voltage, current, phase, sensor
18
+ categorical sensor
19
+ label fault
20
+ architecture transformer
21
+ sequence_length 2
22
+ scaling standard
23
+ missing median
24
+ label_type classification
25
+ batch_size 128
26
+ num_workers 2
27
+ cache_dir .cache/ieee_inverse
28
+ """
29
+
30
+ dataset = Dataset.from_records("ieee", [
31
+ {"voltage": 1.0, "current": 2.0, "phase": 0.2, "fault": 0},
32
+ {"voltage": 1.2, "current": 2.1, "phase": 0.3, "fault": 1},
33
+ ])
34
+ splits = dataset.split(0.8, 0.1, 0.1)
35
+ pipeline = compile_silver(program).fit(splits.train.records())
36
+ loader = pipeline.dataloader(splits.validation.records(), device="cuda")
37
+ print(pipeline.plan(device="cuda").to_dict())
38
+ print(pipeline.benchmark(splits.validation.records(), steps=20))
39
+ ```
40
+
41
+ The compiler has explicit research-safety boundaries:
42
+
43
+ - statistics and vocabularies are fitted only on `splits.train`;
44
+ - missing columns, non-finite numbers, invalid labels, and incompatible
45
+ sequence lengths fail loudly;
46
+ - categorical vocabularies are sorted for reproducibility and reserve index 0
47
+ for unknown values;
48
+ - classification targets are `torch.long`; regression targets use the chosen
49
+ floating dtype;
50
+ - cache keys include the fitted-training fingerprint and transformed rows, and
51
+ cache writes are atomic;
52
+ - loaders are seeded and tune pinning, persistent workers, prefetching, and
53
+ `drop_last` based on the declared runtime.
54
+
55
+ The emitted shapes are `[batch, features]` for MLP, `[batch, 1, features]` for
56
+ CNN, and `[batch, sequence_length, features_per_step]` for RNN/Transformer.
57
+ These are layout contracts, not model implementations. Measure with
58
+ `benchmark()` on the target machine; input speedups depend on storage, CPU,
59
+ worker count, batch size, and accelerator.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "silver-torch"
7
+ version = "0.1.0"
8
+ description = "Inspectable Silver dataset preprocessing and PyTorch input pipelines."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "Apache-2.0"
12
+ keywords = ["pytorch", "preprocessing", "datasets", "machine-learning", "research"]
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ pytorch = ["torch>=1.9.0"]
17
+ dev = ["pytest>=7.0.0"]
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+
22
+ [tool.setuptools.package-dir]
23
+ "" = "src"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Silver's optional, inspectable PyTorch preprocessing layer."""
2
+
3
+ from .pipeline import SilverTorchPipeline, compile_silver, parse_silver
4
+ from .spec import SilverPreprocessSpec
5
+
6
+ __all__ = ["SilverPreprocessSpec", "SilverTorchPipeline", "compile_silver", "parse_silver"]
@@ -0,0 +1,302 @@
1
+ """Compile Silver preprocessing intent into a leakage-safe PyTorch pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import os
9
+ import re
10
+ import tempfile
11
+ import time
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
15
+
16
+ from .spec import SilverPreprocessSpec
17
+
18
+
19
+ def parse_silver(source: str) -> SilverPreprocessSpec:
20
+ """Parse the data-pipeline subset of Silver with strict field handling."""
21
+ values: Dict[str, Any] = {}
22
+ supported = {"features", "categorical", "label", "architecture", "scaling", "missing",
23
+ "label_type", "dtype", "batch_size", "shuffle", "num_workers",
24
+ "prefetch_factor", "drop_last", "sequence_length", "seed", "cache_dir"}
25
+ for raw in source.splitlines():
26
+ line = raw.strip()
27
+ if not line or line.startswith("#"):
28
+ continue
29
+ if line.startswith("pipeline "):
30
+ match = re.fullmatch(r"pipeline\s+([A-Za-z_]\w*)\s*:", line)
31
+ if not match:
32
+ raise ValueError("pipeline declaration must look like 'pipeline name:'")
33
+ values["name"] = match.group(1)
34
+ continue
35
+ parts = line.split(None, 1)
36
+ if len(parts) != 2:
37
+ raise ValueError("Silver pipeline fields must look like 'field value'")
38
+ key, raw_value = parts
39
+ if key not in supported:
40
+ raise ValueError("unsupported Silver preprocessing field: %s" % key)
41
+ raw_value = raw_value.strip().strip('"')
42
+ if key in ("features", "categorical"):
43
+ values[key] = tuple(x.strip() for x in raw_value.split(",") if x.strip())
44
+ elif raw_value.lower() in ("true", "false"):
45
+ values[key] = raw_value.lower() == "true"
46
+ elif re.fullmatch(r"\d+", raw_value):
47
+ values[key] = int(raw_value)
48
+ else:
49
+ values[key] = raw_value
50
+ values.setdefault("name", "silver_pipeline")
51
+ if "features" not in values or "label" not in values:
52
+ raise ValueError("Silver pipeline requires 'features' and 'label'")
53
+ return SilverPreprocessSpec(**values)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class ColumnState:
58
+ kind: str
59
+ location: float = 0.0
60
+ scale: float = 1.0
61
+ categories: Tuple[str, ...] = ()
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class CompiledPlan:
66
+ spec: SilverPreprocessSpec
67
+ feature_count: int
68
+ input_shape: Tuple[int, ...]
69
+ loader_options: Dict[str, Any]
70
+ fitted_rows: int
71
+ fingerprint: str
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ return {"schema": "silver.torch/plan-2", "name": self.spec.name,
75
+ "architecture": self.spec.architecture, "feature_count": self.feature_count,
76
+ "input_shape": list(self.input_shape), "label_type": self.spec.label_type,
77
+ "loader_options": dict(self.loader_options), "fitted_rows": self.fitted_rows,
78
+ "fingerprint": self.fingerprint}
79
+
80
+
81
+ class SilverTorchPipeline:
82
+ """Fit transforms on training rows, then reuse them without data leakage."""
83
+
84
+ def __init__(self, spec: SilverPreprocessSpec):
85
+ self.spec = spec
86
+ self._states: Dict[str, ColumnState] = {}
87
+ self._label_mapping: Dict[str, int] = {}
88
+ self._fitted_rows = 0
89
+ self._fingerprint = ""
90
+
91
+ @property
92
+ def fitted(self) -> bool:
93
+ return bool(self._states)
94
+
95
+ @property
96
+ def statistics(self) -> Dict[str, Dict[str, Any]]:
97
+ """Return fitted state without exposing mutable internal objects."""
98
+ self._require_fit()
99
+ return {name: {"kind": state.kind, "location": state.location,
100
+ "scale": state.scale, "categories": list(state.categories)}
101
+ for name, state in self._states.items()}
102
+
103
+ def fit(self, rows: Iterable[Mapping[str, Any]]) -> "SilverTorchPipeline":
104
+ materialized = list(rows)
105
+ if not materialized:
106
+ raise ValueError("cannot fit an empty dataset")
107
+ missing_columns = [c for c in self.spec.features + (self.spec.label,)
108
+ if c not in materialized[0]]
109
+ if missing_columns:
110
+ raise ValueError("missing dataset columns: %s" % ", ".join(missing_columns))
111
+ states: Dict[str, ColumnState] = {}
112
+ for column in self.spec.features:
113
+ if column in self.spec.categorical:
114
+ categories = sorted({self._as_category(row.get(column)) for row in materialized
115
+ if row.get(column) not in (None, "")})
116
+ states[column] = ColumnState("categorical", categories=tuple(categories))
117
+ continue
118
+ numbers = [self._number(row.get(column), column) for row in materialized]
119
+ observed = [v for v in numbers if v is not None]
120
+ if not observed and self.spec.missing == "error":
121
+ raise ValueError("feature %r has no observed values" % column)
122
+ location = self._impute_location(observed)
123
+ clean = [v if v is not None else location for v in numbers]
124
+ if self.spec.scaling == "standard":
125
+ location = sum(clean) / len(clean)
126
+ scale = math.sqrt(sum((v - location) ** 2 for v in clean) / len(clean)) or 1.0
127
+ elif self.spec.scaling == "minmax":
128
+ location = min(clean)
129
+ scale = (max(clean) - location) or 1.0
130
+ else:
131
+ scale = 1.0
132
+ states[column] = ColumnState("numeric", location, scale)
133
+ if self.spec.label_type == "classification":
134
+ labels = sorted({self._as_category(row.get(self.spec.label)) for row in materialized})
135
+ self._label_mapping = {value: index for index, value in enumerate(labels)}
136
+ self._states = states
137
+ self._fitted_rows = len(materialized)
138
+ self._fingerprint = self._hash_rows(materialized)
139
+ return self
140
+
141
+ def plan(self, device: str = "cpu") -> CompiledPlan:
142
+ self._require_fit()
143
+ feature_count = len(self.spec.features)
144
+ if self.spec.architecture == "mlp":
145
+ shape = (feature_count,)
146
+ elif self.spec.architecture == "cnn":
147
+ shape = (1, feature_count)
148
+ else:
149
+ if feature_count % self.spec.sequence_length:
150
+ raise ValueError("feature count must divide sequence_length for sequential models")
151
+ shape = (self.spec.sequence_length, feature_count // self.spec.sequence_length)
152
+ options: Dict[str, Any] = {"batch_size": self.spec.batch_size, "shuffle": self.spec.shuffle,
153
+ "num_workers": self.spec.num_workers,
154
+ "pin_memory": device.startswith("cuda"),
155
+ "persistent_workers": self.spec.num_workers > 0,
156
+ "drop_last": self.spec.drop_last}
157
+ if self.spec.num_workers > 0:
158
+ options["prefetch_factor"] = self.spec.prefetch_factor
159
+ return CompiledPlan(self.spec, feature_count, shape, options, self._fitted_rows, self._fingerprint)
160
+
161
+ def transform(self, rows: Iterable[Mapping[str, Any]]) -> Tuple[Any, Any]:
162
+ """Materialize a feature tensor and correctly typed target tensor."""
163
+ self._require_fit()
164
+ torch = _torch()
165
+ features: List[List[float]] = []
166
+ labels: List[Any] = []
167
+ for row in rows:
168
+ features.append([self._transform_value(row.get(column), column) for column in self.spec.features])
169
+ labels.append(self._transform_label(row.get(self.spec.label)))
170
+ x = torch.tensor(features, dtype=getattr(torch, self.spec.dtype)).contiguous()
171
+ label_dtype = torch.long if self.spec.label_type == "classification" else getattr(torch, self.spec.dtype)
172
+ y = torch.tensor(labels, dtype=label_dtype).contiguous()
173
+ if self.spec.architecture == "cnn":
174
+ x = x.unsqueeze(1)
175
+ elif self.spec.architecture in ("rnn", "transformer"):
176
+ x = x.reshape(len(features), self.spec.sequence_length, -1)
177
+ return x, y
178
+
179
+ def tensors(self, rows: Iterable[Mapping[str, Any]]) -> Tuple[Any, Any]:
180
+ """Backward-compatible alias for :meth:`transform`."""
181
+ return self.transform(rows)
182
+
183
+ def dataloader(self, rows: Iterable[Mapping[str, Any]], device: str = "cpu") -> Any:
184
+ torch = _torch()
185
+ from torch.utils.data import DataLoader, TensorDataset
186
+ x, y = self._cached_or_transform(rows)
187
+ options = self.plan(device).loader_options
188
+ generator = torch.Generator()
189
+ generator.manual_seed(self.spec.seed)
190
+ options["generator"] = generator
191
+ return DataLoader(TensorDataset(x, y), **options)
192
+
193
+ def benchmark(self, rows: Iterable[Mapping[str, Any]], steps: int = 50,
194
+ device: str = "cpu") -> Dict[str, float]:
195
+ """Measure input throughput, including host-to-device transfer when requested."""
196
+ if steps < 1:
197
+ raise ValueError("steps must be positive")
198
+ loader = self.dataloader(rows, device=device)
199
+ start = time.perf_counter()
200
+ seen = 0
201
+ for index, (features, labels) in enumerate(loader):
202
+ if device.startswith("cuda"):
203
+ features = features.to(device, non_blocking=True)
204
+ labels = labels.to(device, non_blocking=True)
205
+ _torch().cuda.synchronize()
206
+ seen += len(labels)
207
+ if index + 1 >= steps:
208
+ break
209
+ elapsed = max(time.perf_counter() - start, 1e-9)
210
+ return {"samples": float(seen), "seconds": elapsed, "samples_per_second": seen / elapsed}
211
+
212
+ def _cached_or_transform(self, rows: Iterable[Mapping[str, Any]]) -> Tuple[Any, Any]:
213
+ materialized = list(rows)
214
+ if not self.spec.cache_dir:
215
+ return self.transform(materialized)
216
+ torch = _torch()
217
+ cache_root = Path(self.spec.cache_dir)
218
+ cache_root.mkdir(parents=True, exist_ok=True)
219
+ key = hashlib.sha256((self._fingerprint + json.dumps(materialized, sort_keys=True, default=str)).encode()).hexdigest()[:24]
220
+ path = cache_root / (self.spec.name + "-" + key + ".pt")
221
+ if path.exists():
222
+ payload = torch.load(str(path), map_location="cpu")
223
+ return payload["features"], payload["labels"]
224
+ x, y = self.transform(materialized)
225
+ fd, temp_name = tempfile.mkstemp(prefix="silver-", suffix=".pt", dir=str(cache_root))
226
+ os.close(fd)
227
+ try:
228
+ torch.save({"features": x, "labels": y}, temp_name)
229
+ os.replace(temp_name, str(path))
230
+ finally:
231
+ if os.path.exists(temp_name):
232
+ os.unlink(temp_name)
233
+ return x, y
234
+
235
+ def _transform_value(self, value: Any, column: str) -> float:
236
+ state = self._states[column]
237
+ if state.kind == "categorical":
238
+ category = self._as_category(value)
239
+ return float(state.categories.index(category) + 1) if category in state.categories else 0.0
240
+ number = self._number(value, column)
241
+ if number is None:
242
+ if self.spec.missing == "error":
243
+ raise ValueError("missing value in feature %r" % column)
244
+ number = state.location
245
+ return (number - state.location) / state.scale
246
+
247
+ def _transform_label(self, value: Any) -> Any:
248
+ if self.spec.label_type == "classification":
249
+ category = self._as_category(value)
250
+ if category not in self._label_mapping:
251
+ raise ValueError("unknown label %r after fitting" % value)
252
+ return self._label_mapping[category]
253
+ number = self._number(value, self.spec.label)
254
+ if number is None:
255
+ raise ValueError("regression label cannot be missing")
256
+ return number
257
+
258
+ def _impute_location(self, observed: Sequence[float]) -> float:
259
+ if self.spec.missing in ("mean", "unknown") and observed:
260
+ return sum(observed) / len(observed)
261
+ if self.spec.missing == "median" and observed:
262
+ values = sorted(observed)
263
+ middle = len(values) // 2
264
+ return values[middle] if len(values) % 2 else (values[middle - 1] + values[middle]) / 2
265
+ return 0.0
266
+
267
+ def _require_fit(self) -> None:
268
+ if not self.fitted:
269
+ raise RuntimeError("fit the pipeline on the training split before transform")
270
+
271
+ @staticmethod
272
+ def _as_category(value: Any) -> str:
273
+ return "<missing>" if value in (None, "") else str(value)
274
+
275
+ @staticmethod
276
+ def _number(value: Any, column: str) -> Optional[float]:
277
+ if value is None or value == "":
278
+ return None
279
+ try:
280
+ result = float(value)
281
+ except (TypeError, ValueError) as error:
282
+ raise ValueError("feature %r must be numeric" % column) from error
283
+ if not math.isfinite(result):
284
+ raise ValueError("feature %r must be finite" % column)
285
+ return result
286
+
287
+ @staticmethod
288
+ def _hash_rows(rows: Sequence[Mapping[str, Any]]) -> str:
289
+ encoded = json.dumps(rows, sort_keys=True, separators=(",", ":"), default=str)
290
+ return hashlib.sha256(encoded.encode()).hexdigest()[:16]
291
+
292
+
293
+ def compile_silver(source: str) -> SilverTorchPipeline:
294
+ return SilverTorchPipeline(parse_silver(source))
295
+
296
+
297
+ def _torch() -> Any:
298
+ try:
299
+ import torch
300
+ except ImportError as error:
301
+ raise ImportError("install silver-torch[pytorch] to materialize PyTorch tensors") from error
302
+ return torch
@@ -0,0 +1,50 @@
1
+ """Validated, serialisable configuration for Silver's PyTorch compiler."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, Tuple
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class SilverPreprocessSpec:
9
+ name: str
10
+ features: Tuple[str, ...]
11
+ label: str
12
+ architecture: str = "mlp"
13
+ categorical: Tuple[str, ...] = ()
14
+ scaling: str = "standard"
15
+ missing: str = "error"
16
+ label_type: str = "classification"
17
+ dtype: str = "float32"
18
+ batch_size: int = 64
19
+ shuffle: bool = True
20
+ num_workers: int = 0
21
+ prefetch_factor: int = 2
22
+ drop_last: bool = False
23
+ sequence_length: int = 1
24
+ seed: int = 17
25
+ cache_dir: str = ""
26
+ options: Dict[str, Any] = field(default_factory=dict)
27
+
28
+ def __post_init__(self) -> None:
29
+ if not self.name or not self.features or not self.label:
30
+ raise ValueError("name, features, and label are required")
31
+ if len(set(self.features)) != len(self.features):
32
+ raise ValueError("features must not contain duplicates")
33
+ unknown = set(self.categorical) - set(self.features)
34
+ if unknown:
35
+ raise ValueError("categorical columns must be listed in features: %s" % sorted(unknown))
36
+ if self.architecture not in ("mlp", "cnn", "rnn", "transformer"):
37
+ raise ValueError("architecture must be mlp, cnn, rnn, or transformer")
38
+ if self.scaling not in ("none", "standard", "minmax"):
39
+ raise ValueError("scaling must be none, standard, or minmax")
40
+ if self.missing not in ("error", "zero", "mean", "median", "unknown"):
41
+ raise ValueError("missing must be error, zero, mean, median, or unknown")
42
+ if self.label_type not in ("classification", "regression"):
43
+ raise ValueError("label_type must be classification or regression")
44
+ if self.dtype not in ("float16", "float32", "float64", "bfloat16"):
45
+ raise ValueError("dtype must be a floating PyTorch dtype")
46
+ if self.batch_size < 1 or self.num_workers < 0 or self.prefetch_factor < 1:
47
+ raise ValueError("batch_size, workers, and prefetch_factor are invalid")
48
+ if self.sequence_length < 1 or self.seed < 0:
49
+ raise ValueError("sequence_length must be positive and seed non-negative")
50
+
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: silver-torch
3
+ Version: 0.1.0
4
+ Summary: Inspectable Silver dataset preprocessing and PyTorch input pipelines.
5
+ License-Expression: Apache-2.0
6
+ Keywords: pytorch,preprocessing,datasets,machine-learning,research
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Provides-Extra: pytorch
10
+ Requires-Dist: torch>=1.9.0; extra == "pytorch"
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
13
+
14
+ # silver-torch
15
+
16
+ An optional PyTorch layer for Silver. It turns a small Silver preprocessing
17
+ program into a fitted, inspectable, reusable tensor and `DataLoader` pipeline.
18
+
19
+ ```bash
20
+ pip install silver-data
21
+ pip install 'silver-torch[pytorch]'
22
+ ```
23
+
24
+ ```python
25
+ from silver_data import Dataset
26
+ from silver_torch import compile_silver
27
+
28
+ program = """
29
+ pipeline ieee_inverse:
30
+ features voltage, current, phase, sensor
31
+ categorical sensor
32
+ label fault
33
+ architecture transformer
34
+ sequence_length 2
35
+ scaling standard
36
+ missing median
37
+ label_type classification
38
+ batch_size 128
39
+ num_workers 2
40
+ cache_dir .cache/ieee_inverse
41
+ """
42
+
43
+ dataset = Dataset.from_records("ieee", [
44
+ {"voltage": 1.0, "current": 2.0, "phase": 0.2, "fault": 0},
45
+ {"voltage": 1.2, "current": 2.1, "phase": 0.3, "fault": 1},
46
+ ])
47
+ splits = dataset.split(0.8, 0.1, 0.1)
48
+ pipeline = compile_silver(program).fit(splits.train.records())
49
+ loader = pipeline.dataloader(splits.validation.records(), device="cuda")
50
+ print(pipeline.plan(device="cuda").to_dict())
51
+ print(pipeline.benchmark(splits.validation.records(), steps=20))
52
+ ```
53
+
54
+ The compiler has explicit research-safety boundaries:
55
+
56
+ - statistics and vocabularies are fitted only on `splits.train`;
57
+ - missing columns, non-finite numbers, invalid labels, and incompatible
58
+ sequence lengths fail loudly;
59
+ - categorical vocabularies are sorted for reproducibility and reserve index 0
60
+ for unknown values;
61
+ - classification targets are `torch.long`; regression targets use the chosen
62
+ floating dtype;
63
+ - cache keys include the fitted-training fingerprint and transformed rows, and
64
+ cache writes are atomic;
65
+ - loaders are seeded and tune pinning, persistent workers, prefetching, and
66
+ `drop_last` based on the declared runtime.
67
+
68
+ The emitted shapes are `[batch, features]` for MLP, `[batch, 1, features]` for
69
+ CNN, and `[batch, sequence_length, features_per_step]` for RNN/Transformer.
70
+ These are layout contracts, not model implementations. Measure with
71
+ `benchmark()` on the target machine; input speedups depend on storage, CPU,
72
+ worker count, batch size, and accelerator.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/silver_torch/__init__.py
4
+ src/silver_torch/pipeline.py
5
+ src/silver_torch/spec.py
6
+ src/silver_torch.egg-info/PKG-INFO
7
+ src/silver_torch.egg-info/SOURCES.txt
8
+ src/silver_torch.egg-info/dependency_links.txt
9
+ src/silver_torch.egg-info/requires.txt
10
+ src/silver_torch.egg-info/top_level.txt
11
+ tests/test_pipeline.py
@@ -0,0 +1,6 @@
1
+
2
+ [dev]
3
+ pytest>=7.0.0
4
+
5
+ [pytorch]
6
+ torch>=1.9.0
@@ -0,0 +1 @@
1
+ silver_torch
@@ -0,0 +1,141 @@
1
+ import pytest
2
+
3
+ from silver_torch import compile_silver, parse_silver
4
+
5
+
6
+ PROGRAM = """
7
+ pipeline ieee_inverse:
8
+ features voltage, current
9
+ label fault
10
+ architecture transformer
11
+ scaling standard
12
+ missing median
13
+ batch_size 8
14
+ num_workers 2
15
+ """
16
+
17
+
18
+ def test_parse_and_plan_are_inspectable():
19
+ spec = parse_silver(PROGRAM)
20
+ assert spec.name == "ieee_inverse"
21
+ assert spec.features == ("voltage", "current")
22
+ pipeline = compile_silver(PROGRAM).fit(
23
+ [
24
+ {"voltage": 1, "current": 10, "fault": 0},
25
+ {"voltage": 3, "current": 30, "fault": 1},
26
+ ]
27
+ )
28
+ plan = pipeline.plan(device="cuda").to_dict()
29
+ assert plan["schema"] == "silver.torch/plan-2"
30
+ assert plan["input_shape"] == [1, 2]
31
+ assert plan["loader_options"]["pin_memory"] is True
32
+ assert plan["loader_options"]["prefetch_factor"] == 2
33
+
34
+
35
+ def test_fit_uses_training_statistics_and_handles_median():
36
+ pipeline = compile_silver(
37
+ PROGRAM.replace("architecture transformer", "architecture mlp")
38
+ ).fit(
39
+ [
40
+ {"voltage": 1, "current": 10, "fault": 0},
41
+ {"voltage": None, "current": 20, "fault": 1},
42
+ {"voltage": 3, "current": 30, "fault": 1},
43
+ ]
44
+ )
45
+ assert pipeline.statistics["voltage"]["location"] == 2
46
+ assert pipeline.statistics["current"]["location"] == 20
47
+
48
+
49
+ def test_tensors_are_contiguous_when_torch_is_available():
50
+ torch = pytest.importorskip("torch")
51
+ pipeline = compile_silver(PROGRAM).fit(
52
+ [
53
+ {"voltage": 1, "current": 10, "fault": 0},
54
+ {"voltage": 3, "current": 30, "fault": 1},
55
+ ]
56
+ )
57
+ features, labels = pipeline.tensors(
58
+ [{"voltage": 2, "current": 20, "fault": 1}]
59
+ )
60
+ assert features.is_contiguous()
61
+ assert features.dtype == torch.float32
62
+ assert tuple(features.shape) == (1, 1, 2)
63
+ assert tuple(labels.shape) == (1,)
64
+
65
+
66
+ def test_missing_error_is_not_silently_imputed():
67
+ program = PROGRAM.replace("missing median", "missing error")
68
+ pipeline = compile_silver(program).fit(
69
+ [{"voltage": 1, "current": 10, "fault": 0}]
70
+ )
71
+ with pytest.raises(ValueError, match="missing value"):
72
+ pipeline.tensors([{"voltage": None, "current": 10, "fault": 0}])
73
+
74
+
75
+ def test_categorical_features_and_classification_labels_are_encoded():
76
+ program = """
77
+ pipeline categorical:
78
+ features sensor, voltage
79
+ categorical sensor
80
+ label fault
81
+ scaling none
82
+ missing unknown
83
+ """
84
+ torch = pytest.importorskip("torch")
85
+ pipeline = compile_silver(program).fit([
86
+ {"sensor": "a", "voltage": 1, "fault": "healthy"},
87
+ {"sensor": "b", "voltage": 2, "fault": "fault"},
88
+ ])
89
+ features, labels = pipeline.transform([
90
+ {"sensor": "unseen", "voltage": 3, "fault": "fault"}
91
+ ])
92
+ assert features[0, 0].item() == 0.0
93
+ assert labels.dtype == torch.long
94
+
95
+
96
+ def test_regression_and_sequence_layout():
97
+ program = """
98
+ pipeline sequence:
99
+ features x1, x2, x3, x4
100
+ label target
101
+ architecture rnn
102
+ label_type regression
103
+ sequence_length 2
104
+ """
105
+ torch = pytest.importorskip("torch")
106
+ pipeline = compile_silver(program).fit([
107
+ {"x1": 1, "x2": 2, "x3": 3, "x4": 4, "target": 0.5}
108
+ ])
109
+ features, labels = pipeline.transform([
110
+ {"x1": 1, "x2": 2, "x3": 3, "x4": 4, "target": 0.5}
111
+ ])
112
+ assert tuple(features.shape) == (1, 2, 2)
113
+ assert labels.dtype == torch.float32
114
+
115
+
116
+ def test_invalid_sequence_layout_fails_before_materialization():
117
+ program = PROGRAM.replace("architecture transformer", "architecture transformer").replace(
118
+ "features voltage, current", "features voltage, current, phase"
119
+ ) + " sequence_length 2\n"
120
+ pipeline = compile_silver(program).fit([
121
+ {"voltage": 1, "current": 10, "phase": 2, "fault": 0},
122
+ {"voltage": 3, "current": 30, "phase": 4, "fault": 1},
123
+ ])
124
+ with pytest.raises(ValueError, match="feature count"):
125
+ pipeline.plan()
126
+
127
+
128
+ def test_cache_and_benchmark(tmp_path):
129
+ pytest.importorskip("torch")
130
+ program = PROGRAM.replace("num_workers 2", "num_workers 0") + " cache_dir %s\n" % tmp_path
131
+ pipeline = compile_silver(program).fit([
132
+ {"voltage": 1, "current": 10, "fault": 0},
133
+ {"voltage": 3, "current": 30, "fault": 1},
134
+ ])
135
+ rows = [{"voltage": 2, "current": 20, "fault": 1}]
136
+ first = pipeline.dataloader(rows)
137
+ second = pipeline.dataloader(rows)
138
+ assert len(list(first)) == len(list(second)) == 1
139
+ result = pipeline.benchmark(rows, steps=1)
140
+ assert result["samples"] == 1.0
141
+ assert result["samples_per_second"] > 0