sci-modeling-bench 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.
@@ -0,0 +1,63 @@
1
+ """SciModelingBench public package."""
2
+
3
+ from sci_modeling_bench.dataset import (
4
+ AllowedValuesConstraint,
5
+ AlphabetConstraint,
6
+ Dataset,
7
+ DatasetMetadata,
8
+ DatasetSchema,
9
+ DatasetSplit,
10
+ DatasetValidator,
11
+ FieldSpec,
12
+ FiniteConstraint,
13
+ HubDatasetSource,
14
+ Knowledge,
15
+ KnowledgeResource,
16
+ LengthConstraint,
17
+ NumericRangeConstraint,
18
+ ValidationReport,
19
+ Violation,
20
+ )
21
+ from sci_modeling_bench.exceptions import (
22
+ DatasetLoadError,
23
+ ManifestError,
24
+ ObjectiveError,
25
+ ObjectiveInputError,
26
+ ObjectiveLookupError,
27
+ ObjectiveOutputError,
28
+ ProtocolError,
29
+ SchemaMismatchError,
30
+ SciModelingBenchError,
31
+ )
32
+ from sci_modeling_bench.objective import Objective
33
+ from sci_modeling_bench.protocol import Protocol
34
+
35
+ __all__ = [
36
+ "AllowedValuesConstraint",
37
+ "AlphabetConstraint",
38
+ "Dataset",
39
+ "DatasetLoadError",
40
+ "DatasetMetadata",
41
+ "DatasetSchema",
42
+ "DatasetSplit",
43
+ "DatasetValidator",
44
+ "FieldSpec",
45
+ "FiniteConstraint",
46
+ "HubDatasetSource",
47
+ "Knowledge",
48
+ "KnowledgeResource",
49
+ "LengthConstraint",
50
+ "ManifestError",
51
+ "NumericRangeConstraint",
52
+ "Objective",
53
+ "ObjectiveError",
54
+ "ObjectiveInputError",
55
+ "ObjectiveLookupError",
56
+ "ObjectiveOutputError",
57
+ "Protocol",
58
+ "ProtocolError",
59
+ "SchemaMismatchError",
60
+ "SciModelingBenchError",
61
+ "ValidationReport",
62
+ "Violation",
63
+ ]
@@ -0,0 +1,46 @@
1
+ """Dataset loading, schema, validation, and knowledge resources."""
2
+
3
+ from sci_modeling_bench.dataset.dataset import Dataset
4
+ from sci_modeling_bench.dataset.knowledge import Knowledge, KnowledgeResource
5
+ from sci_modeling_bench.dataset.manifest import DatasetSplit
6
+ from sci_modeling_bench.dataset.metadata import (
7
+ Citation,
8
+ DatasetMetadata,
9
+ SourceReference,
10
+ )
11
+ from sci_modeling_bench.dataset.schema import (
12
+ AllowedValuesConstraint,
13
+ AlphabetConstraint,
14
+ DatasetSchema,
15
+ FieldSpec,
16
+ FiniteConstraint,
17
+ LengthConstraint,
18
+ NumericRangeConstraint,
19
+ )
20
+ from sci_modeling_bench.dataset.source import HubDatasetSource
21
+ from sci_modeling_bench.dataset.validation import (
22
+ DatasetValidator,
23
+ ValidationReport,
24
+ Violation,
25
+ )
26
+
27
+ __all__ = [
28
+ "AllowedValuesConstraint",
29
+ "AlphabetConstraint",
30
+ "Citation",
31
+ "Dataset",
32
+ "DatasetMetadata",
33
+ "DatasetSchema",
34
+ "DatasetSplit",
35
+ "DatasetValidator",
36
+ "FieldSpec",
37
+ "FiniteConstraint",
38
+ "HubDatasetSource",
39
+ "Knowledge",
40
+ "KnowledgeResource",
41
+ "LengthConstraint",
42
+ "NumericRangeConstraint",
43
+ "SourceReference",
44
+ "ValidationReport",
45
+ "Violation",
46
+ ]
@@ -0,0 +1,103 @@
1
+ """Internal Hugging Face repository adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Protocol
8
+
9
+ from datasets import load_dataset
10
+ from huggingface_hub import HfApi, hf_hub_download
11
+
12
+ from sci_modeling_bench.exceptions import DatasetLoadError
13
+
14
+
15
+ class DatasetRepository(Protocol):
16
+ """Internal resource interface shared by Dataset and Knowledge."""
17
+
18
+ repo_id: str
19
+ resolved_revision: str
20
+
21
+ def read_text(self, path: str) -> str: ...
22
+
23
+ def load(
24
+ self,
25
+ config_name: str,
26
+ split: str,
27
+ *,
28
+ streaming: bool,
29
+ ) -> Any: ...
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class HubDatasetRepository:
34
+ """Pinned view of one Hugging Face Dataset repository."""
35
+
36
+ repo_id: str
37
+ requested_revision: str | None
38
+ resolved_revision: str
39
+ token: str | None = field(default=None, repr=False, compare=False)
40
+
41
+ @classmethod
42
+ def resolve(
43
+ cls,
44
+ repo_id: str,
45
+ revision: str | None = None,
46
+ *,
47
+ token: str | None = None,
48
+ ) -> HubDatasetRepository:
49
+ try:
50
+ info = HfApi(token=token).dataset_info(repo_id, revision=revision)
51
+ except Exception as exc:
52
+ raise DatasetLoadError(
53
+ f"failed to resolve Hugging Face dataset {repo_id!r}"
54
+ ) from exc
55
+ if not info.sha:
56
+ raise DatasetLoadError(
57
+ f"Hugging Face dataset {repo_id!r} did not return a commit SHA"
58
+ )
59
+ return cls(
60
+ repo_id=repo_id,
61
+ requested_revision=revision,
62
+ resolved_revision=info.sha,
63
+ token=token,
64
+ )
65
+
66
+ def read_text(self, path: str) -> str:
67
+ try:
68
+ local_path = hf_hub_download(
69
+ repo_id=self.repo_id,
70
+ filename=path,
71
+ repo_type="dataset",
72
+ revision=self.resolved_revision,
73
+ token=self.token,
74
+ )
75
+ return Path(local_path).read_text(encoding="utf-8")
76
+ except Exception as exc:
77
+ raise DatasetLoadError(
78
+ f"failed to read {path!r} from dataset {self.repo_id!r} "
79
+ f"at revision {self.resolved_revision}"
80
+ ) from exc
81
+
82
+ def load(
83
+ self,
84
+ config_name: str,
85
+ split: str,
86
+ *,
87
+ streaming: bool,
88
+ ) -> Any:
89
+ try:
90
+ return load_dataset(
91
+ self.repo_id,
92
+ name=config_name,
93
+ split=split,
94
+ revision=self.resolved_revision,
95
+ streaming=streaming,
96
+ token=self.token,
97
+ trust_remote_code=False,
98
+ )
99
+ except Exception as exc:
100
+ raise DatasetLoadError(
101
+ f"failed to load config {config_name!r}, split {split!r} from "
102
+ f"dataset {self.repo_id!r} at revision {self.resolved_revision}"
103
+ ) from exc
@@ -0,0 +1,291 @@
1
+ """Main Dataset aggregate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from sci_modeling_bench.dataset._hub import DatasetRepository, HubDatasetRepository
9
+ from sci_modeling_bench.dataset.knowledge import Knowledge, KnowledgeResource
10
+ from sci_modeling_bench.dataset.manifest import (
11
+ DatasetCollectionManifest,
12
+ DatasetManifest,
13
+ DatasetSplit,
14
+ )
15
+ from sci_modeling_bench.dataset.metadata import DatasetMetadata
16
+ from sci_modeling_bench.dataset.schema import DatasetSchema
17
+ from sci_modeling_bench.dataset.source import HubDatasetSource
18
+ from sci_modeling_bench.dataset.validation import (
19
+ DatasetValidator,
20
+ ValidationReport,
21
+ Violation,
22
+ validate_fields,
23
+ )
24
+ from sci_modeling_bench.exceptions import SchemaMismatchError
25
+
26
+ COLLECTION_MANIFEST_FILENAME = "scimodelingbench.json"
27
+
28
+
29
+ class Dataset:
30
+ """Pinned scientific observations with semantics and validation."""
31
+
32
+ def __init__(
33
+ self,
34
+ manifest: DatasetManifest,
35
+ repository: DatasetRepository,
36
+ *,
37
+ config_name: str,
38
+ validator: DatasetValidator | None = None,
39
+ ) -> None:
40
+ self._manifest = manifest
41
+ self._repository = repository
42
+ self._config_name = config_name
43
+ self._validator = validator or DatasetValidator()
44
+ self._loaded: dict[str, Any] = {}
45
+ self._knowledge = Knowledge(
46
+ {
47
+ name: KnowledgeResource(
48
+ title=spec.title,
49
+ description=spec.description,
50
+ path=spec.path,
51
+ media_type=spec.media_type,
52
+ _reader=repository.read_text,
53
+ )
54
+ for name, spec in manifest.knowledge.items()
55
+ }
56
+ )
57
+
58
+ @classmethod
59
+ def from_hub(
60
+ cls,
61
+ repo_id: str,
62
+ config_name: str | None = None,
63
+ revision: str | None = None,
64
+ *,
65
+ token: str | None = None,
66
+ validator: DatasetValidator | None = None,
67
+ ) -> Dataset:
68
+ """Load one config from a pinned Hugging Face Dataset repository."""
69
+
70
+ return cls.from_source(
71
+ HubDatasetSource(
72
+ repo_id=repo_id,
73
+ config_name=config_name,
74
+ revision=revision,
75
+ ),
76
+ token=token,
77
+ validator=validator,
78
+ )
79
+
80
+ @classmethod
81
+ def from_source(
82
+ cls,
83
+ source: HubDatasetSource,
84
+ *,
85
+ token: str | None = None,
86
+ validator: DatasetValidator | None = None,
87
+ ) -> Dataset:
88
+ """Load a Dataset from an explicit, reusable Hub source reference."""
89
+
90
+ repository = HubDatasetRepository.resolve(
91
+ source.repo_id,
92
+ source.revision,
93
+ token=token,
94
+ )
95
+ collection = DatasetCollectionManifest.from_json(
96
+ repository.read_text(COLLECTION_MANIFEST_FILENAME)
97
+ )
98
+ config_name, config = collection.select(source.config_name)
99
+ manifest = DatasetManifest.from_json(
100
+ repository.read_text(config.manifest)
101
+ )
102
+ return cls(
103
+ manifest,
104
+ repository,
105
+ config_name=config_name,
106
+ validator=validator,
107
+ )
108
+
109
+ @property
110
+ def metadata(self) -> DatasetMetadata:
111
+ return self._manifest.metadata
112
+
113
+ @property
114
+ def schema(self) -> DatasetSchema:
115
+ return self._manifest.dataset_schema
116
+
117
+ @property
118
+ def knowledge(self) -> Knowledge:
119
+ return self._knowledge
120
+
121
+ @property
122
+ def config_name(self) -> str:
123
+ return self._config_name
124
+
125
+ @property
126
+ def default_split(self) -> str:
127
+ return self._manifest.default_split
128
+
129
+ @property
130
+ def splits(self) -> tuple[DatasetSplit, ...]:
131
+ return self._manifest.splits
132
+
133
+ @property
134
+ def available_splits(self) -> tuple[str, ...]:
135
+ return self._manifest.split_names
136
+
137
+ def split(self, name: str | None = None) -> DatasetSplit:
138
+ """Return metadata for a declared split."""
139
+
140
+ selected = name or self.default_split
141
+ for split in self.splits:
142
+ if split.name == selected:
143
+ return split
144
+ available = ", ".join(self.available_splits)
145
+ raise ValueError(
146
+ f"split {selected!r} is not declared for config "
147
+ f"{self.config_name!r}; available: {available}"
148
+ )
149
+
150
+ @property
151
+ def repo_id(self) -> str:
152
+ return self._repository.repo_id
153
+
154
+ @property
155
+ def resolved_revision(self) -> str:
156
+ return self._repository.resolved_revision
157
+
158
+ @property
159
+ def features(self) -> Any:
160
+ """Return physical Hugging Face features for the default split."""
161
+
162
+ return self.load().features
163
+
164
+ def load(
165
+ self,
166
+ split: str | None = None,
167
+ *,
168
+ streaming: bool = False,
169
+ ) -> Any:
170
+ """Load a standard Hugging Face Dataset or IterableDataset."""
171
+
172
+ selected_split = split or self._manifest.default_split
173
+ split_metadata = self.split(selected_split)
174
+ if not streaming and selected_split in self._loaded:
175
+ return self._loaded[selected_split]
176
+
177
+ data = self._repository.load(
178
+ self.config_name,
179
+ selected_split,
180
+ streaming=streaming,
181
+ )
182
+ self._ensure_declared_features(data)
183
+ if not streaming and split_metadata.num_rows is not None:
184
+ actual_rows = len(data)
185
+ if actual_rows != split_metadata.num_rows:
186
+ raise SchemaMismatchError(
187
+ f"config {self.config_name!r}, split {selected_split!r} declares "
188
+ f"{split_metadata.num_rows} rows but loaded {actual_rows}"
189
+ )
190
+ if not streaming:
191
+ self._loaded[selected_split] = data
192
+ return data
193
+
194
+ def validate_inputs(
195
+ self,
196
+ inputs: Mapping[str, Any],
197
+ context: Mapping[str, Any] | None = None,
198
+ ) -> ValidationReport:
199
+ mapping_report = _require_mapping(inputs, "inputs")
200
+ if not mapping_report.valid:
201
+ return mapping_report
202
+ context_values = {} if context is None else context
203
+ context_report = _require_mapping(context_values, "context")
204
+ if not context_report.valid:
205
+ return context_report
206
+
207
+ features = self.features
208
+ return ValidationReport.combine(
209
+ validate_fields(inputs, self.schema.inputs, features=features),
210
+ validate_fields(context_values, self.schema.context, features=features),
211
+ self._validator.validate_inputs(inputs, context),
212
+ )
213
+
214
+ def validate_targets(
215
+ self,
216
+ targets: Mapping[str, Any],
217
+ inputs: Mapping[str, Any] | None = None,
218
+ context: Mapping[str, Any] | None = None,
219
+ ) -> ValidationReport:
220
+ mapping_report = _require_mapping(targets, "targets")
221
+ if not mapping_report.valid:
222
+ return mapping_report
223
+ context_values = {} if context is None else context
224
+ context_report = _require_mapping(context_values, "context")
225
+ if not context_report.valid:
226
+ return context_report
227
+
228
+ features = self.features
229
+ return ValidationReport.combine(
230
+ validate_fields(targets, self.schema.targets, features=features),
231
+ validate_fields(context_values, self.schema.context, features=features),
232
+ self._validator.validate_targets(targets, inputs, context),
233
+ )
234
+
235
+ def validate_observation(
236
+ self,
237
+ observation: Mapping[str, Any],
238
+ ) -> ValidationReport:
239
+ mapping_report = _require_mapping(observation, "observation")
240
+ if not mapping_report.valid:
241
+ return mapping_report
242
+
243
+ features = self.features
244
+ generic = validate_fields(
245
+ observation,
246
+ self.schema.fields,
247
+ features=features,
248
+ reject_unknown=False,
249
+ )
250
+ return ValidationReport.combine(
251
+ generic,
252
+ self._validator.validate_observation(observation),
253
+ )
254
+
255
+ def validate_dataset(self, data: Any | None = None) -> ValidationReport:
256
+ """Validate all observations plus optional dataset-level custom checks."""
257
+
258
+ selected_data = data if data is not None else self.load()
259
+ self._ensure_declared_features(selected_data)
260
+ violations: list[Violation] = []
261
+ for row_index, observation in enumerate(selected_data):
262
+ report = self.validate_observation(observation)
263
+ if report.violations:
264
+ violations.extend(report.at_row(row_index).violations)
265
+ violations.extend(
266
+ self._validator.validate_dataset(selected_data).violations
267
+ )
268
+ return ValidationReport(violations=tuple(violations))
269
+
270
+ def _ensure_declared_features(self, data: Any) -> None:
271
+ features = getattr(data, "features", None)
272
+ if features is None:
273
+ return
274
+ missing = sorted(set(self.schema.field_names) - set(features))
275
+ if missing:
276
+ raise SchemaMismatchError(
277
+ f"dataset {self.metadata.dataset_id!r} is missing declared columns: {missing}"
278
+ )
279
+
280
+
281
+ def _require_mapping(value: Any, label: str) -> ValidationReport:
282
+ if isinstance(value, Mapping):
283
+ return ValidationReport()
284
+ return ValidationReport(
285
+ violations=(
286
+ Violation(
287
+ code="invalid_mapping",
288
+ message=f"{label} must be a mapping keyed by semantic column name",
289
+ ),
290
+ )
291
+ )
@@ -0,0 +1,44 @@
1
+ """Lazy, read-only domain knowledge resources bound to a dataset revision."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterator, Mapping
6
+ from dataclasses import dataclass, field
7
+ from types import MappingProxyType
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class KnowledgeResource:
12
+ """Metadata and lazy text access for one knowledge resource."""
13
+
14
+ title: str
15
+ description: str | None
16
+ path: str
17
+ media_type: str
18
+ _reader: Callable[[str], str] = field(repr=False, compare=False)
19
+
20
+ def read_text(self) -> str:
21
+ """Read this resource from the dataset's resolved repository revision."""
22
+
23
+ return self._reader(self.path)
24
+
25
+
26
+ class Knowledge(Mapping[str, KnowledgeResource]):
27
+ """Read-only mapping of stable names to lazily loaded text resources."""
28
+
29
+ def __init__(self, resources: Mapping[str, KnowledgeResource] | None = None) -> None:
30
+ self._resources = MappingProxyType(dict(resources or {}))
31
+
32
+ def __getitem__(self, name: str) -> KnowledgeResource:
33
+ return self._resources[name]
34
+
35
+ def __iter__(self) -> Iterator[str]:
36
+ return iter(self._resources)
37
+
38
+ def __len__(self) -> int:
39
+ return len(self._resources)
40
+
41
+ def read_text(self, name: str) -> str:
42
+ """Read a named resource without eagerly loading any other resource."""
43
+
44
+ return self[name].read_text()