← Research · Original report · Earlier editorial context
Incoprea Research · Correction dated 2 November 2026
H0199: reconcile the saved numbers
This separate correction preserves the original report and all 10 source files. It corrects numerical narration and identifies implementation limits. It does not independently reproduce the simulations, perform a physical experiment, establish novelty or certify every saved calculation.
Frozen original files (in audit ZIP) · Source hashes (in audit ZIP) · Independent reconciliation code (in audit ZIP) · Machine-readable checks (in audit ZIP)
Corrected current statement
For the five deterministic synthetic fields in the saved 12-witness, quadratic-fit sweep, the specified grid has lower recorded RMSE than the specified edge layout. This is not a universal layout theorem: with a degree-1 estimator on the saved linear field, edge RMSE is lower than grid RMSE. Four grid witnesses do not outperform 36 edge witnesses in the saved radial-field count sweep. Boundary-only observations cannot identify unrestricted interior variation that leaves the boundary unchanged, but the particular approximately 23% bias belongs to the specified field and estimator. The saved random-field and correlation-length comparisons need correction of coordinate-dependent normalization before their design implications can be endorsed. No deposition experiment or independent simulation reproduction is established by this note.
Corrections and remaining implementation issues
1. Four witnesses do not beat 36 edge witnesses
Report lines 94–96 say four distributed witnesses at 1.6% beat 36 edge witnesses. The saved n_sweep instead gives four-grid 1.609537%, 36 edge 0.773497%, and 36 grid 0.318889%. Lower RMSE is better. At equal count, four-grid beats four-edge 9.514729%, and 36 grid beats 36 edge. The original cross-count claim is reversed.
Source: REPORT.md:94–96; JSON /n_sweep indices 0,1,10,11; code/h0199.py:178–184
2. The linear-field results do not tie
The main quadratic-fit sweep gives edge 1.916313% versus grid 0.789271% RMSE for the linear field. The separately saved degree-1 fit gives edge 0.429855% versus grid 0.494446%; edge is lower there. This supports estimator-dependent comparisons, not a universal tie or universal superiority of distributed grids.
Source: REPORT.md:115–118; JSON /main indices 0,1 and /degree1 indices 0,1; code/h0199.py:152–168
3. A grid is not always the lowest-error distributed layout
In the saved smooth-random-field rows, random placement has 3.714480% RMSE, grid 4.302867%, jitter 4.725026%, edge 7.138894%. This is an ordering of the saved rows only. Random placement is also distributed, so “distributed wins” should not be paraphrased as “a grid always wins.” The GP implementation issue below prevents promoting these numbers as a valid matched-field comparison.
Source: REPORT.md:40–58; JSON /main indices 20–23
4. Saved GP comparisons require a model correction
Static inspection shows field("gp",X) divides its random basis sum by max(abs(g)) evaluated on the supplied coordinates. trial() calls it separately for reactive and witness positions. The same seed therefore need not produce the same normalized field at both sets. The intended common-field measurement model is not consistently implemented. Preserve the main gp rows, degree1 gp rows and ell_sweep as supplied outputs, but withhold their physical/design interpretation until this issue is repaired and a separately versioned rerun is checked. No replacement GP values are asserted here.
Source: code/h0199.py:47–54,114–131; JSON /main indices 20–23, /degree1 indices 10–11, /ell_sweep
5. A GP estimator is defined, but no saved sweep invokes it
The source defines gy_estimator and an estimator_kind branch. Every run_sweep trial call uses the default estimator_kind="poly". The saved results cannot substantiate the report’s statement that a Gaussian-process estimator variant “is also run.” A random field family called gp is distinct from GP reconstruction.
Source: REPORT.md:28–30; code/h0199.py:104–115,132–135,146–193
6. Replicate counts and noise units need precise labels
The main and degree1 blocks use 400 seeds. The correlation-length, witness-count and unity_eps blocks use 200. Five field families are deterministic; for those, the seeds change noise and, where relevant, layouts rather than draw a new field. The code uses additive Gaussian noise with standard deviation 0.02 in the simulated signal units. The estimator error is relative; the noise itself is not explicitly multiplied by local signal amplitude.
Source: REPORT.md:28–31,38; code/h0199.py:35–46,119,131,154,165,174,182,190–191
7. The boundary perturbation vanishes; the total field does not
The saved model is f=1+0.5sin(πx)sin(πy). Its total field equals 1 on the boundary. Only the added bump is zero there. The main edge result has 22.863542% mean relative bias and 23.116270% RMSE; these are different metrics. Boundary-only data cannot distinguish arbitrary interior fields sharing the same boundary trace without additional interior constraints. The specific roughly 23% estimator bias is not a universal value for every field, estimator or witness count.
Source: REPORT.md:10–13,62–75; JSON /main index 16; code/h0199.py:45–46
8. The saved count sweep does not test the boundary-null claim
n_sweep uses radial_edge, not boundary_null. unity_eps also uses radial_edge despite its “field that vanishes” code comment. The supplied file does not contain a boundary_null sweep across witness counts. A structural non-identifiability argument survives, but these arrays do not demonstrate a fixed 23% bias at every number of blanks.
Source: REPORT.md:70–71,94–96; code/h0199.py:178–193; JSON /n_sweep and /unity_eps
9. Low-count fits and layout geometry limit interpretation
The quadratic reconstruction has six coefficients. With only four witnesses it is underdetermined; least squares selects a minimum-norm solution. The eight edge points also have a quadratic ambiguity: all lie at the same distance from the center, so a nonzero radial quadratic vanishes at those sample locations. The grid generator keeps the first n points from a ceil(sqrt(n)) square grid: at n=12 it keeps three rows of a 4×4 grid. These finite point layouts should not be advertised as uniformly optimized designs.
Source: code/h0199.py:59–75,89–101; JSON /n_sweep
10. The gain is not exclusively a bias reduction
In the main linear-field rows, edge bias is −0.085248% and standard deviation 1.914415%; grid bias is 0.032817% and standard deviation 0.788589%. The RMSE advantage is mainly a spread/noise-amplification difference in that comparison. Large bias dominates other examples such as edge_enhanced. Report bias and spread separately rather than attributing every gain solely to bias.
Source: REPORT.md:120–122; JSON /main indices 0,1,12,13
11. No general monotonicity, correlation-length or cost rule is established
The saved finite cases do not prove that edge placement is defensible only for monotone fields, nor that every radial field is invisible at an edge. The explicit radial_edge example is a quadratic represented by the default model. The correlation-length rule depends on the flagged GP rows. Equal witness counts do not demonstrate identical engineering costs or patch areas; the code samples points and does not model fabrication, shadowing or laboratory cost.
Source: REPORT.md:51–58,73–92,100–111; code/h0199.py:39–44,59–85
12. Small rounding and wording repairs
At ell=0.8 the saved edge RMSE is 4.249114%, which rounds to 4.2% at one decimal place rather than 4.3%. The README’s 0.8–1.0% distributed-error headline excludes the saved gp rows. Preserve those originals, but attach their finite-case and GP-status qualifications before reuse.
Source: REPORT.md:82–85; README.md:9–12; JSON /ell_sweep index 8
Reconciled saved tables
Every percentage below is exactly 100 times the corresponding saved fraction, rounded to six decimal places for display. The JSON inputs remain unchanged. The gp rows are retained for transparency and are not endorsed as valid common-field comparisons.
Main sweep · 400 seeds · quadratic estimator
| field | layout | RMSE (%) | BIAS (%) | SD (%) |
|---|---|---|---|---|
| linear | edge | 1.916313% | -0.085248% | 1.914415% |
| linear | grid | 0.789271% | 0.032817% | 0.788589% |
| linear | jitter | 0.783880% | 0.009674% | 0.783821% |
| linear | random | 0.811415% | -0.014754% | 0.811281% |
| quadratic | edge | 2.540851% | -0.097358% | 2.538985% |
| quadratic | grid | 1.046900% | 0.046023% | 1.045888% |
| quadratic | jitter | 1.039042% | 0.015345% | 1.038928% |
| quadratic | random | 1.073386% | -0.016878% | 1.073253% |
| radial_edge | edge | 1.947595% | -0.086038% | 1.945694% |
| radial_edge | grid | 0.802176% | 0.033449% | 0.801478% |
| radial_edge | jitter | 0.796670% | 0.009928% | 0.796608% |
| radial_edge | random | 0.824569% | -0.014892% | 0.824434% |
| edge_enhanced | edge | 37.854676% | -37.842277% | 0.968776% |
| edge_enhanced | grid | 1.030687% | 0.122972% | 1.023324% |
| edge_enhanced | jitter | 2.863051% | -1.774844% | 2.246551% |
| edge_enhanced | random | 3.600807% | -1.165529% | 3.406957% |
| boundary_null | edge | 23.116270% | 22.863542% | 3.408870% |
| boundary_null | grid | 0.930893% | -0.074359% | 0.927919% |
| boundary_null | jitter | 1.342850% | -0.101485% | 1.339010% |
| boundary_null | random | 1.912504% | 0.387265% | 1.872885% |
| gp | edge | 7.138894% | 1.216391% | 7.034500% |
| gp | grid | 4.302867% | 0.280934% | 4.293686% |
| gp | jitter | 4.725026% | 0.451098% | 4.703444% |
| gp | random | 3.714480% | -0.074814% | 3.713726% |
Degree-1 sweep · 400 seeds
| field | layout | RMSE (%) | BIAS (%) |
|---|---|---|---|
| linear | edge | 0.429855% | 0.001542% |
| linear | grid | 0.494446% | -0.008282% |
| quadratic | edge | 8.058750% | -8.043753% |
| quadratic | grid | 1.824431% | 1.694533% |
| radial_edge | edge | 14.149564% | 14.138691% |
| radial_edge | grid | 3.314776% | -3.280881% |
| edge_enhanced | edge | 37.796243% | -37.795414% |
| edge_enhanced | grid | 2.406067% | 2.311505% |
| boundary_null | edge | 23.002259% | 22.990416% |
| boundary_null | grid | 4.480613% | -4.448371% |
| gp | edge | 5.000248% | -0.346158% |
| gp | grid | 4.292986% | 0.955061% |
Radial-field witness-count sweep · 200 seeds
| n | layout | RMSE (%) |
|---|---|---|
| 4 | edge | 9.514729% |
| 4 | grid | 1.609537% |
| 8 | edge | 11.820781% |
| 8 | grid | 0.593008% |
| 12 | edge | 1.953903% |
| 12 | grid | 0.710911% |
| 16 | edge | 1.326650% |
| 16 | grid | 0.411678% |
| 24 | edge | 0.993224% |
| 24 | grid | 0.363272% |
| 36 | edge | 0.773497% |
| 36 | grid | 0.318889% |
Reproduce the reconciliation
python3 reconcile.py
This script reads only the frozen JSON. It does not import the supplied research code, generate fields, draw noise, fit a model or recompute a scientific sweep.
Main table CSV (in audit ZIP) · Degree-1 table CSV (in audit ZIP) · Count table CSV (in audit ZIP) · Correction text in Markdown (in audit ZIP) · Line-numbered original report and code
Frozen original report and code · line-numbered reference
REPORT.md
1 # 04 · Where should the blank witnesses go? (H0199) 2 3 **Priority 4 · status: mathematical result turned into a design rule with numbers.** 4 5 The catalogue entry says interleaving inert witness patches among reactive patches estimates 6 deposition bias better than putting all blanks at the edge. The dossier already flags the honest 7 caveat: "drop any universal superiority claim — edge samples can suffice for simple gradients and may 8 win for edge-localized structure." 9 10 This dossier turns that caveat into a quantitative design rule. It also identifies the case where the 11 answer is not "it depends" but a flat failure: **a deposition field that vanishes on the boundary is 12 invisible to edge witnesses, and edge-only blanks then mis-estimate capture efficiency by 23 % at 13 *any* number of blanks.** 14 15 --- 16 17 ## 1. The measurement, stated precisely 18 19 A substrate carries `M = 25` reactive patches. Their measured uptake is 20 21 R_i = eps · f(x_i) + noise, i = 1..M 22 23 where `eps` (capture efficiency) is the unknown we want and `f(x)` is the unknown spatial deposition 24 field. `N = 12` inert witnesses measure the same field up to a known constant: 25 26 W_j = f(x_j) + noise, j = 1..N 27 28 The estimator is `eps_hat = Σ R_i / Σ f_hat(x_i)`, with `f_hat` interpolated from the witnesses 29 (quadratic least squares by default; a Gaussian-process variant is also run). The reported error is 30 `eps_hat/eps − 1`, averaged over 400 independent field draws at fixed noise `σ = 0.02` (relative) and 31 true `eps = 1.3`. 32 33 This is a *synthetic* study. Every field is stated below; no experimental data are used or implied. 34 Code: `code/h0199.py`. 35 36 ## 2. Result 37 38 RMSE of the relative error in `eps_hat`, 400 draws: 39 40 | deposition field | edge only | distributed grid | jittered grid | random | 41 |---|---|---|---|---| 42 | linear gradient | 1.92 % | **0.79 %** | 0.78 % | 0.81 % | 43 | quadratic bowl | 2.54 % | **1.05 %** | 1.04 % | 1.07 % | 44 | centre-to-edge (radial) | 1.95 % | **0.80 %** | 0.80 % | 0.82 % | 45 | edge-enhanced (rim peak) | **37.9 %** | **1.03 %** | 2.86 % | 3.60 % | 46 | vanishes on boundary | **23.1 %** | **0.93 %** | 1.34 % | 1.91 % | 47 | smooth random field, ℓ = 0.35 | 7.1 % | 4.3 % | 4.7 % | **3.7 %** | 48 49 (`figures/rmse_by_field.png`.) Reading: 50 51 1. **Distributed wins everywhere, and the cost is zero** — same number of patches, same area. 52 On the benign fields the gain is a factor 2.4; on the adversarial fields the edge layout is not 53 merely worse, it is wrong by tens of percent. 54 2. **The edge layout's failure mode is structural, not statistical.** A quadratic fit through 55 boundary points must extrapolate to the interior. When the field's interior structure is not in 56 the polynomial span, the extrapolation error does not shrink with more edge blanks. 57 3. **Random placement is not the same as a grid.** On the edge-enhanced field a random layout still 58 has 3.6 % RMSE because a random draw can miss the rim peak entirely; a grid samples it. 59 60 ## 3. The identifiability argument 61 62 Let `f` be continuous on the substrate `Ω` and constant on `∂Ω` (value `f_0`). Every edge witness 63 returns `f_0` up to noise. Any estimator built only from boundary samples therefore has no 64 information about the interior: the family `{f : f|∂Ω = f_0}` is infinite-dimensional, and the 65 area mean `∫f` can take any value above `f_0·|Ω|`. The quadratic correction model masks this by 66 imposing a functional form, but the masking is exactly the extrapolation error in the table. 67 68 The demo field is `f = 1 + 0.5·sin(πx)·sin(πy)`, whose boundary trace is exactly 1. Edge witnesses 69 see a uniform field; the estimator then divides by `f_hat = 1` and overestimates `eps` by the mean of 70 the interior bump — **+22.9 % bias, stable in sign and magnitude, unaffected by adding edge patches** 71 (`figures/boundary_null_layouts.png`, `rmse_vs_n.png`). 72 73 This is not a pathological construction. Centre-to-edge non-uniformity is the canonical defect of 74 CVD, sputtering, evaporation and spin-coating reactors, and a radial profile is one of the few 75 shapes that a rim-only witness ring is guaranteed to miss. 76 77 ## 4. When does the edge suffice? (the decision rule) 78 79 The edge layout is only defensible when the field is *monotone* over the substrate. Sweeping the 80 correlation length of a smooth random field (`figures/rmse_vs_correlation.png`): 81 82 | correlation length ℓ (substrate widths) | 0.12 | 0.20 | 0.30 | 0.50 | 0.80 | 1.50 | 83 |---|---|---|---|---|---|---| 84 | edge only | 41.4 % | 18.0 % | 7.6 % | 5.4 % | 4.3 % | 3.8 % | 85 | distributed | 10.6 % | 7.6 % | 5.0 % | 3.1 % | 2.3 % | 1.8 % | 86 87 The edge layout never *reaches* the 2 % target in this sweep, because its error floor is set by 88 extrapolation, not by sample size. A practical rule: 89 90 > **Use distributed blanks when ℓ < 0.5 substrate widths, or whenever the process can produce a 91 > centre-to-edge or rim profile. Use edge blanks only when the field is known monotone and the 92 > interior cannot be instrumented.** 93 94 Number of witnesses (`rmse_vs_n.png`, radial field): 4 distributed blanks (1.6 % RMSE) already beat 95 36 edge blanks (0.77 % vs 0.32 % — the edge does improve with more points on a field that a 96 quadratic can represent). For the boundary-null field, no number of edge blanks helps. 97 98 ## 5. A cheap physical check before any deposition campaign 99 100 The whole question can be settled for one process in a day, without reactive chemistry: 101 102 1. Use a benign deposition analogue (a dye or fluorescent aerosol, or a sputtered thin film) on a 103 substrate carrying `M` reactive and `N` inert patches. 104 2. Measure the full field independently — profilometry, fluorescence scanning, or a quartz-crystal 105 microbalance raster — so the true `f(x)` is known, not inferred. 106 3. Compare `eps_hat` from edge blanks against `eps_hat` from distributed blanks, at equal patch 107 count and area, in a *pre-registered* head-to-head. 108 4. Report the ratio, not a p-value. The decision is which layout to buy, so the relevant number is 109 the error at the operating point of the real process. 110 111 Cost: one substrate batch, one mapping instrument day. No new apparatus. 112 113 ## 6. What this changes in the catalogue entry 114 115 The entry's stop rule is right, and the analysis sharpens it. It should say: *distributed blanks win 116 on every field family tested except a pure linear gradient, where they tie; edge-only blanks are 117 not merely suboptimal but unidentifiable when the field is constant on the boundary, which includes 118 the standard centre-to-edge non-uniformity of most deposition reactors.* 119 120 The claim "more accurately than edge placement" survives — with the strong caveat that the advantage 121 is large mainly when the field has interior or rim structure, and that it is a *bias* advantage, not 122 a noise advantage. 123 124 ## 7. Limits 125 126 - Two-dimensional, steady deposition; no time variation of `f` during the campaign. 127 - Witness patches are assumed not to perturb the field. In a real reactor they do (they shadow); the 128 control for this is a separate experiment, not modelled here. 129 - The correction model is a low-order polynomial; kriging with a fitted kernel would change the 130 constants but not the identifiability argument, which is model-free. 131 - Capture efficiency is assumed spatially uniform. If `eps` itself varies with position, no layout 132 separates the two effects without an independent measurement. 133 134 Numbers: `results/h0199_sweep.json`. 135 136 137 ## Figures 138 139  140 141 *The failure case: a field that vanishes on the boundary. Edge witnesses (left) all read the same value; distributed witnesses (right) sample the interior.* 142 143  144 145 *RMSE of the estimated capture efficiency, by field family and witness layout, at 12 witnesses and equal area. Note the log scale.* 146 147  148 149 *Edge versus distributed as the field correlation length shrinks. The edge layout has an error floor set by extrapolation, not by sample size.* 150 151  152 153 *Cost of the layout choice: RMSE against the number of witnesses, for a radial non-uniformity.*
README.md
1 # 04 · H0199 where the blank witnesses go 2 3 `REPORT.md` — the measurement model, the identifiability argument, the design rule, a physical check. 4 5 - `code/h0199.py` — field families, four layouts, polynomial and GP estimators, 400-draw RMSE sweep. 6 - `code/make_figures.py` — the four figures. 7 - `results/h0199_sweep.json`. 8 9 Headline: 12 distributed blanks give 0.8–1.0 % error where 12 edge blanks give 1.9–37.9 %, at 10 identical cost. For a field that is constant on the boundary — the standard centre-to-edge 11 non-uniformity of deposition reactors — the edge layout is off by **22.9 %** and no number of edge 12 blanks fixes it.
code/h0199.py
1 """H0199 -- where should inert witness patches go? 2 3 Claim under test (incoprea study-1, ID H0199): 4 "Interleaving chemically inert witness patches among reactive patches estimates spatial 5 deposition bias more accurately than placing all blanks at the sample edge." 6 7 We formalise the measurement. A substrate carries M reactive patches whose uptake is 8 R_i = eps * f(x_i) + noise 9 where eps is the unknown capture efficiency and f(x) the (unknown) deposition field. N inert 10 witness patches measure the same field up to a known constant: 11 W_j = f(x_j) + noise. 12 The quantity of interest is eps (hence the total uptake eps * integral f). The estimator is 13 eps_hat = sum_i R_i / sum_i f_hat(x_i) 14 with f_hat interpolated from the witnesses. The layout question is: where to put the N witnesses. 15 16 Everything is synthetic and explicit: the field is drawn from a stated family, the truth is known, 17 and the reported error is eps_hat - eps. No experimental data are used or implied. 18 """ 19 import json 20 import os 21 22 import numpy as np 23 24 HERE = os.path.dirname(os.path.abspath(__file__)) 25 ROOT = os.path.normpath(os.path.join(HERE, "..")) 26 RES = os.path.join(ROOT, "results") 27 os.makedirs(RES, exist_ok=True) 28 29 rng_global = np.random.default_rng(20261101) 30 31 32 # ---------------------------------------------------------------- fields 33 def field(name, X, ell=0.35, seed=0): 34 x, y = X[:, 0], X[:, 1] 35 if name == "linear": 36 return 1.0 + 0.6 * x + 0.3 * y 37 if name == "quadratic": 38 return 1.0 + 0.8 * (x - 0.5) ** 2 + 0.5 * (y - 0.5) ** 2 39 if name == "radial_edge": # centre-to-edge non-uniformity 40 r = np.hypot(x - 0.5, y - 0.5) / 0.7071 41 return 1.0 + 0.6 * (1 - r ** 2) 42 if name == "edge_enhanced": # deposition peaks at the rim 43 d = np.minimum.reduce([x, 1 - x, y, 1 - y]) 44 return 1.0 + 0.8 * np.exp(-d / 0.08) 45 if name == "boundary_null": # vanishes on the whole boundary 46 return 1.0 + 0.5 * np.sin(np.pi * x) * np.sin(np.pi * y) 47 if name == "gp": # smooth random field 48 rng = np.random.default_rng(seed) 49 k = 24 50 c = rng.uniform(0, 1, (k, 2)) 51 a = rng.normal(0, 1, k) 52 d2 = ((X[:, None, :] - c[None, :, :]) ** 2).sum(-1) 53 g = np.exp(-d2 / (2 * ell ** 2)) @ a 54 return 1.0 + 0.45 * g / (np.abs(g).max() + 1e-12) 55 raise ValueError(name) 56 57 58 # ---------------------------------------------------------------- layouts 59 def layout_edge(n): 60 t = np.linspace(0, 1, n // 4 + 2)[1:-1] 61 pts = [] 62 for u in t: 63 pts += [(u, 0.0), (u, 1.0), (0.0, u), (1.0, u)] 64 return np.array(pts[:n]) 65 66 67 def layout_grid(n, jitter=0.0, rng=None): 68 k = int(np.ceil(np.sqrt(n))) 69 g = (np.arange(k) + 0.5) / k 70 X, Y = np.meshgrid(g, g) 71 pts = np.stack([X.ravel(), Y.ravel()], 1)[:n] 72 if jitter and rng is not None: 73 pts = pts + rng.uniform(-jitter, jitter, pts.shape) 74 pts = np.clip(pts, 0.02, 0.98) 75 return pts 76 77 78 def layout_random(n, rng, margin=0.05): 79 return rng.uniform(margin, 1 - margin, (n, 2)) 80 81 82 def reactive_patches(m=25, inset=0.12): 83 g = np.linspace(inset, 1 - inset, int(np.sqrt(m))) 84 X, Y = np.meshgrid(g, g) 85 return np.stack([X.ravel(), Y.ravel()], 1) 86 87 88 # ---------------------------------------------------------------- estimator 89 def poly_basis(X, deg): 90 x, y = X[:, 0], X[:, 1] 91 cols = [np.ones_like(x), x, y] 92 if deg >= 2: 93 cols += [x * x, x * y, y * y] 94 return np.stack(cols, 1) 95 96 97 def estimator(wit, W, react, deg, ridge=1e-9): 98 """Fit a polynomial surface to the witnesses, predict at reactive patches.""" 99 A = poly_basis(wit, deg) 100 coef, *_ = np.linalg.lstsq(A, W, rcond=None) 101 return poly_basis(react, deg) @ coef 102 103 104 def gy_estimator(wit, W, react, ell): 105 """Gaussian-process (RBF) prediction at the reactive patches.""" 106 def K(a, b): 107 d2 = ((a[:, None, :] - b[None, :, :]) ** 2).sum(-1) 108 return np.exp(-d2 / (2 * ell ** 2)) 109 Kww = K(wit, wit) + 1e-6 * np.eye(len(wit)) 110 alpha = np.linalg.solve(Kww, W) 111 return K(react, wit) @ alpha 112 113 114 def trial(field_name, layout, n_wit, deg, sigma, eps=1.3, m=25, seed=0, ell=0.35, 115 estimator_kind="poly", gy_ell=0.35): 116 rng = np.random.default_rng(seed) 117 react = reactive_patches(m) 118 f_r = field(field_name, react, ell=ell, seed=seed) 119 R = eps * f_r + rng.normal(0, sigma, len(react)) 120 if layout == "edge": 121 wit = layout_edge(n_wit) 122 elif layout == "grid": 123 wit = layout_grid(n_wit) 124 elif layout == "jitter": 125 wit = layout_grid(n_wit, 0.06, rng) 126 elif layout == "random": 127 wit = layout_random(n_wit, rng) 128 else: 129 raise ValueError(layout) 130 f_w = field(field_name, wit, ell=ell, seed=seed) 131 W = f_w + rng.normal(0, sigma, len(wit)) 132 if estimator_kind == "poly": 133 f_hat = estimator(wit, W, react, deg) 134 else: 135 f_hat = gy_estimator(wit, W, react, gy_ell) 136 num = R.sum() 137 den = f_hat.sum() 138 return num / den / eps - 1.0 # relative error 139 140 141 def rmse(v): 142 v = np.asarray(v) 143 return float(np.sqrt(np.mean(v ** 2))) 144 145 146 def run_sweep(): 147 out = {"spec": dict(m=25, n_witness=12, sigma=0.02, eps=1.3, 148 metrics="relative error of eps_hat, RMSE over 400 field draws")} 149 layouts = ["edge", "grid", "jitter", "random"] 150 fields = ["linear", "quadratic", "radial_edge", "edge_enhanced", "boundary_null", "gp"] 151 rows = [] 152 for fld in fields: 153 for lay in layouts: 154 errs = [trial(fld, lay, 12, 2, 0.02, seed=s) for s in range(400)] 155 rows.append(dict(field=fld, layout=lay, rmse=rmse(errs), 156 bias=float(np.mean(errs)), 157 sd=float(np.std(errs)), 158 p90=float(np.percentile(np.abs(errs), 90)))) 159 out["main"] = rows 160 161 # degree-1 correction model (the cheap, common practice) 162 rows1 = [] 163 for fld in fields: 164 for lay in ["edge", "grid"]: 165 errs = [trial(fld, lay, 12, 1, 0.02, seed=s) for s in range(400)] 166 rows1.append(dict(field=fld, layout=lay, rmse=rmse(errs), 167 bias=float(np.mean(errs)))) 168 out["degree1"] = rows1 169 170 # correlation-length sweep with a GP field 171 rows2 = [] 172 for ell in [0.12, 0.2, 0.3, 0.5, 0.8, 1.5]: 173 for lay in ["edge", "grid"]: 174 errs = [trial("gp", lay, 12, 2, 0.02, seed=s, ell=ell) for s in range(200)] 175 rows2.append(dict(ell=ell, layout=lay, rmse=rmse(errs))) 176 out["ell_sweep"] = rows2 177 178 # number of witnesses 179 rows3 = [] 180 for n in [4, 8, 12, 16, 24, 36]: 181 for lay in ["edge", "grid"]: 182 errs = [trial("radial_edge", lay, n, 2, 0.02, seed=s) for s in range(200)] 183 rows3.append(dict(n=n, layout=lay, rmse=rmse(errs))) 184 out["n_sweep"] = rows3 185 186 # the modal argument: a field that vanishes on the boundary 187 rows4 = [] 188 for lay in ["edge", "grid"]: 189 errs = [] 190 for s in range(200): 191 errs.append(trial("radial_edge", lay, 12, 2, 0.02, seed=s, eps=1.0)) 192 rows4.append(dict(layout=lay, rmse=rmse(errs))) 193 out["unity_eps"] = rows4 194 return out 195 196 197 if __name__ == "__main__": 198 res = run_sweep() 199 json.dump(res, open(os.path.join(RES, "h0199_sweep.json"), "w"), indent=2) 200 for r in res["main"]: 201 print("%-14s %-7s rmse %.4f bias %+.4f" % 202 (r["field"], r["layout"], r["rmse"], r["bias"])) 203 print() 204 for r in res["ell_sweep"]: 205 print("ell %.2f %-6s rmse %.4f" % (r["ell"], r["layout"], r["rmse"])) 206 print() 207 for r in res["n_sweep"]: 208 print("n %2d %-6s rmse %.4f" % (r["n"], r["layout"], r["rmse"]))
code/make_figures.py
1 """Figures for the H0199 reference-placement dossier.""" 2 import json 3 import os 4 5 import matplotlib 6 matplotlib.use("Agg") 7 import matplotlib.pyplot as plt 8 import numpy as np 9 10 HERE = os.path.dirname(os.path.abspath(__file__)) 11 ROOT = os.path.normpath(os.path.join(HERE, "..")) 12 RES = os.path.join(ROOT, "results") 13 FIG = os.path.join(ROOT, "figures") 14 os.makedirs(FIG, exist_ok=True) 15 plt.rcParams.update({"font.size": 9, "axes.spines.top": False, 16 "axes.spines.right": False, "figure.dpi": 200}) 17 EDGE, GRID = "#b5651d", "#0b6e6e" 18 19 20 def load(): 21 return json.load(open(os.path.join(RES, "h0199_sweep.json"))) 22 23 24 def fig_main(d): 25 rows = d["main"] 26 fields = [] 27 for r in rows: 28 if r["field"] not in fields: 29 fields.append(r["field"]) 30 lay = ["edge", "grid", "jitter", "random"] 31 fig, ax = plt.subplots(figsize=(5.2, 2.9)) 32 x = np.arange(len(fields)) 33 w = 0.2 34 cols = [EDGE, GRID, "#7a7a7a", "#3b3b6d"] 35 for j, L in enumerate(lay): 36 v = [next(r["rmse"] for r in rows if r["field"] == f and r["layout"] == L) 37 for f in fields] 38 ax.bar(x + (j - 1.5) * w, v, w, label=L, color=cols[j]) 39 ax.set_yscale("log") 40 ax.set_ylim(top=max(r["rmse"] for r in rows) * 2.2) 41 ax.set_xticks(x) 42 ax.set_xticklabels([f.replace("_", "\n") for f in fields], fontsize=7.5) 43 ax.set_ylabel("RMSE of capture efficiency (relative)") 44 ax.axhline(0.02, color="k", lw=0.7, ls=":") 45 ax.text(len(fields) - 0.55, 0.0215, "2 % target", fontsize=7) 46 ax.set_title("12 witnesses, polynomial correction", loc="left", fontsize=10) 47 ax.legend(frameon=False, fontsize=7, ncol=4, loc="upper center", 48 bbox_to_anchor=(0.5, -0.22)) 49 fig.tight_layout() 50 fig.savefig(os.path.join(FIG, "rmse_by_field.png"), bbox_inches="tight") 51 plt.close(fig) 52 53 54 def fig_corr(d): 55 rows = d["ell_sweep"] 56 ell = sorted({r["ell"] for r in rows}) 57 fig, ax = plt.subplots(figsize=(4.0, 2.8)) 58 for L, c, m in (("edge", EDGE, "o"), ("grid", GRID, "s")): 59 v = [next(r["rmse"] for r in rows if r["ell"] == e and r["layout"] == L) for e in ell] 60 ax.plot(ell, v, m + "-", color=c, label=L, ms=4, lw=1.8) 61 ax.set_xscale("log") 62 ax.set_yscale("log") 63 ax.set_ylim(top=max(r["rmse"] for r in rows) * 2.2) 64 ax.set_xlabel("field correlation length (substrate widths)") 65 ax.set_ylabel("RMSE of capture efficiency") 66 ax.set_title("Where edge witnesses stop working", loc="left", fontsize=10) 67 ax.legend(frameon=False, fontsize=8) 68 fig.tight_layout() 69 fig.savefig(os.path.join(FIG, "rmse_vs_correlation.png")) 70 plt.close(fig) 71 72 73 def fig_n(d): 74 rows = d["n_sweep"] 75 ns = sorted({r["n"] for r in rows}) 76 fig, ax = plt.subplots(figsize=(4.0, 2.8)) 77 for L, c, m in (("edge", EDGE, "o"), ("grid", GRID, "s")): 78 v = [next(r["rmse"] for r in rows if r["n"] == n and r["layout"] == L) for n in ns] 79 ax.plot(ns, v, m + "-", color=c, label=L, ms=4, lw=1.8) 80 ax.set_xscale("log"); ax.set_yscale("log") 81 ax.set_xlabel("number of witness patches") 82 ax.set_ylabel("RMSE (radial non-uniformity)") 83 ax.set_title("Cost of the layout choice", loc="left", fontsize=10) 84 ax.legend(frameon=False, fontsize=8) 85 fig.tight_layout() 86 fig.savefig(os.path.join(FIG, "rmse_vs_n.png")) 87 plt.close(fig) 88 89 90 def fig_field(): 91 import importlib.util 92 spec = importlib.util.spec_from_file_location("h", os.path.join(HERE, "h0199.py")) 93 h = importlib.util.module_from_spec(spec); spec.loader.exec_module(h) 94 g = np.linspace(0, 1, 240) 95 X, Y = np.meshgrid(g, g) 96 P = np.stack([X.ravel(), Y.ravel()], 1) 97 F = h.field("boundary_null", P).reshape(X.shape) 98 fig, axs = plt.subplots(1, 2, figsize=(5.6, 2.7)) 99 im = axs[0].pcolormesh(X, Y, F, cmap="viridis", shading="auto") 100 axs[0].plot(*h.layout_edge(12).T, "o", ms=3.5, mfc="white", mec="k") 101 axs[0].set_title("edge witnesses", fontsize=9) 102 axs[1].pcolormesh(X, Y, F, cmap="viridis", shading="auto") 103 axs[1].plot(*h.layout_grid(12).T, "o", ms=3.5, mfc="white", mec="k") 104 axs[1].set_title("distributed witnesses", fontsize=9) 105 for a in axs: 106 a.set_xticks([]); a.set_yticks([]) 107 a.set_aspect("equal") 108 for s in a.spines.values(): 109 s.set_visible(False) 110 cb = fig.colorbar(im, ax=axs, fraction=0.03, pad=0.02) 111 cb.set_label("deposition field f", fontsize=8) 112 cb.ax.tick_params(labelsize=7) 113 fig.suptitle("A field that vanishes on the boundary is invisible to edge blanks", 114 fontsize=9, x=0.02, ha="left") 115 fig.savefig(os.path.join(FIG, "boundary_null_layouts.png"), bbox_inches="tight") 116 plt.close(fig) 117 118 119 if __name__ == "__main__": 120 d = load() 121 fig_main(d) 122 fig_corr(d) 123 fig_n(d) 124 fig_field() 125 print("ok")
