redcedar 0.93.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.
redcedar/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Red Cedar — Python client for the Red Cedar quantum ML server."""
2
+
3
+ __version__ = "0.93.0"
4
+
5
+ from .client import connect, RedCedarClient
6
+
7
+ __all__ = ["connect", "RedCedarClient", "__version__"]
redcedar/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """python -m redcedar — the same as the redcedar command."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
redcedar/analysis.py ADDED
@@ -0,0 +1,74 @@
1
+ """Dataset analysis — PCA, Mutual Information, Resource Estimation."""
2
+
3
+
4
+ class AnalysisManager:
5
+ """Run dataset analysis operations on the server."""
6
+
7
+ def __init__(self, client):
8
+ self._client = client
9
+
10
+ def pca(self, dataset_id, verbose=False, on_progress=None):
11
+ """Run PCA analysis on a dataset.
12
+
13
+ Args:
14
+ dataset_id: Dataset ID from the catalog (e.g., 'wdbc', 'iris').
15
+ verbose: Print progress to stdout; "log" also prints the server's log for the job.
16
+ on_progress: Callback(percent, message) for progress updates.
17
+
18
+ Returns:
19
+ Dict with variance_ratios, cumulative_variance, thresholds, etc.
20
+ """
21
+ if self._client.core: # local mode: call the core directly
22
+ return self._client._run_local(self._client.core.pca, dataset_id,
23
+ verbose=verbose, on_progress=on_progress)
24
+ return self._client._run_job(
25
+ "/analysis/pca",
26
+ "/analysis/pca/{job_id}/stream",
27
+ body={"dataset_id": dataset_id},
28
+ verbose=verbose,
29
+ on_progress=on_progress,
30
+ )
31
+
32
+ def mi(self, dataset_id, verbose=False, on_progress=None):
33
+ """Run Mutual Information feature importance analysis.
34
+
35
+ Args:
36
+ dataset_id: Dataset ID from the catalog.
37
+ verbose: Print progress to stdout; "log" also prints the server's log for the job.
38
+ on_progress: Callback(percent, message) for progress updates.
39
+
40
+ Returns:
41
+ Dict with ranked_features, cumulative_mi, thresholds, etc.
42
+ """
43
+ if self._client.core: # local mode: call the core directly
44
+ return self._client._run_local(self._client.core.mi, dataset_id,
45
+ verbose=verbose, on_progress=on_progress)
46
+ return self._client._run_job(
47
+ "/analysis/mi",
48
+ "/analysis/mi/{job_id}/stream",
49
+ body={"dataset_id": dataset_id},
50
+ verbose=verbose,
51
+ on_progress=on_progress,
52
+ )
53
+
54
+ def qre(self, dataset_id, verbose=False, on_progress=None):
55
+ """Run Quantum Resource Estimation.
56
+
57
+ Args:
58
+ dataset_id: Dataset ID from the catalog.
59
+ verbose: Print progress to stdout; "log" also prints the server's log for the job.
60
+ on_progress: Callback(percent, message) for progress updates.
61
+
62
+ Returns:
63
+ Dict with estimatedQubits, standardDeviation, plotData, etc.
64
+ """
65
+ if self._client.core: # local mode: call the core directly
66
+ return self._client._run_local(self._client.core.qre, dataset_id,
67
+ verbose=verbose, on_progress=on_progress)
68
+ return self._client._run_job(
69
+ "/analysis/qre",
70
+ "/analysis/qre/{job_id}/stream",
71
+ body={"dataset_id": dataset_id},
72
+ verbose=verbose,
73
+ on_progress=on_progress,
74
+ )
redcedar/cli.py ADDED
@@ -0,0 +1,136 @@
1
+ """The redcedar command: train, evaluate and predict from the command line.
2
+
3
+ redcedar train my_data.csv --qubits 6 --seed 123
4
+ redcedar evaluate <train_id>
5
+ redcedar predict <train_id> new_data.csv -o predictions.csv
6
+ redcedar models
7
+ redcedar status
8
+
9
+ The server comes from --url / --api-key, or REDCEDAR_URL / REDCEDAR_API_KEY.
10
+ """
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ import requests
16
+
17
+ from .client import connect
18
+
19
+
20
+ def main(argv=None):
21
+ p = argparse.ArgumentParser(prog="redcedar", description="Red Cedar quantum machine learning")
22
+ p.add_argument("--url", help="server URL (default: REDCEDAR_URL or http://127.0.0.1:8002)")
23
+ p.add_argument("--api-key", help="API key (default: REDCEDAR_API_KEY)")
24
+ p.add_argument("--local", action="store_true",
25
+ help="call the Red Cedar core directly in this process instead of a server "
26
+ "(redcedar_core, installed with pip install -e, or the red-cedar folder "
27
+ "in REDCEDAR_CORE_PATH)")
28
+ p.add_argument("-q", "--quiet", action="store_true", help="no progress output")
29
+ p.add_argument("--log", action="store_true", help="also print the server's log for the job")
30
+ sub = p.add_subparsers(dest="command", required=True)
31
+
32
+ t = sub.add_parser("train", help="train a model on a CSV file or catalog dataset")
33
+ t.add_argument("data", help="CSV file path or catalog dataset ID")
34
+ t.add_argument("--settings", help="settings file (.json or .toml)")
35
+ t.add_argument("--qubits", type=int, help="num_qubits")
36
+ t.add_argument("--layers", type=int, help="train layers 1 to N (min_layers=1, max_layers=N)")
37
+ t.add_argument("--seed", type=int, help="seed for the initial parameters")
38
+ t.add_argument("--backend", help="where the circuits run (default: statevector)")
39
+ t.add_argument("--task", choices=["classification", "regression"],
40
+ help="default: detected from the target column")
41
+ t.add_argument("--test-size", type=float,
42
+ help="held-out test rows, a fraction or a count (0 = train on all rows)")
43
+
44
+ e = sub.add_parser("evaluate", help="score a model on its held-out test rows")
45
+ e.add_argument("train_id")
46
+ e.add_argument("--layer", type=int)
47
+ e.add_argument("--backend")
48
+
49
+ r = sub.add_parser("predict", help="predictions for new rows")
50
+ r.add_argument("train_id")
51
+ r.add_argument("data", help="CSV file with the new rows")
52
+ r.add_argument("-o", "--output", help="write the rows and predictions to this CSV file")
53
+ r.add_argument("--layer", type=int)
54
+ r.add_argument("--backend")
55
+
56
+ sub.add_parser("models", help="list recently trained models")
57
+ sub.add_parser("status", help="check the server and show the server and SDK versions")
58
+
59
+ args = p.parse_args(argv)
60
+ client = connect(url=args.url, api_key=args.api_key, local=args.local or False)
61
+ verbose = False if args.quiet else "log" if args.log else True
62
+ run_settings = {"backend": args.backend} if getattr(args, "backend", None) else None
63
+
64
+ try:
65
+ if args.command == "train":
66
+ from .settings import load_settings
67
+ settings = load_settings(args.settings)
68
+ if args.qubits is not None:
69
+ settings["num_qubits"] = args.qubits
70
+ if args.layers is not None:
71
+ settings["min_layers"], settings["max_layers"] = 1, args.layers
72
+ if args.seed is not None:
73
+ settings["seed"] = args.seed
74
+ if args.backend:
75
+ settings["backend"] = args.backend
76
+ test_size = args.test_size
77
+ if test_size is not None and test_size >= 1:
78
+ test_size = int(test_size)
79
+ result = client.training.run(args.data, settings=settings, verbose=verbose,
80
+ task=args.task, test_size=test_size)
81
+ for layer, v in result.get("validation", {}).items():
82
+ if v.get("test_accuracy") is not None:
83
+ print(f"Layer {layer}: test accuracy {v['test_accuracy']:.1%}")
84
+ elif v.get("test_loss") is not None:
85
+ print(f"Layer {layer}: test loss {v['test_loss']:.4f}")
86
+ else:
87
+ print(f"Layer {layer}: trained on all rows (no test set)")
88
+ print(f"train_id: {result['train_id']}")
89
+
90
+ elif args.command == "evaluate":
91
+ report = client.training.evaluate(args.train_id, layer=args.layer, verbose=verbose,
92
+ settings=run_settings)
93
+ if report.get("task") == "regression":
94
+ print(f"Mean absolute error {report['mean_absolute_error']:.4f}, "
95
+ f"loss {report['overall_loss']:.4f} on {report['test_samples']} test rows")
96
+ else:
97
+ print(f"Accuracy {report['overall_accuracy']:.1%} on {report['test_samples']} test rows")
98
+ for c in report["per_class"]:
99
+ print(f" class {c['class']}: {c['accuracy']:.1%} ({c['samples']} rows)")
100
+
101
+ elif args.command == "predict":
102
+ result = client.inference.run(args.train_id, args.data, layer=args.layer,
103
+ output=args.output, verbose=verbose,
104
+ settings=run_settings)
105
+ if args.output:
106
+ print(f"{result['rows']} predictions written to {args.output}")
107
+ else:
108
+ for p_row in result["predictions"]:
109
+ print(p_row["prediction"])
110
+ if "accuracy" in result:
111
+ print(f"Accuracy on these rows: {result['accuracy']:.1%}")
112
+ if "mean_absolute_error" in result:
113
+ print(f"Mean absolute error on these rows: {result['mean_absolute_error']:.4f}")
114
+
115
+ elif args.command == "models":
116
+ for m in client.inference.models()["models"]:
117
+ print(m["train_id"], m.get("dataset_display_name") or m.get("dataset", ""),
118
+ m.get("task", ""), f"{m.get('num_qubits')} qubits", f"layers {m.get('layers')}")
119
+
120
+ elif args.command == "status":
121
+ s = client.status()
122
+ print(f"Server: {client.url or 'none (local mode: the Red Cedar core in this process)'}")
123
+ print(f"Server version: {s['server_version'] or 'unknown'}")
124
+ print(f"SDK version: {s['sdk_version']}")
125
+ if not s["ok"]:
126
+ print(f"Error: {s['error']}", file=sys.stderr)
127
+ return 1
128
+
129
+ except (RuntimeError, requests.HTTPError, requests.ConnectionError, FileNotFoundError) as err:
130
+ print(f"Error: {err}", file=sys.stderr)
131
+ return 1
132
+ return 0
133
+
134
+
135
+ if __name__ == "__main__":
136
+ sys.exit(main())
redcedar/client.py ADDED
@@ -0,0 +1,366 @@
1
+ """Core client — HTTP requests, SSE streaming, authentication."""
2
+
3
+ import os
4
+ import sys
5
+ import json
6
+ import time
7
+ import threading
8
+ import requests
9
+
10
+ from .datasets import DatasetManager
11
+ from .analysis import AnalysisManager
12
+ from .training import TrainingManager
13
+ from .inference import InferenceManager
14
+ from .explainable import ExplainableManager
15
+
16
+
17
+ def connect(url=None, api_key=None, local=False):
18
+ """Create a RedCedarClient connected to the server.
19
+
20
+ Args:
21
+ url: Server URL. Defaults to REDCEDAR_URL env var or http://127.0.0.1:8002
22
+ (the port run_server uses).
23
+ api_key: API key. Defaults to REDCEDAR_API_KEY env var.
24
+ local: For development with the Red Cedar source. True calls the Red Cedar
25
+ core (redcedar_core, installed with `pip install -e .` in a
26
+ red-cedar folder) directly in this process, so a debugger steps into
27
+ the Red Cedar code. Or the path of a red-cedar folder to use.
28
+
29
+ Returns:
30
+ RedCedarClient instance.
31
+ """
32
+ if local:
33
+ from .local import import_core
34
+ return RedCedarClient(None, None, core=import_core(None if local is True else local))
35
+ url = url or os.environ.get("REDCEDAR_URL", "http://127.0.0.1:8002")
36
+ api_key = api_key or os.environ.get("REDCEDAR_API_KEY", "")
37
+ return RedCedarClient(url, api_key)
38
+
39
+
40
+ def _raise_for_status(resp):
41
+ """Like resp.raise_for_status(), but the message includes the server's reason
42
+ ('detail' from the Red Cedar server, 'error' from the gateway)."""
43
+ if resp.ok:
44
+ return
45
+ try:
46
+ body = resp.json()
47
+ reason = body.get("detail") or body.get("error") if isinstance(body, dict) else None
48
+ except ValueError:
49
+ reason = None
50
+ msg = f"{resp.status_code} {resp.reason}"
51
+ if reason:
52
+ msg += f": {reason}"
53
+ raise requests.HTTPError(msg, response=resp)
54
+
55
+
56
+ class _ProgressLine:
57
+ """A job's verbose output: one line, rewritten on each progress update, with a dot
58
+ added while the job is quiet so it's clear it's still working. The dots come every
59
+ 2 seconds at first, every 5 after 20 seconds and every 10 after a minute, counted
60
+ from the last update."""
61
+
62
+ def __init__(self, out=None):
63
+ self._out = out or sys.stdout
64
+ self._lock = threading.Lock()
65
+ self._text = ""
66
+ self._ndots = 0
67
+ self._width = 0 # characters on the current line
68
+ self._restart()
69
+ self._stop = threading.Event()
70
+ threading.Thread(target=self._dots, daemon=True).start()
71
+
72
+ def _restart(self):
73
+ self._since = time.monotonic()
74
+ self._next = self._since + 2
75
+
76
+ def _draw(self):
77
+ """Write the whole line again, text and dots. Jupyter treats \\r as overwriting
78
+ the line from its start, so the line is always redrawn rather than added to;
79
+ trailing spaces clear what's left of a longer line."""
80
+ s = self._text + (" " + "." * self._ndots if self._ndots else "")
81
+ print(f"\r{s}{' ' * max(self._width - len(s), 0)}", end="", flush=True, file=self._out)
82
+ self._width = len(s)
83
+
84
+ def _dots(self):
85
+ while not self._stop.wait(0.5):
86
+ with self._lock:
87
+ now = time.monotonic()
88
+ if now >= self._next:
89
+ self._ndots += 1
90
+ self._draw()
91
+ quiet = now - self._since
92
+ self._next = now + (2 if quiet < 20 else 5 if quiet < 60 else 10)
93
+
94
+ def update(self, text):
95
+ """Rewrite the line with new text; the dots start again."""
96
+ with self._lock:
97
+ self._text, self._ndots = text, 0
98
+ self._draw()
99
+ self._restart()
100
+
101
+ def message(self, text):
102
+ """A line of its own (a log line, an error), below the progress line."""
103
+ with self._lock:
104
+ if self._width:
105
+ print(file=self._out)
106
+ print(text, flush=True, file=self._out)
107
+ self._text, self._ndots, self._width = "", 0, 0
108
+ self._restart()
109
+
110
+ def close(self):
111
+ self._stop.set()
112
+ with self._lock:
113
+ if self._width:
114
+ print(file=self._out)
115
+ self._text, self._ndots, self._width = "", 0, 0
116
+
117
+
118
+ class RedCedarClient:
119
+ """Main client for interacting with the Red Cedar server."""
120
+
121
+ def __init__(self, url, api_key, core=None):
122
+ self.url = url.rstrip("/") if url else None
123
+ self.api_key = api_key
124
+ # Local mode (connect(local=True)): the redcedar_core module. Each SDK call then
125
+ # calls its core function directly instead of sending a request to a server.
126
+ self.core = core
127
+ self._last_job_id = None
128
+ self._server_version = None
129
+ self.datasets = DatasetManager(self)
130
+ self.analysis = AnalysisManager(self)
131
+ self.training = TrainingManager(self)
132
+ self.inference = InferenceManager(self)
133
+ self.explainable = ExplainableManager(self)
134
+
135
+ def _server(self, path=""):
136
+ """The server URL. Local mode has no server: every SDK call has a direct one."""
137
+ if self.url is None:
138
+ raise NotImplementedError(f"Local mode has no direct call for {path}")
139
+ return self.url
140
+
141
+ def _headers(self):
142
+ h = {"Content-Type": "application/json"}
143
+ if self.api_key:
144
+ h["x-api-key"] = self.api_key
145
+ return h
146
+
147
+ def _local_call(self, fn, *args, **kwargs):
148
+ """Call a redcedar_core function (local mode). Something not found, or a bad
149
+ request, raises requests.HTTPError with the server's message, as in server mode."""
150
+ try:
151
+ return fn(*args, **kwargs)
152
+ except FileNotFoundError as e:
153
+ raise requests.HTTPError(f"404 Not Found: {e}") from e
154
+ except ValueError as e:
155
+ raise requests.HTTPError(f"400 Bad Request: {e}") from e
156
+
157
+ def _run_local(self, fn, *args, verbose=False, on_progress=None, events=None, **kwargs):
158
+ """Run a redcedar_core job function in this thread (local mode). Its events go
159
+ to the same handler as a server job's: progress, log lines (verbose="log"), and the
160
+ other events into the events list. Returns the job's result."""
161
+ out = sys.stdout # while the job runs, print() goes to its log
162
+ line = _ProgressLine(out) if verbose else None
163
+ on_event = lambda e: self._handle_event(e, verbose, on_progress, events, line) # noqa: E731
164
+ try:
165
+ return self._local_call(fn, *args, on_event=on_event, **kwargs)
166
+ finally:
167
+ if line:
168
+ line.close()
169
+
170
+ def _get(self, path):
171
+ resp = requests.get(f"{self._server(path)}{path}", headers=self._headers())
172
+ _raise_for_status(resp)
173
+ return resp.json()
174
+
175
+ def _post(self, path, body=None):
176
+ resp = requests.post(
177
+ f"{self._server(path)}{path}",
178
+ headers=self._headers(),
179
+ json=body or {},
180
+ )
181
+ _raise_for_status(resp)
182
+ return resp.json()
183
+
184
+ def _post_file(self, path, file_path):
185
+ """Upload a file as multipart form data (field name 'file')."""
186
+ headers = {"x-api-key": self.api_key} if self.api_key else {}
187
+ with open(file_path, "rb") as f:
188
+ resp = requests.post(
189
+ f"{self._server(path)}{path}",
190
+ headers=headers,
191
+ files={"file": (os.path.basename(file_path), f)},
192
+ )
193
+ _raise_for_status(resp)
194
+ return resp.json()
195
+
196
+ def _open_stream(self, path, line=None):
197
+ """Open a job's SSE stream. A queued job has no stream until it starts
198
+ (the server answers 404), so wait for it to start."""
199
+ waiting = False
200
+ while True:
201
+ resp = requests.get(
202
+ f"{self._server(path)}{path}",
203
+ headers=self._headers(),
204
+ stream=True,
205
+ )
206
+ if resp.status_code != 404:
207
+ _raise_for_status(resp)
208
+ return resp
209
+ resp.close()
210
+
211
+ # Also launches the next queued job on servers that only do so on a request.
212
+ qs = self._get("/jobs/status")
213
+ if not qs.get("total"):
214
+ # Nothing queued or running, so the job is gone
215
+ _raise_for_status(resp)
216
+ if line and not waiting:
217
+ line.update(" Waiting in queue...")
218
+ waiting = True
219
+ time.sleep(2)
220
+
221
+ def _stream_sse(self, path, line=None):
222
+ """Consume an SSE stream, yielding parsed events."""
223
+ resp = self._open_stream(path, line)
224
+
225
+ event_type = "message"
226
+ data_buf = ""
227
+
228
+ for line in resp.iter_lines(decode_unicode=True):
229
+ if line is None:
230
+ continue
231
+ if line == "":
232
+ # End of event
233
+ if data_buf:
234
+ try:
235
+ parsed = json.loads(data_buf)
236
+ parsed["_event"] = event_type
237
+ yield parsed
238
+ except json.JSONDecodeError:
239
+ pass
240
+ event_type = "message"
241
+ data_buf = ""
242
+ elif line.startswith("event: "):
243
+ event_type = line[7:]
244
+ elif line.startswith("data: "):
245
+ data_buf = line[6:]
246
+
247
+ def _run_job(self, post_path, stream_path, body=None, verbose=False, on_progress=None,
248
+ events=None):
249
+ """Start a job via POST, consume SSE until done. Returns summary.
250
+
251
+ If events is a list, every other event the job sends (e.g. training
252
+ datapoints and validation results) is appended to it."""
253
+ result = self._post(post_path, body)
254
+ job_id = result.get("job_id") or result.get("train_id")
255
+ self._last_job_id = job_id
256
+ if not job_id:
257
+ return result
258
+
259
+ # Trigger server-side queue cleanup and job launch before connecting
260
+ # to SSE. Without this, stale queue entries from prior jobs block
261
+ # new jobs from starting and training_state is never populated.
262
+ try:
263
+ self._get("/jobs/status")
264
+ except Exception:
265
+ pass
266
+
267
+ line = _ProgressLine() if verbose else None
268
+ try:
269
+ for event in self._stream_sse(stream_path.format(job_id=job_id), line):
270
+ self._handle_event(event, verbose, on_progress, events, line)
271
+ etype = event.get("_event", event.get("type", ""))
272
+ if etype == "done":
273
+ return event.get("summary", event)
274
+ elif etype == "error":
275
+ raise RuntimeError(event.get("message", "Job failed"))
276
+ elif etype == "stopped":
277
+ raise RuntimeError(event.get("message", "Job stopped"))
278
+ finally:
279
+ if line:
280
+ line.close()
281
+
282
+ raise RuntimeError("SSE stream ended without done/error event")
283
+
284
+ def _handle_event(self, event, verbose, on_progress, events, line=None):
285
+ """Report one job event: progress and messages (verbose; the server's log lines
286
+ only with verbose="log"), the on_progress callback, and the other events into
287
+ the events list. line is the job's _ProgressLine when verbose."""
288
+ etype = event.get("_event", event.get("type", ""))
289
+
290
+ if etype == "progress":
291
+ pct = event.get("percent", 0)
292
+ msg = event.get("message", "")
293
+ if verbose:
294
+ line.update(f" [{pct:3d}%] {msg}")
295
+ if on_progress:
296
+ on_progress(pct, msg)
297
+
298
+ elif etype == "done":
299
+ if verbose:
300
+ line.update(" [100%] Done.")
301
+ line.close()
302
+
303
+ elif etype == "log":
304
+ if verbose == "log":
305
+ msg = event.get("message", "")
306
+ if msg:
307
+ line.message(f" {msg}")
308
+
309
+ elif etype == "error":
310
+ if verbose:
311
+ line.message(f" ERROR: {event.get('message', 'Job failed')}")
312
+
313
+ elif etype == "stopped":
314
+ if verbose:
315
+ line.message(f" {event.get('message', 'Job stopped')}")
316
+
317
+ else:
318
+ # A training layer's test result
319
+ if etype == "validation" and verbose:
320
+ acc, loss = event.get("test_accuracy"), event.get("test_loss")
321
+ if acc is not None:
322
+ line.message(f" Layer {event.get('layer')}: test accuracy {acc:.1%}")
323
+ elif loss is not None:
324
+ line.message(f" Layer {event.get('layer')}: test loss {loss:.4f}")
325
+ if events is not None:
326
+ events.append(event)
327
+
328
+ def status(self):
329
+ """Check the server, and which versions are in use.
330
+
331
+ Returns:
332
+ Dict with 'ok', 'server_version' (None if the server doesn't report
333
+ one) and 'sdk_version' (this package's version; its major.minor
334
+ follows the server's), plus 'error' when 'ok' is False. In local
335
+ mode, server_version is the version of the Red Cedar code in use.
336
+ """
337
+ from . import __version__
338
+ if self.core:
339
+ return {"ok": True, "server_version": self.core.version(), "sdk_version": __version__}
340
+ try:
341
+ resp = requests.get(f"{self._server('/health')}/health", headers=self._headers(), timeout=5)
342
+ except requests.ConnectionError:
343
+ return {"ok": False, "server_version": None, "sdk_version": __version__,
344
+ "error": "Cannot reach server"}
345
+ try:
346
+ body = resp.json()
347
+ except ValueError:
348
+ body = {}
349
+ if not isinstance(body, dict):
350
+ body = {}
351
+ # Through the gateway (/api/health) the server's answer is under "backend"
352
+ server = body.get("backend", body)
353
+ if not isinstance(server, dict):
354
+ server = {}
355
+ ok = resp.status_code == 200 and server.get("status") == "ok"
356
+ result = {"ok": ok, "server_version": server.get("version"), "sdk_version": __version__}
357
+ if not ok:
358
+ result["error"] = server.get("error") or f"HTTP {resp.status_code}"
359
+ return result
360
+
361
+ @property
362
+ def server_version(self):
363
+ """The version of the server this client is connected to (asked once)."""
364
+ if self._server_version is None:
365
+ self._server_version = self.status().get("server_version")
366
+ return self._server_version
redcedar/datasets.py ADDED
@@ -0,0 +1,45 @@
1
+ """Dataset listing and selection."""
2
+
3
+ import os
4
+
5
+
6
+ class DatasetManager:
7
+ """Access the dataset catalog on the server."""
8
+
9
+ def __init__(self, client):
10
+ self._client = client
11
+
12
+ def list(self):
13
+ """List all available datasets in the catalog.
14
+
15
+ Returns:
16
+ List of dataset dicts with id, name, description, rows, columns, classes.
17
+ """
18
+ if self._client.core: # local mode: call the core directly
19
+ return self._client.core.datasets()
20
+ return self._client._get("/datasets")
21
+
22
+ def upload(self, file_path):
23
+ """Upload a CSV file to the server for training.
24
+
25
+ The last column is the target (class label); a header row is optional.
26
+
27
+ Args:
28
+ file_path: Path to a local .csv file.
29
+
30
+ Returns:
31
+ The dataset ID to pass to training.run().
32
+ """
33
+ if self._client.core:
34
+ return self._client._local_call(self._client.core.upload, file_path)
35
+ return self._client._post_file("/upload", file_path)["jobId"]
36
+
37
+ def _as_id(self, data):
38
+ """A CSV file path is uploaded and replaced by its dataset ID; anything
39
+ else (a catalog ID or an upload ID) is returned as it is."""
40
+ if isinstance(data, os.PathLike) or (isinstance(data, str) and os.path.isfile(data)):
41
+ return self.upload(os.fspath(data))
42
+ # IDs never end in .csv, so this is a file path that doesn't exist
43
+ if isinstance(data, str) and data.lower().endswith(".csv"):
44
+ raise FileNotFoundError(f"No such file: {data}")
45
+ return data
@@ -0,0 +1,38 @@
1
+ """Explainable AI — identify which features drive classification."""
2
+
3
+
4
+ class ExplainableManager:
5
+ """Run explainable AI analysis on trained models."""
6
+
7
+ def __init__(self, client):
8
+ self._client = client
9
+
10
+ def run(self, train_id, verbose=False, on_progress=None, settings=None):
11
+ """Run explainable AI analysis on a trained model.
12
+
13
+ Constructs quantum circuits to identify which input
14
+ features are most important for classification decisions.
15
+
16
+ Args:
17
+ train_id: Training job ID (from training.run() result).
18
+ verbose: Print progress to stdout; "log" also prints the server's log for the job.
19
+ on_progress: Callback(percent, message) for progress updates.
20
+ settings: Optional dict of runtime overrides (e.g. backend, parallelize).
21
+
22
+ Returns:
23
+ Dict with qubit_map (feature importance per qubit),
24
+ distributions, convergence values, feature_indices, etc.
25
+ """
26
+ if self._client.core: # local mode: call the core directly
27
+ return self._client._run_local(self._client.core.explain, train_id, settings=settings,
28
+ verbose=verbose, on_progress=on_progress)
29
+ body = {"train_id": train_id, "user_id": "sdk"}
30
+ if settings:
31
+ body["settings"] = settings
32
+ return self._client._run_job(
33
+ "/applications/explainable",
34
+ "/applications/explainable/{job_id}/stream",
35
+ body=body,
36
+ verbose=verbose,
37
+ on_progress=on_progress,
38
+ )