welt-client 0.7.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.
- welt/__init__.py +37 -0
- welt/causal.py +353 -0
- welt/client.py +589 -0
- welt/credentials.py +38 -0
- welt/errors.py +100 -0
- welt/estimators.py +248 -0
- welt/research.py +102 -0
- welt_client-0.7.0.dist-info/METADATA +102 -0
- welt_client-0.7.0.dist-info/RECORD +11 -0
- welt_client-0.7.0.dist-info/WHEEL +4 -0
- welt_client-0.7.0.dist-info/licenses/LICENSE +202 -0
welt/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Welt Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import AsyncClient, AsyncJob, Client, Job
|
|
4
|
+
from .credentials import Credential
|
|
5
|
+
from .errors import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
CapacityError,
|
|
8
|
+
ExecutionError,
|
|
9
|
+
InvalidInputError,
|
|
10
|
+
ModelUnavailableError,
|
|
11
|
+
WeltError,
|
|
12
|
+
ConflictError, JobCancelledError, JobTimeoutError, NotFoundError,
|
|
13
|
+
PermissionDeniedError, PredictionPendingError, RateLimitError,
|
|
14
|
+
ResultExpiredError, ResultDeletedError, TransportError,
|
|
15
|
+
)
|
|
16
|
+
from .estimators import Classifier, Regressor
|
|
17
|
+
from .causal import CausalDiscovery, CausalResult
|
|
18
|
+
from .errors import InvalidCausalResultError, OptionalDependencyError, UnsupportedGraphConversionError
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AsyncClient", "AsyncJob", "Credential", "ConflictError", "JobCancelledError",
|
|
22
|
+
"JobTimeoutError", "NotFoundError", "PermissionDeniedError", "PredictionPendingError",
|
|
23
|
+
"RateLimitError", "ResultExpiredError", "ResultDeletedError", "TransportError",
|
|
24
|
+
"AuthenticationError",
|
|
25
|
+
"CapacityError",
|
|
26
|
+
"Classifier",
|
|
27
|
+
"Client",
|
|
28
|
+
"ExecutionError",
|
|
29
|
+
"InvalidInputError",
|
|
30
|
+
"Job",
|
|
31
|
+
"ModelUnavailableError",
|
|
32
|
+
"Regressor",
|
|
33
|
+
"WeltError",
|
|
34
|
+
"CausalDiscovery", "CausalResult", "InvalidCausalResultError",
|
|
35
|
+
"OptionalDependencyError", "UnsupportedGraphConversionError",
|
|
36
|
+
]
|
|
37
|
+
__version__ = "0.7.0"
|
welt/causal.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Owned discovery results and explicit local graph conversion.
|
|
2
|
+
|
|
3
|
+
No model runtime or optional causal dependency is imported during SDK import.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
|
|
11
|
+
from sklearn.base import BaseEstimator
|
|
12
|
+
|
|
13
|
+
from .credentials import credential
|
|
14
|
+
from .errors import (
|
|
15
|
+
InvalidCausalResultError,
|
|
16
|
+
OptionalDependencyError,
|
|
17
|
+
UnsupportedGraphConversionError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _freeze(value):
|
|
22
|
+
if isinstance(value, dict):
|
|
23
|
+
return MappingProxyType({k: _freeze(v) for k, v in value.items()})
|
|
24
|
+
if isinstance(value, (list, tuple)):
|
|
25
|
+
return tuple(_freeze(v) for v in value)
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _thaw(value):
|
|
30
|
+
if isinstance(value, MappingProxyType):
|
|
31
|
+
return {k: _thaw(v) for k, v in value.items()}
|
|
32
|
+
if isinstance(value, tuple):
|
|
33
|
+
return [_thaw(v) for v in value]
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _graph(payload):
|
|
38
|
+
if not isinstance(payload, dict):
|
|
39
|
+
raise ValueError("Result must be a structured causal object.")
|
|
40
|
+
names = payload["variables"]
|
|
41
|
+
if (
|
|
42
|
+
not isinstance(names, (list, tuple))
|
|
43
|
+
or not names
|
|
44
|
+
or any(not isinstance(n, str) or not n or len(n) > 128 for n in names)
|
|
45
|
+
or len(names) != len(set(names))
|
|
46
|
+
):
|
|
47
|
+
raise ValueError("Causal variables must retain unique ordered names.")
|
|
48
|
+
if payload["native_graph_type"] not in ("dag", "directed"):
|
|
49
|
+
raise ValueError("This SDK slice supports native directed graphs only.")
|
|
50
|
+
edges, pairs = [], set()
|
|
51
|
+
for value in payload["edge_marks"]:
|
|
52
|
+
if not isinstance(value, dict) or set(value) != {"u", "v", "u_mark", "v_mark"}:
|
|
53
|
+
raise ValueError("Edges must have explicit native endpoint marks.")
|
|
54
|
+
u, v = value["u"], value["v"]
|
|
55
|
+
marks = value["u_mark"], value["v_mark"]
|
|
56
|
+
if (
|
|
57
|
+
u not in names
|
|
58
|
+
or v not in names
|
|
59
|
+
or u == v
|
|
60
|
+
or marks not in (("tail", "arrow"), ("arrow", "tail"))
|
|
61
|
+
):
|
|
62
|
+
raise ValueError("Unsupported or invalid native directed edge.")
|
|
63
|
+
pair = (u, v) if marks == ("tail", "arrow") else (v, u)
|
|
64
|
+
if pair in pairs:
|
|
65
|
+
raise ValueError("Duplicate directed edge.")
|
|
66
|
+
pairs.add(pair)
|
|
67
|
+
edges.append(deepcopy(value))
|
|
68
|
+
if payload["native_graph_type"] == "dag":
|
|
69
|
+
children = {n: [] for n in names}
|
|
70
|
+
indegree = dict.fromkeys(names, 0)
|
|
71
|
+
for source, target in pairs:
|
|
72
|
+
children[source].append(target)
|
|
73
|
+
indegree[target] += 1
|
|
74
|
+
ready = [n for n in names if indegree[n] == 0]
|
|
75
|
+
visited = 0
|
|
76
|
+
while ready:
|
|
77
|
+
source = ready.pop()
|
|
78
|
+
visited += 1
|
|
79
|
+
for target in children[source]:
|
|
80
|
+
indegree[target] -= 1
|
|
81
|
+
if indegree[target] == 0:
|
|
82
|
+
ready.append(target)
|
|
83
|
+
if visited != len(names):
|
|
84
|
+
raise ValueError("Native DAG contains a directed cycle.")
|
|
85
|
+
return tuple(names), edges
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, init=False)
|
|
89
|
+
class CausalResult:
|
|
90
|
+
"""Immutable native result; score fetches use explicit client/job ownership."""
|
|
91
|
+
|
|
92
|
+
_payload: object = field(repr=False)
|
|
93
|
+
|
|
94
|
+
def __init__(self, payload):
|
|
95
|
+
try:
|
|
96
|
+
_graph(payload)
|
|
97
|
+
for name in (
|
|
98
|
+
"id",
|
|
99
|
+
"workspace_id",
|
|
100
|
+
"dataset_id",
|
|
101
|
+
"model_version",
|
|
102
|
+
"configuration_version",
|
|
103
|
+
"score_reference",
|
|
104
|
+
):
|
|
105
|
+
if not isinstance(payload[name], str) or not payload[name]:
|
|
106
|
+
raise ValueError("Missing result provenance.")
|
|
107
|
+
if payload["score_semantics"] != "native_directed_edge_probability":
|
|
108
|
+
raise ValueError("Unsupported causal score semantics.")
|
|
109
|
+
if not isinstance(payload["diagnostics"], dict) or not isinstance(
|
|
110
|
+
payload["decoder"], dict
|
|
111
|
+
):
|
|
112
|
+
raise ValueError("Result must preserve decoder/provenance metadata.")
|
|
113
|
+
job_id = payload["diagnostics"].get("job_id")
|
|
114
|
+
if not isinstance(job_id, str) or not job_id or len(job_id) > 128:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"Result must preserve its owned discovery job identity."
|
|
117
|
+
)
|
|
118
|
+
if not isinstance(payload["assumptions"], list) or any(
|
|
119
|
+
not isinstance(a, str) for a in payload["assumptions"]
|
|
120
|
+
):
|
|
121
|
+
raise ValueError("Assumptions must be explicit strings.")
|
|
122
|
+
if not isinstance(payload["constraints_applied"], dict):
|
|
123
|
+
raise ValueError("Constraints must be explicit.")
|
|
124
|
+
except (KeyError, TypeError, ValueError):
|
|
125
|
+
raise InvalidCausalResultError(
|
|
126
|
+
"Invalid native causal result; no graph repair was applied.",
|
|
127
|
+
code="invalid_causal_result",
|
|
128
|
+
) from None
|
|
129
|
+
object.__setattr__(self, "_payload", _freeze(deepcopy(payload)))
|
|
130
|
+
|
|
131
|
+
def to_dict(self):
|
|
132
|
+
return _thaw(self._payload)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def variables(self):
|
|
136
|
+
return self._payload["variables"]
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def id(self):
|
|
140
|
+
return self._payload["id"]
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def score_reference(self):
|
|
144
|
+
return self._payload["score_reference"]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def model_version(self):
|
|
148
|
+
return self._payload["model_version"]
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def dataset_id(self):
|
|
152
|
+
return self._payload["dataset_id"]
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def job_id(self):
|
|
156
|
+
return self._payload["diagnostics"]["job_id"]
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def configuration_version(self):
|
|
160
|
+
return self._payload["configuration_version"]
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def diagnostics(self):
|
|
164
|
+
return self._payload["diagnostics"]
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def decoder(self):
|
|
168
|
+
return self._payload["decoder"]
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def assumptions(self):
|
|
172
|
+
return self._payload["assumptions"]
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def edge_marks(self):
|
|
176
|
+
return self._payload["edge_marks"]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def native_graph_type(self):
|
|
180
|
+
return self._payload["native_graph_type"]
|
|
181
|
+
|
|
182
|
+
def to_ergodic(self):
|
|
183
|
+
"""Convert a declared native DAG locally, preserving marks and all nodes."""
|
|
184
|
+
if self.native_graph_type != "dag":
|
|
185
|
+
raise UnsupportedGraphConversionError(
|
|
186
|
+
"Only native DAG conversion is qualified; no projection was applied.",
|
|
187
|
+
code="unsupported_graph_conversion",
|
|
188
|
+
)
|
|
189
|
+
try:
|
|
190
|
+
if version("ergodic") != "0.1.1":
|
|
191
|
+
raise OptionalDependencyError(
|
|
192
|
+
"Install the approved Ergodic co-release artifact; the public dependency is not yet available.",
|
|
193
|
+
code="optional_dependency_unavailable",
|
|
194
|
+
)
|
|
195
|
+
import ergodic
|
|
196
|
+
|
|
197
|
+
if not hasattr(ergodic, "MixedGraph"):
|
|
198
|
+
raise OptionalDependencyError(
|
|
199
|
+
"The installed Ergodic package lacks the approved graph API.",
|
|
200
|
+
code="optional_dependency_unavailable",
|
|
201
|
+
)
|
|
202
|
+
except (ImportError, PackageNotFoundError):
|
|
203
|
+
raise OptionalDependencyError(
|
|
204
|
+
"Install the approved Ergodic co-release artifact; Welt does not download it automatically.",
|
|
205
|
+
code="optional_dependency_unavailable",
|
|
206
|
+
) from None
|
|
207
|
+
payload = dict(
|
|
208
|
+
kind="dag",
|
|
209
|
+
nodes=list(self.variables),
|
|
210
|
+
edges=[_thaw(edge) for edge in self.edge_marks],
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
graph = ergodic.MixedGraph.from_dict(payload)
|
|
214
|
+
except (ValueError, TypeError, AttributeError):
|
|
215
|
+
raise InvalidCausalResultError(
|
|
216
|
+
"Ergodic rejected native DAG conversion; no graph repair was applied.",
|
|
217
|
+
code="invalid_causal_result",
|
|
218
|
+
) from None
|
|
219
|
+
# MixedGraph stores node sets; ensure actual conversion retains all names.
|
|
220
|
+
if graph.nodes != frozenset(self.variables):
|
|
221
|
+
raise InvalidCausalResultError(
|
|
222
|
+
"Ergodic conversion changed native variables.",
|
|
223
|
+
code="invalid_causal_result",
|
|
224
|
+
)
|
|
225
|
+
return graph
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def validated_scores(result, payload):
|
|
229
|
+
"""Bounded original-axis scores; never decode or threshold another graph."""
|
|
230
|
+
import numpy as np
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if (
|
|
234
|
+
not isinstance(payload, dict)
|
|
235
|
+
or payload["result_id"] != result.id
|
|
236
|
+
or payload["score_reference"] != result.score_reference
|
|
237
|
+
):
|
|
238
|
+
raise ValueError("Scores are not bound to this result.")
|
|
239
|
+
if (
|
|
240
|
+
tuple(payload["variables"]) != result.variables
|
|
241
|
+
or payload["score_axes"] != "source_row_target_column"
|
|
242
|
+
or payload["score_semantics"] != "native_directed_edge_probability"
|
|
243
|
+
):
|
|
244
|
+
raise ValueError("Score semantics/order differ from the native result.")
|
|
245
|
+
values = payload["values"]
|
|
246
|
+
if any(
|
|
247
|
+
isinstance(value, bool) or not isinstance(value, (int, float))
|
|
248
|
+
for row in values
|
|
249
|
+
for value in row
|
|
250
|
+
):
|
|
251
|
+
raise ValueError("Scores must be numeric.")
|
|
252
|
+
array = np.asarray(values, dtype=float)
|
|
253
|
+
if (
|
|
254
|
+
array.shape != (len(result.variables), len(result.variables))
|
|
255
|
+
or not np.isfinite(array).all()
|
|
256
|
+
or not ((array >= 0) & (array <= 1)).all()
|
|
257
|
+
or not (array.diagonal() == 0).all()
|
|
258
|
+
):
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"Scores require native square finite probability semantics."
|
|
261
|
+
)
|
|
262
|
+
except (KeyError, TypeError, ValueError, OverflowError):
|
|
263
|
+
raise InvalidCausalResultError(
|
|
264
|
+
"Invalid result-bound causal scores; no transpose or repair was applied.",
|
|
265
|
+
code="invalid_causal_result",
|
|
266
|
+
) from None
|
|
267
|
+
return deepcopy(payload)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class CausalDiscovery(BaseEstimator):
|
|
271
|
+
"""Dataset-based discovery facade with reusable sklearn-style parameters."""
|
|
272
|
+
|
|
273
|
+
def __init__(
|
|
274
|
+
self,
|
|
275
|
+
model="arrow",
|
|
276
|
+
configuration="default",
|
|
277
|
+
base_url=None,
|
|
278
|
+
api_key=None,
|
|
279
|
+
random_state=0,
|
|
280
|
+
timeout=600,
|
|
281
|
+
):
|
|
282
|
+
self.model, self.configuration, self.base_url = model, configuration, base_url
|
|
283
|
+
self.api_key = credential(api_key)
|
|
284
|
+
self.random_state, self.timeout = random_state, timeout
|
|
285
|
+
|
|
286
|
+
def _client(self):
|
|
287
|
+
from .client import Client
|
|
288
|
+
|
|
289
|
+
return Client(base_url=self.base_url, api_key=self.api_key)
|
|
290
|
+
|
|
291
|
+
def set_params(self, **params):
|
|
292
|
+
if "api_key" in params:
|
|
293
|
+
params["api_key"] = credential(params["api_key"])
|
|
294
|
+
return super().set_params(**params)
|
|
295
|
+
|
|
296
|
+
def submit_discover(
|
|
297
|
+
self,
|
|
298
|
+
X=None,
|
|
299
|
+
*,
|
|
300
|
+
dataset_id=None,
|
|
301
|
+
constraints=None,
|
|
302
|
+
model_version=None,
|
|
303
|
+
idempotency_key=None,
|
|
304
|
+
):
|
|
305
|
+
if (X is None) == (dataset_id is None):
|
|
306
|
+
raise ValueError("Supply exactly one observational table or dataset_id.")
|
|
307
|
+
if (
|
|
308
|
+
type(self.random_state) is not int
|
|
309
|
+
or not 0 <= self.random_state <= 2**32 - 1
|
|
310
|
+
):
|
|
311
|
+
raise ValueError("random_state must be an unsigned32-bit integer.")
|
|
312
|
+
client = self._client()
|
|
313
|
+
try:
|
|
314
|
+
if dataset_id is None:
|
|
315
|
+
from .estimators import table
|
|
316
|
+
|
|
317
|
+
columns, rows, _ = table(X)
|
|
318
|
+
dataset_id = client.upload(columns=columns, rows=rows, target=None)[
|
|
319
|
+
"id"
|
|
320
|
+
]
|
|
321
|
+
return client.submit_discover(
|
|
322
|
+
dataset_id,
|
|
323
|
+
model=self.model,
|
|
324
|
+
configuration=self.configuration,
|
|
325
|
+
seed=self.random_state,
|
|
326
|
+
constraints=constraints,
|
|
327
|
+
model_version=model_version,
|
|
328
|
+
idempotency_key=idempotency_key,
|
|
329
|
+
)
|
|
330
|
+
except BaseException:
|
|
331
|
+
client.close()
|
|
332
|
+
raise
|
|
333
|
+
|
|
334
|
+
def discover(
|
|
335
|
+
self,
|
|
336
|
+
X=None,
|
|
337
|
+
*,
|
|
338
|
+
dataset_id=None,
|
|
339
|
+
constraints=None,
|
|
340
|
+
model_version=None,
|
|
341
|
+
idempotency_key=None,
|
|
342
|
+
):
|
|
343
|
+
job = self.submit_discover(
|
|
344
|
+
X,
|
|
345
|
+
dataset_id=dataset_id,
|
|
346
|
+
constraints=constraints,
|
|
347
|
+
model_version=model_version,
|
|
348
|
+
idempotency_key=idempotency_key,
|
|
349
|
+
)
|
|
350
|
+
try:
|
|
351
|
+
return job.result(timeout=self.timeout)
|
|
352
|
+
finally:
|
|
353
|
+
job.client.close()
|