optivibe-reference 0.2.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.
- optivibe_reference/__init__.py +14 -0
- optivibe_reference/_envelope.py +19 -0
- optivibe_reference/_paths.py +66 -0
- optivibe_reference/_probe_common.py +142 -0
- optivibe_reference/build_data.py +271 -0
- optivibe_reference/build_steps.py +182 -0
- optivibe_reference/catalog_build.py +1138 -0
- optivibe_reference/data/operand_semantics.json +2192 -0
- optivibe_reference/data/operand_synonyms.json +1687 -0
- optivibe_reference/data/provenance_grants.txt +5 -0
- optivibe_reference/data/tolerance_operand_synonyms.json +307 -0
- optivibe_reference/errors.py +57 -0
- optivibe_reference/glass_build.py +433 -0
- optivibe_reference/glass_dispersion.py +207 -0
- optivibe_reference/glass_math.py +383 -0
- optivibe_reference/glass_parse.py +168 -0
- optivibe_reference/manual_build.py +672 -0
- optivibe_reference/manual_enrich.py +490 -0
- optivibe_reference/probe_operands.py +226 -0
- optivibe_reference/probe_tolerances.py +371 -0
- optivibe_reference/server.py +547 -0
- optivibe_reference/tolerance_build.py +554 -0
- optivibe_reference/tools/__init__.py +5 -0
- optivibe_reference/tools/_glass_common.py +137 -0
- optivibe_reference/tools/_glass_kind.py +133 -0
- optivibe_reference/tools/_query_params.py +57 -0
- optivibe_reference/tools/_rag_match.py +92 -0
- optivibe_reference/tools/find_glass_pair.py +574 -0
- optivibe_reference/tools/find_glasses.py +339 -0
- optivibe_reference/tools/lookup_glass.py +262 -0
- optivibe_reference/tools/lookup_operand.py +463 -0
- optivibe_reference/tools/search_reference.py +321 -0
- optivibe_reference-0.2.0.dist-info/METADATA +44 -0
- optivibe_reference-0.2.0.dist-info/RECORD +37 -0
- optivibe_reference-0.2.0.dist-info/WHEEL +5 -0
- optivibe_reference-0.2.0.dist-info/entry_points.txt +2 -0
- optivibe_reference-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""optivibe_reference — lookup tables + RAG + intent->ZOS-API grounding index.
|
|
2
|
+
|
|
3
|
+
The grounding layer that makes a smaller local orchestrator viable: an FTS5 RAG
|
|
4
|
+
over reference material plus an intent->ZOS-API mapping index the agent consults
|
|
5
|
+
FIRST (probe-first).
|
|
6
|
+
|
|
7
|
+
Layer 1 (this increment): the ``MeritOperandType`` operand catalog (438 live
|
|
8
|
+
codes, authored/manual descriptions pending, live-probed parameter-cell layout
|
|
9
|
+
battery) + the typed, session-free, never-raise ``lookup_operand`` MCP tool that
|
|
10
|
+
grounds design intent onto a merit-function operand. The catalog is built from a
|
|
11
|
+
gitignored, user-built normalized-LF JSON at runtime; the ``.db`` is never committed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""_envelope.py — the reference-layer expected-failure envelope.
|
|
2
|
+
|
|
3
|
+
Re-implemented (NOT imported) from the harness ``_analysis_common.error_envelope``
|
|
4
|
+
shape so the reference package stays decoupled. A handler constructs this for an
|
|
5
|
+
EXPECTED failure class (unknown operand, malformed FTS query); it does NOT raise
|
|
6
|
+
past its own boundary for those.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def error_envelope(tool, error_family, error, **extra):
|
|
11
|
+
"""Build the locked expected-failure envelope (never raised past boundary).
|
|
12
|
+
|
|
13
|
+
``{"ok": False, "tool": tool, "error_family": error_family, "error": error}``
|
|
14
|
+
plus any tool-specific diagnostic fields in ``extra`` (e.g. ``query=...``).
|
|
15
|
+
Mirrors the harness 4-key+extra shape exactly.
|
|
16
|
+
"""
|
|
17
|
+
env = {"ok": False, "tool": tool, "error_family": error_family, "error": error}
|
|
18
|
+
env.update(extra)
|
|
19
|
+
return env
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""_paths.py — the ONE resolver for where build inputs live (clone vs installed wheel).
|
|
2
|
+
|
|
3
|
+
Two locations used to be derived from ``__file__`` as ``<package root>/scripts/...`` and
|
|
4
|
+
``<repo root>/PROVENANCE.md``. Both exist in a source checkout and NEITHER exists in an
|
|
5
|
+
installed wheel (``site-packages/optivibe_reference`` has no ``scripts/`` sibling and no
|
|
6
|
+
repo root), so every reader and writer routes through this module instead:
|
|
7
|
+
|
|
8
|
+
* ``captures_dir()`` — the live-probe captures + verbatim raw oracles. A source checkout
|
|
9
|
+
keeps them at ``packages/optivibe-reference/scripts/captures`` (unchanged, gitignored);
|
|
10
|
+
an installed package keeps them at ``<package>/data/captures`` (created on first write).
|
|
11
|
+
* ``provenance_ledger_path()`` — the file the two fail-closed provenance gates
|
|
12
|
+
(``glass_build.assert_agf_grant_on_file``, ``manual_build.assert_manual_grant_on_file``)
|
|
13
|
+
read. A source checkout reads the repo-root ledger and ONLY that — deleting it, or the
|
|
14
|
+
row in it, still refuses, exactly as before. An installed package has no repo ledger, so
|
|
15
|
+
it reads the marker file shipped beside the code (``data/provenance_grants.txt``). The
|
|
16
|
+
marker check itself is unchanged; this only chooses which file it reads.
|
|
17
|
+
|
|
18
|
+
"Source checkout" is decided by the CLONE LAYOUT (``pyproject.toml`` + ``scripts/`` at the
|
|
19
|
+
package root), never by whether ``scripts/captures`` exists: that dir is gitignored and
|
|
20
|
+
absent on a fresh clone until the probes run, and keying on it would send a fresh clone's
|
|
21
|
+
probe output into the package's ``data/`` tree instead.
|
|
22
|
+
|
|
23
|
+
Stdlib only; imports nothing from this package (every build module imports it).
|
|
24
|
+
"""
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
_HERE = os.path.dirname(os.path.abspath(__file__)) # .../optivibe_reference
|
|
28
|
+
# packages/optivibe-reference in a clone (src/optivibe_reference -> ../..); some unrelated
|
|
29
|
+
# dir (e.g. ``<venv>/Lib``) in an installed wheel, which is why it is CHECKED, not trusted.
|
|
30
|
+
_PKG_ROOT = os.path.dirname(os.path.dirname(_HERE))
|
|
31
|
+
_REPO_ROOT = os.path.dirname(os.path.dirname(_PKG_ROOT))
|
|
32
|
+
|
|
33
|
+
# The repo-root provenance ledger a source checkout must carry.
|
|
34
|
+
LEDGER_NAME = "PROVENANCE.md"
|
|
35
|
+
# The marker file an installed package ships (package-data) in place of the ledger. It
|
|
36
|
+
# holds the ledger-row marker strings the two gates look for, one per line.
|
|
37
|
+
PROVENANCE_MARKER_NAME = "provenance_grants.txt"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_source_checkout(pkg_root=_PKG_ROOT):
|
|
41
|
+
"""True iff ``pkg_root`` is a package root in a clone (pyproject + scripts/ beside src)."""
|
|
42
|
+
return (os.path.isfile(os.path.join(pkg_root, "pyproject.toml"))
|
|
43
|
+
and os.path.isdir(os.path.join(pkg_root, "scripts")))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def captures_dir(pkg_root=_PKG_ROOT, package_dir=_HERE):
|
|
47
|
+
"""Return the live-probe captures dir (NOT created here — writers create it).
|
|
48
|
+
|
|
49
|
+
Clone: ``<pkg_root>/scripts/captures`` (the historic location, unchanged).
|
|
50
|
+
Installed: ``<package_dir>/data/captures``.
|
|
51
|
+
"""
|
|
52
|
+
if is_source_checkout(pkg_root):
|
|
53
|
+
return os.path.join(pkg_root, "scripts", "captures")
|
|
54
|
+
return os.path.join(package_dir, "data", "captures")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def provenance_ledger_path(pkg_root=_PKG_ROOT, repo_root=_REPO_ROOT, package_dir=_HERE):
|
|
58
|
+
"""Return the file the provenance gates read (fail-closed either way).
|
|
59
|
+
|
|
60
|
+
Clone: the repo-root ledger, whether or not it exists — a missing ledger in a clone
|
|
61
|
+
must refuse, never fall through to the shipped marker. Installed: the shipped marker
|
|
62
|
+
file; if that is missing too the returned path does not exist and the gate refuses.
|
|
63
|
+
"""
|
|
64
|
+
if is_source_checkout(pkg_root):
|
|
65
|
+
return os.path.join(repo_root, LEDGER_NAME)
|
|
66
|
+
return os.path.join(package_dir, "data", PROVENANCE_MARKER_NAME)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""_probe_common.py — the shared plumbing of the two live operand probes.
|
|
2
|
+
|
|
3
|
+
``probe_operands`` (MeritOperandType) and ``probe_tolerances`` (ToleranceOperandType)
|
|
4
|
+
each boot a headless OpticStudio, dump an enum surface to the captures dir, and reap the
|
|
5
|
+
session. This module holds what they share: loading pythonnet, resolving the ZOS-API
|
|
6
|
+
assemblies without a hardcoded install path, the never-raise ``capture`` helper, and the
|
|
7
|
+
JSON writer.
|
|
8
|
+
|
|
9
|
+
Importing this module (or either probe) needs NO pythonnet: ``clr`` is imported inside
|
|
10
|
+
``load_clr`` only, and the ``PYTHONNET_RUNTIME`` selector is set there too, so the
|
|
11
|
+
harness publication suite (which imports every shipped module) never touches .NET.
|
|
12
|
+
It imports nothing from optivibe_harness (zero cross-package coupling).
|
|
13
|
+
"""
|
|
14
|
+
import glob
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from . import _paths
|
|
19
|
+
|
|
20
|
+
# The install hint a missing pythonnet names: the probes are an OPTIONAL extra.
|
|
21
|
+
PROBE_EXTRA_HINT = 'install optivibe-reference[probe] (pip install "optivibe-reference[probe]")'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PythonnetMissing(ImportError):
|
|
25
|
+
"""pythonnet (``clr``) is not importable, so no live probe can run."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_clr():
|
|
29
|
+
"""Select the .NET Framework runtime, then import and return ``clr``.
|
|
30
|
+
|
|
31
|
+
The runtime selector MUST be set before ``import clr``. Raises ``PythonnetMissing``
|
|
32
|
+
(an ``ImportError``) naming the ``[probe]`` extra when pythonnet is absent.
|
|
33
|
+
"""
|
|
34
|
+
os.environ.setdefault("PYTHONNET_RUNTIME", "netfx")
|
|
35
|
+
try:
|
|
36
|
+
import clr # noqa: E402 (pythonnet; after the runtime selector is set)
|
|
37
|
+
except ImportError as exc:
|
|
38
|
+
raise PythonnetMissing(
|
|
39
|
+
"pythonnet is not installed, so the live probes cannot run - "
|
|
40
|
+
+ PROBE_EXTRA_HINT) from exc
|
|
41
|
+
return clr
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def out_dir():
|
|
45
|
+
"""Where the probes write: the captures dir (``_paths.captures_dir``)."""
|
|
46
|
+
return _paths.captures_dir()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_nethelper():
|
|
50
|
+
"""Resolve ZOSAPI_NetHelper.dll without hardcoding an install path."""
|
|
51
|
+
override = os.environ.get("ZOSAPI_NETHELPER")
|
|
52
|
+
if override and os.path.isfile(override):
|
|
53
|
+
return override
|
|
54
|
+
docs = os.path.join(os.path.expanduser("~"), "Documents", "Zemax", "ZOS-API", "Libraries")
|
|
55
|
+
candidate = os.path.join(docs, "ZOSAPI_NetHelper.dll")
|
|
56
|
+
if os.path.isfile(candidate):
|
|
57
|
+
return candidate
|
|
58
|
+
root = os.path.join(os.path.expanduser("~"), "Documents", "Zemax")
|
|
59
|
+
hits = glob.glob(os.path.join(root, "**", "ZOSAPI_NetHelper.dll"), recursive=True)
|
|
60
|
+
if hits:
|
|
61
|
+
return hits[0]
|
|
62
|
+
raise FileNotFoundError(
|
|
63
|
+
"ZOSAPI_NetHelper.dll not found; set ZOSAPI_NETHELPER to its path."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_zemax_dir(initializer):
|
|
68
|
+
"""Resolve the OpticStudio install dir (holding ZOSAPI.dll) without hardcoding."""
|
|
69
|
+
def has_dll(d):
|
|
70
|
+
return bool(d) and os.path.isfile(os.path.join(d, "ZOSAPI.dll"))
|
|
71
|
+
|
|
72
|
+
override = os.environ.get("ZEMAX_DIR")
|
|
73
|
+
if has_dll(override):
|
|
74
|
+
initializer.Initialize(override)
|
|
75
|
+
return override, "env:ZEMAX_DIR"
|
|
76
|
+
if initializer.Initialize():
|
|
77
|
+
d = initializer.GetZemaxDirectory()
|
|
78
|
+
if has_dll(d):
|
|
79
|
+
return d, "registry-autodetect"
|
|
80
|
+
candidates = []
|
|
81
|
+
for base in (os.environ.get("ProgramFiles", r"C:\Program Files"),
|
|
82
|
+
os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")):
|
|
83
|
+
candidates += glob.glob(os.path.join(base, "*Zemax OpticStudio*"))
|
|
84
|
+
candidates = sorted((c for c in candidates if has_dll(c)), reverse=True)
|
|
85
|
+
if candidates:
|
|
86
|
+
d = candidates[0]
|
|
87
|
+
initializer.Initialize(d)
|
|
88
|
+
return d, "program-files-scan"
|
|
89
|
+
raise RuntimeError("Could not resolve an OpticStudio install dir with ZOSAPI.dll.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_zosapi(clr):
|
|
93
|
+
"""Reference the NetHelper + ZOS-API assemblies; return the ``ZOSAPI`` namespace."""
|
|
94
|
+
nethelper = find_nethelper()
|
|
95
|
+
print(f"[probe] NetHelper: {nethelper}")
|
|
96
|
+
clr.AddReference(nethelper)
|
|
97
|
+
import ZOSAPI_NetHelper # noqa: E402
|
|
98
|
+
|
|
99
|
+
zemax_dir, how = resolve_zemax_dir(ZOSAPI_NetHelper.ZOSAPI_Initializer)
|
|
100
|
+
print(f"[probe] Zemax directory: {zemax_dir} (via {how})")
|
|
101
|
+
clr.AddReference(os.path.join(zemax_dir, "ZOSAPI.dll"))
|
|
102
|
+
clr.AddReference(os.path.join(zemax_dir, "ZOSAPI_Interfaces.dll"))
|
|
103
|
+
import ZOSAPI # noqa: E402
|
|
104
|
+
return ZOSAPI
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def enum_members(enum_type):
|
|
108
|
+
"""Member names of a .NET enum via System.Enum.GetNames (the live truth)."""
|
|
109
|
+
from System import Enum # noqa: E402
|
|
110
|
+
return [str(n) for n in Enum.GetNames(enum_type)]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def public_members(obj):
|
|
114
|
+
"""Non-underscore attribute names on a live object (defensive discovery)."""
|
|
115
|
+
try:
|
|
116
|
+
return sorted(n for n in dir(obj) if not n.startswith("_"))
|
|
117
|
+
except Exception as exc: # noqa: BLE001
|
|
118
|
+
return f"<unavailable: {type(exc).__name__}: {exc}>"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def capture(fn):
|
|
122
|
+
"""Call fn(), returning its value or a recorded failure string (never raises)."""
|
|
123
|
+
try:
|
|
124
|
+
v = fn()
|
|
125
|
+
# keep JSON-serializable
|
|
126
|
+
if isinstance(v, (str, int, float, bool)) or v is None:
|
|
127
|
+
return v
|
|
128
|
+
return str(v)
|
|
129
|
+
except Exception as exc: # noqa: BLE001
|
|
130
|
+
return f"<err: {type(exc).__name__}: {exc}>"[:200]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def write_json(name, payload):
|
|
134
|
+
"""Write ``payload`` to ``<captures>/<name>`` (dir created on write); return the path."""
|
|
135
|
+
target = out_dir()
|
|
136
|
+
os.makedirs(target, exist_ok=True)
|
|
137
|
+
path = os.path.join(target, name)
|
|
138
|
+
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
|
139
|
+
json.dump(payload, fh, indent=2)
|
|
140
|
+
fh.write("\n")
|
|
141
|
+
print(f"[probe] wrote {path}")
|
|
142
|
+
return path
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""build_data.py — one-shot local rebuild of all user-side vendor data (``optivibe-build-data``).
|
|
2
|
+
|
|
3
|
+
OptiVibe ships code only: the glass catalog, the manual FTS5 corpus, and the merit +
|
|
4
|
+
tolerance operand catalogs are built LOCALLY from YOUR licensed Zemax OpticStudio install
|
|
5
|
+
and are never committed. Run this once after install (and again after upgrading
|
|
6
|
+
OpticStudio):
|
|
7
|
+
|
|
8
|
+
optivibe-build-data # installed (pip install optivibe-mcp)
|
|
9
|
+
optivibe-build-data --probe # first run: probe the operand surfaces, then build
|
|
10
|
+
|
|
11
|
+
It is the in-package equivalent of the published ``scripts/build_vendor_data.py`` and
|
|
12
|
+
needs no clone. Steps (each calls the package-level builder; nothing is duplicated here):
|
|
13
|
+
|
|
14
|
+
0. (``--probe`` only) the live operand + tolerance probes (``probe_operands.main``,
|
|
15
|
+
``probe_tolerances.main``): boot a headless OpticStudio, write the captures steps 2-4
|
|
16
|
+
need, reap the engine. They need pythonnet (``optivibe-reference[probe]``; the
|
|
17
|
+
optivibe-mcp install already has it). A missing pythonnet or a probe error FAILS
|
|
18
|
+
that probe, and the build steps still run.
|
|
19
|
+
1. Glass catalog — ``build_steps.build_glass_catalog``: the ``.agf`` files from your
|
|
20
|
+
install's Glasscat dir (``ZEMAX_GLASSCAT`` env override, else
|
|
21
|
+
``~/Documents/Zemax/Glasscat``) -> ``data/glass_catalog.json``.
|
|
22
|
+
2. Manual corpus — ``build_steps.build_manual_corpus``: your install's
|
|
23
|
+
``OpticStudio_UserManual_en.pdf`` (``OPTIVIBE_MANUAL_PDF`` env override, else the
|
|
24
|
+
standard install dir) -> the local ``data/manual_corpus.db`` + text-free
|
|
25
|
+
``data/manual_manifest.json``, plus (``--emit-raw``) the verbatim raw
|
|
26
|
+
operand/tolerance oracles steps 3-4 merge as local descriptions. Needs PyMuPDF
|
|
27
|
+
(``optivibe-reference[manual]``).
|
|
28
|
+
3. Merit operand catalog — ``catalog_build.main`` -> ``data/operand_catalog.json``.
|
|
29
|
+
4. Tolerance operand catalog — ``tolerance_build.main`` ->
|
|
30
|
+
``data/tolerance_operand_catalog.json``.
|
|
31
|
+
|
|
32
|
+
A missing input (no Glasscat dir, no manual PDF, no PyMuPDF, no live-probe captures) is
|
|
33
|
+
reported and SKIPPED: that tool family answers a typed ``*_unavailable`` envelope until
|
|
34
|
+
the build runs. An unexpected error FAILS the step. Exit code = number of FAILED (not
|
|
35
|
+
skipped) steps. ASCII status output only.
|
|
36
|
+
"""
|
|
37
|
+
import argparse
|
|
38
|
+
import importlib.util
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import re
|
|
42
|
+
import sys
|
|
43
|
+
|
|
44
|
+
from . import _paths, build_steps, catalog_build, tolerance_build
|
|
45
|
+
|
|
46
|
+
# The live-probe captures each operand-catalog build REQUIRES (regenerated from the
|
|
47
|
+
# user's OpticStudio install); absent -> that step is a fresh-install SKIP.
|
|
48
|
+
OPERAND_CAPTURES = ("operand_inventory_438.json", "probe_operands_capture.json")
|
|
49
|
+
TOLERANCE_CAPTURES = ("tolerance_inventory_62.json", "probe_tolerances_capture.json")
|
|
50
|
+
# The capture the manual step's ``--emit-raw`` opens unconditionally (tolerance oracle).
|
|
51
|
+
TOLERANCE_INVENTORY_CAPTURE = "tolerance_inventory_62.json"
|
|
52
|
+
# ``--emit-raw`` opens BOTH inventories: the merit one for the glued-code gate, the
|
|
53
|
+
# tolerance one for the tolerance oracle. Either absent -> no ``--emit-raw``.
|
|
54
|
+
EMIT_RAW_CAPTURES = (OPERAND_CAPTURES[0], TOLERANCE_INVENTORY_CAPTURE)
|
|
55
|
+
|
|
56
|
+
_DESCRIPTION = (
|
|
57
|
+
"Build the OptiVibe reference data (glass catalog, manual corpus, merit and "
|
|
58
|
+
"tolerance operand catalogs) locally from your licensed OpticStudio install. "
|
|
59
|
+
"A missing input is SKIPPED; the exit code is the number of FAILED steps."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ascii(text):
|
|
64
|
+
"""``text`` with every non-ASCII character replaced (status output is ASCII-only)."""
|
|
65
|
+
return str(text).encode("ascii", "replace").decode("ascii")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _say(text):
|
|
69
|
+
print(_ascii(text))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _error_text(exc):
|
|
73
|
+
return _ascii("%s: %s" % (type(exc).__name__, exc))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _captures_present(names, captures=None):
|
|
77
|
+
captures = _paths.captures_dir() if captures is None else captures
|
|
78
|
+
return all(os.path.isfile(os.path.join(captures, n)) for n in names)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def probed_count(capture_path):
|
|
82
|
+
"""The ``inventory_count`` a probe recorded in its capture, or None.
|
|
83
|
+
|
|
84
|
+
None when the capture is absent, unreadable, or carries no integer count.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
with open(capture_path, encoding="utf-8") as fh:
|
|
88
|
+
count = json.load(fh).get("inventory_count")
|
|
89
|
+
except (OSError, ValueError, AttributeError):
|
|
90
|
+
return None
|
|
91
|
+
return count if isinstance(count, int) and not isinstance(count, bool) else None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def catalog_step_blocker(captures, captures_dir=None):
|
|
95
|
+
"""Why this operand/tolerance catalog cannot be built for the probed install, or None.
|
|
96
|
+
|
|
97
|
+
``captures`` is ``(<kind>_inventory_<N>.json, probe_<kind>s_capture.json)``; N is the
|
|
98
|
+
operand count the shipped curated data (sign conventions, run-verdict buckets) was
|
|
99
|
+
built against. The probe names its inventory by the LIVE count, so an OpticStudio
|
|
100
|
+
with a different operand list writes a different file and the curated data does not
|
|
101
|
+
cover it. That is not a missing probe, so it must not be reported as one.
|
|
102
|
+
"""
|
|
103
|
+
captures_dir = _paths.captures_dir() if captures_dir is None else captures_dir
|
|
104
|
+
curated = int(re.search(r"_(\d+)\.json$", captures[0]).group(1))
|
|
105
|
+
probed = probed_count(os.path.join(captures_dir, captures[1]))
|
|
106
|
+
if probed is None or probed == curated:
|
|
107
|
+
return None
|
|
108
|
+
kind = "tolerance" if captures[0].startswith("tolerance") else "merit"
|
|
109
|
+
return ("your OpticStudio exposes %d %s operands; the curated data shipped with "
|
|
110
|
+
"this release covers the %d it was built against, so this catalog cannot "
|
|
111
|
+
"be built for your OpticStudio version yet (re-running the probes will not "
|
|
112
|
+
"change that)." % (probed, kind, curated))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _run_probe(label, probe_main):
|
|
116
|
+
"""Run one live probe; return 1 iff it FAILED (a missing pythonnet included)."""
|
|
117
|
+
# Lazy: importing the probe modules is cheap and clr-free, but keep them off the
|
|
118
|
+
# import path of a plain build run.
|
|
119
|
+
from ._probe_common import PROBE_EXTRA_HINT, PythonnetMissing
|
|
120
|
+
try:
|
|
121
|
+
rc = probe_main()
|
|
122
|
+
except PythonnetMissing:
|
|
123
|
+
_say("[%s] FAILED - pythonnet is not installed, so the live probe cannot run. "
|
|
124
|
+
"%s." % (label, PROBE_EXTRA_HINT))
|
|
125
|
+
return 1
|
|
126
|
+
except Exception as exc: # noqa: BLE001 — report per-step, keep going
|
|
127
|
+
_say("[%s] FAILED - %s" % (label, _error_text(exc)))
|
|
128
|
+
return 1
|
|
129
|
+
if rc:
|
|
130
|
+
_say("[%s] FAILED - probe returned exit code %d (see messages above)." % (label, rc))
|
|
131
|
+
return 1
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_probes():
|
|
136
|
+
"""Step 0: the two live probes, merit first. Returns the number that FAILED."""
|
|
137
|
+
from . import probe_operands, probe_tolerances
|
|
138
|
+
failures = 0
|
|
139
|
+
_say("=== [probe 1/2] merit operand surface (boots a headless OpticStudio) ===")
|
|
140
|
+
failures += _run_probe("probe-operands", lambda: probe_operands.main())
|
|
141
|
+
_say("=== [probe 2/2] tolerance operand surface (boots a headless OpticStudio) ===")
|
|
142
|
+
failures += _run_probe("probe-tolerances", lambda: probe_tolerances.main())
|
|
143
|
+
return failures
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _run_catalog_step(label, captures, build_main):
|
|
147
|
+
"""Run a user-built operand/tolerance catalog build; return 1 iff it FAILED.
|
|
148
|
+
|
|
149
|
+
Absent live-probe captures = a fresh install that has not probed yet -> SKIP
|
|
150
|
+
(that domain's operand tools answer operand_catalog_unavailable until built).
|
|
151
|
+
Present captures + a raised error OR a nonzero build exit code -> FAILED.
|
|
152
|
+
"""
|
|
153
|
+
blocker = catalog_step_blocker(captures)
|
|
154
|
+
if blocker is not None:
|
|
155
|
+
_say("[%s] SKIPPED - %s" % (label, blocker))
|
|
156
|
+
_say("[%s] this operand family answers operand_catalog_unavailable."
|
|
157
|
+
% label)
|
|
158
|
+
return 0
|
|
159
|
+
if not _captures_present(captures):
|
|
160
|
+
_say("[%s] SKIPPED - live-probe captures not present in %s. Run the probes "
|
|
161
|
+
"against your OpticStudio first: optivibe-build-data --probe"
|
|
162
|
+
% (label, _paths.captures_dir()))
|
|
163
|
+
_say("[%s] this operand family answers operand_catalog_unavailable until "
|
|
164
|
+
"the build runs." % label)
|
|
165
|
+
return 0
|
|
166
|
+
try:
|
|
167
|
+
rc = build_main()
|
|
168
|
+
except Exception as exc: # noqa: BLE001 — report per-step, keep going
|
|
169
|
+
_say("[%s] FAILED - %s" % (label, _error_text(exc)))
|
|
170
|
+
return 1
|
|
171
|
+
if rc:
|
|
172
|
+
_say("[%s] FAILED - build returned exit code %d (see messages above)."
|
|
173
|
+
% (label, rc))
|
|
174
|
+
return 1
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _run_glass_step():
|
|
179
|
+
"""Step 1; return 1 iff it FAILED. No Glasscat dir -> SKIPPED."""
|
|
180
|
+
try:
|
|
181
|
+
build_steps.build_glass_catalog()
|
|
182
|
+
except FileNotFoundError as exc:
|
|
183
|
+
_say("[glass-build] SKIPPED - %s" % _ascii(exc))
|
|
184
|
+
_say("[glass-build] glass tools answer glass_catalog_unavailable until "
|
|
185
|
+
"this build runs.")
|
|
186
|
+
return 0
|
|
187
|
+
except Exception as exc: # noqa: BLE001 — report per-step, keep going
|
|
188
|
+
_say("[glass-build] FAILED - %s" % _error_text(exc))
|
|
189
|
+
return 1
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _run_manual_step():
|
|
194
|
+
"""Step 2; return 1 iff it FAILED. No PDF or no PyMuPDF -> SKIPPED."""
|
|
195
|
+
pdf = build_steps.find_manual_pdf()
|
|
196
|
+
if pdf is None:
|
|
197
|
+
_say("[manual-build] SKIPPED - manual PDF not present (set OPTIVIBE_MANUAL_PDF "
|
|
198
|
+
"or install OpticStudio).")
|
|
199
|
+
_say("[manual-build] search_reference answers corpus_unavailable until this "
|
|
200
|
+
"build runs.")
|
|
201
|
+
return 0
|
|
202
|
+
if importlib.util.find_spec("fitz") is None:
|
|
203
|
+
_say("[manual-build] SKIPPED - PyMuPDF is not installed, so the manual PDF "
|
|
204
|
+
"cannot be read. install optivibe-reference[manual] "
|
|
205
|
+
'(pip install "optivibe-reference[manual]").')
|
|
206
|
+
return 0
|
|
207
|
+
try:
|
|
208
|
+
# --emit-raw ALSO writes the gitignored operand/tolerance raw oracles that
|
|
209
|
+
# steps 3-4 merge as verbatim-local descriptions. It opens BOTH inventory
|
|
210
|
+
# captures unconditionally (merit for the glued-code gate, tolerance for the
|
|
211
|
+
# tolerance oracle) — either absent, --emit-raw raises FileNotFoundError mid-run
|
|
212
|
+
# and the whole manual step is mis-reported FAILED. Preflight both: emit the
|
|
213
|
+
# oracles only when both are present, else build the corpus WITHOUT --emit-raw —
|
|
214
|
+
# the SAME fresh-install degrade steps 3-4 take (operand/tolerance stay
|
|
215
|
+
# synonyms-only / unavailable), never a FAIL.
|
|
216
|
+
if _captures_present(EMIT_RAW_CAPTURES):
|
|
217
|
+
emit_args = ["--emit-raw"]
|
|
218
|
+
else:
|
|
219
|
+
emit_args = []
|
|
220
|
+
_say("[manual-build] NOTE: an operand inventory capture is absent -> building "
|
|
221
|
+
"corpus WITHOUT --emit-raw; operand/tolerance catalogs stay "
|
|
222
|
+
"synonyms-only until the live-probe captures are regenerated.")
|
|
223
|
+
build_steps.build_manual_corpus(emit_args, find_pdf=lambda: pdf)
|
|
224
|
+
except Exception as exc: # noqa: BLE001 — report per-step, keep going
|
|
225
|
+
_say("[manual-build] FAILED - %s" % _error_text(exc))
|
|
226
|
+
return 1
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def parse_args(argv):
|
|
231
|
+
parser = argparse.ArgumentParser(prog="optivibe-build-data", description=_DESCRIPTION)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
"--probe", action="store_true",
|
|
234
|
+
help="first run the live operand + tolerance probes (boots a headless "
|
|
235
|
+
"OpticStudio; needs pythonnet: optivibe-reference[probe])")
|
|
236
|
+
return parser.parse_args(argv)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main(argv=None):
|
|
240
|
+
"""Run every step; return the number of FAILED steps (``--help`` exits 0)."""
|
|
241
|
+
args = parse_args(list(sys.argv[1:] if argv is None else argv))
|
|
242
|
+
failures = 0
|
|
243
|
+
|
|
244
|
+
if args.probe:
|
|
245
|
+
failures += run_probes()
|
|
246
|
+
|
|
247
|
+
_say("=== [1/4] glass catalog (from your install's .agf Glasscat) ===")
|
|
248
|
+
failures += _run_glass_step()
|
|
249
|
+
|
|
250
|
+
_say("=== [2/4] manual corpus + verbatim raw oracles (from your install's PDF) ===")
|
|
251
|
+
failures += _run_manual_step()
|
|
252
|
+
|
|
253
|
+
_say("=== [3/4] merit operand catalog (user-built; synonyms + local oracle) ===")
|
|
254
|
+
failures += _run_catalog_step(
|
|
255
|
+
"operand-build", OPERAND_CAPTURES, lambda: catalog_build.main()
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
_say("=== [4/4] tolerance operand catalog (user-built; synonyms + local oracle) ===")
|
|
259
|
+
failures += _run_catalog_step(
|
|
260
|
+
"tolerance-build", TOLERANCE_CAPTURES, lambda: tolerance_build.main()
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if failures:
|
|
264
|
+
_say("done with %d FAILED step(s) - see messages above." % failures)
|
|
265
|
+
else:
|
|
266
|
+
_say("done. Built data is local-only and gitignored - never commit it.")
|
|
267
|
+
return failures
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
sys.exit(main())
|