fix: block influence flatness reads raw push; the switch stays visible and works when the status fails or a file is damaged; a block some clip never measured is left out of the groups; the rated-runs label says what it counts
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
7a809ae7d2
commit
7bf9b51007
@@ -74,12 +74,14 @@ export const taste = (app) => function mount() {
|
||||
function drawResearch() {
|
||||
const rows = [c.label.section({ text: "Block influence (research)" })];
|
||||
if (influence && influence.error) rows.push(c.banner.warn({ text: `Could not read the recording: ${influence.error}` }));
|
||||
// The switch is always here, even when the report could not be read: a failed read must not take away the way to turn recording off.
|
||||
rows.push(c.toggle.default({ label: "Record block influence", hint: "On: each run on a Taste key is measured, and kept once you rate it. Off: nothing is recorded.",
|
||||
checked: influence && influence.state ? !!influence.state.enabled : true,
|
||||
onChange: async (on) => { try { await app.api.setBlockInfluence(on); } catch (err) { tell(err.message); } refreshResearch(); } }));
|
||||
if (influence && influence.state) {
|
||||
const { state, four } = influence;
|
||||
rows.push(c.toggle.default({ label: "Record block influence", hint: "On: each run on a Taste key is measured, and kept once you rate it. Off: nothing is recorded.",
|
||||
checked: !!state.enabled, onChange: async (on) => { try { await app.api.setBlockInfluence(on); } catch (err) { tell(err.message); } refreshResearch(); } }));
|
||||
if (state.problem) rows.push(c.banner.warn({ text: state.problem }));
|
||||
rows.push(c.settingsRow.default({ label: "Runs measured", hint: `Runs on this key that have a rating: ${state.runs}, of which ${state.skipped} had no usable picture rows`,
|
||||
rows.push(c.settingsRow.default({ label: "Rated runs kept", hint: `Runs on this key that have a rating: ${state.runs}, of which ${state.skipped} had no usable picture rows`,
|
||||
control: c.hint.default({ text: `${state.used} kept` }) }));
|
||||
if (state.used) rows.push(c.settingsRow.default({ label: "Flatness", hint: "How evenly the blocks move the picture. Near 0 means every block moves it equally, so there is nothing to aim at.",
|
||||
control: c.hint.default({ text: state.flatness == null ? "n/a" : state.flatness.toFixed(3) }) }));
|
||||
|
||||
@@ -78,3 +78,20 @@ test("the block influence readout shows flatness, and Clear asks before it clear
|
||||
assert.match(page.node.textContent, /Download/);
|
||||
assert.match(page.node.textContent, /Clear/);
|
||||
});
|
||||
|
||||
test("a failed status request still shows the switch, so recording can be turned off", async () => {
|
||||
const sets = [];
|
||||
const app = { api: {
|
||||
tasteKeys: async () => ({ keys: [] }),
|
||||
blockInfluence: async () => { throw new Error("HTTP 500"); },
|
||||
blockInfluenceGroups: async () => ({ key: "fox", used: 0, counts: {}, cos: {}, chance: {}, note: "nothing recorded yet" }),
|
||||
setBlockInfluence: async (on) => { sets.push(on); return { enabled: on }; },
|
||||
} };
|
||||
const page = taste(app)();
|
||||
document.body.append(page.node);
|
||||
await tick(); await tick();
|
||||
const toggle = page.node.querySelector('input[type="checkbox"]');
|
||||
assert.ok(toggle, "the switch is still there");
|
||||
toggle.click(); await tick(); await tick();
|
||||
assert.deepEqual(sets, [false], "the unknown state shows on, so one click turns it off");
|
||||
});
|
||||
|
||||
@@ -129,6 +129,8 @@ def routes(table, base, web):
|
||||
state = measure.state(key_of(req, body)) # refuse BEFORE flipping the switch
|
||||
except ValueError as exc:
|
||||
return web.json_response({"why": str(exc)}, status=400)
|
||||
except Exception: # a damaged file must not keep the switch from going off
|
||||
state = {}
|
||||
measure.set_enabled(body.get("enabled") is True)
|
||||
return web.json_response({**state, "enabled": measure.enabled()})
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ def profile(rows) -> dict:
|
||||
`overall` is where the model does its work regardless of ratings; `share` is
|
||||
each block's RAW push as a fraction of all blocks' (rank by this: the ratio
|
||||
shrinks with depth by construction, since the stream grows); `difference` is
|
||||
liked minus disliked; `flatness` is the spread of `overall` as a fraction of
|
||||
liked minus disliked; `flatness` is the spread of RAW push as a fraction of
|
||||
its mean -- near 0 means every block moves the stream equally and there is
|
||||
nothing to aim at. `novelty` ~1: a block amplifies its predecessor, ~0 it
|
||||
adds something new, negative it partly undoes it.
|
||||
@@ -135,7 +135,9 @@ def profile(rows) -> dict:
|
||||
difference = None
|
||||
if int(liked.sum()) >= MIN_PER_GROUP and int(disliked.sum()) >= MIN_PER_GROUP:
|
||||
difference = table(mean("ratio", liked) - mean("ratio", disliked))
|
||||
vals = overall[torch.isfinite(overall)]
|
||||
# Flatness is read on RAW push: the ratio falls with depth by construction (the stream grows), so
|
||||
# a flat model would read as uneven on it.
|
||||
vals = raw[torch.isfinite(raw)]
|
||||
flat = float(vals.std(unbiased=False) / vals.mean()) if len(vals) and abs(float(vals.mean())) > 1e-12 else None
|
||||
nov = novelty[torch.isfinite(novelty)]
|
||||
return {"used": len(rows), "overall": table(overall), "difference": difference, "share": share,
|
||||
@@ -170,7 +172,11 @@ def groups(rows, shuffles=200, seed=0) -> dict:
|
||||
n = len(rows[-1]["rows"]["ratio"])
|
||||
rows = [r for r in rows if len(r["rows"]["ratio"]) == n]
|
||||
stack = torch.stack([r["rows"]["ratio"].float() for r in rows])
|
||||
stack = torch.where(torch.isfinite(stack), stack, torch.nanmean(stack, dim=0).nan_to_num(0.0))
|
||||
# A block some clip never measured is left out of every group, not filled in with a guess.
|
||||
stack = stack[:, torch.isfinite(stack).all(dim=0)]
|
||||
if stack.shape[1] == 0:
|
||||
return {"used": len(rows), "counts": {g: sum(1 for r in rows if _group(r) == g) for g in GROUPS},
|
||||
"cos": {}, "note": "no block was measured on every clip"}
|
||||
labels = [_group(r) for r in rows]
|
||||
counts = {g: labels.count(g) for g in GROUPS}
|
||||
|
||||
|
||||
@@ -349,3 +349,12 @@ def test_the_panel_says_a_taste_key_is_needed_even_when_toggled_on_after_install
|
||||
assert "Taste key" in measure.state("default")["problem"]
|
||||
_load(tiny_h3) # a key is set: the banner goes
|
||||
assert measure.state("default")["problem"] is None
|
||||
|
||||
|
||||
def test_flatness_reads_raw_push_so_a_growing_stream_with_equal_pushes_is_flat():
|
||||
from modules.sampling.block_influence.measure import profile
|
||||
# every block pushes 1.0 raw, but the stream grows with depth: the ratio falls, the raw push does not
|
||||
ratio = [1.0 / (1.0 + i * 0.5) for i in range(10)]
|
||||
rows = [{"reward": w, "rows": {"ratio": torch.tensor(ratio), "raw": torch.ones(10),
|
||||
"novelty": torch.full((10,), 0.5)}} for w in (1.0, 1.0, -1.0, -1.0)]
|
||||
assert profile(rows)["flatness"] == pytest.approx(0.0, abs=1e-6)
|
||||
|
||||
@@ -60,3 +60,12 @@ def test_the_panel_report_reads_the_key_and_says_its_counts(monkeypatch):
|
||||
assert out["key"] == "fox" and out["used"] == 6
|
||||
assert out["counts"]["image"] == 2 and out["counts"]["composition"] == 2
|
||||
assert "composition vs image" in out["cos"]
|
||||
|
||||
|
||||
def test_a_block_some_clip_never_measured_is_left_out_not_guessed():
|
||||
nan = float("nan")
|
||||
rows = [_row(1, None, [1, 0, nan]), _row(1, None, [1, 0, nan]), _row(-1, "image", [0, 1, nan]),
|
||||
_row(-1, "image", [0, 1, nan]), _row(-1, "composition", [0, -1, 0.5]), _row(-1, "composition", [0, -1, 0.5])]
|
||||
out = measure.groups(rows, shuffles=10)
|
||||
assert out["cos"], "the blocks everyone measured still compare"
|
||||
assert out["counts"]["composition"] == 2
|
||||
|
||||
Reference in New Issue
Block a user