opendxp 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 (44) hide show
  1. opendxp/__init__.py +26 -0
  2. opendxp/__main__.py +3 -0
  3. opendxp/answers.py +96 -0
  4. opendxp/calibration.py +71 -0
  5. opendxp/cli.py +203 -0
  6. opendxp/conformance.py +325 -0
  7. opendxp/data/requests-0.1.jsonl +52 -0
  8. opendxp/errors.py +23 -0
  9. opendxp/export/__init__.py +7 -0
  10. opendxp/export/common.py +88 -0
  11. opendxp/export/decider.py +169 -0
  12. opendxp/export/encoder.py +105 -0
  13. opendxp/export/julia.py +373 -0
  14. opendxp/export/laya.py +241 -0
  15. opendxp/export/onnx_graph.py +200 -0
  16. opendxp/native/__init__.py +120 -0
  17. opendxp/native/decider.py +348 -0
  18. opendxp/native/julia.py +317 -0
  19. opendxp/package.py +148 -0
  20. opendxp/profiles/__init__.py +0 -0
  21. opendxp/profiles/causal_letters.py +344 -0
  22. opendxp/profiles/encoder_markers.py +378 -0
  23. opendxp/providers.py +143 -0
  24. opendxp/request.py +114 -0
  25. opendxp/runtime.py +32 -0
  26. opendxp/schemas/calibration.schema.json +34 -0
  27. opendxp/schemas/check-report.schema.json +41 -0
  28. opendxp/schemas/conformance-case.schema.json +42 -0
  29. opendxp/schemas/odxp.schema.json +101 -0
  30. opendxp/schemas/prompt.schema.json +112 -0
  31. opendxp/schemas/request.schema.json +62 -0
  32. opendxp/schemas/response.schema.json +56 -0
  33. opendxp/schemas/template.schema.json +98 -0
  34. opendxp/schemas.py +44 -0
  35. opendxp/spec.py +52 -0
  36. opendxp/text.py +64 -0
  37. opendxp/tokens.py +39 -0
  38. opendxp/validate.py +65 -0
  39. opendxp-0.1.0.dist-info/METADATA +211 -0
  40. opendxp-0.1.0.dist-info/RECORD +44 -0
  41. opendxp-0.1.0.dist-info/WHEEL +4 -0
  42. opendxp-0.1.0.dist-info/entry_points.txt +2 -0
  43. opendxp-0.1.0.dist-info/licenses/LICENSE +176 -0
  44. opendxp-0.1.0.dist-info/licenses/NOTICE +21 -0
opendxp/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """OpenDXP: a portable runtime standard for calibrated single-pass decision models.
2
+
3
+ This package is the reference implementation: the runtimes that run any
4
+ conforming package without model-specific code, the converters that make
5
+ packages from the models' own checkpoints, and the conformance tools.
6
+
7
+ import opendxp
8
+ model = opendxp.load("path/to/package") # device chosen from what is available
9
+ model.predict(state, questions) # {"answers": {...}, "usage": {...}}
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ __version__ = "0.1.0"
17
+
18
+
19
+ def load(package_dir: Any, **options: Any) -> Any:
20
+ """Open a package and start the reference runtime for its profile."""
21
+ from opendxp.runtime import load as _load
22
+
23
+ return _load(package_dir, **options)
24
+
25
+
26
+ __all__ = ["__version__", "load"]
opendxp/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from opendxp.cli import main
2
+
3
+ raise SystemExit(main())
opendxp/answers.py ADDED
@@ -0,0 +1,96 @@
1
+ """From scores to System One answers: the calibrated softmax, confidence, answer objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Sequence
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+
11
+ from opendxp.errors import OpenDXPError
12
+ from opendxp.request import Question
13
+ from opendxp.spec import DECIMALS
14
+
15
+
16
+ def softmax(logits: Sequence[float], temperature: float = 1.0) -> list[float]:
17
+ """softmax(z / T) in float64, over the logits given (the valid options only)."""
18
+ z = np.asarray(logits, dtype=np.float64) / float(temperature)
19
+ if not np.all(np.isfinite(z)):
20
+ raise OpenDXPError("the model returned non-finite scores")
21
+ e = np.exp(z - z.max())
22
+ return (e / e.sum()).tolist()
23
+
24
+
25
+ def normalise(values: Sequence[float]) -> list[float]:
26
+ total = float(sum(values))
27
+ if total <= 0:
28
+ return [1.0 / len(values)] * len(values)
29
+ return [float(v) / total for v in values]
30
+
31
+
32
+ def confidence(rule: str, p: Sequence[float]) -> float | None:
33
+ """The confidence rules a package can declare (SPEC.md, "Answers")."""
34
+ n = len(p)
35
+ if rule == "none":
36
+ return None
37
+ if rule == "max-probability":
38
+ return float(max(p))
39
+ if rule == "entropy":
40
+ if n < 2:
41
+ return 1.0
42
+ h = -sum(x * math.log(min(max(x, 1e-12), 1.0)) for x in p)
43
+ return min(1.0, max(0.0, 1.0 - h / math.log(n)))
44
+ if rule == "typesafe":
45
+ if n < 2:
46
+ return 1.0
47
+ return min(1.0, max(0.0, (n * max(p) - 1) / (n - 1)))
48
+ if rule == "typesafe-ordinal":
49
+ if n < 2:
50
+ return 1.0
51
+ k = max(range(n), key=p.__getitem__)
52
+ spread = sum(x * abs(i - k) for i, x in enumerate(p))
53
+ uniform = sum(abs(i - (n - 1) / 2) for i in range(n)) / n
54
+ return min(1.0, max(0.0, 1.0 - spread / uniform))
55
+ raise OpenDXPError(f"unknown confidence rule {rule!r}")
56
+
57
+
58
+ def argmax(p: Sequence[float]) -> int:
59
+ """The first index of the largest value."""
60
+ return max(range(len(p)), key=p.__getitem__)
61
+
62
+
63
+ def answer(
64
+ question: Question,
65
+ p: Sequence[float],
66
+ rules: dict[str, str] | None = None,
67
+ decimals: int | None = DECIMALS,
68
+ ) -> dict[str, Any]:
69
+ """One answer in the System One format; probabilities rounded to `decimals` if set."""
70
+
71
+ def r(x: float) -> float:
72
+ return round(float(x), decimals) if decimals is not None else float(x)
73
+
74
+ rule = (rules or {}).get(question.type, "none")
75
+ conf = confidence(rule, p)
76
+ keys = question.keys
77
+ best = argmax(p)
78
+ out: dict[str, Any]
79
+ if question.type == "choice":
80
+ out = {
81
+ "type": "choice",
82
+ "choice": keys[best],
83
+ "probabilities": {k: r(x) for k, x in zip(keys, p, strict=True)},
84
+ }
85
+ elif question.type == "score":
86
+ out = {
87
+ "type": "score",
88
+ "score": r(sum(i * x for i, x in enumerate(p))),
89
+ "legend": question.legend(),
90
+ "probabilities": {k: r(x) for k, x in zip(keys, p, strict=True)},
91
+ }
92
+ else:
93
+ out = {"type": "noul", "noul": r(p[1])}
94
+ if conf is not None:
95
+ out["confidence"] = r(conf)
96
+ return out
opendxp/calibration.py ADDED
@@ -0,0 +1,71 @@
1
+ """calibration.json: the temperature each answer is divided by before the softmax.
2
+
3
+ A temperature is looked up by question type and option count: the first entry
4
+ of `by_option_count` whose type matches and whose [min, max] range holds the
5
+ count wins; otherwise the per-type temperature; otherwise 1.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from opendxp.errors import PackageError
16
+ from opendxp.spec import QUESTION_TYPES
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Bucket:
21
+ type: str
22
+ min: int
23
+ max: int | None
24
+ temperature: float
25
+
26
+ def holds(self, qtype: str, count: int) -> bool:
27
+ return qtype == self.type and count >= self.min and (self.max is None or count <= self.max)
28
+
29
+
30
+ class Calibration:
31
+ def __init__(self, data: dict[str, Any] | None = None) -> None:
32
+ data = data or {}
33
+ self.data = data
34
+ self.by_type: dict[str, float] = {}
35
+ for qtype, value in (data.get("temperature") or {}).items():
36
+ if qtype not in QUESTION_TYPES:
37
+ raise PackageError(f"calibration: unknown question type {qtype!r}")
38
+ self.by_type[qtype] = _positive(value, f"temperature.{qtype}")
39
+ self.buckets: list[Bucket] = []
40
+ for i, entry in enumerate(data.get("by_option_count") or []):
41
+ qtype = entry.get("type")
42
+ if qtype not in QUESTION_TYPES:
43
+ raise PackageError(f"calibration: by_option_count[{i}] has no valid type")
44
+ low, high = entry.get("min", 2), entry.get("max")
45
+ if not isinstance(low, int) or (high is not None and not isinstance(high, int)):
46
+ raise PackageError(f"calibration: by_option_count[{i}] min/max must be integers")
47
+ if high is not None and high < low:
48
+ raise PackageError(f"calibration: by_option_count[{i}] has max < min")
49
+ self.buckets.append(
50
+ Bucket(qtype, low, high, _positive(entry.get("temperature"), f"bucket {i}"))
51
+ )
52
+
53
+ @classmethod
54
+ def from_file(cls, path: Path) -> Calibration:
55
+ return cls(json.loads(Path(path).read_text()))
56
+
57
+ def temperature(self, qtype: str, count: int) -> float:
58
+ for bucket in self.buckets:
59
+ if bucket.holds(qtype, count):
60
+ return bucket.temperature
61
+ return self.by_type.get(qtype, 1.0)
62
+
63
+
64
+ def _positive(value: Any, where: str) -> float:
65
+ try:
66
+ number = float(value)
67
+ except (TypeError, ValueError):
68
+ raise PackageError(f"calibration: {where} is not a number") from None
69
+ if not number > 0 or number == float("inf"):
70
+ raise PackageError(f"calibration: {where} must be a positive finite number")
71
+ return number
opendxp/cli.py ADDED
@@ -0,0 +1,203 @@
1
+ """The `opendxp` command.
2
+
3
+ opendxp export laya|julia|decider CHECKPOINT OUT_DIR
4
+ opendxp conformance generate PACKAGE --native CHECKPOINT --runtime laya|julia|decider
5
+ opendxp check PACKAGE [--device cpu|auto|coreml|cuda|openvino|qnn|directml|gpu]
6
+ opendxp validate PACKAGE
7
+ opendxp run PACKAGE --request request.json
8
+ opendxp info
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from opendxp import __version__
20
+
21
+
22
+ def _print_json(data: Any) -> None:
23
+ print(json.dumps(data, ensure_ascii=False, indent=2))
24
+
25
+
26
+ def cmd_export(args: argparse.Namespace) -> int:
27
+ mode = "copy" if args.copy else "link"
28
+ options: dict[str, Any] = {"weights_mode": mode}
29
+ if args.name:
30
+ options["name"] = args.name
31
+ if args.family == "laya":
32
+ from opendxp.export import laya as exporter
33
+ elif args.family == "julia":
34
+ from opendxp.export import julia as exporter
35
+
36
+ if args.max_tokens:
37
+ options["max_tokens"] = args.max_tokens
38
+ if args.official_onnx:
39
+ options["official_onnx"] = Path(args.official_onnx)
40
+ else:
41
+ from opendxp.export import decider as exporter
42
+ manifest = exporter.export(Path(args.checkpoint), Path(args.out_dir), **options)
43
+ print(f"wrote {Path(args.out_dir) / 'odxp.json'} ({manifest['profile']})")
44
+ print(
45
+ "next: opendxp conformance generate",
46
+ args.out_dir,
47
+ "--native",
48
+ args.checkpoint,
49
+ "--runtime",
50
+ args.family,
51
+ )
52
+ return 0
53
+
54
+
55
+ def cmd_generate(args: argparse.Namespace) -> int:
56
+ from opendxp.conformance import DEFAULT_REQUESTS, generate, read_jsonl
57
+ from opendxp.native import load_native
58
+
59
+ requests = read_jsonl(Path(args.requests) if args.requests else DEFAULT_REQUESTS)
60
+ if args.limit:
61
+ requests = requests[: args.limit]
62
+ print(f"loading the model's own runtime ({args.runtime}) from {args.native}")
63
+ native = load_native(args.runtime, Path(args.native), threads=args.threads)
64
+ try:
65
+ summary = generate(Path(args.package), native, requests)
66
+ finally:
67
+ native.close()
68
+ _print_json(summary)
69
+ return 0
70
+
71
+
72
+ def cmd_check(args: argparse.Namespace) -> int:
73
+ from opendxp.conformance import check
74
+
75
+ options: dict[str, Any] = {}
76
+ if args.optimization:
77
+ options["optimization"] = args.optimization
78
+ report = check(
79
+ Path(args.package),
80
+ device=args.device,
81
+ threads=args.threads,
82
+ hashes=not args.no_hashes,
83
+ **options,
84
+ )
85
+ path = Path(args.report) if args.report else Path(args.package) / f"check-{args.device}.json"
86
+ path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
87
+ lat = report["latency_ms"]
88
+ agree = report["argmax_agreement"]
89
+ print(f"{report['package']['name']} [{report['package']['profile']}] on {args.device}")
90
+ print(
91
+ f" cases {report['cases_passed']}/{report['cases']} passed, "
92
+ f"questions {report['questions']}, max |dp| {report['max_abs_dp']:.2e}, "
93
+ f"argmax {agree['agree']}/{agree['total']}, "
94
+ f"errors matched {report['errors']['matched']}/{report['errors']['expected']}"
95
+ )
96
+ print(
97
+ f" latency: load {lat['load']} ms, request median {lat['request_median']} ms, "
98
+ f"p90 {lat['request_p90']} ms, per question {lat['question_median']} ms"
99
+ )
100
+ for problem in report["package_problems"]:
101
+ print(f" package: {problem}")
102
+ for failure in report["failures"][:10]:
103
+ print(f" FAIL {failure['id']}: {failure.get('reason') or failure.get('questions')}")
104
+ verdict = "PASS" if report["passed"] else "FAIL"
105
+ if report["passed"] and not report["compatible"]:
106
+ verdict += " (coverage below the minimum: not badge-eligible)"
107
+ print(f" {verdict}; report: {path}")
108
+ return 0 if report["passed"] else 1
109
+
110
+
111
+ def cmd_validate(args: argparse.Namespace) -> int:
112
+ from opendxp.validate import validate_package
113
+
114
+ problems = validate_package(Path(args.package), hashes=not args.no_hashes)
115
+ for problem in problems:
116
+ print(problem)
117
+ print("valid" if not problems else f"{len(problems)} problem(s)")
118
+ return 0 if not problems else 1
119
+
120
+
121
+ def cmd_run(args: argparse.Namespace) -> int:
122
+ from opendxp.runtime import load
123
+
124
+ text = sys.stdin.read() if args.request == "-" else Path(args.request).read_text()
125
+ request = json.loads(text)
126
+ model = load(Path(args.package), device=args.device, threads=args.threads)
127
+ try:
128
+ _print_json(model.predict(request.get("state", ""), request["questions"]))
129
+ finally:
130
+ model.close()
131
+ return 0
132
+
133
+
134
+ def cmd_info(args: argparse.Namespace) -> int:
135
+ from opendxp.providers import describe_machine
136
+
137
+ _print_json({"opendxp": __version__, **describe_machine()})
138
+ return 0
139
+
140
+
141
+ def main(argv: list[str] | None = None) -> int:
142
+ parser = argparse.ArgumentParser(prog="opendxp", description="OpenDXP reference tools")
143
+ parser.add_argument("--version", action="version", version=f"opendxp {__version__}")
144
+ sub = parser.add_subparsers(dest="command", required=True)
145
+
146
+ p = sub.add_parser("export", help="convert a model's checkpoint into an OpenDXP package")
147
+ p.add_argument("family", choices=["laya", "julia", "decider"])
148
+ p.add_argument("checkpoint")
149
+ p.add_argument("out_dir")
150
+ p.add_argument("--name", help="the package name (default: the model's registry name)")
151
+ p.add_argument("--copy", action="store_true", help="copy weights instead of hard-linking")
152
+ p.add_argument("--max-tokens", type=int, help="julia: total token budget (default 8192)")
153
+ p.add_argument(
154
+ "--official-onnx",
155
+ help="julia: map SupersonicLabs/Julia-1-ONNX's model.onnx instead of exporting a graph",
156
+ )
157
+ p.set_defaults(func=cmd_export)
158
+
159
+ p = sub.add_parser("conformance", help="conformance files")
160
+ csub = p.add_subparsers(dest="action", required=True)
161
+ g = csub.add_parser("generate", help="record the model's own answers into conformance.jsonl")
162
+ g.add_argument("package")
163
+ g.add_argument(
164
+ "--native", required=True, help="the checkpoint directory the model's code reads"
165
+ )
166
+ g.add_argument("--runtime", required=True, choices=["laya", "julia", "decider"])
167
+ g.add_argument("--requests", help="a requests JSONL (default: the OpenDXP 0.1 set)")
168
+ g.add_argument("--threads", type=int, default=4)
169
+ g.add_argument("--limit", type=int, help="only the first N requests (for a quick try)")
170
+ g.set_defaults(func=cmd_generate)
171
+
172
+ p = sub.add_parser("check", help="run the conformance file through the reference runtime")
173
+ p.add_argument("package")
174
+ p.add_argument(
175
+ "--device", default="cpu", help="cpu (the reference), auto, coreml, cuda, gpu, ..."
176
+ )
177
+ p.add_argument("--threads", type=int)
178
+ p.add_argument("--report", help="where to write the JSON report")
179
+ p.add_argument("--no-hashes", action="store_true", help="skip the sha256 checks")
180
+ p.add_argument("--optimization", choices=["disable", "basic", "extended", "all"])
181
+ p.set_defaults(func=cmd_check)
182
+
183
+ p = sub.add_parser("validate", help="check a package's files against the schemas and hashes")
184
+ p.add_argument("package")
185
+ p.add_argument("--no-hashes", action="store_true")
186
+ p.set_defaults(func=cmd_validate)
187
+
188
+ p = sub.add_parser("run", help="answer one request with a package")
189
+ p.add_argument("package")
190
+ p.add_argument("--request", required=True, help="a request JSON file, or - for stdin")
191
+ p.add_argument("--device", default="auto")
192
+ p.add_argument("--threads", type=int)
193
+ p.set_defaults(func=cmd_run)
194
+
195
+ p = sub.add_parser("info", help="this machine's backends and devices")
196
+ p.set_defaults(func=cmd_info)
197
+
198
+ args = parser.parse_args(argv)
199
+ return int(args.func(args))
200
+
201
+
202
+ if __name__ == "__main__":
203
+ raise SystemExit(main())