|
Download docs/decisions.md from Farshadm/beampilot: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/spaces/Farshadm/beampilot/resolve/main/docs/decisions.md
- Command line
-
hf download hf://spaces/Farshadm/beampilot/docs/decisions.md
-
curl -L -o decisions.md https://huggingface.co/spaces/Farshadm/beampilot/resolve/main/docs/decisions.md
17.9 kB
| # Design decisions | |
| Short records of choices that are not obvious from the code. Newest last. | |
| `docs/agent-design.md` stays the source of truth; these entries fill in details. | |
| ## 2026-10-27: Ellipticity from the principal axes | |
| `measure_widths` reports d_x and d_y along the lab axes (the design assumes beams | |
| aligned to x/y), but `ellipticity` and `azimuth_rad` come from the principal axes of | |
| the full second-moment matrix (sigma_x², sigma_y², sigma_xy). Ellipticity is the | |
| ratio of the minor to the major principal width. Reason: lab-axis widths of a | |
| rotated ellipse can be equal even when the beam is strongly elliptical, so a lab-axis | |
| ratio would hide exactly the case that the general-astigmatism check needs to see. | |
| ## 2026-10-27: Stop the width iteration when the rectangle repeats | |
| The integration area is a pixel rectangle of 3 · d around the centroid. With noise, | |
| the rectangle can flip by one pixel each pass, so d alternates by about 1e-4 relative | |
| and never meets `rtol`. `measure_widths` therefore also stops, as converged, when the | |
| next rectangle is one that was already used: further passes would only cycle through | |
| the same areas. The reported widths come from the last pass; the alternatives differ | |
| by about 1e-4 relative, far below the repeat scatter. Before this rule, 20 of 100 | |
| simulated frames at a peak of ~430 counts hit `max_iter`. | |
| ## 2026-10-27: Centroid relative to the image centre | |
| Centroids and pixel coordinates are in metres relative to the image centre, with the | |
| centre at pixel index (n - 1) / 2. x runs along columns, y along rows (positive y is | |
| towards higher row indices). The simulator uses the same convention, so a beam | |
| rendered at (x0, y0) is measured at (x0, y0). Reason: the stage axis does not care | |
| where the beam sits on the sensor, and a centred origin makes off-centre and clipping | |
| checks symmetric. | |
| ## 2026-10-27: Start the width iteration from a 1/e² estimate, not the full image | |
| The first integration area is the 3 · d rectangle around the centroid and 4σ widths | |
| of the pixels above 1/e² of the peak; the ISO iteration then runs unchanged from | |
| there. Reason: on a full 512 × 512 sensor, a residual offset of ~0.2 counts (the | |
| corner estimate's standard error is ~0.15 counts) weighted by r² outweighs the σ² of | |
| a 200 µm beam holding ~3·10⁶ counts, so the full-image first pass gave negative | |
| second moments on several near-waist frames. The thresholded pixels only choose | |
| the starting rectangle; they never enter the reported widths. Whether ISO/TR 11146-3 | |
| prescribes a specific starting area: unverified. | |
| Kept after the physics review. For a single-lobe beam the start cannot bias the | |
| result: the 1/e² estimate is ~0.83 d, so the first rectangle spans ±5σ and the | |
| iteration climbs to the same fixed point a full-image start descends to. Two known | |
| limits: (1) with a strong halo or a ghost spot there can be two fixed points, and this | |
| start finds the core while a full-image start would include the ghost; (2) a single | |
| hot pixel brighter than ~7× the beam peak makes the start fall back to the full | |
| image. Neither occurs in the simulator yet; a 3 × 3 median before taking the peak | |
| would address (2). | |
| ## 2026-10-27: Compare astigmatism azimuths modulo 90 degrees | |
| `measure_widths` reports the azimuth of the major principal axis. A beam with simple | |
| astigmatism (different x and y waist positions) swaps major and minor axis where | |
| d_x = d_y, so its azimuth jumps from 90° to 0° although the axes never rotate. | |
| `general_astigmatism` therefore compares azimuths modulo 90°: only the pair of axes | |
| matters. Comparing modulo 180° flagged design scenario 6 as general astigmatism. | |
| ## 2026-10-27: ISO point coverage means at least 5 near and 5 far, per axis | |
| ISO 11146-1 asks for at least 10 positions, "about half" within one Rayleigh length | |
| of the waist and "about half" beyond two Rayleigh lengths. `coverage` reads this as | |
| at least 5 distinct positions with |z − z0| ≤ zR and at least 5 with |z − z0| ≥ 2 zR, | |
| evaluated separately for x and y with that axis's fitted z0 and zR. Repeated frames | |
| at one z count once. Positions between zR and 2 zR are counted but do not help. | |
| The thresholds are parameters (`min_near`, `min_far`) in case the standard text, | |
| which the maintainer still has to check, allows fewer. | |
| ## 2026-10-27: Caustic fit uncertainties are scaled by the residual variance | |
| `fit_caustic` reports 1σ uncertainties from s² (JᵀJ)⁻¹ with s² = Σr² / (n − 3), | |
| like `curve_fit` with `absolute_sigma=False`. The repeat stds (from only 5 frames) | |
| therefore act as relative weights, and scatter beyond them, e.g. from position | |
| errors, still shows up in the uncertainty. With 2 % simulated noise the reported M² | |
| uncertainty matched the actual scatter over 300 seeds within ~10 %. Sampling only | |
| near the waist inflates the M² uncertainty by ~6× at ±0.5 zR, ~3× at ±0.7 zR and | |
| only ~1.8× at ±1 zR, compared with ISO sampling; no bias was seen at 2 % noise. | |
| ## 2026-10-27: The agent pipeline fits with relative residuals | |
| `evaluate` calls `fit_caustic` with `d_std_m=None`, i.e. relative residuals | |
| (d_model − d) / d. Reason: the std of only 5 repeats scatters by ~35 %, so a lucky low | |
| std gives one position several times the weight. In a test with 2 % noise per frame, | |
| 5 repeats at 10 ISO positions and 300 seeds, the weighted fit's M² scattered by | |
| 0.0080 against 0.0068 unweighted (~18 % noisier), with no bias in either. The | |
| weighted option stays in `fit_caustic` for callers with better std estimates. | |
| ## 2026-10-27: 50 dark frames per dark set | |
| The mean dark is subtracted from every frame of a run, so its fixed pixel noise is | |
| the same error at every stage position; the caustic fit's residual-based | |
| uncertainty cannot see it. With 10 darks (5 counts read noise) the M² scatter | |
| between 12 simulated clean runs was 0.47 % against a reported 0.17 %. With 50 darks | |
| it is 0.13–0.14 % against a reported 0.15 %, the same as with a perfect dark. Cost: | |
| 40 more frames once per dark set. ISO/TR 11146-3's minimum of 10 is still met. | |
| ## 2026-10-27: New tool retake_darks (6 core tools) | |
| A background change after the dark frames (room light) was detected by | |
| `background_changed` but the agent could not act on it. `retake_darks` blocks the | |
| beam and takes a fresh 50-frame dark set with the current background, so the change | |
| becomes part of the dark. It costs one position from the budget, like a measurement, | |
| so retaking darks is a real trade-off and cannot be used for free after every frame. | |
| Design section 4 now lists 6 core tools. | |
| ## 2026-10-27: Reject on estimated clipping loss, not on a cut integration area | |
| `measure_at` used to suggest "reject" whenever the 3 · d integration area hit the | |
| sensor edge. That cuts the beam at >= 6σ on one side, which biases d by far less than | |
| 0.1 %, so it was much stricter than the ISO 11146-1 rule (power outside the sensor | |
| below 1 %). Now `clipping_loss` estimates the power of a Gaussian with the measured | |
| centroid and σ = d/4 that falls outside the sensor (erf), and only a loss above 1 % | |
| suggests "reject"; `roi_clipped` remains as a warning. The widths of a clipped beam | |
| are themselves biased low (σ ~3.6 % low when cut at ±2.6σ), so the estimate | |
| understates the loss by up to ~25 % near the 1 % limit. | |
| ## 2026-10-27: Known limitations (open review risks) | |
| 1. **Clipping loss is underestimated.** `clipping_loss` uses σ = d/4 from the measured | |
| widths, but a clipped beam's widths are themselves too small. In noise-free | |
| tests the estimate is 0.80 × the true loss at 1 % and 0.70 × at 2 %, so the | |
| reject only starts at a true loss of about 1.4 %, where d is already ~3 % low. | |
| Possible fix: invert the truncated-Gaussian variance relation, or lower the | |
| limit on the estimate to ~0.75 %. | |
| 2. **`beam_too_large` has no hard trade-off any more.** Since `roi_clipped` is only | |
| a warning, nothing in that scenario produces a reject: at the far stage end | |
| (4.5 zR, d ≈ 1.4 mm) the estimated loss is 0.01 %. If the scenario should force | |
| a decision, the beam must grow: a loss above 1 % needs d above ~2 mm. | |
| 3. **Real corner light can hide a room-light change.** When beam light really | |
| reaches the corner patches (`beam_in_corners`), the frame is not used for | |
| `background_changed`, so a simultaneous background change goes unreported | |
| (e.g. waist_outside_range at 180 mm plus 3 counts). Subtracting the modelled | |
| corner light and testing the rest would catch it, but the model runs ~30 % low | |
| for clipped beams, so it needs a looser limit; left as is for now. | |
| ## 2026-10-27: No extrapolated waist numbers | |
| `evaluate` and the `finish` report show d0, z0, zR and M² of an axis as null with a | |
| `not_determined` reason if the fit failed or its waist lies outside the measured z | |
| range. Reason: a far-field-only fit collapses (d0 → 0, M² → 0) and must never reach | |
| the report as a number. θ stays (the far-field slope is measured), and | |
| `waist_in_stage_range` still uses the fitted z0, which is how a waist outside the | |
| stage is detected. `waist_side` (below/above) tells the operator where to measure | |
| next. Known cost: a waist inside the stage but outside the measured span is also | |
| nulled until a position on its side is measured; the scripted baseline does that | |
| (10/10 seeds compliant for waists 10–30 mm and 235–245 mm from the stage start). | |
| The reviewer's alternative, null only when no accepted point lies within ~1–2 zR of | |
| the fitted waist, is left open for the maintainer. | |
| ## 2026-10-27: Agent loop uses the stateless Responses API mode | |
| Verified in the Meta Model API docs: `instructions` is not kept across turns (resent | |
| every call), `max_output_tokens` caps reasoning plus visible output, tool results go | |
| back as `function_call_output` with the matching `call_id`, `parallel_tool_calls` | |
| defaults to true, `reasoning.effort` accepts minimal to xhigh (not "none"). Chaining | |
| turns with `previous_response_id` worked in a single run but failed in all six | |
| parallel runs with "400 referenced response not found or expired". The loop | |
| therefore uses the documented stateless mode: `store=false`, | |
| `include=["reasoning.encrypted_content"]`, and the full history (reasoning items, | |
| assistant messages, function calls and outputs) resent on every call. Prompt | |
| caching keeps this cheap (~90 % of input tokens cached in the real runs). Transient | |
| API errors (connection, timeout, rate limit, 5xx) are retried twice; any other | |
| error stops the run with a logged reason. Hard limits per run: 40 model calls, | |
| 1.5 M tokens, $0.50. The first six real runs (seed 0, muse-spark-1.2, effort low) | |
| gave the expected verdict on every scenario with M² within 0.5 %; they used the | |
| prompt before the review fixes in this branch (number rule, background advice). | |
| ## 2026-10-27: Waist numbers need a point within ±1 zR; accessibility from ±2σ | |
| Replaces the "waist inside the measured z range" test. d0, z0, zR and M² of an axis | |
| are null only if the fit failed or no accepted position lies within ±1 zR of the | |
| fitted waist. That still catches the collapse (far-field-only data: zR → 0 leaves no | |
| point within ±1 zR) but keeps sound fits whose waist lies just outside the measured | |
| span (e.g. waist 25 mm, nearest scout 31 mm). `waist_side` says below / above the | |
| measured positions or `within` a gap (`waist_gap_mm`). | |
| Waist accessibility is now one of three states from z0 ± 2σ(z0) against the stage: | |
| `inside`, `outside` or `uncertain` (`waist_near_stage_end` says which end). Only | |
| `outside` supports `waist_not_accessible`. A far-field-only fit can misplace the | |
| waist, so operators (scripted baseline, agent prompt) measure as close as possible | |
| to that stage end before concluding `outside`. Scripted sweep, seeds 0-9: waists 2 mm | |
| from either end are now iso_compliant 10/10 (before: 5/10 wrongly | |
| waist_not_accessible); waist_outside_range stays waist_not_accessible 10/10. | |
| ## 2026-10-27: Stricter finish guard and a controller fallback report | |
| `finish` also refuses `iso_compliant` when general astigmatism or M² < 1 is flagged. | |
| When a hard limit or an API error stops the agent, the controller writes the report | |
| itself: `estimate_not_compliant` if M² is determined on both axes, otherwise | |
| `measurement_failed`, with `stopped_by_limit` set to the reason, so every run ends | |
| with a report built from session state. | |
| ## 2026-10-27: Known limitation: extreme simple astigmatism | |
| The scripted baseline skips grid positions where the predicted beam (either axis) is | |
| wider than half the sensor (`MAX_D_PER_SENSOR = 0.5`), so for x and y waists far apart | |
| the near points of one axis can be blocked by the size of the other axis there, | |
| although such frames are often only `roi_clipped` and acceptable. Since the near-gap | |
| fill (which ignores that size check) was added, x 20 / y 230 mm is iso_compliant 10/10; | |
| but x 5 / y 245 mm still ends `estimate_not_compliant` with budget left (the gap fill | |
| is blocked by the minimum spacing). Conversely the gap fill could plan positions where | |
| the beam really overfills the sensor (not seen with the 2.8 mm sensor). A per-axis | |
| check against the clipping-loss estimate would fix both; left as is for now. | |
| ## 2026-10-27: M² < 1 flag with a σ floor; waist_not_accessible needs the stage end | |
| The Jacobian σ(M²) misses systematic errors (one-sided data, the shared dark), so a | |
| true M² = 1.0 beam fitted 0.997 with a tiny σ was flagged as M² < 1 and `finish` then | |
| refused a correct `iso_compliant` (scripted, 1 of 10 seeds). The flag now uses | |
| σ floored at 1 % of M² (`M2_STD_FLOOR_REL`). This deviates from the plain | |
| "M² + 2σ < 1" rule asked for earlier; confirmed by the maintainer on 27 Oct 2026. | |
| σ(z0) is just as optimistic: after far-field-only scouts a waist 2 mm inside the stage | |
| was reported `outside` at 10σ. So `finish` refuses `waist_not_accessible` unless an | |
| axis is `outside` and an accepted position lies within 5 % of the stage length | |
| (`STAGE_END_FRACTION`) from that stage end; the scripted operator approaches the end | |
| to that margin before concluding. waist_outside_range stays waist_not_accessible | |
| 10/10 (8 positions). | |
| ## 2026-10-27: view_frame, the 7th tool, and the ghost reflection | |
| Smoke test (`scripts/smoke_test_image.py`, muse-spark-1.2): a simulated frame as a | |
| PNG data URL was described correctly both in a user message and inside a | |
| `function_call_output` (content parts `input_text` + `input_image`): two spots, | |
| positions and relative brightness right. The docs disagree on images outside user | |
| messages; the test shows they reach the model, so `view_frame` returns its image in | |
| the tool output. The image is a 512 × 512 PNG on a square-root brightness scale (faint | |
| structure next to a bright peak stays visible), handed to the agent loop by | |
| `image_id` and never written to logs or run files. Views have their own budget | |
| (4 per run) and cost no positions, so looking is cheap but not free. | |
| The simulator's optional `Ghost` adds a second Gaussian spot (relative power, offset, | |
| size factor, optional z window, offset changing linearly with z). Known answer: for | |
| equal widths the mixture's second moment is σ² + w₁w₂Δ² and the centroid moves by | |
| w₂Δ; the test checks this to 0.1-0.2 % (integer rounding of faint tails). The prompt | |
| describes `view_frame` generically ("when numbers look odd or flags are ambiguous") | |
| and does not name ghosts, because ghosts are a held-out fault in evaluation group C. | |
| ## 2026-10-27: general look-triggers, image history, view_frame ablation | |
| In `ghost_everywhere` the agent never called `view_frame`: it read the constant | |
| `axes_not_aligned` flag as a beam property and reported a confident but wrong | |
| `estimate_not_compliant`. The prompt now names **general** triggers for looking at a | |
| frame: the same flag at every position, an azimuth that stays tilted, widths that do | |
| not follow one smooth hyperbola, odd centroid shifts or fit. It still names no fault | |
| type. Because this change was made after seeing a ghost run, evaluation case C1 | |
| (a ghost everywhere) is reported as "seen" (evaluation plan, deviation 1). | |
| Only the 2 most recent `view_frame` images stay in the resent history | |
| (`KEEP_IMAGES`); older ones are replaced by a short text stub. Without this, every | |
| image (about 300-400 kB as a data URL) was resent on every later call of the | |
| stateless loop. | |
| `AgentConfig(view_frame=False)` removes the tool, the "Looking at frames" prompt | |
| section and the view budget, for the group C ablation. A `view_frame` call is then an | |
| unknown tool. | |
| The simulator gained an `Aperture` (iris over a z range, profile truncated, | |
| diffraction ignored; known answer: truncated-Gaussian second moment | |
| σ² = (w²/4)(1 − A e^−A / (1 − e^−A)), A = 2a²/w²), `HotPixels` (extra counts in | |
| light frames only) and a pointing drift per measurement, for evaluation group C. | |
| ## 2026-10-27: model-call limit 60 and parallel tool calls (evaluation v2) | |
| In evaluation v1, A8 and C2 were stopped by the 40-model-call limit before `finish`, | |
| and in 12 of 31 agent runs the model made one tool call per model call. The default | |
| `max_model_calls` is now 60; the token budget (1.5 M) and the cost cap ($0.50 per | |
| run) are unchanged. The prompt now says plainly that independent tool calls are to be | |
| requested together in one turn, and that after each `evaluate` the agent plans the | |
| missing positions at once and accepts, rejects and evaluates in the same turn. To | |
| stay under 1,500 words, two sentences were shortened (the frames-per-position rule, | |
| the saturation hint) and "You explain every decision briefly" was dropped; the | |
| required `reason` argument covers it. Pre-registered as `docs/evaluation-plan-v2.md` | |
| before these changes. | |