geometric-function-atlas 0.1.1__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.
- geometric_function_atlas/__init__.py +46 -0
- geometric_function_atlas/__main__.py +5 -0
- geometric_function_atlas/catalog.py +98 -0
- geometric_function_atlas/cli.py +282 -0
- geometric_function_atlas/coefficients.py +206 -0
- geometric_function_atlas/contracts.py +1334 -0
- geometric_function_atlas/counterexamples.py +308 -0
- geometric_function_atlas/fekete_szego.py +325 -0
- geometric_function_atlas/implementation_registry.py +37 -0
- geometric_function_atlas/models.py +261 -0
- geometric_function_atlas/py.typed +0 -0
- geometric_function_atlas/schema/error.schema.json +17 -0
- geometric_function_atlas/schema/result.schema.json +272 -0
- geometric_function_atlas/version.py +8 -0
- geometric_function_atlas-0.1.1.dist-info/METADATA +149 -0
- geometric_function_atlas-0.1.1.dist-info/RECORD +20 -0
- geometric_function_atlas-0.1.1.dist-info/WHEEL +5 -0
- geometric_function_atlas-0.1.1.dist-info/entry_points.txt +3 -0
- geometric_function_atlas-0.1.1.dist-info/licenses/LICENSE +21 -0
- geometric_function_atlas-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Public API for reproducible Geometric Function Atlas computations."""
|
|
2
|
+
|
|
3
|
+
from .catalog import get_generator, list_generators
|
|
4
|
+
from .coefficients import GeneratorSeriesResult, generator_series, taylor_coefficients
|
|
5
|
+
from .contracts import ( # noqa: F401
|
|
6
|
+
RESULT_SCHEMA_VERSION,
|
|
7
|
+
CheckStatus,
|
|
8
|
+
CorruptArtifactError,
|
|
9
|
+
FailureState,
|
|
10
|
+
InvalidInputError,
|
|
11
|
+
LiteratureStatus,
|
|
12
|
+
ResourceLimitError,
|
|
13
|
+
UnresolvedError,
|
|
14
|
+
UnsupportedError,
|
|
15
|
+
VerificationCheck,
|
|
16
|
+
VerificationReport,
|
|
17
|
+
failure_payload,
|
|
18
|
+
load_error_schema,
|
|
19
|
+
load_result_schema,
|
|
20
|
+
validate_error_payload,
|
|
21
|
+
validate_result_payload,
|
|
22
|
+
)
|
|
23
|
+
from .counterexamples import CounterexampleResult, verify_counterexample
|
|
24
|
+
from .fekete_szego import FeketeSzegoResult, fekete_szego
|
|
25
|
+
from .implementation_registry import ( # noqa: F401
|
|
26
|
+
get_trusted_implementation,
|
|
27
|
+
list_trusted_implementations,
|
|
28
|
+
)
|
|
29
|
+
from .models import Generator
|
|
30
|
+
from .models import Z as z
|
|
31
|
+
from .version import __version__
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CounterexampleResult",
|
|
35
|
+
"FeketeSzegoResult",
|
|
36
|
+
"Generator",
|
|
37
|
+
"GeneratorSeriesResult",
|
|
38
|
+
"__version__",
|
|
39
|
+
"fekete_szego",
|
|
40
|
+
"generator_series",
|
|
41
|
+
"get_generator",
|
|
42
|
+
"list_generators",
|
|
43
|
+
"taylor_coefficients",
|
|
44
|
+
"verify_counterexample",
|
|
45
|
+
"z",
|
|
46
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Curated Ma–Minda generators used by the published radius portfolio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sympy as sp
|
|
6
|
+
|
|
7
|
+
from .models import Generator, Z
|
|
8
|
+
from .version import GENERATOR_CATALOG_VERSION
|
|
9
|
+
|
|
10
|
+
_GENERATORS = (
|
|
11
|
+
Generator(
|
|
12
|
+
key="starlike",
|
|
13
|
+
name="Classical starlike class",
|
|
14
|
+
expression=(1 + Z) / (1 - Z),
|
|
15
|
+
citation="Classical starlike class; Janowski (1973) with A=1, B=-1",
|
|
16
|
+
),
|
|
17
|
+
Generator(
|
|
18
|
+
key="bell",
|
|
19
|
+
name="Bell-number starlike class",
|
|
20
|
+
expression=sp.exp(sp.exp(Z) - 1),
|
|
21
|
+
citation="Cho, Kumar, Kumar & Ravichandran (2019)",
|
|
22
|
+
),
|
|
23
|
+
Generator(
|
|
24
|
+
key="cosh_sqrt",
|
|
25
|
+
name="Cosh-square-root starlike class",
|
|
26
|
+
expression=sp.cosh(sp.sqrt(Z)),
|
|
27
|
+
citation="Mundalia & Swaminathan (2023)",
|
|
28
|
+
),
|
|
29
|
+
Generator(
|
|
30
|
+
key="crescent",
|
|
31
|
+
name="Crescent (lune) starlike class",
|
|
32
|
+
expression=Z + sp.sqrt(1 + Z**2),
|
|
33
|
+
citation="Raina & Sokół (2015)",
|
|
34
|
+
),
|
|
35
|
+
Generator(
|
|
36
|
+
key="exponential",
|
|
37
|
+
name="Exponential starlike class",
|
|
38
|
+
expression=sp.exp(Z),
|
|
39
|
+
citation="Mendiratta, Nagpal & Ravichandran (2015)",
|
|
40
|
+
),
|
|
41
|
+
Generator(
|
|
42
|
+
key="lemniscate",
|
|
43
|
+
name="Lemniscate starlike class",
|
|
44
|
+
expression=sp.sqrt(1 + Z),
|
|
45
|
+
citation="Sokół & Stankiewicz (1996)",
|
|
46
|
+
),
|
|
47
|
+
Generator(
|
|
48
|
+
key="rational_kr",
|
|
49
|
+
name="Kumar–Ravichandran rational starlike class",
|
|
50
|
+
expression=(
|
|
51
|
+
1
|
|
52
|
+
+ Z
|
|
53
|
+
* ((1 + sp.sqrt(2)) + Z)
|
|
54
|
+
/ ((1 + sp.sqrt(2)) * ((1 + sp.sqrt(2)) - Z))
|
|
55
|
+
),
|
|
56
|
+
citation="Kumar & Ravichandran (2016)",
|
|
57
|
+
),
|
|
58
|
+
Generator(
|
|
59
|
+
key="sigmoid",
|
|
60
|
+
name="Modified-sigmoid starlike class",
|
|
61
|
+
expression=2 / (1 + sp.exp(-Z)),
|
|
62
|
+
citation="Goel & Kumar (2020)",
|
|
63
|
+
),
|
|
64
|
+
Generator(
|
|
65
|
+
key="sine",
|
|
66
|
+
name="Sine starlike class",
|
|
67
|
+
expression=1 + sp.sin(Z),
|
|
68
|
+
citation="Cho, Kumar, Kumar & Ravichandran (2019)",
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
_BY_KEY = {generator.key: generator for generator in _GENERATORS}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def list_generators() -> tuple[Generator, ...]:
|
|
76
|
+
"""Return the built-in generators in stable key order."""
|
|
77
|
+
|
|
78
|
+
return tuple(sorted(_GENERATORS, key=lambda generator: generator.key))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_generator(key: str) -> Generator:
|
|
82
|
+
"""Return a built-in generator or fail with a discoverable error."""
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
return _BY_KEY[key]
|
|
86
|
+
except KeyError as exc:
|
|
87
|
+
available = ", ".join(sorted(_BY_KEY))
|
|
88
|
+
raise KeyError(f"unknown generator {key!r}; available: {available}") from exc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def generator_artifact_version(generator: Generator) -> str:
|
|
92
|
+
"""Return the catalog version or identify a caller-supplied definition."""
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
GENERATOR_CATALOG_VERSION
|
|
96
|
+
if _BY_KEY.get(generator.key) is generator
|
|
97
|
+
else "user-supplied"
|
|
98
|
+
)
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Command-line interface for independent reproduction workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import contextlib
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .catalog import get_generator, list_generators
|
|
14
|
+
from .coefficients import generator_series
|
|
15
|
+
from .contracts import (
|
|
16
|
+
CorruptArtifactError,
|
|
17
|
+
FailureState,
|
|
18
|
+
InvalidInputError,
|
|
19
|
+
ResourceLimitError,
|
|
20
|
+
UnresolvedError,
|
|
21
|
+
UnsupportedError,
|
|
22
|
+
failure_payload,
|
|
23
|
+
validate_error_payload,
|
|
24
|
+
)
|
|
25
|
+
from .counterexamples import verify_counterexample
|
|
26
|
+
from .fekete_szego import fekete_szego
|
|
27
|
+
from .version import __version__
|
|
28
|
+
|
|
29
|
+
EXIT_SUCCESS = 0
|
|
30
|
+
EXIT_INVALID_INPUT = 2
|
|
31
|
+
EXIT_UNSUPPORTED = 3
|
|
32
|
+
EXIT_UNRESOLVED = 4
|
|
33
|
+
EXIT_RESOURCE_LIMIT = 5
|
|
34
|
+
EXIT_CORRUPT_ARTIFACT = 6
|
|
35
|
+
|
|
36
|
+
EXIT_CODES = {
|
|
37
|
+
FailureState.INVALID_INPUT.value: EXIT_INVALID_INPUT,
|
|
38
|
+
FailureState.UNSUPPORTED.value: EXIT_UNSUPPORTED,
|
|
39
|
+
FailureState.UNRESOLVED.value: EXIT_UNRESOLVED,
|
|
40
|
+
FailureState.RESOURCE_LIMIT.value: EXIT_RESOURCE_LIMIT,
|
|
41
|
+
FailureState.CORRUPT_ARTIFACT.value: EXIT_CORRUPT_ARTIFACT,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _write(payload: Any, *, as_json: bool) -> None:
|
|
46
|
+
if as_json:
|
|
47
|
+
_write_utf8(
|
|
48
|
+
json.dumps(
|
|
49
|
+
payload,
|
|
50
|
+
indent=2,
|
|
51
|
+
ensure_ascii=False,
|
|
52
|
+
allow_nan=False,
|
|
53
|
+
sort_keys=True,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
elif isinstance(payload, list):
|
|
57
|
+
for row in payload:
|
|
58
|
+
print(f"{row['key']}: {row['formula']} — {row['citation']}")
|
|
59
|
+
else:
|
|
60
|
+
for key, value in payload.items():
|
|
61
|
+
print(f"{key}: {value}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _write_utf8(text: str, *, stream: Any = None) -> None:
|
|
65
|
+
"""Write JSON as UTF-8 even when the console locale is not UTF-8."""
|
|
66
|
+
|
|
67
|
+
target = sys.stdout if stream is None else stream
|
|
68
|
+
buffer = getattr(target, "buffer", None)
|
|
69
|
+
if buffer is not None:
|
|
70
|
+
buffer.write(text.encode("utf-8") + b"\n")
|
|
71
|
+
buffer.flush()
|
|
72
|
+
else:
|
|
73
|
+
target.write(f"{text}\n")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _generators(args: argparse.Namespace) -> None:
|
|
77
|
+
payload = [
|
|
78
|
+
{
|
|
79
|
+
"key": generator.key,
|
|
80
|
+
"name": generator.name,
|
|
81
|
+
"formula": generator.formula,
|
|
82
|
+
"citation": generator.citation,
|
|
83
|
+
"reference_url": generator.reference_url,
|
|
84
|
+
}
|
|
85
|
+
for generator in list_generators()
|
|
86
|
+
]
|
|
87
|
+
_write(payload, as_json=args.json)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _coefficients(args: argparse.Namespace) -> None:
|
|
91
|
+
result = generator_series(get_generator(args.generator), order=args.order)
|
|
92
|
+
_write(result.to_dict(), as_json=args.json)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _fekete_szego(args: argparse.Namespace) -> None:
|
|
96
|
+
result = fekete_szego(args.generator, mu=args.mu)
|
|
97
|
+
_write(result.to_dict(precision=args.precision), as_json=args.json)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _comma_separated_numbers(value: str, *, label: str) -> list[float]:
|
|
101
|
+
if len(value) > 4096:
|
|
102
|
+
raise ResourceLimitError(f"{label} input is too long")
|
|
103
|
+
parts = [part.strip() for part in value.split(",")]
|
|
104
|
+
if not parts or any(not part for part in parts):
|
|
105
|
+
raise InvalidInputError(f"{label} must be comma-separated real numbers")
|
|
106
|
+
try:
|
|
107
|
+
return [float(part) for part in parts]
|
|
108
|
+
except ValueError as exc:
|
|
109
|
+
raise InvalidInputError(
|
|
110
|
+
f"{label} must be comma-separated real numbers"
|
|
111
|
+
) from exc
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _verify_counterexample(args: argparse.Namespace) -> None:
|
|
115
|
+
coefficients = _comma_separated_numbers(
|
|
116
|
+
args.coefficients, label="coefficients"
|
|
117
|
+
)
|
|
118
|
+
point = _comma_separated_numbers(args.point, label="point")
|
|
119
|
+
if len(point) != 2:
|
|
120
|
+
raise InvalidInputError("point must have the form real,imaginary")
|
|
121
|
+
result = verify_counterexample(
|
|
122
|
+
coefficients,
|
|
123
|
+
point=(point[0], point[1]),
|
|
124
|
+
property=args.property,
|
|
125
|
+
)
|
|
126
|
+
if args.json:
|
|
127
|
+
_write(result.to_dict(), as_json=True)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if result.certified and result.property == "starlike":
|
|
131
|
+
verdict = "CERTIFIED COUNTEREXAMPLE"
|
|
132
|
+
elif result.certified:
|
|
133
|
+
verdict = "CERTIFIED CRITERION VIOLATION"
|
|
134
|
+
elif result.property == "starlike":
|
|
135
|
+
verdict = "NO COUNTEREXAMPLE CERTIFIED AT THIS POINT"
|
|
136
|
+
else:
|
|
137
|
+
verdict = "NO CRITERION VIOLATION CERTIFIED AT THIS POINT"
|
|
138
|
+
criterion_names = {
|
|
139
|
+
"starlike": "starlikeness",
|
|
140
|
+
"becker_univalent": "Becker criterion",
|
|
141
|
+
"nehari_univalent": "Nehari criterion",
|
|
142
|
+
}
|
|
143
|
+
real, imaginary = result.point
|
|
144
|
+
sign = "+" if imaginary >= 0 else "-"
|
|
145
|
+
comparison = "<=" if result.property == "starlike" else ">"
|
|
146
|
+
lines = [
|
|
147
|
+
verdict,
|
|
148
|
+
f"Criterion: {criterion_names[result.property]}",
|
|
149
|
+
f"Witness: z = {real:g} {sign} {abs(imaginary):g}i",
|
|
150
|
+
(
|
|
151
|
+
"Certified value: "
|
|
152
|
+
f"[{result.interval_lower:g}, {result.interval_upper:g}]"
|
|
153
|
+
),
|
|
154
|
+
f"Counterexample condition: value {comparison} {result.threshold:g}",
|
|
155
|
+
]
|
|
156
|
+
_write_utf8("\n".join(lines))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _parser() -> argparse.ArgumentParser:
|
|
160
|
+
parser = argparse.ArgumentParser(
|
|
161
|
+
prog="geometric-function-atlas",
|
|
162
|
+
description="Reproduce and review Geometric Function Atlas computations.",
|
|
163
|
+
)
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"--version",
|
|
166
|
+
action="version",
|
|
167
|
+
version=f"%(prog)s {__version__}",
|
|
168
|
+
)
|
|
169
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
170
|
+
|
|
171
|
+
generators = subparsers.add_parser("generators", help="list built-in generators")
|
|
172
|
+
generators.add_argument("--json", action="store_true", help="emit JSON")
|
|
173
|
+
generators.set_defaults(handler=_generators)
|
|
174
|
+
|
|
175
|
+
coefficients = subparsers.add_parser(
|
|
176
|
+
"coefficients", help="compute exact generator Taylor coefficients"
|
|
177
|
+
)
|
|
178
|
+
coefficients.add_argument("generator")
|
|
179
|
+
coefficients.add_argument("--order", type=int, required=True)
|
|
180
|
+
coefficients.add_argument("--json", action="store_true", help="emit JSON")
|
|
181
|
+
coefficients.set_defaults(handler=_coefficients)
|
|
182
|
+
|
|
183
|
+
fs = subparsers.add_parser(
|
|
184
|
+
"fekete-szego", help="compute an exact Ma-Minda Fekete-Szego constant"
|
|
185
|
+
)
|
|
186
|
+
fs.add_argument("generator")
|
|
187
|
+
fs.add_argument("--mu", required=True, help="real rational value, e.g. 1/2")
|
|
188
|
+
fs.add_argument("--precision", type=int, default=16)
|
|
189
|
+
fs.add_argument("--json", action="store_true", help="emit JSON")
|
|
190
|
+
fs.set_defaults(handler=_fekete_szego)
|
|
191
|
+
|
|
192
|
+
counterexample = subparsers.add_parser(
|
|
193
|
+
"verify-counterexample",
|
|
194
|
+
help="rigorously check a supplied counterexample witness",
|
|
195
|
+
)
|
|
196
|
+
counterexample.add_argument(
|
|
197
|
+
"--coefficients",
|
|
198
|
+
required=True,
|
|
199
|
+
help="comma-separated a2,a3,... for f(z)=z+a2*z^2+...",
|
|
200
|
+
)
|
|
201
|
+
counterexample.add_argument(
|
|
202
|
+
"--point",
|
|
203
|
+
required=True,
|
|
204
|
+
help="witness point as real,imaginary; use --point=-0.75,0 for negatives",
|
|
205
|
+
)
|
|
206
|
+
counterexample.add_argument("--property", default="starlike")
|
|
207
|
+
counterexample.add_argument("--json", action="store_true", help="emit JSON")
|
|
208
|
+
counterexample.set_defaults(handler=_verify_counterexample)
|
|
209
|
+
|
|
210
|
+
return parser
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
214
|
+
parser = _parser()
|
|
215
|
+
command_line = list(sys.argv[1:] if argv is None else argv)
|
|
216
|
+
wants_json = "--json" in command_line
|
|
217
|
+
if wants_json:
|
|
218
|
+
parse_errors = io.StringIO()
|
|
219
|
+
try:
|
|
220
|
+
with contextlib.redirect_stderr(parse_errors):
|
|
221
|
+
args = parser.parse_args(command_line)
|
|
222
|
+
except SystemExit as exc:
|
|
223
|
+
if exc.code == 0:
|
|
224
|
+
raise
|
|
225
|
+
payload = failure_payload(
|
|
226
|
+
FailureState.INVALID_INPUT,
|
|
227
|
+
parse_errors.getvalue().strip() or "invalid command-line arguments",
|
|
228
|
+
)
|
|
229
|
+
validate_error_payload(payload)
|
|
230
|
+
_write_utf8(
|
|
231
|
+
json.dumps(
|
|
232
|
+
payload,
|
|
233
|
+
indent=2,
|
|
234
|
+
ensure_ascii=False,
|
|
235
|
+
allow_nan=False,
|
|
236
|
+
sort_keys=True,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
return EXIT_INVALID_INPUT
|
|
240
|
+
else:
|
|
241
|
+
args = parser.parse_args(command_line)
|
|
242
|
+
try:
|
|
243
|
+
args.handler(args)
|
|
244
|
+
except (
|
|
245
|
+
CorruptArtifactError,
|
|
246
|
+
UnresolvedError,
|
|
247
|
+
UnsupportedError,
|
|
248
|
+
ResourceLimitError,
|
|
249
|
+
InvalidInputError,
|
|
250
|
+
KeyError,
|
|
251
|
+
TypeError,
|
|
252
|
+
ValueError,
|
|
253
|
+
) as exc:
|
|
254
|
+
state = _failure_state(exc)
|
|
255
|
+
if getattr(args, "json", False):
|
|
256
|
+
payload = failure_payload(state, str(exc))
|
|
257
|
+
validate_error_payload(payload)
|
|
258
|
+
_write_utf8(
|
|
259
|
+
json.dumps(
|
|
260
|
+
payload,
|
|
261
|
+
indent=2,
|
|
262
|
+
ensure_ascii=False,
|
|
263
|
+
allow_nan=False,
|
|
264
|
+
sort_keys=True,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
return EXIT_CODES[state]
|
|
268
|
+
print(f"{parser.prog}: error: {exc}", file=sys.stderr)
|
|
269
|
+
return EXIT_CODES[state]
|
|
270
|
+
return EXIT_SUCCESS
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _failure_state(exc: Exception) -> str:
|
|
274
|
+
if isinstance(exc, CorruptArtifactError):
|
|
275
|
+
return FailureState.CORRUPT_ARTIFACT.value
|
|
276
|
+
if isinstance(exc, UnresolvedError):
|
|
277
|
+
return FailureState.UNRESOLVED.value
|
|
278
|
+
if isinstance(exc, UnsupportedError):
|
|
279
|
+
return FailureState.UNSUPPORTED.value
|
|
280
|
+
if isinstance(exc, ResourceLimitError):
|
|
281
|
+
return FailureState.RESOURCE_LIMIT.value
|
|
282
|
+
return FailureState.INVALID_INPUT.value
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Exact coefficient operations for Ma–Minda generators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import sympy as sp
|
|
9
|
+
|
|
10
|
+
from .catalog import generator_artifact_version, get_generator
|
|
11
|
+
from .contracts import (
|
|
12
|
+
CheckStatus,
|
|
13
|
+
FailureState,
|
|
14
|
+
InvalidInputError,
|
|
15
|
+
ResourceLimitError,
|
|
16
|
+
VerificationCheck,
|
|
17
|
+
VerificationReport,
|
|
18
|
+
build_result_payload,
|
|
19
|
+
)
|
|
20
|
+
from .models import Generator, canonical_expression_dag, validate_exact_expression
|
|
21
|
+
from .version import GENERATOR_FIXTURE_ID, SOURCE_ARTIFACT_COMMIT
|
|
22
|
+
|
|
23
|
+
MAX_TAYLOR_ORDER = 64
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class GeneratorSeriesResult:
|
|
28
|
+
"""Exact generator coefficients with reproducibility metadata."""
|
|
29
|
+
|
|
30
|
+
generator: Generator
|
|
31
|
+
order: int
|
|
32
|
+
coefficients: tuple[sp.Expr, ...]
|
|
33
|
+
method: str = "exact_symbolic_taylor_series"
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if isinstance(self.order, bool) or not isinstance(self.order, int):
|
|
37
|
+
raise TypeError("order must be a positive integer")
|
|
38
|
+
if self.order < 1:
|
|
39
|
+
raise InvalidInputError("order must be at least 1")
|
|
40
|
+
if self.order > MAX_TAYLOR_ORDER:
|
|
41
|
+
raise ResourceLimitError(f"order must be at most {MAX_TAYLOR_ORDER}")
|
|
42
|
+
if not isinstance(self.generator, Generator):
|
|
43
|
+
raise TypeError("generator must be a Generator")
|
|
44
|
+
if self.method != "exact_symbolic_taylor_series":
|
|
45
|
+
raise ValueError("method is a closed package-owned value")
|
|
46
|
+
normalized: list[sp.Expr] = []
|
|
47
|
+
for value in self.coefficients:
|
|
48
|
+
if isinstance(value, (bool, float)):
|
|
49
|
+
raise TypeError("coefficients must be exact symbolic values")
|
|
50
|
+
exact = sp.Integer(value) if isinstance(value, int) else value
|
|
51
|
+
if not isinstance(exact, sp.Expr):
|
|
52
|
+
raise TypeError("coefficients must be exact symbolic values")
|
|
53
|
+
validate_exact_expression(exact)
|
|
54
|
+
normalized.append(exact)
|
|
55
|
+
if len(normalized) != self.order:
|
|
56
|
+
raise ValueError("coefficients length must equal order")
|
|
57
|
+
object.__setattr__(self, "coefficients", tuple(normalized))
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def evidence_status(self) -> str:
|
|
61
|
+
"""Return the package-derived computational evidence state."""
|
|
62
|
+
|
|
63
|
+
if not self.verification_report.success:
|
|
64
|
+
return FailureState.UNRESOLVED.value
|
|
65
|
+
return "proven_exact_under_declared_assumptions"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def verification_report(self) -> VerificationReport:
|
|
69
|
+
normalized = sp.simplify(
|
|
70
|
+
self.generator.expression.subs(self.generator.variable, 0)
|
|
71
|
+
)
|
|
72
|
+
expected: Any
|
|
73
|
+
try:
|
|
74
|
+
expected_coefficients = taylor_coefficients(
|
|
75
|
+
self.generator, order=self.order
|
|
76
|
+
)
|
|
77
|
+
coefficients_ok = expected_coefficients == self.coefficients
|
|
78
|
+
expected = [str(value) for value in expected_coefficients]
|
|
79
|
+
reason = "stored coefficients do not match the exact expansion"
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
expected = "unavailable (invalid or over-limit order)"
|
|
82
|
+
coefficients_ok = False
|
|
83
|
+
reason = "exact expansion could not be recomputed"
|
|
84
|
+
return VerificationReport(
|
|
85
|
+
checks=(
|
|
86
|
+
VerificationCheck(
|
|
87
|
+
name="generator_normalization",
|
|
88
|
+
checked="generator value at z = 0",
|
|
89
|
+
expected="1",
|
|
90
|
+
observed=str(normalized),
|
|
91
|
+
status=CheckStatus.PASS if normalized == 1 else CheckStatus.FAIL,
|
|
92
|
+
scope="generator definition",
|
|
93
|
+
failure_reason=(
|
|
94
|
+
None
|
|
95
|
+
if normalized == 1
|
|
96
|
+
else "generator is not normalized at the origin"
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
VerificationCheck(
|
|
100
|
+
name="exact_taylor_coefficients",
|
|
101
|
+
checked=f"Taylor coefficients through order {self.order}",
|
|
102
|
+
expected=expected,
|
|
103
|
+
observed=[str(value) for value in self.coefficients],
|
|
104
|
+
status=(
|
|
105
|
+
CheckStatus.PASS if coefficients_ok else CheckStatus.FAIL
|
|
106
|
+
),
|
|
107
|
+
scope="exact symbolic expansion",
|
|
108
|
+
failure_reason=None if coefficients_ok else reason,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> dict[str, Any]:
|
|
114
|
+
return build_result_payload(
|
|
115
|
+
result_type="generator_series",
|
|
116
|
+
canonical_inputs={"generator": self.generator.key, "order": self.order},
|
|
117
|
+
exact_expressions={
|
|
118
|
+
"generator": self.generator.formula,
|
|
119
|
+
"coefficients": [str(value) for value in self.coefficients],
|
|
120
|
+
},
|
|
121
|
+
method=self.method,
|
|
122
|
+
evidence_status=self.evidence_status,
|
|
123
|
+
assumptions=("phi(0) = 1", "expression uses only the variable z"),
|
|
124
|
+
source_references=(self.generator.citation,),
|
|
125
|
+
artifact_versions={
|
|
126
|
+
"generator_catalog": generator_artifact_version(self.generator),
|
|
127
|
+
"source_commit": SOURCE_ARTIFACT_COMMIT
|
|
128
|
+
if generator_artifact_version(self.generator) != "user-supplied"
|
|
129
|
+
else "caller-supplied",
|
|
130
|
+
"fixture_or_proof": GENERATOR_FIXTURE_ID,
|
|
131
|
+
},
|
|
132
|
+
verification=self.verification_report,
|
|
133
|
+
exact_expression_dag=canonical_expression_dag(
|
|
134
|
+
{
|
|
135
|
+
"generator": self.generator.expression,
|
|
136
|
+
"coefficients": self.coefficients,
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
provenance=(
|
|
140
|
+
"caller_supplied"
|
|
141
|
+
if generator_artifact_version(self.generator) == "user-supplied"
|
|
142
|
+
else "built_in"
|
|
143
|
+
),
|
|
144
|
+
legacy_fields={
|
|
145
|
+
"generator": self.generator.key,
|
|
146
|
+
"generator_formula": self.generator.formula,
|
|
147
|
+
"generator_citation": self.generator.citation,
|
|
148
|
+
"order": self.order,
|
|
149
|
+
"coefficients": [str(value) for value in self.coefficients],
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _resolve_generator(generator: str | Generator) -> Generator:
|
|
155
|
+
if isinstance(generator, str):
|
|
156
|
+
return get_generator(generator)
|
|
157
|
+
if not isinstance(generator, Generator):
|
|
158
|
+
raise TypeError("generator must be a built-in key or Generator")
|
|
159
|
+
return generator
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def taylor_coefficients(
|
|
163
|
+
generator: str | Generator,
|
|
164
|
+
*,
|
|
165
|
+
order: int,
|
|
166
|
+
) -> tuple[sp.Expr, ...]:
|
|
167
|
+
"""Return exact ``B_1, …, B_order`` for ``phi(z) = 1 + Σ B_n z^n``.
|
|
168
|
+
|
|
169
|
+
Custom :class:`Generator` objects are accepted so reproductions are not
|
|
170
|
+
limited to the built-in catalog. The normalization ``phi(0) = 1`` is checked
|
|
171
|
+
exactly before coefficients are returned.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
if isinstance(order, bool) or not isinstance(order, int):
|
|
175
|
+
raise TypeError("order must be a positive integer")
|
|
176
|
+
if order < 1:
|
|
177
|
+
raise InvalidInputError("order must be at least 1")
|
|
178
|
+
if order > MAX_TAYLOR_ORDER:
|
|
179
|
+
raise ResourceLimitError(f"order must be at most {MAX_TAYLOR_ORDER}")
|
|
180
|
+
|
|
181
|
+
resolved = _resolve_generator(generator)
|
|
182
|
+
expression = resolved.expression
|
|
183
|
+
variable = resolved.variable
|
|
184
|
+
if sp.simplify(expression.subs(variable, 0) - 1) != 0:
|
|
185
|
+
raise InvalidInputError(f"generator {resolved.key!r}: phi(0) must equal 1")
|
|
186
|
+
|
|
187
|
+
polynomial = sp.series(expression, variable, 0, order + 1).removeO().expand()
|
|
188
|
+
return tuple(
|
|
189
|
+
sp.simplify(polynomial.coeff(variable, degree))
|
|
190
|
+
for degree in range(1, order + 1)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def generator_series(
|
|
195
|
+
generator: str | Generator,
|
|
196
|
+
*,
|
|
197
|
+
order: int,
|
|
198
|
+
) -> GeneratorSeriesResult:
|
|
199
|
+
"""Return exact coefficients plus a structured provenance record."""
|
|
200
|
+
|
|
201
|
+
resolved = _resolve_generator(generator)
|
|
202
|
+
return GeneratorSeriesResult(
|
|
203
|
+
generator=resolved,
|
|
204
|
+
order=order,
|
|
205
|
+
coefficients=taylor_coefficients(resolved, order=order),
|
|
206
|
+
)
|