Deploy MEIDNet Studio
Browse files- meidnet/generate.py +6 -2
- meidnet/report.py +24 -6
- meidnet/studio/server.py +10 -5
- meidnet/studio/studio.html +40 -17
- meidnet/svg.py +3 -1
- site/search/search_index.json +0 -0
- site/sitemap.xml.gz +0 -0
- site/studio.html +0 -0
- site/understand/interpretability.html +5 -2
- site/use/reports.html +1 -1
- site/use/studio.html +4 -1
meidnet/generate.py
CHANGED
|
@@ -502,9 +502,13 @@ class Designer:
|
|
| 502 |
if v < lo - 0.05 * span or v > hi + 0.05 * span:
|
| 503 |
flags.append(f"predicted {prop} {v:.2f} is outside the training range [{lo:.2f}, {hi:.2f}] "
|
| 504 |
"- the model is extrapolating")
|
|
|
|
|
|
|
|
|
|
| 505 |
nz = float(np.linalg.norm(z))
|
| 506 |
-
if
|
| 507 |
-
flags.append(f"latent length {nz:.1f}
|
|
|
|
| 508 |
return flags
|
| 509 |
|
| 510 |
|
|
|
|
| 502 |
if v < lo - 0.05 * span or v > hi + 0.05 * span:
|
| 503 |
flags.append(f"predicted {prop} {v:.2f} is outside the training range [{lo:.2f}, {hi:.2f}] "
|
| 504 |
"- the model is extrapolating")
|
| 505 |
+
# The search always lets latents grow past the unit length the model was trained on (typically 3-5), so a
|
| 506 |
+
# per-candidate warning would fire on every candidate; the reports state it once per run instead
|
| 507 |
+
# (latent_norm stays in candidates.csv). Only a latent pinned at the clip bound is singled out.
|
| 508 |
nz = float(np.linalg.norm(z))
|
| 509 |
+
if float(np.abs(z).max()) >= self.g.z_clip - 1e-6:
|
| 510 |
+
flags.append(f"latent hit the search limit (z_clip {self.g.z_clip:g}, length {nz:.1f}) "
|
| 511 |
+
"- predictions are less reliable")
|
| 512 |
return flags
|
| 513 |
|
| 514 |
|
meidnet/report.py
CHANGED
|
@@ -194,7 +194,9 @@ def _predictions(lm, dataset):
|
|
| 194 |
|
| 195 |
|
| 196 |
def quality_word(mae, std):
|
| 197 |
-
|
|
|
|
|
|
|
| 198 |
if r < 0.25:
|
| 199 |
return "good", r
|
| 200 |
if r < 0.5:
|
|
@@ -218,13 +220,22 @@ def training_report(cfg, lm, history, train_set, val_set, path) -> str:
|
|
| 218 |
word, ratio = quality_word(mae, std)
|
| 219 |
words.append(word)
|
| 220 |
unit = stats.units[j]
|
|
|
|
|
|
|
|
|
|
| 221 |
rows.append(f"<tr><td>{esc(stats.labels[j])}</td><td>{mae:.3g} {esc(unit)}</td><td>{std:.3g} {esc(unit)}</td>"
|
| 222 |
-
f"<td>{
|
| 223 |
parity.append(f"<div><h3>{esc(stats.labels[j])}</h3>" + svg.scatter(
|
| 224 |
-
Y[:, j], P[:, j], f"Predicted vs true {stats.labels[j]}", f"true
|
| 225 |
diagonal=True) + "</div>")
|
| 226 |
last_val = history["val"][-1] if history.get("val") else None
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
v = verdict("good", "The model predicts every property well from structure alone.",
|
| 229 |
"<p>Typical errors are under a quarter of the natural spread of each property.</p>")
|
| 230 |
elif "weak" in words:
|
|
@@ -357,8 +368,9 @@ def generation_report(cfg, lm, family, res, path) -> str:
|
|
| 357 |
"different variant, or relax that rule in <code>generation.overrides</code>.</p>")
|
| 358 |
elif flagged:
|
| 359 |
v = verdict("warn", f"{len(saved)} candidate(s) saved; {len(flagged)} rely on extrapolated predictions.",
|
| 360 |
-
"<p>All saved candidates obey every rule applied in this run.
|
| 361 |
-
"what the model saw in training — treat those numbers with
|
|
|
|
| 362 |
else:
|
| 363 |
v = verdict("good", f"{len(saved)} candidate(s) saved for {len(res.targets)} target(s).",
|
| 364 |
f"<p>Each one passed all {n_rules} rules of the {esc(family.title)} family.</p>")
|
|
@@ -366,6 +378,12 @@ def generation_report(cfg, lm, family, res, path) -> str:
|
|
| 366 |
"for ones that decode into chemically sensible crystals, and kept the candidates whose predicted "
|
| 367 |
"properties are closest to the target. <b>Predicted values come from the model and must be confirmed by "
|
| 368 |
"calculation or experiment.</b></p>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
off = ""
|
| 370 |
if g.disabled_rules: # say plainly which of the family's rules this run did not apply
|
| 371 |
off = (f"<p class='flag'>⚠ Switched off for this run (<code>disabled_rules</code>): "
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
def quality_word(mae, std):
|
| 197 |
+
if not std > 1e-12: # every value the same: an error cannot be compared with a spread
|
| 198 |
+
return "not judged", None
|
| 199 |
+
r = mae / std
|
| 200 |
if r < 0.25:
|
| 201 |
return "good", r
|
| 202 |
if r < 0.5:
|
|
|
|
| 220 |
word, ratio = quality_word(mae, std)
|
| 221 |
words.append(word)
|
| 222 |
unit = stats.units[j]
|
| 223 |
+
u = f" [{unit}]" if unit else ""
|
| 224 |
+
css = {"good": "pass", "fair": "flag", "weak": "fail"}.get(word, "muted")
|
| 225 |
+
shown = word if word != "not judged" else f"not judged (all {which.split()[0]} values are equal)"
|
| 226 |
rows.append(f"<tr><td>{esc(stats.labels[j])}</td><td>{mae:.3g} {esc(unit)}</td><td>{std:.3g} {esc(unit)}</td>"
|
| 227 |
+
f"<td>{'–' if r2 != r2 else f'{r2:.2f}'}</td><td class='{css}'>{esc(shown)}</td></tr>")
|
| 228 |
parity.append(f"<div><h3>{esc(stats.labels[j])}</h3>" + svg.scatter(
|
| 229 |
+
Y[:, j], P[:, j], f"Predicted vs true {stats.labels[j]}", f"true{u}", f"predicted{u}",
|
| 230 |
diagonal=True) + "</div>")
|
| 231 |
last_val = history["val"][-1] if history.get("val") else None
|
| 232 |
+
judged = [w for w in words if w != "not judged"]
|
| 233 |
+
if len(judged) < len(words):
|
| 234 |
+
v = verdict("warn", "Some properties cannot be judged on this validation set.",
|
| 235 |
+
"<p>Every validation material has the same value for at least one property, so its error cannot "
|
| 236 |
+
"be compared with a spread. Use more data (or a larger <code>val_fraction</code>) to judge the "
|
| 237 |
+
"model.</p>")
|
| 238 |
+
elif all(w == "good" for w in words):
|
| 239 |
v = verdict("good", "The model predicts every property well from structure alone.",
|
| 240 |
"<p>Typical errors are under a quarter of the natural spread of each property.</p>")
|
| 241 |
elif "weak" in words:
|
|
|
|
| 368 |
"different variant, or relax that rule in <code>generation.overrides</code>.</p>")
|
| 369 |
elif flagged:
|
| 370 |
v = verdict("warn", f"{len(saved)} candidate(s) saved; {len(flagged)} rely on extrapolated predictions.",
|
| 371 |
+
"<p>All saved candidates obey every rule applied in this run. For the flagged ones (⚠ on the card) a "
|
| 372 |
+
"predicted property value lies outside what the model saw in training — treat those numbers with "
|
| 373 |
+
"caution.</p>")
|
| 374 |
else:
|
| 375 |
v = verdict("good", f"{len(saved)} candidate(s) saved for {len(res.targets)} target(s).",
|
| 376 |
f"<p>Each one passed all {n_rules} rules of the {esc(family.title)} family.</p>")
|
|
|
|
| 378 |
"for ones that decode into chemically sensible crystals, and kept the candidates whose predicted "
|
| 379 |
"properties are closest to the target. <b>Predicted values come from the model and must be confirmed by "
|
| 380 |
"calculation or experiment.</b></p>")
|
| 381 |
+
if saved:
|
| 382 |
+
norms = [c.latent_norm for c in saved]
|
| 383 |
+
span = f"{min(norms):.1f}" if max(norms) - min(norms) < 0.05 else f"{min(norms):.1f}–{max(norms):.1f}"
|
| 384 |
+
intro += (f"<p class='muted'>The model was trained on latents of length 1; the search moves them further out "
|
| 385 |
+
f"(here length {span}), so every predicted value below is the model's extrapolation along that "
|
| 386 |
+
f"direction. This is how MEIDNet searches, not a fault of one candidate.</p>")
|
| 387 |
off = ""
|
| 388 |
if g.disabled_rules: # say plainly which of the family's rules this run did not apply
|
| 389 |
off = (f"<p class='flag'>⚠ Switched off for this run (<code>disabled_rules</code>): "
|
meidnet/studio/server.py
CHANGED
|
@@ -311,7 +311,7 @@ class Studio:
|
|
| 311 |
"has_model": bool(s and s.lm),
|
| 312 |
"using_own": self._own(s), "columns": s.columns if s else [], "n_rows": s.n_rows if s else 0,
|
| 313 |
"n_cifs": s.n_cifs if s else 0, "checked": bool(s and s.check), "busy": bool(s and s.busy),
|
| 314 |
-
"source": os.path.basename(s.table_path) if (s and s.table_path) else None},
|
| 315 |
}
|
| 316 |
|
| 317 |
# ── data: published / project / uploaded ────────────────────────────────
|
|
@@ -359,7 +359,8 @@ class Studio:
|
|
| 359 |
vals = np.array([r.properties[i] for r in recs]) if recs else np.array([])
|
| 360 |
out["charts"][p.column] = svg.histogram(vals, p.display, p.unit or p.display)
|
| 361 |
if len(vals):
|
| 362 |
-
out["stats"][p.column] = {"min": float(vals.min()), "median": float(np.median(vals)), "max": float(vals.max())
|
|
|
|
| 363 |
if rep.elements:
|
| 364 |
out["charts"]["elements"] = svg.hbars(list(rep.elements.items()), "Most common elements", max_items=16)
|
| 365 |
if rep.skipped:
|
|
@@ -540,7 +541,8 @@ class Studio:
|
|
| 540 |
raw = {"name": "your_data", "output_dir": s.dir, "family": family, "data": data,
|
| 541 |
"training": {"epochs": 20, "batch_size": 16, "device": "cpu"}, "generation": gen}
|
| 542 |
cfg = config_from_dict(raw, base_dir=s.data_dir)
|
| 543 |
-
summary, info = self._check_summary(cfg, source=os.path.basename(s.table_path),
|
|
|
|
| 544 |
s.check_info = info
|
| 545 |
s.chem_cache = {}
|
| 546 |
# targets default to the median of each property so the first search is sensible
|
|
@@ -824,7 +826,10 @@ class Studio:
|
|
| 824 |
if self.public:
|
| 825 |
raw["plugins"] = []
|
| 826 |
cfg = config_from_dict(raw, base_dir=base._base_dir)
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
| 828 |
return {"ok": False, "error": str(e)}
|
| 829 |
with self.jobs_lock:
|
| 830 |
refused = self._slot_error(s)
|
|
@@ -1000,7 +1005,7 @@ class Studio:
|
|
| 1000 |
return {"available": False, "note": "no usable structures in this table - see the Data block"}
|
| 1001 |
if "data" not in s.chem_cache:
|
| 1002 |
s.chem_cache["data"] = cs.records_dataset(s.check_info["records"], s.cfg.data.properties,
|
| 1003 |
-
os.path.basename(s.table_path or "table"), cap)
|
| 1004 |
return s.chem_cache["data"]
|
| 1005 |
if self.cfg.data is not None: # the project's own data ...
|
| 1006 |
if "data" not in self.chem_cache:
|
|
|
|
| 311 |
"has_model": bool(s and s.lm),
|
| 312 |
"using_own": self._own(s), "columns": s.columns if s else [], "n_rows": s.n_rows if s else 0,
|
| 313 |
"n_cifs": s.n_cifs if s else 0, "checked": bool(s and s.check), "busy": bool(s and s.busy),
|
| 314 |
+
"source": (s.table_name or os.path.basename(s.table_path)) if (s and s.table_path) else None},
|
| 315 |
}
|
| 316 |
|
| 317 |
# ── data: published / project / uploaded ────────────────────────────────
|
|
|
|
| 359 |
vals = np.array([r.properties[i] for r in recs]) if recs else np.array([])
|
| 360 |
out["charts"][p.column] = svg.histogram(vals, p.display, p.unit or p.display)
|
| 361 |
if len(vals):
|
| 362 |
+
out["stats"][p.column] = {"min": float(vals.min()), "median": float(np.median(vals)), "max": float(vals.max()),
|
| 363 |
+
"std": float(vals.std())}
|
| 364 |
if rep.elements:
|
| 365 |
out["charts"]["elements"] = svg.hbars(list(rep.elements.items()), "Most common elements", max_items=16)
|
| 366 |
if rep.skipped:
|
|
|
|
| 541 |
raw = {"name": "your_data", "output_dir": s.dir, "family": family, "data": data,
|
| 542 |
"training": {"epochs": 20, "batch_size": 16, "device": "cpu"}, "generation": gen}
|
| 543 |
cfg = config_from_dict(raw, base_dir=s.data_dir)
|
| 544 |
+
summary, info = self._check_summary(cfg, source=s.table_name or os.path.basename(s.table_path),
|
| 545 |
+
keep_structures=True)
|
| 546 |
s.check_info = info
|
| 547 |
s.chem_cache = {}
|
| 548 |
# targets default to the median of each property so the first search is sensible
|
|
|
|
| 826 |
if self.public:
|
| 827 |
raw["plugins"] = []
|
| 828 |
cfg = config_from_dict(raw, base_dir=base._base_dir)
|
| 829 |
+
from meidnet.pipeline import family_for
|
| 830 |
+
# refuse at once (not seconds later from the job) a target or window the model cannot predict
|
| 831 |
+
self._check_against_model(family_for(cfg, need_variant=True), cfg.generation, self._model_for(s))
|
| 832 |
+
except (Exception, SystemExit) as e: # family_for reports family problems as SystemExit
|
| 833 |
return {"ok": False, "error": str(e)}
|
| 834 |
with self.jobs_lock:
|
| 835 |
refused = self._slot_error(s)
|
|
|
|
| 1005 |
return {"available": False, "note": "no usable structures in this table - see the Data block"}
|
| 1006 |
if "data" not in s.chem_cache:
|
| 1007 |
s.chem_cache["data"] = cs.records_dataset(s.check_info["records"], s.cfg.data.properties,
|
| 1008 |
+
s.table_name or os.path.basename(s.table_path or "table"), cap)
|
| 1009 |
return s.chem_cache["data"]
|
| 1010 |
if self.cfg.data is not None: # the project's own data ...
|
| 1011 |
if "data" not in self.chem_cache:
|
meidnet/studio/studio.html
CHANGED
|
@@ -105,6 +105,9 @@ pre{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:
|
|
| 105 |
code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;background:var(--bg);padding:1px 4px;border-radius:4px}
|
| 106 |
.behind{margin-top:12px}.behind .tabs{display:flex;gap:6px;margin:6px 0}.behind .tabs button.on{background:var(--accent);border-color:var(--accent);color:#fff}
|
| 107 |
.prog{height:9px;background:var(--line);border-radius:5px;overflow:hidden;margin:6px 0}.prog i{display:block;height:100%;background:var(--accent);width:0;transition:width .3s}
|
|
|
|
|
|
|
|
|
|
| 108 |
.cand{border:1px solid var(--line);border-radius:12px;padding:12px;margin:8px 0;display:grid;grid-template-columns:150px 1fr;gap:12px}
|
| 109 |
.cand h3{margin:0 0 4px;color:var(--ink);font-size:18px}.cand .meta{grid-column:1/-1}@media(max-width:600px){.cand{grid-template-columns:1fr}}
|
| 110 |
ul.chk{list-style:none;margin:6px 0;padding:0}ul.chk li{padding:1px 0;font-size:13px}
|
|
@@ -155,7 +158,7 @@ body>div:empty:not([id]):not([class]){position:absolute;top:0;left:0;width:0;hei
|
|
| 155 |
<div class="chain" id="chain"><span class="lead">Change anything above or in a block — what it does to the blocks after it appears here.</span></div>
|
| 156 |
<details id="flowlog"><summary>Flow log (0)</summary><ol id="flowlog-list"></ol></details>
|
| 157 |
<div class="cols">
|
| 158 |
-
<div id="panel"></div>
|
| 159 |
<aside>
|
| 160 |
<div class="card" id="live"></div>
|
| 161 |
<div class="card small" id="legend"></div>
|
|
@@ -176,7 +179,7 @@ body>div:empty:not([id]):not([class]){position:absolute;top:0;left:0;width:0;hei
|
|
| 176 |
<div class="row" style="margin-top:8px"><button class="primary" onclick="applyYaml()">Apply</button><button onclick="validateYaml()">Validate only</button><button onclick="downloadYaml()">⬇ Download meidnet.yaml</button><button onclick="openYaml()">Reset</button></div>
|
| 177 |
<div id="yaml-msg"></div>
|
| 178 |
</div>
|
| 179 |
-
<div class="modal" id="modal" onclick="if(event.target===this)closeModal()"><div class="card" id="modal-body"></div></div>
|
| 180 |
<script>
|
| 181 |
/* ═══════════════════════════ state & constants ═══════════════════════════ */
|
| 182 |
const STATIC = window.MEIDNET_STATIC || null;
|
|
@@ -218,7 +221,8 @@ const esc = s => String(s??'').replace(/[&<>"']/g, c => ({'&':'&','<':'<'
|
|
| 218 |
/* a value as a JS string literal inside an onclick="…" attribute: safe for names such as "Young's modulus" */
|
| 219 |
const jsq = s => esc(JSON.stringify(String(s??'')));
|
| 220 |
const fmt = v => { if(v===null||v===undefined||Number.isNaN(+v)) return '–'; const x=+v;
|
| 221 |
-
if(Math.abs(x)>=
|
|
|
|
| 222 |
let s=x.toPrecision(3); if(s.includes('.')&&!/e/i.test(s)) s=s.replace(/0+$/,'').replace(/\.$/,''); // 1.50→1.5, 100 stays 100
|
| 223 |
return s; };
|
| 224 |
const $ = id => document.getElementById(id);
|
|
@@ -363,6 +367,12 @@ function ruleTitle(n, params){ if(n==='property_window') return 'Predicted '+lab
|
|
| 363 |
function ruleText(n){ if(n==='property_window') return 'The predicted value must lie inside the window.'; const r=(S.fam.space.rules||[]).find(x=>x.name===n); return r? r.text : ''; }
|
| 364 |
function label(p){ const m=S.state.model.properties.find(x=>x.column===p); return m? m.label : p; }
|
| 365 |
function unit(p){ const m=S.state.model.properties.find(x=>x.column===p); return m&&m.unit ? ' '+m.unit : ''; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
/* a search result's rule, in words: property windows name their property (the result says "predicted dir_gap = …") */
|
| 367 |
function resultTitle(r){ if(r.name==='symmetry_refinement') return 'Symmetry analysis';
|
| 368 |
if(/^property_window/.test(r.name)){ const m=/predicted (.+?) =/.exec(r.detail||''); return 'Predicted '+(m?label(m[1]):'property')+' window'; }
|
|
@@ -412,7 +422,9 @@ function statusAll(){
|
|
| 412 |
if(S.fam) status('family', `${S.fam.variant||S.fam.name}`, `${nfmt(ev?ev.rows.length:S.fam.space.rows.length)} compositions`);
|
| 413 |
nodeState('family', ev&&ev.rows.length ? 'ok' : 'warn');
|
| 414 |
if(ev){ status('rules', `${activeRules().length} rules`, `${nfmt(ev.alive.length)} pass all`); nodeState('rules', ev.alive.length ? 'ok' : 'warn'); }
|
| 415 |
-
|
|
|
|
|
|
|
| 416 |
nodeState('targets', outOfRange().length ? 'warn' : (ev&&ev.ranked.length ? 'ok' : 'warn'));
|
| 417 |
const st=S.search.status; if(!st) { status('search','idle',''); nodeState('search','idle'); }
|
| 418 |
else if(!st.running){ status('search', st.error?'failed':'finished', ''); nodeState('search', st.error?'error':'ok'); }
|
|
@@ -448,8 +460,8 @@ function drawPipe(){
|
|
| 448 |
<text class="s" id="st-${n.id}" x="${x+12}" y="${y+56}">…</text><text class="s" id="st2-${n.id}" x="${x+12}" y="${y+73}"></text></g>`; });
|
| 449 |
$('pipe').innerHTML=s;
|
| 450 |
}
|
| 451 |
-
function status(id, a, b){ const fit=s=>{ s=String(s||''); return s.length>21 ? s.slice(0,20)+'…' : s; }; // ~21 characters fit a 130-px node at 11 px
|
| 452 |
-
const e=$('st-'+id); if(!e) return; e.textContent=fit(a); e.parentNode.querySelector('title').textContent = blockName(id)+' — '+String(a||'')+(b?' · '+b:'');
|
| 453 |
const e2=$('st2-'+id); if(e2) e2.textContent=fit(b); }
|
| 454 |
function nodeState(id, st){ const b=$('bd-'+id); if(b) b.setAttribute('class','badge '+(st||'idle')); }
|
| 455 |
function flowFrom(id){
|
|
@@ -501,7 +513,7 @@ function scatter(rows, px, py, target, highlight){
|
|
| 501 |
for(const [x,y,r] of pts){ if(!r._ok) s+=`<circle class="ptx" cx="${X(x)}" cy="${Y(y)}" r="2.4"><title>${esc(r.f)} (rejected)</title></circle>`; }
|
| 502 |
for(const [x,y,r] of pts){ if(r._ok&&!hs.has(r.f)) s+=`<circle class="pt" cx="${X(x)}" cy="${Y(y)}" r="3" onclick="inspect(${jsq(r.f)})" style="cursor:pointer"><title>${esc(r.f)}: ${fmt(x)}, ${fmt(y)}</title></circle>`; }
|
| 503 |
for(const [x,y,r] of pts){ if(hs.has(r.f)) s+=`<path class="star" d="M${X(x)},${Y(y)-8} l2.3,5.5 5.7,0 -4.6,3.5 2,5.5 -5.4,-3.4 -5.4,3.4 2,-5.5 -4.6,-3.5 5.7,0z" onclick="inspect(${jsq(r.f)})" style="cursor:pointer"><title>${esc(r.f)} (found by search)</title></path>`; }
|
| 504 |
-
s+=`<text class="lab" x="${(ml+W-mr)/2}" y="${H-4}" text-anchor="middle">predicted ${esc(label(px))}${esc(
|
| 505 |
return `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img"><title>Predicted properties of every composition; dashed lines = target</title>${s}</svg>`;
|
| 506 |
}
|
| 507 |
function gauge(p, value, target){
|
|
@@ -761,8 +773,12 @@ function trainBox(){
|
|
| 761 |
if(st){ const p=st.progress; const frac=p.epochs?p.epoch/p.epochs:0;
|
| 762 |
b+=`<h4>${st.running?'Training':(st.error?'Failed':'Finished')}</h4><p class="small">epoch ${p.epoch}/${p.epochs}${p.loss!=null?' · loss '+fmt(p.loss):''} · ${st.seconds.toFixed(0)} s</p><div class="prog"><i style="width:${(100*frac).toFixed(1)}%"></i></div>`;
|
| 763 |
if(st.error) b+=`<div class="verdict bad">${esc(st.error)}</div>`;
|
| 764 |
-
if(p.curve&&p.curve.length>1) b+=`<div class="loss"><div>${lineChart({'
|
| 765 |
-
if(p.val&&p.val.mae){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
if(st.done&&!st.error) b+=`<div class="verdict good">✅ Model ready — the blocks below now use <b>your</b> model and properties.${st.report?` <a href="/files/${esc(st.report)}" target="_blank">training report</a> · <a href="/files/${esc(st.report.replace('training_report.html','model.pt'))}" download="model.pt">download model.pt</a>`:''}</div>`; }
|
| 767 |
return b;
|
| 768 |
}
|
|
@@ -844,11 +860,11 @@ function panelTargets(){
|
|
| 844 |
const ev=S.ev||evaluateSpace(); const props=S.state.model.properties; let b=`<p class="muted small">Each objective pulls the search; weights say how much. The chart and table update as you drag.</p>`;
|
| 845 |
S.ui.objectives.forEach((o,i)=>{ const r=S.state.ranges[o.property]||[0,1]; const span=(r[1]-r[0])||1; const t=+S.ui.target[o.property];
|
| 846 |
const vals=ev.rows.map(x=>x.p[o.property]); const okm=ev.rows.map(x=>x._ok);
|
| 847 |
-
b+=`<div class="rule"><div class="hd"><b>${esc(label(o.property))}${esc(
|
| 848 |
<div class="params"><div class="param"><label>target</label><input type="number" step="0.01" value="${t}" onchange="setTarget(${jsq(o.property)},this.value)"><br><input type="range" min="${(r[0]-.2*span).toFixed(3)}" max="${(r[1]+.2*span).toFixed(3)}" step="${(span/200).toFixed(4)}" value="${t}" oninput="setTarget(${jsq(o.property)},this.value,true)" onchange="render()" style="width:230px"></div>
|
| 849 |
<div class="param"><label>how to compare</label><select onchange="S.ui.objectives[${i}].loss=this.value;S.ui.objectives[${i}].select_loss=this.value;recompute('targets',${jsq('Comparison for '+label(o.property)+': ')}+this.value);render()">${['l2','l1','at_most','at_least'].map(k=>`<option value="${k}" ${k===o.loss?'selected':''}>${{l2:'as close as possible (squared)',l1:'as close as possible (absolute)',at_most:'at most the target',at_least:'at least the target'}[k]}</option>`).join('')}</select></div>
|
| 850 |
<div class="param"><label>weight in search</label><input type="number" step="100" value="${o.weight}" onchange="S.ui.objectives[${i}].weight=+this.value"></div><div class="param"><label>weight in ranking</label><input type="number" step="0.1" value="${o.select_weight}" onchange="S.ui.objectives[${i}].select_weight=+this.value;recompute('targets','Ranking weight changed');render()"></div></div>
|
| 851 |
-
${hist(vals, 'predicted '+label(o.property), 'predicted '+label(o.property)+
|
| 852 |
${(t<r[0]||t>r[1])?`<p class="flag small">⚠ ${fmt(t)} is outside the training range ${fmt(r[0])}–${fmt(r[1])}: the model would be extrapolating.</p>`:''}</div>`; });
|
| 853 |
const more=(S.ui.extraTargets||[]).length;
|
| 854 |
if(more) b+=`<p class="small muted">The search also runs ${more} more target${more>1?'s':''} (${S.ui.extraTargets.map(t=>esc(Object.entries(t).map(([k,v])=>label(k)+' '+fmt(v)).join(', '))).join(' · ')}); change ${more>1?'them':'it'} in <a href="#" onclick="openYaml();return false">✎ Edit as text</a>.</p>`;
|
|
@@ -871,7 +887,7 @@ function panelSearch(){
|
|
| 871 |
if(STATIC) b+=`<div class="verdict warn">The static demo cannot run a search. Use the hosted Studio, or <code>pip install meidnet</code> then <code>meidnet studio</code>.</div>`;
|
| 872 |
b+=`<div class="row"><button class="primary" ${S.search.running||STATIC?'disabled':''} onclick="startSearch()">▶ Run search</button><button class="danger" ${S.search.running?'':'disabled'} onclick="stopSearch()">■ Stop</button><button onclick="downloadYaml()">⬇ Export meidnet.yaml</button></div>`;
|
| 873 |
if(st){ const p=st.progress; const frac=p.steps?((p.step+1)/p.steps):0;
|
| 874 |
-
b+=`<h3>${st.running?'Running':(st.error?'Failed':'Finished')}</h3><p class="small">target ${p.target}/${p.targets} · round ${p.round}/${p.rounds} · step ${p.step}/${p.steps}${p.loss!==null&&p.loss!==undefined?
|
| 875 |
if(st.error) b+=`<pre>${esc(st.error)}</pre>`;
|
| 876 |
b+=`<details ${(S.logOpen??st.running)?'open':''} ontoggle="S.logOpen=this.open"><summary>log</summary><pre id="log">${esc(st.log.join('\n'))}</pre></details>`;
|
| 877 |
if(st.report) b+=`<p><a href="/files/${esc(st.report)}" target="_blank">open the full generation report</a></p>`; }
|
|
@@ -901,7 +917,7 @@ function panelCandidates(){
|
|
| 901 |
function renderLive(){
|
| 902 |
const ev=S.ev; const L=$('live'); if(!ev || !S.fam){ L.innerHTML='<h2 style="font-size:17px">Live effects</h2><p class="muted small">…</p>'; return; } const top=ev.ranked;
|
| 903 |
let b=`<h2 style="font-size:17px">Live effects</h2><div class="kpi side"><div><b>${nfmt(ev.rows.length)}</b><span><i style="background:var(--c-family)"></i>compositions</span></div><div><b>${nfmt(ev.alive.length)}</b><span><i style="background:var(--c-rules)"></i>pass all rules</span></div><div><b>${foundCands().length}</b><span><i style="background:var(--c-candidates)"></i>found by search</span></div></div>`;
|
| 904 |
-
if(top.length){ b+=`<h4 style="color:var(--c-targets)">Predicted closest to target</h4><table><tr><th>#</th><th>composition</th>${S.ui.objectives.map(o=>`<th>${esc(label(o.property))}${esc(
|
| 905 |
top.slice(0,8).forEach(([s,r],i)=>{ b+=`<tr class="clk" onclick="inspect(${jsq(r.f)})"><td>${i+1}</td><td><b>${esc(r.f)}</b></td>${S.ui.objectives.map(o=>`<td>${fmt(r.p[o.property])}</td>`).join('')}</tr>`; }); b+='</table><p class="small muted">encoder prediction on the ideal prototype · click a row for the checklist and 3D cell</p>'; }
|
| 906 |
else if(S.fam.space.columns.length) b+=`<p class="muted small">No composition passes all rules.</p>`;
|
| 907 |
if(S.fam.space.sampled) b+=`<p class="flag small">⚠ family too large to enumerate fully; a random sample of ${nfmt(S.fam.space.rows.length)} of ${nfmt(S.fam.space.total)} compositions is shown.</p>`;
|
|
@@ -956,7 +972,9 @@ function drawerGeneration(){ const p=generationPayload(); const base=S.state.con
|
|
| 956 |
return out; }
|
| 957 |
async function startSearch(){ try{ const fam=S.fam; const r=await api('/api/search/start',{generation:generationPayload()});
|
| 958 |
if(!r.ok){ renderChain([{block:'search', text:(r.busy?'server busy — ':'')+r.error}]); return; }
|
| 959 |
-
S.search.fam=fam; S.search.running=true; S.logOpen=undefined; status('search','running'); nodeState('search','running'); flowFrom('targets');
|
|
|
|
|
|
|
| 960 |
async function stopSearch(){ try{ await api('/api/search/stop',{}); }catch(e){} }
|
| 961 |
function typing(){ const a=document.activeElement; return !!a && $('panel').contains(a) && /^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName); }
|
| 962 |
function lastLine(err){ return String(err||'').trim().split('\n').pop(); }
|
|
@@ -1113,13 +1131,18 @@ function inspect(formula){ const r=S.fam.space.rows.find(x=>x.f===formula); if(!
|
|
| 1113 |
if(ru.name==='bond_window') w=`allowed ${esc(ru.params.low)}–${esc(ru.params.high)}`; else if(ru.params.min!==undefined||ru.params.max!==undefined) w=`allowed ${esc(ru.params.min??'…')}–${esc(ru.params.max??'…')}`;
|
| 1114 |
b+=`<li><span class="${ok?'pass':'fail'}">${ok?'✓':'✗'}</span> ${esc(ruleTitle(k||ru.name, ru.params))}${v!==undefined&&v!==null&&ru.name!=='charge_neutrality'?' = <b>'+fmt(v)+'</b>':''}${ru.name==='property_window'?' = <b>'+fmt(r.p[ru.params.property])+'</b>':''} <span class="muted small">${w}</span><div class="small muted beginner-only">${esc(ruleText(k||ru.name))}</div></li>`; }
|
| 1115 |
b+='</ul><p class="small muted">Descriptor values are computed by the same Python functions the search uses.</p><button onclick="closeModal()">close</button>';
|
| 1116 |
-
$('modal-body').innerHTML=b; $('modal').classList.add('open'); mountAll3d();
|
| 1117 |
-
|
|
|
|
|
|
|
|
|
|
| 1118 |
document.addEventListener('keydown', e=>{ if(e.key==='Escape'){ closeModal(); closeYaml(); }
|
| 1119 |
const t=e.target.closest && e.target.closest('[data-show]'); // blocks and chain steps work from the keyboard too
|
| 1120 |
if(t && (e.key==='Enter'||e.key===' ')){ e.preventDefault(); show(t.dataset.show); } });
|
| 1121 |
window.addEventListener('resize', ()=>{ clearTimeout(window._rz); window._rz=setTimeout(mountAll3d, 200); });
|
| 1122 |
-
|
|
|
|
|
|
|
| 1123 |
</script>
|
| 1124 |
</body>
|
| 1125 |
</html>
|
|
|
|
| 105 |
code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;background:var(--bg);padding:1px 4px;border-radius:4px}
|
| 106 |
.behind{margin-top:12px}.behind .tabs{display:flex;gap:6px;margin:6px 0}.behind .tabs button.on{background:var(--accent);border-color:var(--accent);color:#fff}
|
| 107 |
.prog{height:9px;background:var(--line);border-radius:5px;overflow:hidden;margin:6px 0}.prog i{display:block;height:100%;background:var(--accent);width:0;transition:width .3s}
|
| 108 |
+
.prog i.indeterminate{width:35%;background:var(--c-family);animation:slide 1.4s ease-in-out infinite}
|
| 109 |
+
@keyframes slide{0%{margin-left:-35%}100%{margin-left:100%}}
|
| 110 |
+
@media (prefers-reduced-motion: reduce){.prog i.indeterminate{animation:none;width:100%;opacity:.5}}
|
| 111 |
.cand{border:1px solid var(--line);border-radius:12px;padding:12px;margin:8px 0;display:grid;grid-template-columns:150px 1fr;gap:12px}
|
| 112 |
.cand h3{margin:0 0 4px;color:var(--ink);font-size:18px}.cand .meta{grid-column:1/-1}@media(max-width:600px){.cand{grid-template-columns:1fr}}
|
| 113 |
ul.chk{list-style:none;margin:6px 0;padding:0}ul.chk li{padding:1px 0;font-size:13px}
|
|
|
|
| 158 |
<div class="chain" id="chain"><span class="lead">Change anything above or in a block — what it does to the blocks after it appears here.</span></div>
|
| 159 |
<details id="flowlog"><summary>Flow log (0)</summary><ol id="flowlog-list"></ol></details>
|
| 160 |
<div class="cols">
|
| 161 |
+
<div id="panel"><div class="card loading-card"><h2>Loading MEIDNet Studio…</h2><p class="muted">Fetching the model and every composition of the perovskite family. This takes a few seconds; on the hosted Studio the very first visit can take up to a minute while the server wakes up.</p><div class="prog"><i class="indeterminate"></i></div></div></div>
|
| 162 |
<aside>
|
| 163 |
<div class="card" id="live"></div>
|
| 164 |
<div class="card small" id="legend"></div>
|
|
|
|
| 179 |
<div class="row" style="margin-top:8px"><button class="primary" onclick="applyYaml()">Apply</button><button onclick="validateYaml()">Validate only</button><button onclick="downloadYaml()">⬇ Download meidnet.yaml</button><button onclick="openYaml()">Reset</button></div>
|
| 180 |
<div id="yaml-msg"></div>
|
| 181 |
</div>
|
| 182 |
+
<div class="modal" id="modal" onclick="if(event.target===this)closeModal()"><div class="card" id="modal-body" role="dialog" aria-modal="true" aria-label="Composition details"></div></div>
|
| 183 |
<script>
|
| 184 |
/* ═══════════════════════════ state & constants ═══════════════════════════ */
|
| 185 |
const STATIC = window.MEIDNET_STATIC || null;
|
|
|
|
| 221 |
/* a value as a JS string literal inside an onclick="…" attribute: safe for names such as "Young's modulus" */
|
| 222 |
const jsq = s => esc(JSON.stringify(String(s??'')));
|
| 223 |
const fmt = v => { if(v===null||v===undefined||Number.isNaN(+v)) return '–'; const x=+v;
|
| 224 |
+
if(Math.abs(x)>=1e6||(Math.abs(x)<0.01&&x!==0)) return x.toPrecision(2);
|
| 225 |
+
if(Math.abs(x)>=1000) return Math.round(x).toLocaleString('en-US'); // 1,500 rather than 1.5e+3
|
| 226 |
let s=x.toPrecision(3); if(s.includes('.')&&!/e/i.test(s)) s=s.replace(/0+$/,'').replace(/\.$/,''); // 1.50→1.5, 100 stays 100
|
| 227 |
return s; };
|
| 228 |
const $ = id => document.getElementById(id);
|
|
|
|
| 367 |
function ruleText(n){ if(n==='property_window') return 'The predicted value must lie inside the window.'; const r=(S.fam.space.rules||[]).find(x=>x.name===n); return r? r.text : ''; }
|
| 368 |
function label(p){ const m=S.state.model.properties.find(x=>x.column===p); return m? m.label : p; }
|
| 369 |
function unit(p){ const m=S.state.model.properties.find(x=>x.column===p); return m&&m.unit ? ' '+m.unit : ''; }
|
| 370 |
+
/* the unit in brackets after a property name: "Direct band gap (eV)" */
|
| 371 |
+
function unitP(p){ const u=unit(p).trim(); return u ? ' ('+u+')' : ''; }
|
| 372 |
+
/* a validation error in words, as the training report judges it (error / spread of the property in the data) */
|
| 373 |
+
function quality(col, mae){ const st=((S.data||{}).stats||{})[col]; const sd=st&&st.std;
|
| 374 |
+
if(!(sd>1e-12)) return {word:'—', css:'muted'}; const r=mae/sd;
|
| 375 |
+
return r<0.25 ? {word:'good', css:'pass'} : r<0.5 ? {word:'fair', css:'flag'} : {word:'weak', css:'fail'}; }
|
| 376 |
/* a search result's rule, in words: property windows name their property (the result says "predicted dir_gap = …") */
|
| 377 |
function resultTitle(r){ if(r.name==='symmetry_refinement') return 'Symmetry analysis';
|
| 378 |
if(/^property_window/.test(r.name)){ const m=/predicted (.+?) =/.exec(r.detail||''); return 'Predicted '+(m?label(m[1]):'property')+' window'; }
|
|
|
|
| 422 |
if(S.fam) status('family', `${S.fam.variant||S.fam.name}`, `${nfmt(ev?ev.rows.length:S.fam.space.rows.length)} compositions`);
|
| 423 |
nodeState('family', ev&&ev.rows.length ? 'ok' : 'warn');
|
| 424 |
if(ev){ status('rules', `${activeRules().length} rules`, `${nfmt(ev.alive.length)} pass all`); nodeState('rules', ev.alive.length ? 'ok' : 'warn'); }
|
| 425 |
+
{ const full=S.ui.objectives.map(o=>`${label(o.property)} ${fmt(+S.ui.target[o.property])}${unit(o.property)}`).join(', ');
|
| 426 |
+
const short=S.ui.objectives.map(o=>`${o.property} ${fmt(+S.ui.target[o.property])}`).join(', '); // the node fits ~21 characters; the tooltip has the words
|
| 427 |
+
status('targets', full.length<=21 ? full : short, ev&&ev.ranked[0]?`best: ${ev.ranked[0][1].f}`:'no match', full); }
|
| 428 |
nodeState('targets', outOfRange().length ? 'warn' : (ev&&ev.ranked.length ? 'ok' : 'warn'));
|
| 429 |
const st=S.search.status; if(!st) { status('search','idle',''); nodeState('search','idle'); }
|
| 430 |
else if(!st.running){ status('search', st.error?'failed':'finished', ''); nodeState('search', st.error?'error':'ok'); }
|
|
|
|
| 460 |
<text class="s" id="st-${n.id}" x="${x+12}" y="${y+56}">…</text><text class="s" id="st2-${n.id}" x="${x+12}" y="${y+73}"></text></g>`; });
|
| 461 |
$('pipe').innerHTML=s;
|
| 462 |
}
|
| 463 |
+
function status(id, a, b, tip){ const fit=s=>{ s=String(s||''); return s.length>21 ? s.slice(0,20)+'…' : s; }; // ~21 characters fit a 130-px node at 11 px
|
| 464 |
+
const e=$('st-'+id); if(!e) return; e.textContent=fit(a); e.parentNode.querySelector('title').textContent = blockName(id)+' — '+String(tip||a||'')+(b?' · '+b:'');
|
| 465 |
const e2=$('st2-'+id); if(e2) e2.textContent=fit(b); }
|
| 466 |
function nodeState(id, st){ const b=$('bd-'+id); if(b) b.setAttribute('class','badge '+(st||'idle')); }
|
| 467 |
function flowFrom(id){
|
|
|
|
| 513 |
for(const [x,y,r] of pts){ if(!r._ok) s+=`<circle class="ptx" cx="${X(x)}" cy="${Y(y)}" r="2.4"><title>${esc(r.f)} (rejected)</title></circle>`; }
|
| 514 |
for(const [x,y,r] of pts){ if(r._ok&&!hs.has(r.f)) s+=`<circle class="pt" cx="${X(x)}" cy="${Y(y)}" r="3" onclick="inspect(${jsq(r.f)})" style="cursor:pointer"><title>${esc(r.f)}: ${fmt(x)}, ${fmt(y)}</title></circle>`; }
|
| 515 |
for(const [x,y,r] of pts){ if(hs.has(r.f)) s+=`<path class="star" d="M${X(x)},${Y(y)-8} l2.3,5.5 5.7,0 -4.6,3.5 2,5.5 -5.4,-3.4 -5.4,3.4 2,-5.5 -4.6,-3.5 5.7,0z" onclick="inspect(${jsq(r.f)})" style="cursor:pointer"><title>${esc(r.f)} (found by search)</title></path>`; }
|
| 516 |
+
s+=`<text class="lab" x="${(ml+W-mr)/2}" y="${H-4}" text-anchor="middle">predicted ${esc(label(px))}${esc(unitP(px))}</text><text class="lab" transform="translate(12,${(mt+H-mb)/2}) rotate(-90)" text-anchor="middle">predicted ${esc(label(py))}${esc(unitP(py))}</text>`;
|
| 517 |
return `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img"><title>Predicted properties of every composition; dashed lines = target</title>${s}</svg>`;
|
| 518 |
}
|
| 519 |
function gauge(p, value, target){
|
|
|
|
| 773 |
if(st){ const p=st.progress; const frac=p.epochs?p.epoch/p.epochs:0;
|
| 774 |
b+=`<h4>${st.running?'Training':(st.error?'Failed':'Finished')}</h4><p class="small">epoch ${p.epoch}/${p.epochs}${p.loss!=null?' · loss '+fmt(p.loss):''} · ${st.seconds.toFixed(0)} s</p><div class="prog"><i style="width:${(100*frac).toFixed(1)}%"></i></div>`;
|
| 775 |
if(st.error) b+=`<div class="verdict bad">${esc(st.error)}</div>`;
|
| 776 |
+
if(p.curve&&p.curve.length>1) b+=`<div class="loss"><div>${lineChart({'training loss':p.curve.map(c=>[c[0],c[1]])},{log:true,xlabel:'epoch',title:'training loss (lower is better)'})}</div><div>${lineChart({'structure–property alignment':p.curve.map(c=>[c[0],c[2]])},{xlabel:'epoch',title:'alignment (higher is better)'})}</div></div>`;
|
| 777 |
+
if(p.val&&p.val.mae){ const q=Object.entries(p.val.mae).map(([k,v])=>quality(k,v));
|
| 778 |
+
b+=`<table><tr><th>held-out materials (epoch ${p.val.epoch})</th>${Object.keys(p.val.mae).map(k=>`<th>${esc(label(k)||k)}</th>`).join('')}<th title="how often a material's own properties are the closest match to its structure in the latent space">matched correctly</th></tr>
|
| 779 |
+
<tr><td>typical error (MAE)</td>${Object.entries(p.val.mae).map(([k,v])=>`<td>${fmt(v)}${esc(unit(k))}</td>`).join('')}<td>${(100*p.val.retrieval_top1).toFixed(0)}%</td></tr>
|
| 780 |
+
<tr><td>quality</td>${q.map(x=>`<td class="${x.css}">${x.word}</td>`).join('')}<td></td></tr></table>
|
| 781 |
+
<p class="small muted beginner-only">The typical error is compared with how much each property varies in your data: under 25% of that spread is <b>good</b>, under 50% <b>fair</b>, otherwise <b>weak</b>. "Matched correctly" is how often a material's structure and its properties land closest to each other in the model's hidden space (higher is better). A few epochs give a rough model; more epochs or more data usually help.</p>`; }
|
| 782 |
if(st.done&&!st.error) b+=`<div class="verdict good">✅ Model ready — the blocks below now use <b>your</b> model and properties.${st.report?` <a href="/files/${esc(st.report)}" target="_blank">training report</a> · <a href="/files/${esc(st.report.replace('training_report.html','model.pt'))}" download="model.pt">download model.pt</a>`:''}</div>`; }
|
| 783 |
return b;
|
| 784 |
}
|
|
|
|
| 860 |
const ev=S.ev||evaluateSpace(); const props=S.state.model.properties; let b=`<p class="muted small">Each objective pulls the search; weights say how much. The chart and table update as you drag.</p>`;
|
| 861 |
S.ui.objectives.forEach((o,i)=>{ const r=S.state.ranges[o.property]||[0,1]; const span=(r[1]-r[0])||1; const t=+S.ui.target[o.property];
|
| 862 |
const vals=ev.rows.map(x=>x.p[o.property]); const okm=ev.rows.map(x=>x._ok);
|
| 863 |
+
b+=`<div class="rule"><div class="hd"><b>${esc(label(o.property))}${esc(unitP(o.property))}</b>${S.ui.objectives.length>1?`<button onclick="removeObjective(${i})">remove</button>`:''}</div>
|
| 864 |
<div class="params"><div class="param"><label>target</label><input type="number" step="0.01" value="${t}" onchange="setTarget(${jsq(o.property)},this.value)"><br><input type="range" min="${(r[0]-.2*span).toFixed(3)}" max="${(r[1]+.2*span).toFixed(3)}" step="${(span/200).toFixed(4)}" value="${t}" oninput="setTarget(${jsq(o.property)},this.value,true)" onchange="render()" style="width:230px"></div>
|
| 865 |
<div class="param"><label>how to compare</label><select onchange="S.ui.objectives[${i}].loss=this.value;S.ui.objectives[${i}].select_loss=this.value;recompute('targets',${jsq('Comparison for '+label(o.property)+': ')}+this.value);render()">${['l2','l1','at_most','at_least'].map(k=>`<option value="${k}" ${k===o.loss?'selected':''}>${{l2:'as close as possible (squared)',l1:'as close as possible (absolute)',at_most:'at most the target',at_least:'at least the target'}[k]}</option>`).join('')}</select></div>
|
| 866 |
<div class="param"><label>weight in search</label><input type="number" step="100" value="${o.weight}" onchange="S.ui.objectives[${i}].weight=+this.value"></div><div class="param"><label>weight in ranking</label><input type="number" step="0.1" value="${o.select_weight}" onchange="S.ui.objectives[${i}].select_weight=+this.value;recompute('targets','Ranking weight changed');render()"></div></div>
|
| 867 |
+
${hist(vals, 'predicted '+label(o.property), 'predicted '+label(o.property)+unitP(o.property)+' · grey = rejected by the rules', {marks:{target:t}, okMask:okm, bins:30})}
|
| 868 |
${(t<r[0]||t>r[1])?`<p class="flag small">⚠ ${fmt(t)} is outside the training range ${fmt(r[0])}–${fmt(r[1])}: the model would be extrapolating.</p>`:''}</div>`; });
|
| 869 |
const more=(S.ui.extraTargets||[]).length;
|
| 870 |
if(more) b+=`<p class="small muted">The search also runs ${more} more target${more>1?'s':''} (${S.ui.extraTargets.map(t=>esc(Object.entries(t).map(([k,v])=>label(k)+' '+fmt(v)).join(', '))).join(' · ')}); change ${more>1?'them':'it'} in <a href="#" onclick="openYaml();return false">✎ Edit as text</a>.</p>`;
|
|
|
|
| 887 |
if(STATIC) b+=`<div class="verdict warn">The static demo cannot run a search. Use the hosted Studio, or <code>pip install meidnet</code> then <code>meidnet studio</code>.</div>`;
|
| 888 |
b+=`<div class="row"><button class="primary" ${S.search.running||STATIC?'disabled':''} onclick="startSearch()">▶ Run search</button><button class="danger" ${S.search.running?'':'disabled'} onclick="stopSearch()">■ Stop</button><button onclick="downloadYaml()">⬇ Export meidnet.yaml</button></div>`;
|
| 889 |
if(st){ const p=st.progress; const frac=p.steps?((p.step+1)/p.steps):0;
|
| 890 |
+
b+=`<h3>${st.running?'Running':(st.error?'Failed':'Finished')}</h3><p class="small">target ${p.target}/${p.targets} · round ${p.round}/${p.rounds} · step ${p.step}/${p.steps}${p.loss!==null&&p.loss!==undefined?` · <span class="muted" title="the quantity the search minimises: lower is better; its size depends on the weights, so compare it only within one search">loss ${fmt(p.loss)}</span>`:''} · ${st.seconds.toFixed(0)} s</p><div class="prog"><i style="width:${(100*frac).toFixed(1)}%"></i></div>`;
|
| 891 |
if(st.error) b+=`<pre>${esc(st.error)}</pre>`;
|
| 892 |
b+=`<details ${(S.logOpen??st.running)?'open':''} ontoggle="S.logOpen=this.open"><summary>log</summary><pre id="log">${esc(st.log.join('\n'))}</pre></details>`;
|
| 893 |
if(st.report) b+=`<p><a href="/files/${esc(st.report)}" target="_blank">open the full generation report</a></p>`; }
|
|
|
|
| 917 |
function renderLive(){
|
| 918 |
const ev=S.ev; const L=$('live'); if(!ev || !S.fam){ L.innerHTML='<h2 style="font-size:17px">Live effects</h2><p class="muted small">…</p>'; return; } const top=ev.ranked;
|
| 919 |
let b=`<h2 style="font-size:17px">Live effects</h2><div class="kpi side"><div><b>${nfmt(ev.rows.length)}</b><span><i style="background:var(--c-family)"></i>compositions</span></div><div><b>${nfmt(ev.alive.length)}</b><span><i style="background:var(--c-rules)"></i>pass all rules</span></div><div><b>${foundCands().length}</b><span><i style="background:var(--c-candidates)"></i>found by search</span></div></div>`;
|
| 920 |
+
if(top.length){ b+=`<h4 style="color:var(--c-targets)">Predicted closest to target</h4><table><tr><th>#</th><th>composition</th>${S.ui.objectives.map(o=>`<th>${esc(label(o.property))}${esc(unitP(o.property))}</th>`).join('')}</tr>`;
|
| 921 |
top.slice(0,8).forEach(([s,r],i)=>{ b+=`<tr class="clk" onclick="inspect(${jsq(r.f)})"><td>${i+1}</td><td><b>${esc(r.f)}</b></td>${S.ui.objectives.map(o=>`<td>${fmt(r.p[o.property])}</td>`).join('')}</tr>`; }); b+='</table><p class="small muted">encoder prediction on the ideal prototype · click a row for the checklist and 3D cell</p>'; }
|
| 922 |
else if(S.fam.space.columns.length) b+=`<p class="muted small">No composition passes all rules.</p>`;
|
| 923 |
if(S.fam.space.sampled) b+=`<p class="flag small">⚠ family too large to enumerate fully; a random sample of ${nfmt(S.fam.space.rows.length)} of ${nfmt(S.fam.space.total)} compositions is shown.</p>`;
|
|
|
|
| 972 |
return out; }
|
| 973 |
async function startSearch(){ try{ const fam=S.fam; const r=await api('/api/search/start',{generation:generationPayload()});
|
| 974 |
if(!r.ok){ renderChain([{block:'search', text:(r.busy?'server busy — ':'')+r.error}]); return; }
|
| 975 |
+
S.search.fam=fam; S.search.running=true; S.logOpen=undefined; status('search','running'); nodeState('search','running'); flowFrom('targets');
|
| 976 |
+
const s=S.ui.search; renderChain([{block:'search', text:`started: up to ${s.rounds} round(s) of ${s.steps} steps with ${s.population} latents per target`},{block:'candidates', text:`each candidate appears here as soon as it is found (aim: ${s.per_target} per target)`}]);
|
| 977 |
+
poll(); render(); }catch(e){ renderChain([{block:'search', text:e.message}]); } }
|
| 978 |
async function stopSearch(){ try{ await api('/api/search/stop',{}); }catch(e){} }
|
| 979 |
function typing(){ const a=document.activeElement; return !!a && $('panel').contains(a) && /^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName); }
|
| 980 |
function lastLine(err){ return String(err||'').trim().split('\n').pop(); }
|
|
|
|
| 1131 |
if(ru.name==='bond_window') w=`allowed ${esc(ru.params.low)}–${esc(ru.params.high)}`; else if(ru.params.min!==undefined||ru.params.max!==undefined) w=`allowed ${esc(ru.params.min??'…')}–${esc(ru.params.max??'…')}`;
|
| 1132 |
b+=`<li><span class="${ok?'pass':'fail'}">${ok?'✓':'✗'}</span> ${esc(ruleTitle(k||ru.name, ru.params))}${v!==undefined&&v!==null&&ru.name!=='charge_neutrality'?' = <b>'+fmt(v)+'</b>':''}${ru.name==='property_window'?' = <b>'+fmt(r.p[ru.params.property])+'</b>':''} <span class="muted small">${w}</span><div class="small muted beginner-only">${esc(ruleText(k||ru.name))}</div></li>`; }
|
| 1133 |
b+='</ul><p class="small muted">Descriptor values are computed by the same Python functions the search uses.</p><button onclick="closeModal()">close</button>';
|
| 1134 |
+
$('modal-body').innerHTML=b; S.modalReturn=document.activeElement; $('modal').classList.add('open'); mountAll3d();
|
| 1135 |
+
const first=$('modal-body').querySelector('button'); if(first) first.focus(); // keyboard users land in the window
|
| 1136 |
+
}
|
| 1137 |
+
function closeModal(){ const was=$('modal').classList.contains('open'); $('modal').classList.remove('open');
|
| 1138 |
+
if(was && S.modalReturn && document.body.contains(S.modalReturn)) S.modalReturn.focus(); }
|
| 1139 |
document.addEventListener('keydown', e=>{ if(e.key==='Escape'){ closeModal(); closeYaml(); }
|
| 1140 |
const t=e.target.closest && e.target.closest('[data-show]'); // blocks and chain steps work from the keyboard too
|
| 1141 |
if(t && (e.key==='Enter'||e.key===' ')){ e.preventDefault(); show(t.dataset.show); } });
|
| 1142 |
window.addEventListener('resize', ()=>{ clearTimeout(window._rz); window._rz=setTimeout(mountAll3d, 200); });
|
| 1143 |
+
drawPipe(); // the workflow is visible at once; its numbers arrive with the data
|
| 1144 |
+
boot().catch(e=>{ renderChain([{block:'data', text:'Could not start: '+e.message}]); console.error(e);
|
| 1145 |
+
const lc=document.querySelector('.loading-card'); if(lc) lc.innerHTML=`<h2>The Studio could not start</h2><p>${esc(e.message)}</p><p class="muted">Reload the page in a minute. If it keeps failing, the server may be restarting or busy.</p>`; });
|
| 1146 |
</script>
|
| 1147 |
</body>
|
| 1148 |
</html>
|
meidnet/svg.py
CHANGED
|
@@ -42,8 +42,10 @@ def _ticks(lo, hi, n=5):
|
|
| 42 |
|
| 43 |
|
| 44 |
def _fmt(v):
|
| 45 |
-
if abs(v) >=
|
| 46 |
return f"{v:.2g}"
|
|
|
|
|
|
|
| 47 |
return f"{v:.3g}"
|
| 48 |
|
| 49 |
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
def _fmt(v):
|
| 45 |
+
if abs(v) >= 1e6 or (abs(v) < 0.01 and v != 0):
|
| 46 |
return f"{v:.2g}"
|
| 47 |
+
if abs(v) >= 1000: # axis counts read 1,000 and 1,500 rather than 1e+03 and 1.5e+03
|
| 48 |
+
return f"{v:,.0f}"
|
| 49 |
return f"{v:.3g}"
|
| 50 |
|
| 51 |
|
site/search/search_index.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
site/sitemap.xml.gz
CHANGED
|
Binary files a/site/sitemap.xml.gz and b/site/sitemap.xml.gz differ
|
|
|
site/studio.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
site/understand/interpretability.html
CHANGED
|
@@ -1703,8 +1703,11 @@
|
|
| 1703 |
<li><strong>The checklist</strong> — every rule's measured value and allowed window. These are deterministic chemistry
|
| 1704 |
(charges, radii, distances), not model outputs.</li>
|
| 1705 |
<li><strong>The gauges</strong> — where the prediction sits relative to the target and to the training range.</li>
|
| 1706 |
-
<li><strong>Warnings</strong> — <em>predicted value outside the training range</em> (extrapolation) and <em>latent
|
| 1707 |
-
|
|
|
|
|
|
|
|
|
|
| 1708 |
<li><strong>Encoder vs search</strong> — in the Studio, compare the search's decoded prediction with the structure encoder's
|
| 1709 |
prediction for the same composition. Large disagreement is a red flag.</li>
|
| 1710 |
</ul>
|
|
|
|
| 1703 |
<li><strong>The checklist</strong> — every rule's measured value and allowed window. These are deterministic chemistry
|
| 1704 |
(charges, radii, distances), not model outputs.</li>
|
| 1705 |
<li><strong>The gauges</strong> — where the prediction sits relative to the target and to the training range.</li>
|
| 1706 |
+
<li><strong>Warnings</strong> — <em>predicted value outside the training range</em> (extrapolation) and <em>latent hit the search limit</em>
|
| 1707 |
+
(the search pushed a latent to the <code>z_clip</code> bound; predictions there are less reliable). The model is trained
|
| 1708 |
+
on latents of length 1 and every search moves them further out (typically to length 3–5), so the report says
|
| 1709 |
+
this once for the whole run rather than on every card; each candidate's length is the <code>latent_norm</code> column of
|
| 1710 |
+
<code>candidates.csv</code>.</li>
|
| 1711 |
<li><strong>Encoder vs search</strong> — in the Studio, compare the search's decoded prediction with the structure encoder's
|
| 1712 |
prediction for the same composition. Large disagreement is a red flag.</li>
|
| 1713 |
</ul>
|
site/use/reports.html
CHANGED
|
@@ -1712,7 +1712,7 @@
|
|
| 1712 |
</ul>
|
| 1713 |
<h2 id="generation_reporthtml"><code>generation_report.html</code><a class="headerlink" href="#generation_reporthtml" title="Permanent link">¶</a></h2>
|
| 1714 |
<ul>
|
| 1715 |
-
<li><strong>Verdict</strong>: how many candidates, and whether any
|
| 1716 |
<li>Per target: the <strong>funnel</strong> (compositions alive after each rule — the biggest drop is the rule that limits the
|
| 1717 |
family most), the <strong>candidate cards</strong>, and <strong>examples of rejected compositions</strong> with the rule and the
|
| 1718 |
measured value.</li>
|
|
|
|
| 1712 |
</ul>
|
| 1713 |
<h2 id="generation_reporthtml"><code>generation_report.html</code><a class="headerlink" href="#generation_reporthtml" title="Permanent link">¶</a></h2>
|
| 1714 |
<ul>
|
| 1715 |
+
<li><strong>Verdict</strong>: how many candidates, and whether any predicted value lies outside the training range.</li>
|
| 1716 |
<li>Per target: the <strong>funnel</strong> (compositions alive after each rule — the biggest drop is the rule that limits the
|
| 1717 |
family most), the <strong>candidate cards</strong>, and <strong>examples of rejected compositions</strong> with the rule and the
|
| 1718 |
measured value.</li>
|
site/use/studio.html
CHANGED
|
@@ -2056,7 +2056,10 @@ changed.</p>
|
|
| 2056 |
(with examples), the distribution of each property and the elements present.</li>
|
| 2057 |
<li><strong>Train</strong> in the Model block: choose the number of epochs and press ▶ Train. The loss and alignment curves
|
| 2058 |
grow while it trains; at the end the validation errors appear and <strong>every block switches to your model and
|
| 2059 |
-
your properties</strong> — the design space is re-scored, the targets are your properties.
|
|
|
|
|
|
|
|
|
|
| 2060 |
<li>Switch between <strong>your model</strong> and the <strong>published model</strong> at any time in the Model block. <em>Start over</em>
|
| 2061 |
removes your upload and model from the session.</li>
|
| 2062 |
</ol>
|
|
|
|
| 2056 |
(with examples), the distribution of each property and the elements present.</li>
|
| 2057 |
<li><strong>Train</strong> in the Model block: choose the number of epochs and press ▶ Train. The loss and alignment curves
|
| 2058 |
grow while it trains; at the end the validation errors appear and <strong>every block switches to your model and
|
| 2059 |
+
your properties</strong> — the design space is re-scored, the targets are your properties. Each property's typical
|
| 2060 |
+
error (MAE) is judged in words against the spread (standard deviation) of that property in your data — <em>good</em> below a quarter of
|
| 2061 |
+
the spread, <em>fair</em> below half, <em>weak</em> above — the same rule the training report uses; "matched correctly" is
|
| 2062 |
+
how often a validation structure is matched to its own properties.</li>
|
| 2063 |
<li>Switch between <strong>your model</strong> and the <strong>published model</strong> at any time in the Model block. <em>Start over</em>
|
| 2064 |
removes your upload and model from the session.</li>
|
| 2065 |
</ol>
|