Skip to content
Back to skills

Bench Catalog

ASecurity

Use when the user wants to benchmark / rank a FOLDER of lens designs (or patents) against

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 13, 2026
ai-agentspythonrustbashgit

Works with

  • claude code
  • cli
  • mcp

Security analysis

A100/100

Scanned October 13, 2026

npx -y skills add chuang44-tiff/optivibe-mcp --skill bench-catalog --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bench Catalog?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Bench Catalog
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/chuang44-tiff-bench-catalog/badge)](https://www.skillsdirectory.com/skills/chuang44-tiff-bench-catalog)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: bench-catalog
description: Use when the user wants to benchmark / rank a FOLDER of lens designs (or patents) against
  a target spec or each other. Triggers on "benchmark these lenses", "rank this folder of designs",
  "compare these patents/zmx against a 50mm f/1.4", "which of these is best for <spec>".
---

# Bench-Catalog — Folder → Normalized Matrix + Plots + Ranking

## Overview

Turns a FOLDER of lens designs (`.zmx` files and/or patents) into a normalized metric
matrix (`catalog_metrics.csv`), a provenance manifest, two figures (a bar-per-KPI panel
and a labeled layout montage), and a reproducible ranking against a target spec or the
folder's own median. **You (the model) recommend; the user is the judge.**

Distinct from `design-compare` (2–4 designs the user hand-lists, live MCP, a table only):
bench-catalog is a **folder → CSV + manifest + plots + ranking**, batch, patent-capable,
and **single-seat-clean via a thin CLI** — you never hold the OpticStudio seat during a
bench cycle.

## The fixed workflow (never flexes)

```
prompt → TEMPLATE  →  INGEST (classify zmx vs patent; deep-research build) → stage (native scale)
        →  run the CLI (build + bench + render + plot + rank, ONE engine, reaped)
        →  read CSV + manifest + ranking_scores.json → NARRATE → present matrix + graphs + ranking
```

## Step 1 — prompt → template

Map the user's ask to a `template.json` written to `<workdir>/out/template.json`:

```json
{
  "target_basis": {"efl": 50.0, "fnum": 1.4, "fov": 19.0,
                   "wavelengths": [[0.4861,1.0],[0.5876,1.0],[0.6563,1.0]], "mtf_frequency": 50.0},
  "metrics": ["first_order","rms_spot","mtf","strehl","clearance"],
  "plots": ["bar_kpi","montage"],
  "ranking": {"priorities": ["mtf_worst","total_track_mm"], "weights": {"mtf_worst": 3}}
}
```

- `target_basis` keys are ALL optional. **Target given** (common): parse EFL, f/#, FOV
  (a scalar half-field angle in deg, or an explicit `[[x,y,w],…]` list), wavelengths,
  application. Only pin `fnum` if the user pins it; set `wavelengths` only if stated
  (else native/photopic F/d/C `[0.4861,0.5876,0.6563]`); `mtf_frequency` from the ask
  else **50 cyc/mm** (locked default — all designs share the basis EFL so one frequency
  is comparable). Ask about frequency ONLY if the user names a sensor/pixel context.
- **No target** → OMIT `target_basis.efl` → the bench runs a read-only median-EFL census
  (EFL = median of finite, non-afocal native EFLs; FOV = 3 angle fields at the median
  max-semi-field; f/# native). Just leave the key out.
- **Application → sensible defaults** (your judgment, STATED to the user before the run):
  "machine vision 25mm f/2.8" → efl 25, fnum 2.8, modest FOV, prioritize distortion +
  corner MTF; "mobile" → compact + rel_illum + distortion. State the derived template and
  let the user correct it before the (multi-minute) CLI run.

### The metric menu — the EXHAUSTIVE ALLOWLIST

Map intent to metric keys ONLY from this menu. An unknown/unsupported requested metric →
**CLARIFY or DECLINE** (name the menu) — **NEVER a silent nearest-guess**.

<!-- MENU_TABLE_START -->

| registry key | tool | CSV columns | dir | intent it serves |
|---|---|---|---|---|
| `first_order` | get_first_order | `fnum`, `total_track_mm`, `bfd_mm` (+ fixed `native_efl_mm`/`norm_efl_mm`) | f#↓ track↓ | focal length, f/#, size/compactness, back focus |
| `rms_spot` | get_spot | `rms_spot_um_worst`, `rms_spot_worst_field`, `rms_spot_valid_fields` | ↓ | geometric blur / spot size, worst field |
| `mtf` | get_mtf | `mtf_worst`, `mtf_worst_field` | ↑ | sharpness / resolution / contrast at the basis frequency |
| `strehl` | analyze_strehl | `strehl_field1` | ↑ | diffraction quality (Strehl) at field 1 of the field table, wherever it sits |
| `wavefront` | analyze_wavefront | `rms_wfe_axis_waves` | ↓ | RMS wavefront error on-axis (Hx=Hy=0), not the worst field |
| `distortion` | analyze_distortion | `distortion_max_pct` | ↓ | geometric distortion |
| `rel_illum` | analyze_relative_illumination | `rel_illum_min` | ↑ | relative illumination / corner brightness / vignetting |
| `lateral_color` | analyze_lateral_color | `lateral_color_max_um` | ↓ | lateral (transverse) chromatic aberration |
| `axial_color` | analyze_axial_color | `axial_color_fc_mm` | ↓ | axial (longitudinal) chromatic aberration |
| `aspheric_profile` | analyze_aspheric_profile | `asphere_max_departure`, `asphere_slope_diff`, `asphere_bfs_radius` | ↓ | asphere manufacturability (departure/slope) |
| `clearance` | check_clearance | `clearance_status`, `n_clearance_violations`, `min_gap_mm` | gap↑ | edge/center thickness manufacturability |
| `collimation` | verify_collimation | `collimation_verdict`, `worst_residual_mrad` | ↓ | afocal / collimation quality |
| _(always)_ | _bench-derived_ | `n_elements` | ↓ | element count / complexity |

<!-- MENU_TABLE_END -->

The exact `tool_name` is a 1:1 alias for its key (`get_spot` → `rms_spot`) — an explicit
tool name in the ask is honored. `n_elements` is ALWAYS emitted (bench-derived) — the user
never selects it, and it has **no registry key**. The **default template** (unspecified
metrics) = `["first_order","rms_spot","mtf","strehl","clearance"]` (+ `n_elements`).

The machine-readable menu (must equal the 12 registry keys above — the served-boundary
allowlist; the drift test parses this block):

<!-- BENCH_CATALOG_MENU_KEYS
["first_order","rms_spot","mtf","strehl","wavefront","distortion","rel_illum",
 "lateral_color","axial_color","aspheric_profile","clearance","collimation"]
-->

### The intent → key phrase map

<!-- PHRASE_MAP_START -->

| user phrase (examples) | keys |
|---|---|
| sharpness · resolution · contrast · "corner MTF" · MTF | `mtf` |
| spot · blur · spot size · geometric | `rms_spot` |
| Strehl · diffraction-limited (read at field 1) | `strehl` |
| wavefront · WFE · OPD · on-axis quality | `wavefront` |
| distortion · straight lines · barrel/pincushion | `distortion` |
| vignetting · relative illumination · corner brightness · falloff | `rel_illum` |
| lateral color · transverse chromatic · color fringing | `lateral_color` |
| axial color · longitudinal chromatic · focus shift with color | `axial_color` |
| aspheric quality · sag departure · asphere manufacturability | `aspheric_profile` |
| manufacturability · edge/center thickness · clearance · thin elements | `clearance` |
| compact · small · short · total track · size | `first_order` |
| simple · fewest elements · complexity | `first_order` |
| collimator · afocal · beam quality | `collimation` |

<!-- PHRASE_MAP_END -->

Mapping rules:
1. Explicit metric/tool names → resolve directly (allowlist).
2. Phrase matches → the mapped keys (union; dedup).
3. A phrase with NO menu match (e.g. "veiling glare", "ghosting", "stray light",
   "thermal") → **CLARIFY, do not silently substitute a neighbor**: "I can profile these
   metrics: <menu>. I don't have a '<phrase>' analysis — proceed with the default set, or
   pick from the menu?" NEVER emit a key that is not on the menu.
4. Nothing selected → the lean default template.

## Step 2 — ingestion

Create `<workdir>/{inputs,staging,out}/`. Classify each input:

- **`.zmx`** (case-insensitive) → a Zemax design → `copy` VERBATIM into `staging/`
  (NATIVE scale; scaling is the bench's job). A stem collision suffixes `-2` (noted in
  the report).
- **patent / literature** (a `.pdf`/`.txt`/`.md`, a patent number `US-XXXXXXX`, a URL, or
  a prose design description) → the deep-research path below.
- **anything else** (unreadable / unknown) → SKIPPED with a REPORTED note (never a silent
  drop).

### Patent → `staging/<stem>.spec.json` via `deep-research`

For each patent/doc, invoke the **`deep-research` skill** (or dispatch it as a subagent for
parallel fan-out) with a prescription-specific ask:

> "Extract the full lens PRESCRIPTION for <patent id / title>, embodiment <n if known>: a
> surface-by-surface table (surface #, radius mm, thickness mm, glass/material,
> semi-diameter, conic), the stop surface, the system aperture (EPD or f/#), the field/FOV,
> and the design wavelengths. Return the numbers, cite the source."

Convert the returned prescription into an **`apply_lens_spec`-shaped `LensSpec` JSON** at
NATIVE scale (surfaces + aperture + fields + wavelengths — match the `apply_lens_spec`
schema exactly). For a patent glass not resolvable directly, use the **seat-free**
reference tools `lookup_glass` / `find_glass_pair` to map it to an available catalog glass
(or the nearest nd/vd match) — record the substitution in that surface's `comment`
(the spec has no `notes` key, and an unknown key is refused). Write
`staging/<stem>.spec.json`. The CLI Phase 1 turns each spec into `<stem>.zmx`.

**Extraction failure is honest:** if deep-research cannot return a full, buildable table,
write NO spec.json — record it in your running notes → surfaced in the final report as
"could not build: <patent> (<reason>)". Nothing faked.

## Step 3 — run the CLI (the single-seat discipline — LOAD-BEARING)

Run the CLI as a **detached background Bash** (`run_in_background: true` — no 600 s Bash
wall-cap; a killed foreground run would orphan the engine holding the single seat):

```bash
conda run -n optivibe-harness python -m optivibe_harness.catalog bench \
  --staging <workdir>/staging --out <workdir>/out --template <workdir>/out/template.json
```

You are notified on exit and read the stdout one-line JSON summary. A healthy run prints
nothing to stderr (only warnings and faults), so silence means it is still working.

**The single-seat discipline (a hard rule):** *during a bench cycle call NO seat-holding
OptiVibe tool* — no `load_design` / `apply_lens_spec` / `get_*` / `render_layout` /
`optimize` / any harness/design tool. **All engine work is the CLI's** (it opens ONE
engine, builds + benches + renders + reaps it in `finally`). Before the CLI runs you may
use ONLY the seat-free reference tools and `deep-research`:

- **The seat-free reference tools** (route to the reference dispatcher; never grab the N=1 seat):
  **`lookup_glass`, `lookup_operand`, `search_reference`, `find_glasses`, `find_glass_pair`**.

Keeping the skill seat-free makes the CLI the sole engine. **Whether a second OpticStudio
engine can start at all depends on the edition and licence in use** — where it can, it coexists
and loads fine (the `FRU__delta_init` banner is cosmetic). Either way **OptiVibe keeps to ONE engine** for
adaptability, so the bench CLI **SOFT-DECLINES** when an engine is already running: rather than
open a second engine it returns an `ok:false` `engine_busy` envelope (`rc 3`, a stderr remedy)
and benches nothing. It is a one-engine POLICY, not a license block. If EVERY design still fails
to LOAD after a bench — bad/relative staging paths (a relative `--staging` now auto-resolves
against your CWD, so this is rarer), a FOREIGN OpticStudio holding the seat, OR corrupt `.zmx` —
the CLI flags the run `all_designs_failed_load` (`rc 3`, a whole-run failure, artifacts kept as
diagnostics only; the attribution is NEUTRAL because the post-bench half has no ledger proof of a
seat). If the MCP engine was warmed earlier this session, recover by EITHER: (a) run the bench
from a **COLD session** (a fresh Claude Code session that made no design-touching MCP call), OR
(b) the **profile-via-held-seat fallback** — since you already hold the seat, profile each design
directly through the MCP tools (`load_design` + `set_ray_aiming(real)` for fast/displaced-stop
lenses + `set_aperture` `ImageSpaceFNum <f/#>` + the `analyze_*`/`get_*` tools) at the template
basis and hand-write `catalog_metrics.csv` + `ranking.md` (the sanctioned recovery — same
numbers, your engine). Before trusting ANY catalog artifact, check the CLI rc AND the top-level
JSON `ok`; a failed run's `load_not_found`/`load_failed` rows are an ENGINE/PATH symptom, not a
design verdict.

**Runtime expectation:** a bench is minutes-to-over-an-hour for a 30–50-design folder (each
design = ~2–4 loads + the full metric set; a no-target run adds a census pass; a warm
concurrent engine makes the CLI SOFT-DECLINE at preflight (rc 3, `engine_busy`) — see the
single-seat rule above). This is exactly why the CLI runs detached.

## Step 4 — rank + present

Read `out/ranking_scores.json` (the DETERMINISTIC ranked order — the CLI already computed
it), `out/catalog_metrics.csv` (the numbers), and `out/manifest.json` (status tokens, so
**null ≠ 0**). Write `out/ranking.md`: the deterministic ranked list VERBATIM (order +
scores), a per-design one-liner (wins on X, weak on Y, **"no data" on Z — NEVER "scored 0
on Z"**), the top recommendation citing the numbers + the layout form, and the explicit
"**the user is the judge**." You may DISCUSS but must NOT re-order the deterministic
ranking. Present `ranking.md` alongside the matrix + `out/catalog_bars.png` +
`out/catalog_montage.png`.

**Null-handling rule (the reproducibility contract):** a design missing a metric is flagged
**"no data"** for that axis — never scored 0 or perfect. A real `0.0` is a real measured
value. The `rank` scorer enforces this in code; your narrative must match it.

## Artifacts (the working dir)

```
<workdir>/
  inputs/                 # the user's raw inputs (reference copy)
  staging/                # native-scale .zmx (copied + patent-built) + *.spec.json
  out/
    template.json         # the resolved template (you write it, provenance)
    build_ledger.json     # patent-build outcomes {built:[…], failed:[…]}   (CLI)
    catalog_metrics.csv   # the tidy matrix — one row/design, null = empty cell, 0 = real (CLI)
    manifest.json         # full provenance: basis-once + per-metric {value,status,reason} (CLI)
    layouts/<stem>.png    # per-design geometry-only layout PNGs                (CLI)
    catalog_bars.png      # bar-per-KPI (spec bands shaded when a target is set) (CLI)
    catalog_montage.png   # one labeled layout tile per design                  (CLI)
    plot_warnings.txt     # the plotter's warnings sidecar                      (CLI)
    ranking_scores.json   # the DETERMINISTIC ranked order + per-axis scores    (CLI)
    ranking.md            # the model-agnostic narrative around the order       (you)
```

## Key rules

- **You never hold the seat during a bench cycle** — the CLI is the sole engine; you use
  only the seat-free reference tools + `deep-research` + Bash beforehand.
- **One-engine SOFT DECLINE** — even where engines can coexist, OptiVibe keeps to
  ONE engine on purpose, so if you already touched a design this session the CLI soft-declines at
  preflight (`engine_busy`, rc 3) rather than opening a second; a genuinely-unloadable staging
  (bad/relative paths, foreign seat, corrupt files) is caught after the bench
  (`all_designs_failed_load`, rc 3). Recover via a cold session OR the profile-via-held-seat
  fallback; NEVER trust a failed run's rows as design verdicts.
- **Run the CLI detached** (`run_in_background: true`) — no 600 s wall-cap → no orphaned-seat
  hazard.
- **Menu is an allowlist** — an unknown metric → CLARIFY, never a silent nearest-guess.
- **Null ≠ 0** — a missing metric is "no data", never scored 0 or perfect; a real 0 is real.
- **The user is the judge** — you recommend, cite the numbers + the layout, never auto-decide.
- **Nothing dropped, nothing faked** — a patent that couldn't build → the ledger + the
  report; a design that couldn't measure → a `partial`/`failed` row + an empty (never-0)
  cell; a missing layout → a labeled placeholder tile.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.