optivibe-reference 0.2.0__tar.gz → 0.2.1__tar.gz
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-0.2.0 → optivibe_reference-0.2.1}/PKG-INFO +2 -3
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/pyproject.toml +3 -4
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/__init__.py +1 -1
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/build_data.py +40 -8
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/catalog_build.py +3 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/glass_build.py +30 -4
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/glass_parse.py +30 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/manual_enrich.py +12 -8
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/probe_operands.py +4 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/probe_tolerances.py +4 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tolerance_build.py +3 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/find_glass_pair.py +13 -8
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/find_glasses.py +151 -7
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/PKG-INFO +2 -3
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/README.md +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/setup.cfg +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/_envelope.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/_paths.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/_probe_common.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/build_steps.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/data/operand_semantics.json +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/data/operand_synonyms.json +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/data/provenance_grants.txt +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/data/tolerance_operand_synonyms.json +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/errors.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/glass_dispersion.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/glass_math.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/manual_build.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/server.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/__init__.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_glass_common.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_glass_kind.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_query_params.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_rag_match.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/lookup_glass.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/lookup_operand.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/search_reference.py +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/SOURCES.txt +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/dependency_links.txt +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/entry_points.txt +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/requires.txt +0 -0
- {optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optivibe-reference
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: OptiVibe reference layer: lookup tables + RAG + intent->ZOS-API grounding
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/chuang44-tiff/optivibe-mcp
|
|
7
7
|
Project-URL: Issues, https://github.com/chuang44-tiff/optivibe-mcp/issues
|
|
8
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
8
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
9
|
Requires-Python: >=3.11
|
|
11
10
|
Description-Content-Type: text/markdown
|
|
@@ -20,18 +20,17 @@
|
|
|
20
20
|
# licensed install, with no clone. The `probe` extra adds pythonnet for
|
|
21
21
|
# `optivibe-build-data --probe`; the optivibe-mcp install already carries it.
|
|
22
22
|
[build-system]
|
|
23
|
-
requires = ["setuptools>=
|
|
23
|
+
requires = ["setuptools>=77"]
|
|
24
24
|
build-backend = "setuptools.build_meta"
|
|
25
25
|
|
|
26
26
|
[project]
|
|
27
27
|
name = "optivibe-reference"
|
|
28
|
-
version = "0.2.
|
|
28
|
+
version = "0.2.1"
|
|
29
29
|
description = "OptiVibe reference layer: lookup tables + RAG + intent->ZOS-API grounding"
|
|
30
30
|
requires-python = ">=3.11"
|
|
31
31
|
readme = "README.md"
|
|
32
|
-
license =
|
|
32
|
+
license = "Apache-2.0"
|
|
33
33
|
classifiers = [
|
|
34
|
-
"License :: OSI Approved :: Apache Software License",
|
|
35
34
|
"Programming Language :: Python :: 3.11",
|
|
36
35
|
]
|
|
37
36
|
dependencies = []
|
|
@@ -69,8 +69,18 @@ def _say(text):
|
|
|
69
69
|
print(_ascii(text))
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
_NOT_WRITABLE_HINT = (
|
|
73
|
+
" (if the install location is not writable -- the build writes into the "
|
|
74
|
+
"package's data and captures directories, so install optivibe-mcp into a virtual "
|
|
75
|
+
"environment, or use `pip install --user`, then re-run.)"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
72
79
|
def _error_text(exc):
|
|
73
|
-
|
|
80
|
+
text = "%s: %s" % (type(exc).__name__, exc)
|
|
81
|
+
if isinstance(exc, PermissionError):
|
|
82
|
+
text += _NOT_WRITABLE_HINT
|
|
83
|
+
return _ascii(text)
|
|
74
84
|
|
|
75
85
|
|
|
76
86
|
def _captures_present(names, captures=None):
|
|
@@ -91,6 +101,16 @@ def probed_count(capture_path):
|
|
|
91
101
|
return count if isinstance(count, int) and not isinstance(count, bool) else None
|
|
92
102
|
|
|
93
103
|
|
|
104
|
+
def _capture_incomplete(capture_path):
|
|
105
|
+
"""True iff the capture reads and says ``ok: false``: a probe that failed after its first
|
|
106
|
+
step writes its capture early (so the count matches the new inventory) but never marks it ok."""
|
|
107
|
+
try:
|
|
108
|
+
with open(capture_path, encoding="utf-8") as fh:
|
|
109
|
+
return json.load(fh).get("ok") is False
|
|
110
|
+
except (OSError, ValueError, AttributeError):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
94
114
|
def catalog_step_blocker(captures, captures_dir=None):
|
|
95
115
|
"""Why this operand/tolerance catalog cannot be built for the probed install, or None.
|
|
96
116
|
|
|
@@ -102,8 +122,12 @@ def catalog_step_blocker(captures, captures_dir=None):
|
|
|
102
122
|
"""
|
|
103
123
|
captures_dir = _paths.captures_dir() if captures_dir is None else captures_dir
|
|
104
124
|
curated = int(re.search(r"_(\d+)\.json$", captures[0]).group(1))
|
|
105
|
-
|
|
125
|
+
capture = os.path.join(captures_dir, captures[1])
|
|
126
|
+
probed = probed_count(capture)
|
|
106
127
|
if probed is None or probed == curated:
|
|
128
|
+
if _capture_incomplete(capture):
|
|
129
|
+
return ("the last live probe did not finish (%s is marked incomplete), so this "
|
|
130
|
+
"catalog is not built from it; re-run `optivibe-build-data --probe`." % captures[1])
|
|
107
131
|
return None
|
|
108
132
|
kind = "tolerance" if captures[0].startswith("tolerance") else "merit"
|
|
109
133
|
return ("your OpticStudio exposes %d %s operands; the curated data shipped with "
|
|
@@ -112,11 +136,20 @@ def catalog_step_blocker(captures, captures_dir=None):
|
|
|
112
136
|
"change that)." % (probed, kind, curated))
|
|
113
137
|
|
|
114
138
|
|
|
115
|
-
def _run_probe(label,
|
|
116
|
-
"""Run one live probe; return 1 iff it FAILED (a missing pythonnet included).
|
|
139
|
+
def _run_probe(label, module_name):
|
|
140
|
+
"""Run one live probe; return 1 iff it FAILED (a missing pythonnet included).
|
|
141
|
+
|
|
142
|
+
The probe module is imported INSIDE the guard, so a broken import is that probe's
|
|
143
|
+
FAILED report and the other probe still runs.
|
|
144
|
+
"""
|
|
117
145
|
# Lazy: importing the probe modules is cheap and clr-free, but keep them off the
|
|
118
146
|
# import path of a plain build run.
|
|
119
|
-
|
|
147
|
+
try:
|
|
148
|
+
from ._probe_common import PROBE_EXTRA_HINT, PythonnetMissing
|
|
149
|
+
probe_main = importlib.import_module("." + module_name, __package__).main
|
|
150
|
+
except Exception as exc: # noqa: BLE001 — report per-step, keep going
|
|
151
|
+
_say("[%s] FAILED - cannot import the probe: %s" % (label, _error_text(exc)))
|
|
152
|
+
return 1
|
|
120
153
|
try:
|
|
121
154
|
rc = probe_main()
|
|
122
155
|
except PythonnetMissing:
|
|
@@ -134,12 +167,11 @@ def _run_probe(label, probe_main):
|
|
|
134
167
|
|
|
135
168
|
def run_probes():
|
|
136
169
|
"""Step 0: the two live probes, merit first. Returns the number that FAILED."""
|
|
137
|
-
from . import probe_operands, probe_tolerances
|
|
138
170
|
failures = 0
|
|
139
171
|
_say("=== [probe 1/2] merit operand surface (boots a headless OpticStudio) ===")
|
|
140
|
-
failures += _run_probe("probe-operands",
|
|
172
|
+
failures += _run_probe("probe-operands", "probe_operands")
|
|
141
173
|
_say("=== [probe 2/2] tolerance operand surface (boots a headless OpticStudio) ===")
|
|
142
|
-
failures += _run_probe("probe-tolerances",
|
|
174
|
+
failures += _run_probe("probe-tolerances", "probe_tolerances")
|
|
143
175
|
return failures
|
|
144
176
|
|
|
145
177
|
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/catalog_build.py
RENAMED
|
@@ -519,6 +519,9 @@ def build_catalog_json(
|
|
|
519
519
|
"""
|
|
520
520
|
inventory = _load_json(inventory_path)
|
|
521
521
|
probe = _load_json(probe_capture_path)
|
|
522
|
+
_require(probe.get("ok") is not False,
|
|
523
|
+
"the probe capture %s is marked incomplete (ok: false): the last live probe did not "
|
|
524
|
+
"finish; re-run `optivibe-build-data --probe`" % probe_capture_path)
|
|
522
525
|
|
|
523
526
|
synonyms = load_synonyms_rows(synonyms_path) if synonyms_path else {}
|
|
524
527
|
# The verbatim-local descriptions (gitignored). ``None`` (path absent or not
|
|
@@ -25,6 +25,7 @@ CLOSED unless the ``.agf``-derivation ledger row is on file in ``PROVENANCE.md``
|
|
|
25
25
|
"""
|
|
26
26
|
import json
|
|
27
27
|
import os
|
|
28
|
+
import re
|
|
28
29
|
import sqlite3
|
|
29
30
|
|
|
30
31
|
from . import _paths
|
|
@@ -42,7 +43,7 @@ from .glass_dispersion import (
|
|
|
42
43
|
)
|
|
43
44
|
from .glass_parse import find_catalogs, iter_glass_records
|
|
44
45
|
|
|
45
|
-
SCHEMA_VERSION =
|
|
46
|
+
SCHEMA_VERSION = 2
|
|
46
47
|
BUILDER_VERSION = 1
|
|
47
48
|
|
|
48
49
|
# A computed value whose |nF-nC| denominator is below this floor is a near-flat-
|
|
@@ -108,6 +109,11 @@ def in_band(min_wave, max_wave, line):
|
|
|
108
109
|
and min_wave <= line <= max_wave)
|
|
109
110
|
|
|
110
111
|
|
|
112
|
+
# NM status code -> name (the five names; any other code or None reads None).
|
|
113
|
+
STATUS_NAMES = {0: "standard", 1: "preferred", 2: "obsolete", 3: "special", 4: "melt"}
|
|
114
|
+
_BUILD_KEYS = ("tce", "relative_cost", "climate", "stain", "acid", "alkali", "phosphate")
|
|
115
|
+
|
|
116
|
+
|
|
111
117
|
def _build_row(rec):
|
|
112
118
|
"""Recompute one parsed record into a committed-JSON row dict (§5/§6)."""
|
|
113
119
|
formula = rec["formula"]
|
|
@@ -169,6 +175,9 @@ def _build_row(rec):
|
|
|
169
175
|
"nd_valid": nd_valid,
|
|
170
176
|
"pg_f_valid": pg_f_valid,
|
|
171
177
|
"provenance": "agf_recompute",
|
|
178
|
+
"status": STATUS_NAMES.get(rec.get("status_code")),
|
|
179
|
+
**{k: rec.get(k) for k in _BUILD_KEYS},
|
|
180
|
+
"it": rec.get("it") or [],
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
|
|
@@ -289,6 +298,9 @@ def write_glass_catalog_json(out_path, catalog):
|
|
|
289
298
|
"""
|
|
290
299
|
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
291
300
|
text = json.dumps(catalog, indent=2, ensure_ascii=True)
|
|
301
|
+
# each row's ``it`` list on ONE line (it would otherwise take one line per number)
|
|
302
|
+
text = re.sub(r'(?<="it": )\[\n.*?\n \]',
|
|
303
|
+
lambda m: json.dumps(json.loads(m.group(0))), text, flags=re.DOTALL)
|
|
292
304
|
with open(out_path, "w", encoding="utf-8", newline="\n") as fh:
|
|
293
305
|
fh.write(text)
|
|
294
306
|
fh.write("\n")
|
|
@@ -321,6 +333,15 @@ CREATE TABLE glass (
|
|
|
321
333
|
nd_valid INTEGER NOT NULL,
|
|
322
334
|
pg_f_valid INTEGER NOT NULL,
|
|
323
335
|
provenance TEXT,
|
|
336
|
+
status TEXT,
|
|
337
|
+
tce REAL,
|
|
338
|
+
relative_cost REAL,
|
|
339
|
+
climate REAL,
|
|
340
|
+
stain REAL,
|
|
341
|
+
acid REAL,
|
|
342
|
+
alkali REAL,
|
|
343
|
+
phosphate REAL,
|
|
344
|
+
it TEXT,
|
|
324
345
|
PRIMARY KEY (catalog, name)
|
|
325
346
|
)
|
|
326
347
|
"""
|
|
@@ -354,7 +375,8 @@ def build_glass_db(catalog_json_path, conn):
|
|
|
354
375
|
in use — YAGNI).
|
|
355
376
|
|
|
356
377
|
The SQL ``PRIMARY KEY (catalog, name)`` raises ``IntegrityError`` on a
|
|
357
|
-
duplicate (the storage-layer dedupe). ``cd``
|
|
378
|
+
duplicate (the storage-layer dedupe). ``cd`` and ``it`` are stored as JSON strings (a schema-1
|
|
379
|
+
JSON has no buildability keys: those columns read NULL). The
|
|
358
380
|
``*_valid`` flags are stored as 0/1 INTEGERs. Returns ``conn``.
|
|
359
381
|
"""
|
|
360
382
|
# One-shot precondition: a pre-existing ``glass`` table means this connection
|
|
@@ -374,8 +396,9 @@ def build_glass_db(catalog_json_path, conn):
|
|
|
374
396
|
conn.execute(
|
|
375
397
|
"INSERT INTO glass (catalog, name, formula, formula_name, cd, "
|
|
376
398
|
"min_wave, max_wave, nd_stored, vd_stored, delta_pg_f, nd, ng, nf, "
|
|
377
|
-
"nc, vd, pg_f, nd_valid, pg_f_valid, provenance
|
|
378
|
-
"
|
|
399
|
+
"nc, vd, pg_f, nd_valid, pg_f_valid, provenance, status, tce, "
|
|
400
|
+
"relative_cost, climate, stain, acid, alkali, phosphate, it) "
|
|
401
|
+
"VALUES (" + ", ".join("?" * 28) + ")",
|
|
379
402
|
(
|
|
380
403
|
row["catalog"],
|
|
381
404
|
row["name"],
|
|
@@ -396,6 +419,9 @@ def build_glass_db(catalog_json_path, conn):
|
|
|
396
419
|
1 if row.get("nd_valid") else 0,
|
|
397
420
|
1 if row.get("pg_f_valid") else 0,
|
|
398
421
|
row.get("provenance"),
|
|
422
|
+
row.get("status"),
|
|
423
|
+
*(row.get(k) for k in _BUILD_KEYS),
|
|
424
|
+
json.dumps(row["it"]) if row.get("it") is not None else None,
|
|
399
425
|
),
|
|
400
426
|
)
|
|
401
427
|
conn.commit()
|
|
@@ -19,11 +19,15 @@ glass names are committed).
|
|
|
19
19
|
Record grammar (opened by ``NM``):
|
|
20
20
|
name=NM[1], formula=parse_formula_field(NM[2]), nd_stored=NM[4], vd_stored=NM[5];
|
|
21
21
|
density=ED[3], dpgf=ED[4]; cd=CD[1:]; min_wave=LD[1], max_wave=LD[2].
|
|
22
|
+
Also (-1 or absent reads None): status_code=NM[7], tce=ED[1] (0 reads None), OD[1..6], IT rows.
|
|
22
23
|
"""
|
|
23
24
|
import glob
|
|
24
25
|
import os
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
_OD_KEYS = ("relative_cost", "climate", "stain", "acid", "alkali", "phosphate")
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
def parse_formula_field(tok):
|
|
28
32
|
"""Parse NM field 3 robustly (handles '1.00000000E+00' sci-notation -> 1).
|
|
29
33
|
|
|
@@ -54,6 +58,14 @@ def parse_float(tok):
|
|
|
54
58
|
return None
|
|
55
59
|
|
|
56
60
|
|
|
61
|
+
def _field(p, i):
|
|
62
|
+
"""Float of split field ``i``, or None when absent, unparseable or the file's unset marker -1."""
|
|
63
|
+
v = parse_float(p[i]) if len(p) > i else None
|
|
64
|
+
if v is None or abs(v + 1.0) < 1e-9:
|
|
65
|
+
return None
|
|
66
|
+
return v
|
|
67
|
+
|
|
68
|
+
|
|
57
69
|
def _decode_agf(raw):
|
|
58
70
|
"""Decode raw .agf bytes: BOM sniff -> BOM-less UTF-8 attempt -> latin-1.
|
|
59
71
|
|
|
@@ -105,6 +117,9 @@ def iter_glass_records(path):
|
|
|
105
117
|
minw = maxw = None
|
|
106
118
|
have = False
|
|
107
119
|
formula_is_sci = False
|
|
120
|
+
status_code = tce = None
|
|
121
|
+
od = {}
|
|
122
|
+
it = []
|
|
108
123
|
|
|
109
124
|
def emit():
|
|
110
125
|
return {
|
|
@@ -112,6 +127,7 @@ def iter_glass_records(path):
|
|
|
112
127
|
"density": density, "dpgf": dpgf, "cd": cd,
|
|
113
128
|
"min_wave": minw, "max_wave": maxw, "catalog": os.path.basename(path),
|
|
114
129
|
"formula_is_sci": formula_is_sci,
|
|
130
|
+
"status_code": status_code, "tce": tce, "it": it, **od,
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
with open(path, "rb") as fh:
|
|
@@ -131,14 +147,28 @@ def iter_glass_records(path):
|
|
|
131
147
|
density = dpgf = None
|
|
132
148
|
cd = None
|
|
133
149
|
minw = maxw = None
|
|
150
|
+
status_code = _field(p, 7)
|
|
151
|
+
status_code = int(status_code) if status_code is not None and status_code.is_integer() else None
|
|
152
|
+
tce = None
|
|
153
|
+
od = dict.fromkeys(_OD_KEYS)
|
|
154
|
+
it = []
|
|
134
155
|
have = True
|
|
135
156
|
elif ln.startswith("ED ") and have:
|
|
136
157
|
# ED tce1 tce2 density dPgF ... (dPgF = ED field 4 -> token[4])
|
|
137
158
|
p = ln.split()
|
|
138
159
|
density = parse_float(p[3]) if len(p) > 3 else None
|
|
139
160
|
dpgf = parse_float(p[4]) if len(p) > 4 else None
|
|
161
|
+
tce = _field(p, 1) or None
|
|
140
162
|
elif ln.startswith("CD ") and have:
|
|
141
163
|
cd = [parse_float(x) for x in ln.split()[1:]]
|
|
164
|
+
elif ln.startswith("OD ") and have:
|
|
165
|
+
p = ln.split()
|
|
166
|
+
od = {k: _field(p, i) for i, k in enumerate(_OD_KEYS, 1)}
|
|
167
|
+
elif ln.startswith("IT ") and have:
|
|
168
|
+
p = ln.split()
|
|
169
|
+
w, t, d = _field(p, 1), _field(p, 2), _field(p, 3)
|
|
170
|
+
if None not in (w, t, d) and d > 0:
|
|
171
|
+
it.append([w, t, d])
|
|
142
172
|
elif ln.startswith("LD ") and have:
|
|
143
173
|
p = ln.split()
|
|
144
174
|
minw = parse_float(p[1]) if len(p) > 1 else None
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/manual_enrich.py
RENAMED
|
@@ -38,8 +38,8 @@ MERIT_INVENTORY_PATH = os.path.join(
|
|
|
38
38
|
def load_merit_operand_inventory(path=MERIT_INVENTORY_PATH):
|
|
39
39
|
"""Return the merit operand code set (a ``frozenset``) from the user-built inventory.
|
|
40
40
|
|
|
41
|
-
A missing file names its PRODUCER: the capture is written by
|
|
42
|
-
(a
|
|
41
|
+
A missing file names its PRODUCER: the capture is written by the live probe
|
|
42
|
+
(``optivibe-build-data --probe``; in a clone also ``scripts/probe_operands.py``).
|
|
43
43
|
|
|
44
44
|
Raises loudly (``OSError`` / ``KeyError`` / ``ValueError``) on a missing or
|
|
45
45
|
malformed file — the caller must never silently fall back to the ungated pairer.
|
|
@@ -49,9 +49,11 @@ def load_merit_operand_inventory(path=MERIT_INVENTORY_PATH):
|
|
|
49
49
|
inventory = json.load(fh)
|
|
50
50
|
except FileNotFoundError as exc:
|
|
51
51
|
raise FileNotFoundError(
|
|
52
|
-
f"merit operand inventory {path} is missing -- it is written by "
|
|
53
|
-
f"
|
|
54
|
-
f"
|
|
52
|
+
f"merit operand inventory {path} is missing -- it is written by the live "
|
|
53
|
+
f"probe: run `optivibe-build-data --probe` (searched captures dir: "
|
|
54
|
+
f"{os.path.dirname(os.path.abspath(path))}). In a clone the same probe is "
|
|
55
|
+
f"packages/optivibe-reference/scripts/probe_operands.py "
|
|
56
|
+
f"(operand_inventory_<N>.json under scripts/captures/)") from exc
|
|
55
57
|
codes = frozenset(m["code"] for m in inventory["members"])
|
|
56
58
|
if len(codes) != inventory["total_members"]:
|
|
57
59
|
raise ValueError(
|
|
@@ -75,9 +77,11 @@ def load_tolerance_operand_inventory(path=None):
|
|
|
75
77
|
inventory = json.load(fh)
|
|
76
78
|
except FileNotFoundError as exc:
|
|
77
79
|
raise FileNotFoundError(
|
|
78
|
-
f"tolerance operand inventory {path} is missing -- it is written by "
|
|
79
|
-
f"
|
|
80
|
-
f"
|
|
80
|
+
f"tolerance operand inventory {path} is missing -- it is written by the "
|
|
81
|
+
f"live probe: run `optivibe-build-data --probe` (searched captures dir: "
|
|
82
|
+
f"{os.path.dirname(os.path.abspath(path))}). In a clone the same probe is "
|
|
83
|
+
f"packages/optivibe-reference/scripts/probe_tolerances.py "
|
|
84
|
+
f"(tolerance_inventory_<N>.json under scripts/captures/)") from exc
|
|
81
85
|
codes = frozenset(m["code"] for m in inventory["members"])
|
|
82
86
|
if len(codes) != inventory["total_members"]:
|
|
83
87
|
raise ValueError(
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/probe_operands.py
RENAMED
|
@@ -198,6 +198,10 @@ def main():
|
|
|
198
198
|
}
|
|
199
199
|
report["inventory_count"] = n
|
|
200
200
|
report["inventory_file"] = write_json(f"operand_inventory_{n}.json", inventory)
|
|
201
|
+
# Write the capture NOW (ok False = incomplete) so its inventory_count matches the
|
|
202
|
+
# inventory just written even if P2-P4 or the teardown raises; the final write
|
|
203
|
+
# below overwrites it.
|
|
204
|
+
write_json("probe_operands_capture.json", report)
|
|
201
205
|
|
|
202
206
|
# --- P2: live-label existence ---
|
|
203
207
|
print("[probe] P2 — live-label existence (TypeName/RowTypeName/AvailableOperandTypes)")
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/probe_tolerances.py
RENAMED
|
@@ -341,6 +341,10 @@ def main():
|
|
|
341
341
|
}
|
|
342
342
|
report["inventory_count"] = n
|
|
343
343
|
report["inventory_file"] = write_json(f"tolerance_inventory_{n}.json", inventory)
|
|
344
|
+
# Write the capture NOW (ok False = incomplete) so its inventory_count matches the
|
|
345
|
+
# inventory just written even if P2-P4 or the teardown raises; the final write
|
|
346
|
+
# below overwrites it.
|
|
347
|
+
write_json("probe_tolerances_capture.json", report)
|
|
344
348
|
|
|
345
349
|
# --- P2: live-label existence ---
|
|
346
350
|
print("[probe] P2 — live-label existence (TypeName/RowTypeName/Comment/...)")
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tolerance_build.py
RENAMED
|
@@ -255,6 +255,9 @@ def build_tolerance_catalog_json(
|
|
|
255
255
|
"""
|
|
256
256
|
inventory = _load_json(inventory_path)
|
|
257
257
|
capture = _load_json(capture_path)
|
|
258
|
+
catalog_build._require(capture.get("ok") is not False,
|
|
259
|
+
"the probe capture %s is marked incomplete (ok: false): the last live probe did not "
|
|
260
|
+
"finish; re-run `optivibe-build-data --probe`" % capture_path)
|
|
258
261
|
|
|
259
262
|
synonyms = (
|
|
260
263
|
catalog_build.load_synonyms_rows(synonyms_path, tolerance=True)
|
|
@@ -184,7 +184,7 @@ def _load_pair_eligible(db_conn, catalogs):
|
|
|
184
184
|
where += f" AND LOWER(catalog) IN ({marks})"
|
|
185
185
|
sql_params.extend(c.lower() for c in catalogs)
|
|
186
186
|
sql = (
|
|
187
|
-
"SELECT catalog, name, vd, pg_f, nd FROM glass "
|
|
187
|
+
"SELECT catalog, name, vd, pg_f, nd, tce FROM glass "
|
|
188
188
|
f"WHERE {where} ORDER BY pg_f, catalog, name"
|
|
189
189
|
)
|
|
190
190
|
return db_conn.execute(sql, sql_params).fetchall()
|
|
@@ -226,7 +226,7 @@ def _resolve_anchor(db_conn, anchor, anchor_catalog, catalogs):
|
|
|
226
226
|
sql_params.append(anchor_catalog)
|
|
227
227
|
rows = db_conn.execute(
|
|
228
228
|
"SELECT catalog, name, vd, pg_f, nd, pg_f_valid, "
|
|
229
|
-
"(vd IS NOT NULL), nd_valid FROM glass "
|
|
229
|
+
"(vd IS NOT NULL), nd_valid, tce FROM glass "
|
|
230
230
|
f"WHERE {where} ORDER BY catalog",
|
|
231
231
|
sql_params,
|
|
232
232
|
).fetchall()
|
|
@@ -298,18 +298,22 @@ def _pair_numbers(a, b):
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
|
|
301
|
-
def _pair_dict(a, b, delta_vd, delta_pg_f, score):
|
|
301
|
+
def _pair_dict(a, b, delta_vd, delta_pg_f, score, anchor_row=None):
|
|
302
302
|
"""Build one result pair dict with the HIGHER-Vd glass as ``a`` for stability."""
|
|
303
303
|
# Canonical orientation: the higher-Vd (crown-ish) glass first is a stable,
|
|
304
304
|
# readable convention. a/b are (catalog, name, vd, pg_f, nd) tuples.
|
|
305
305
|
if a[2] < b[2]:
|
|
306
306
|
a, b = b, a
|
|
307
|
+
# delta_tce = tce(second) - tce(first), the anchor first when there is one; None if either is unknown.
|
|
308
|
+
first, second = (b, a) if anchor_row and (b[0], b[1]) == anchor_row[:2] else (a, b)
|
|
309
|
+
delta_tce = None if None in (first[5], second[5]) else round(second[5] - first[5], 3)
|
|
307
310
|
return {
|
|
308
311
|
"a": {"catalog": a[0], "name": a[1], "vd": a[2], "pg_f": a[3], "nd": a[4]},
|
|
309
312
|
"b": {"catalog": b[0], "name": b[1], "vd": b[2], "pg_f": b[3], "nd": b[4]},
|
|
310
313
|
"delta_vd": delta_vd,
|
|
311
314
|
"delta_pg_f": delta_pg_f,
|
|
312
315
|
"score": score,
|
|
316
|
+
"delta_tce": delta_tce,
|
|
313
317
|
**_pair_numbers(a, b),
|
|
314
318
|
}
|
|
315
319
|
|
|
@@ -334,14 +338,14 @@ def _emit_pairs(eligible, cluster_ids, max_delta_pg_f, min_delta_vd, anchor_row)
|
|
|
334
338
|
# so an emitted (A, B) never re-appears as (B, A).
|
|
335
339
|
hi = 0
|
|
336
340
|
for i in range(n):
|
|
337
|
-
cat_i, name_i, vd_i, pgf_i, nd_i = eligible[i]
|
|
341
|
+
cat_i, name_i, vd_i, pgf_i, nd_i = eligible[i][:5]
|
|
338
342
|
cid_i = cluster_ids[i]
|
|
339
343
|
if hi < i + 1:
|
|
340
344
|
hi = i + 1
|
|
341
345
|
while hi < n and (eligible[hi][3] - pgf_i) <= max_delta_pg_f:
|
|
342
346
|
hi += 1
|
|
343
347
|
for j in range(i + 1, hi):
|
|
344
|
-
cat_j, name_j, vd_j, pgf_j, nd_j = eligible[j]
|
|
348
|
+
cat_j, name_j, vd_j, pgf_j, nd_j = eligible[j][:5]
|
|
345
349
|
cid_j = cluster_ids[j]
|
|
346
350
|
# Self-pair exclusion: same physical glass (same cluster) OR the exact
|
|
347
351
|
# same (catalog, name) row. A same-name-but-DIFFERENT-properties pair
|
|
@@ -365,7 +369,7 @@ def _emit_pairs(eligible, cluster_ids, max_delta_pg_f, min_delta_vd, anchor_row)
|
|
|
365
369
|
continue
|
|
366
370
|
score = _score(delta_vd, delta_pg_f)
|
|
367
371
|
pair = _pair_dict(
|
|
368
|
-
eligible[i], eligible[j], delta_vd, delta_pg_f, score
|
|
372
|
+
eligible[i], eligible[j], delta_vd, delta_pg_f, score, anchor_row
|
|
369
373
|
)
|
|
370
374
|
# Stash the unordered physical-identity key for the dedup step.
|
|
371
375
|
pair["_cluster_key"] = frozenset((cid_i, cid_j))
|
|
@@ -492,7 +496,7 @@ def find_glass_pair(db_conn, params):
|
|
|
492
496
|
"dispersion) - no pairs"
|
|
493
497
|
),
|
|
494
498
|
}
|
|
495
|
-
anchor_row = (resolved[0], resolved[1], resolved[2], resolved[3], resolved[4])
|
|
499
|
+
anchor_row = (resolved[0], resolved[1], resolved[2], resolved[3], resolved[4], resolved[8])
|
|
496
500
|
|
|
497
501
|
eligible, left_out_kinds, unclassified = _build_pool(
|
|
498
502
|
db_conn, catalogs, excluded, anchor_row, max_delta_pg_f, min_delta_vd
|
|
@@ -555,7 +559,8 @@ FIND_GLASS_PAIR_SPEC = ToolSpec(
|
|
|
555
559
|
"heuristic prefilter, and cut at limit; a smaller max_delta_pg_f brings "
|
|
556
560
|
"low secondary-colour pairs to the top. Each pair also carries "
|
|
557
561
|
"secondary_per_focal, power_sum and petzval_per_power for a thin cemented "
|
|
558
|
-
"doublet of the pair
|
|
562
|
+
"doublet of the pair, and delta_tce (thermal expansion of the second glass minus "
|
|
563
|
+
"the first, the anchor first; null when either is unknown); read caveat in the result. Gotcha: an ambiguous bare anchor name returns an "
|
|
559
564
|
"ambiguous_glass candidate list - qualify the catalog. See find_glasses to "
|
|
560
565
|
"window-search single glasses. See lookup_glass for one glass's full data."
|
|
561
566
|
),
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/find_glasses.py
RENAMED
|
@@ -20,7 +20,7 @@ NEVER-RAISE contract (mirrors ``lookup_glass`` / §7):
|
|
|
20
20
|
- a ``min > max`` window is a ``ToolParamError`` (an empty-by-construction window
|
|
21
21
|
is a caller mistake, not a legitimately-empty result);
|
|
22
22
|
- AT LEAST ONE window bound is required (a bare ``find_glasses`` with no window
|
|
23
|
-
would dump the whole
|
|
23
|
+
would dump the whole 4782-row catalog — refuse it as a ``ToolParamError``).
|
|
24
24
|
|
|
25
25
|
THE VALIDITY GATE (the cardinal defense surfaced at the window boundary): a
|
|
26
26
|
computed field (``vd``, ``pg_f``, ``nd``) is NULL in the row when it was NOT
|
|
@@ -45,14 +45,26 @@ see ``_glass_kind``) leaves rows of those kinds out; the result carries ``left_o
|
|
|
45
45
|
(always), ``left_out_kinds`` (only when > 0) and ``unclassified`` (only when > 0).
|
|
46
46
|
Rows are classified in Python BEFORE the limit, so the counts cover every match.
|
|
47
47
|
|
|
48
|
+
COPIES: one glass redistributed by several catalogs (same name, ``_same_physical_glass``) is
|
|
49
|
+
listed once, after the kind and opt-in screens and before the limit. The first copy in the sort
|
|
50
|
+
order stays and names the catalogs of the removed copies in ``also_in``. A copy is removed only
|
|
51
|
+
when it adds nothing: the optical values (nd, vd, pg_f, and the ``pg_f_anomaly`` computed from
|
|
52
|
+
them) match within the identity tolerance, and every other listed value other than ``catalog`` plus
|
|
53
|
+
the stored transmission data is equal; otherwise both stay. The kept row's ``pg_f_anomaly`` is its
|
|
54
|
+
own. Two nulls are equal, a null against a known value differs. ``matched`` counts
|
|
55
|
+
the rows after removal, ``duplicates_removed`` the rows removed; ``left_out``, ``screened_out``
|
|
56
|
+
and ``unknown_kept`` count before removal. ``dedupe=False`` lists every copy.
|
|
57
|
+
|
|
48
58
|
An empty result over a WELL-FORMED window is ``ok: True`` with an empty
|
|
49
59
|
``candidates`` list — a valid window can legitimately match nothing.
|
|
50
60
|
"""
|
|
61
|
+
import bisect
|
|
62
|
+
import json
|
|
51
63
|
import math
|
|
52
64
|
|
|
53
65
|
from ..errors import ToolParamError
|
|
54
66
|
from ..server import ToolSpec
|
|
55
|
-
from ._glass_common import check_catalogs_exist, normalize_catalogs
|
|
67
|
+
from ._glass_common import _same_physical_glass, check_catalogs_exist, normalize_catalogs
|
|
56
68
|
from ._glass_kind import material_kind, parse_exclude
|
|
57
69
|
|
|
58
70
|
_TOOL = "find_glasses"
|
|
@@ -83,8 +95,21 @@ _RESULT_COLUMNS = (
|
|
|
83
95
|
"formula_name",
|
|
84
96
|
"min_wave",
|
|
85
97
|
"max_wave",
|
|
86
|
-
"
|
|
98
|
+
"status",
|
|
99
|
+
"relative_cost",
|
|
100
|
+
"climate",
|
|
101
|
+
"stain",
|
|
102
|
+
"acid",
|
|
103
|
+
"tce",
|
|
104
|
+
"pg_f_anomaly", # stays the last key
|
|
87
105
|
)
|
|
106
|
+
# A copy is removed only when every one of these (plus the stored transmission data) equals the kept row's.
|
|
107
|
+
_COPY_COLUMNS = ("delta_pg_f", "formula", "formula_name", "min_wave", "max_wave", "status",
|
|
108
|
+
"relative_cost", "climate", "stain", "acid", "tce")
|
|
109
|
+
_STATUS_NAMES = ("standard", "preferred", "obsolete", "special", "melt")
|
|
110
|
+
# Opt-in caps: param -> result column.
|
|
111
|
+
_MAX_FILTERS = (("max_relative_cost", "relative_cost"), ("max_climate", "climate"),
|
|
112
|
+
("max_stain", "stain"))
|
|
88
113
|
|
|
89
114
|
# The window params: each maps the public ``<field>_min`` / ``<field>_max`` pair
|
|
90
115
|
# onto a real glass column. ``valid_only=True`` adds the ``<col> IS NOT NULL``
|
|
@@ -144,6 +169,39 @@ def _validate_bound(name, value):
|
|
|
144
169
|
return float(value)
|
|
145
170
|
|
|
146
171
|
|
|
172
|
+
def _resolve_dedupe(params):
|
|
173
|
+
"""Resolve ``dedupe`` (default True); a bool only, so 0 / 1 / "true" are refused."""
|
|
174
|
+
value = params.get("dedupe", True)
|
|
175
|
+
if not isinstance(value, bool):
|
|
176
|
+
raise ToolParamError(f"'dedupe' must be a bool; got {type(value).__name__}")
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _remove_copies(rows):
|
|
181
|
+
"""Drop later copies of one glass; return ``(kept, also_in, removed)``.
|
|
182
|
+
|
|
183
|
+
A row is a copy of a kept row of the same name when ``_same_physical_glass`` says so AND its
|
|
184
|
+
every other served column and the stored transmission data equal the kept row's (two nulls are
|
|
185
|
+
equal, a null against a known value is not). ``also_in[i]`` lists the catalogs removed into ``kept[i]``, in the order met.
|
|
186
|
+
"""
|
|
187
|
+
col = _RESULT_COLUMNS.index
|
|
188
|
+
key = lambda r: (r[0], r[1], r[col("vd")], r[col("pg_f")], r[col("nd")]) # noqa: E731
|
|
189
|
+
same_data = [col(c) for c in _COPY_COLUMNS] + [-1] # -1: the fetched "it" transmission data
|
|
190
|
+
same = lambda a, b: all(a[i] == b[i] for i in same_data) # noqa: E731
|
|
191
|
+
kept, also_in, by_name, removed = [], [], {}, 0
|
|
192
|
+
for r in rows:
|
|
193
|
+
twin = next((i for i in by_name.get(r[1], ())
|
|
194
|
+
if _same_physical_glass(key(kept[i]), key(r)) and same(kept[i], r)), None)
|
|
195
|
+
if twin is None:
|
|
196
|
+
by_name.setdefault(r[1], []).append(len(kept))
|
|
197
|
+
kept.append(r)
|
|
198
|
+
also_in.append([])
|
|
199
|
+
else:
|
|
200
|
+
also_in[twin].append(r[0])
|
|
201
|
+
removed += 1
|
|
202
|
+
return kept, also_in, removed
|
|
203
|
+
|
|
204
|
+
|
|
147
205
|
def _resolve_limit(params):
|
|
148
206
|
"""Resolve the effective candidate cap (default 50, hard cap 200).
|
|
149
207
|
|
|
@@ -208,6 +266,51 @@ def _row_to_dict(row):
|
|
|
208
266
|
return dict(zip(_RESULT_COLUMNS, row))
|
|
209
267
|
|
|
210
268
|
|
|
269
|
+
def _transmission(it_json, wavelength):
|
|
270
|
+
"""10 mm internal transmission at ``wavelength`` (um): rows rescaled by t ** (10 / d), linear
|
|
271
|
+
between neighbours, None outside the rows' range or with no rows."""
|
|
272
|
+
if not it_json:
|
|
273
|
+
return None
|
|
274
|
+
pts = sorted((w, t ** (10.0 / d)) for w, t, d in json.loads(it_json))
|
|
275
|
+
if not pts or not pts[0][0] <= wavelength <= pts[-1][0]:
|
|
276
|
+
return None
|
|
277
|
+
i = bisect.bisect_left([w for w, _ in pts], wavelength)
|
|
278
|
+
w1, t1 = pts[i]
|
|
279
|
+
if w1 == wavelength or i == 0:
|
|
280
|
+
return round(t1, 4)
|
|
281
|
+
w0, t0 = pts[i - 1]
|
|
282
|
+
return round(t0 + (t1 - t0) * (wavelength - w0) / (w1 - w0), 4)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _resolve_buildability(params):
|
|
286
|
+
"""Validate the opt-in params; return ``(rules, wavelength)``, rules = ``(column, test)`` pairs
|
|
287
|
+
(a glass passes when the value is unknown or the test holds)."""
|
|
288
|
+
rules = []
|
|
289
|
+
if "status" in params:
|
|
290
|
+
raw = params["status"]
|
|
291
|
+
names = {x.strip().lower() for x in raw.split(",")} if isinstance(raw, str) else set()
|
|
292
|
+
if not names or not names <= set(_STATUS_NAMES):
|
|
293
|
+
raise ToolParamError(
|
|
294
|
+
f"'status' must be a comma-separated list of {', '.join(_STATUS_NAMES)}; got {raw!r}"
|
|
295
|
+
)
|
|
296
|
+
rules.append(("status", names.__contains__))
|
|
297
|
+
rules += [(col, lambda v, b=_validate_bound(key, params[key]): v <= b)
|
|
298
|
+
for key, col in _MAX_FILTERS if key in params]
|
|
299
|
+
wavelength = None
|
|
300
|
+
if "transmission_wavelength" in params:
|
|
301
|
+
wavelength = _validate_bound("transmission_wavelength", params["transmission_wavelength"])
|
|
302
|
+
if wavelength <= 0:
|
|
303
|
+
raise ToolParamError("'transmission_wavelength' must be > 0 (micrometres)")
|
|
304
|
+
if "min_transmission" in params:
|
|
305
|
+
low = _validate_bound("min_transmission", params["min_transmission"])
|
|
306
|
+
if not 0.0 <= low <= 1.0:
|
|
307
|
+
raise ToolParamError(f"'min_transmission' must be in [0, 1]; got {low}")
|
|
308
|
+
if wavelength is None:
|
|
309
|
+
raise ToolParamError("'min_transmission' needs 'transmission_wavelength'")
|
|
310
|
+
rules.append(("transmission", lambda v: v >= low))
|
|
311
|
+
return rules, wavelength
|
|
312
|
+
|
|
313
|
+
|
|
211
314
|
def find_glasses(db_conn, params):
|
|
212
315
|
"""Window-search the glass catalog by Vd / Pg,F / Nd / ΔPg,F (never raises).
|
|
213
316
|
|
|
@@ -216,8 +319,10 @@ def find_glasses(db_conn, params):
|
|
|
216
319
|
catalogs = normalize_catalogs(params.get("catalog"))
|
|
217
320
|
excluded = parse_exclude(params.get("exclude"))
|
|
218
321
|
limit = _resolve_limit(params)
|
|
322
|
+
dedupe = _resolve_dedupe(params)
|
|
219
323
|
sort, order = _resolve_sort(params)
|
|
220
324
|
clauses, sql_params, queried_fields = _collect_windows(params)
|
|
325
|
+
rules, t_wave = _resolve_buildability(params)
|
|
221
326
|
|
|
222
327
|
if not queried_fields:
|
|
223
328
|
raise ToolParamError(
|
|
@@ -239,7 +344,7 @@ def find_glasses(db_conn, params):
|
|
|
239
344
|
# left out covers the whole match set), ordered by the sort field (vd by default, a
|
|
240
345
|
# stable key for crown/flint scanning; a null field last in either direction) with
|
|
241
346
|
# catalog and name as tiebreakers for determinism.
|
|
242
|
-
cols = ", ".join(_RESULT_COLUMNS)
|
|
347
|
+
cols = ", ".join(_RESULT_COLUMNS + ("it",)) # "it": fetched, never returned
|
|
243
348
|
direction = " DESC" if order == "descending" else ""
|
|
244
349
|
sql = (
|
|
245
350
|
f"SELECT {cols} FROM {_FROM} WHERE {where_sql} "
|
|
@@ -250,6 +355,7 @@ def find_glasses(db_conn, params):
|
|
|
250
355
|
rows = []
|
|
251
356
|
left_out_kinds = {}
|
|
252
357
|
unclassified = 0
|
|
358
|
+
screened_out = unknown_kept = 0
|
|
253
359
|
for r in all_rows:
|
|
254
360
|
kind = material_kind(r[0], r[1])
|
|
255
361
|
if kind in excluded:
|
|
@@ -257,18 +363,37 @@ def find_glasses(db_conn, params):
|
|
|
257
363
|
continue
|
|
258
364
|
if kind == "unclassified":
|
|
259
365
|
unclassified += 1
|
|
366
|
+
if rules:
|
|
367
|
+
vals = [(_transmission(r[-1], t_wave) if c == "transmission"
|
|
368
|
+
else r[_RESULT_COLUMNS.index(c)], ok) for c, ok in rules]
|
|
369
|
+
if any(v is not None and not ok(v) for v, ok in vals):
|
|
370
|
+
screened_out += 1
|
|
371
|
+
continue
|
|
372
|
+
unknown_kept += any(v is None for v, _ in vals)
|
|
260
373
|
rows.append(r)
|
|
261
374
|
|
|
375
|
+
# Copy removal runs after the kind and opt-in screens and BEFORE the limit.
|
|
376
|
+
if dedupe:
|
|
377
|
+
rows, also_in, duplicates_removed = _remove_copies(rows)
|
|
378
|
+
else:
|
|
379
|
+
also_in, duplicates_removed = [[] for _ in rows], 0
|
|
262
380
|
matched = len(rows)
|
|
263
381
|
truncated = matched > limit
|
|
264
|
-
rows = rows[:limit]
|
|
382
|
+
rows, also_in = rows[:limit], also_in[:limit]
|
|
265
383
|
candidates = [_row_to_dict(r) for r in rows]
|
|
384
|
+
for cand, r, others in zip(candidates, rows, also_in):
|
|
385
|
+
anomaly = cand.pop("pg_f_anomaly")
|
|
386
|
+
cand["also_in"] = others
|
|
387
|
+
cand["transmission"] = None if t_wave is None else _transmission(r[-1], t_wave)
|
|
388
|
+
cand["pg_f_anomaly"] = anomaly
|
|
266
389
|
|
|
267
390
|
result = {
|
|
268
391
|
"ok": True,
|
|
269
392
|
"candidates": candidates,
|
|
270
393
|
"candidate_count": len(candidates),
|
|
271
394
|
"matched": matched,
|
|
395
|
+
"duplicates_removed": duplicates_removed,
|
|
396
|
+
"dedupe": dedupe,
|
|
272
397
|
"truncated": truncated,
|
|
273
398
|
"sort": sort,
|
|
274
399
|
"order": order,
|
|
@@ -285,6 +410,10 @@ def find_glasses(db_conn, params):
|
|
|
285
410
|
result["left_out_kinds"] = left_out_kinds
|
|
286
411
|
if unclassified:
|
|
287
412
|
result["unclassified"] = unclassified
|
|
413
|
+
if rules:
|
|
414
|
+
result["screened_out"] = screened_out
|
|
415
|
+
if unknown_kept:
|
|
416
|
+
result["unknown_kept"] = unknown_kept
|
|
288
417
|
if "delta_pg_f" in queried_fields:
|
|
289
418
|
# Flag the caveat: delta_pg_f is raw, unvalidated .agf data.
|
|
290
419
|
result["delta_pg_f_unvalidated"] = True
|
|
@@ -315,10 +444,21 @@ FIND_GLASSES_SPEC = ToolSpec(
|
|
|
315
444
|
"order=descending reverses; sort=pg_f_anomaly ranks by pg_f minus the normal line "
|
|
316
445
|
"0.6438 - 0.001682*vd, null when either is null, window pg_f_anomaly_min/max; a "
|
|
317
446
|
"truncated list carries truncated_note saying what is hidden and how to see it) and cut at limit; matched counts the glasses in the "
|
|
318
|
-
"window that are not left out, so truncated means only the first limit of matched are listed. Gotcha: a glass whose computed "
|
|
447
|
+
"window that are not left out, after copies are removed (left_out, screened_out and unknown_kept count before removal), so truncated means only the first limit of matched are listed. Gotcha: a glass whose computed "
|
|
319
448
|
"field is out-of-band (null) never matches that window, and the delta-Pg,F "
|
|
320
449
|
"bounds are raw manufacturer data (unvalidated); a stored delta-Pg,F beyond "
|
|
321
|
-
"+-0.1 reads as null.
|
|
450
|
+
"+-0.1 reads as null. Every candidate also carries status (standard, preferred, "
|
|
451
|
+
"obsolete, special, melt), relative_cost, climate, stain, acid, tce and transmission "
|
|
452
|
+
"(internal, 10 mm path, at transmission_wavelength in micrometres, else null). "
|
|
453
|
+
"Opt-in filters, none applied by default: status (comma list), max_relative_cost, "
|
|
454
|
+
"max_climate, max_stain, min_transmission (needs transmission_wavelength); a glass "
|
|
455
|
+
"whose value is unknown is kept and counted in unknown_kept, screened_out counts "
|
|
456
|
+
"the rest, and the thresholds are the caller's. "
|
|
457
|
+
"One glass that several catalogs carry (same name, same optics) is listed once and "
|
|
458
|
+
"the kept row names the other catalogs in also_in; a copy is removed only when "
|
|
459
|
+
"its optical values (nd, vd, pg_f and the pg_f_anomaly computed from them) match "
|
|
460
|
+
"within a small tolerance and every other listed value and the transmission data are "
|
|
461
|
+
"equal, otherwise both stay, and dedupe=false lists every copy. See find_glass_pair for "
|
|
322
462
|
"matched apochromat pairs. See lookup_glass for one glass's full data."
|
|
323
463
|
),
|
|
324
464
|
kind="glass",
|
|
@@ -326,6 +466,7 @@ FIND_GLASSES_SPEC = ToolSpec(
|
|
|
326
466
|
"catalog": "string",
|
|
327
467
|
"exclude": "string",
|
|
328
468
|
"limit": "integer",
|
|
469
|
+
"dedupe": "boolean",
|
|
329
470
|
"sort": "string",
|
|
330
471
|
"order": "string",
|
|
331
472
|
"vd_min": "number", "vd_max": "number",
|
|
@@ -333,6 +474,9 @@ FIND_GLASSES_SPEC = ToolSpec(
|
|
|
333
474
|
"nd_min": "number", "nd_max": "number",
|
|
334
475
|
"delta_pg_f_min": "number", "delta_pg_f_max": "number",
|
|
335
476
|
"pg_f_anomaly_min": "number", "pg_f_anomaly_max": "number",
|
|
477
|
+
"status": "string", "max_relative_cost": "number", "max_climate": "number",
|
|
478
|
+
"max_stain": "number", "min_transmission": "number",
|
|
479
|
+
"transmission_wavelength": "number",
|
|
336
480
|
},
|
|
337
481
|
)
|
|
338
482
|
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/PKG-INFO
RENAMED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optivibe-reference
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: OptiVibe reference layer: lookup tables + RAG + intent->ZOS-API grounding
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/chuang44-tiff/optivibe-mcp
|
|
7
7
|
Project-URL: Issues, https://github.com/chuang44-tiff/optivibe-mcp/issues
|
|
8
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
8
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
9
|
Requires-Python: >=3.11
|
|
11
10
|
Description-Content-Type: text/markdown
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/_probe_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/glass_dispersion.py
RENAMED
|
File without changes
|
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/manual_build.py
RENAMED
|
File without changes
|
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/__init__.py
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_glass_common.py
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_glass_kind.py
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_query_params.py
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/_rag_match.py
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/lookup_glass.py
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference/tools/lookup_operand.py
RENAMED
|
File without changes
|
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/requires.txt
RENAMED
|
File without changes
|
{optivibe_reference-0.2.0 → optivibe_reference-0.2.1}/src/optivibe_reference.egg-info/top_level.txt
RENAMED
|
File without changes
|