feat(dashboard): aquarium click handler with info panel + ancestor lineage
Rimuove sidebar acquario (slider max-pesci, toggle label): la dimensione popolazione è già definita dal GA, le label sono ridondanti col pannello di ispezione. Mostra tutti i pesci della generazione selezionata. Aggiunge `build_lineage_index` (mappa ogni genome_id della run ai suoi attributi) e `trace_ancestors` (BFS sui parent_ids fino a max_levels, guardia su cicli). `build_fish_dataset` accetta ora il lineage_index e allega il campo `ancestors` ad ogni pesce; conserva la firma legacy per compat con i fixture di test esistenti. `build_aquarium_html` perde `show_labels`. Embedda click handler con hit-test in canvas pixel space (account per CSS scaling) + pannello info top-right con stile, fitness/DSR/Sharpe/maxDD/trades, prompt e albero discendenza colorato per cognitive_style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ def test_dashboard_data_helpers_signatures():
|
||||
assert hasattr(data, "genomes_df")
|
||||
|
||||
|
||||
def test_aquarium_helper_builds_html():
|
||||
def test_aquarium_helper_builds_html_with_click_handler():
|
||||
from multi_swarm.dashboard.aquarium import build_aquarium_html
|
||||
|
||||
fish = [
|
||||
@@ -26,40 +26,55 @@ def test_aquarium_helper_builds_html():
|
||||
"cognitive_style": "physicist",
|
||||
"n_trades": 30,
|
||||
"dsr": 0.7,
|
||||
},
|
||||
{
|
||||
"id": "def456",
|
||||
"fitness": 0.0,
|
||||
"cognitive_style": "biologist",
|
||||
"n_trades": 0,
|
||||
"dsr": 0.0,
|
||||
},
|
||||
"sharpe": 1.2,
|
||||
"max_dd": 0.1,
|
||||
"system_prompt": "test",
|
||||
"temperature": 0.9,
|
||||
"lookback_window": 200,
|
||||
"feature_access": ["close"],
|
||||
"model_tier": "C",
|
||||
"generation": 1,
|
||||
"parent_ids": [],
|
||||
"ancestors": [],
|
||||
}
|
||||
]
|
||||
html = build_aquarium_html(fish, canvas_w=800, canvas_h=400, show_labels=True)
|
||||
html = build_aquarium_html(fish, canvas_w=800, canvas_h=400)
|
||||
assert "canvas" in html
|
||||
assert "abc123" in html
|
||||
assert "physicist" in html or "4cc9f0" in html
|
||||
assert "abc123" in html # fish id present in JSON payload
|
||||
assert "addEventListener('click'" in html
|
||||
assert "fish-info-panel" in html
|
||||
assert "showFishInfo" in html
|
||||
assert "Discendenza" in html
|
||||
assert "requestAnimationFrame" in html
|
||||
|
||||
|
||||
def test_aquarium_build_fish_dataset_sorts_and_caps():
|
||||
def test_aquarium_build_fish_dataset_legacy_path():
|
||||
from multi_swarm.dashboard.aquarium import build_fish_dataset
|
||||
|
||||
df = pd.DataFrame(
|
||||
[
|
||||
{"genome_id": "low", "fitness": 0.1, "cognitive_style": "physicist",
|
||||
"n_trades": 1, "dsr": 0.0},
|
||||
{"genome_id": "high", "fitness": 0.9, "cognitive_style": "biologist",
|
||||
"n_trades": 10, "dsr": 0.5},
|
||||
{"genome_id": "mid", "fitness": 0.5, "cognitive_style": "engineer",
|
||||
"n_trades": 5, "dsr": 0.2},
|
||||
{
|
||||
"genome_id": "low",
|
||||
"fitness": 0.1,
|
||||
"cognitive_style": "physicist",
|
||||
"n_trades": 1,
|
||||
"dsr": 0.0,
|
||||
},
|
||||
{
|
||||
"genome_id": "high",
|
||||
"fitness": 0.9,
|
||||
"cognitive_style": "biologist",
|
||||
"n_trades": 10,
|
||||
"dsr": 0.5,
|
||||
},
|
||||
]
|
||||
)
|
||||
out = build_fish_dataset(df, max_fish=2)
|
||||
assert len(out) == 2
|
||||
assert out[0]["id"] == "high"
|
||||
assert out[1]["id"] == "mid"
|
||||
assert out[0]["cognitive_style"] == "biologist"
|
||||
out = build_fish_dataset(df)
|
||||
ids = {f["id"] for f in out}
|
||||
assert ids == {"low", "high"}
|
||||
high = next(f for f in out if f["id"] == "high")
|
||||
assert high["cognitive_style"] == "biologist"
|
||||
assert high["ancestors"] == []
|
||||
|
||||
|
||||
def test_aquarium_build_fish_dataset_drops_nan_fitness():
|
||||
@@ -67,10 +82,20 @@ def test_aquarium_build_fish_dataset_drops_nan_fitness():
|
||||
|
||||
df = pd.DataFrame(
|
||||
[
|
||||
{"genome_id": "ok", "fitness": 0.4, "cognitive_style": "historian",
|
||||
"n_trades": 2, "dsr": 0.1},
|
||||
{"genome_id": "bad", "fitness": float("nan"), "cognitive_style": "ecologist",
|
||||
"n_trades": 0, "dsr": 0.0},
|
||||
{
|
||||
"genome_id": "ok",
|
||||
"fitness": 0.4,
|
||||
"cognitive_style": "historian",
|
||||
"n_trades": 2,
|
||||
"dsr": 0.1,
|
||||
},
|
||||
{
|
||||
"genome_id": "bad",
|
||||
"fitness": float("nan"),
|
||||
"cognitive_style": "ecologist",
|
||||
"n_trades": 0,
|
||||
"dsr": 0.0,
|
||||
},
|
||||
]
|
||||
)
|
||||
out = build_fish_dataset(df)
|
||||
@@ -85,3 +110,201 @@ def test_aquarium_empty_input_returns_empty():
|
||||
html = build_aquarium_html([], canvas_w=400, canvas_h=200)
|
||||
assert "canvas" in html
|
||||
assert "Acquario vuoto" in html
|
||||
|
||||
|
||||
def test_build_lineage_index_returns_dict_keyed_by_id():
|
||||
from multi_swarm.dashboard.aquarium import build_lineage_index
|
||||
|
||||
genomes = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"generation_idx": 0,
|
||||
"generation": 0,
|
||||
"system_prompt": "x",
|
||||
"feature_access": ["close"],
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.95,
|
||||
"model_tier": "C",
|
||||
"lookback_window": 100,
|
||||
"cognitive_style": "physicist",
|
||||
"parent_ids": [],
|
||||
},
|
||||
{
|
||||
"id": "g2",
|
||||
"generation_idx": 1,
|
||||
"generation": 1,
|
||||
"system_prompt": "y",
|
||||
"feature_access": ["close", "volume"],
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.95,
|
||||
"model_tier": "C",
|
||||
"lookback_window": 200,
|
||||
"cognitive_style": "biologist",
|
||||
"parent_ids": ["g1"],
|
||||
},
|
||||
]
|
||||
)
|
||||
evals = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"genome_id": "g1",
|
||||
"fitness": 0.5,
|
||||
"dsr": 0.6,
|
||||
"sharpe": 1.2,
|
||||
"max_dd": 0.1,
|
||||
"n_trades": 30,
|
||||
"parse_error": None,
|
||||
"raw_text": "",
|
||||
},
|
||||
{
|
||||
"genome_id": "g2",
|
||||
"fitness": 0.7,
|
||||
"dsr": 0.8,
|
||||
"sharpe": 1.5,
|
||||
"max_dd": 0.05,
|
||||
"n_trades": 40,
|
||||
"parse_error": None,
|
||||
"raw_text": "",
|
||||
},
|
||||
]
|
||||
)
|
||||
idx = build_lineage_index(genomes, evals)
|
||||
assert "g1" in idx and "g2" in idx
|
||||
assert idx["g2"]["parent_ids"] == ["g1"]
|
||||
assert idx["g2"]["fitness"] == 0.7
|
||||
assert idx["g1"]["cognitive_style"] == "physicist"
|
||||
assert idx["g2"]["feature_access"] == ["close", "volume"]
|
||||
|
||||
|
||||
def test_trace_ancestors_walks_levels():
|
||||
from multi_swarm.dashboard.aquarium import trace_ancestors
|
||||
|
||||
idx = {
|
||||
"child": {
|
||||
"id": "child",
|
||||
"parent_ids": ["p1", "p2"],
|
||||
"fitness": 0.8,
|
||||
"generation": 2,
|
||||
"cognitive_style": "physicist",
|
||||
},
|
||||
"p1": {
|
||||
"id": "p1",
|
||||
"parent_ids": ["gp1"],
|
||||
"fitness": 0.5,
|
||||
"generation": 1,
|
||||
"cognitive_style": "biologist",
|
||||
},
|
||||
"p2": {
|
||||
"id": "p2",
|
||||
"parent_ids": [],
|
||||
"fitness": 0.3,
|
||||
"generation": 1,
|
||||
"cognitive_style": "engineer",
|
||||
},
|
||||
"gp1": {
|
||||
"id": "gp1",
|
||||
"parent_ids": [],
|
||||
"fitness": 0.2,
|
||||
"generation": 0,
|
||||
"cognitive_style": "historian",
|
||||
},
|
||||
}
|
||||
levels = trace_ancestors("child", idx, max_levels=5)
|
||||
assert len(levels) == 2
|
||||
assert {a["id"] for a in levels[0]} == {"p1", "p2"}
|
||||
assert {a["id"] for a in levels[1]} == {"gp1"}
|
||||
|
||||
|
||||
def test_trace_ancestors_handles_cycles():
|
||||
from multi_swarm.dashboard.aquarium import trace_ancestors
|
||||
|
||||
# Pathological cycle: a <-> b. Should terminate cleanly.
|
||||
idx = {
|
||||
"a": {
|
||||
"id": "a",
|
||||
"parent_ids": ["b"],
|
||||
"fitness": 0.1,
|
||||
"generation": 1,
|
||||
"cognitive_style": "physicist",
|
||||
},
|
||||
"b": {
|
||||
"id": "b",
|
||||
"parent_ids": ["a"],
|
||||
"fitness": 0.2,
|
||||
"generation": 0,
|
||||
"cognitive_style": "biologist",
|
||||
},
|
||||
}
|
||||
levels = trace_ancestors("a", idx, max_levels=5)
|
||||
# a -> b at level 0; b's only parent is a, already seen -> stop.
|
||||
assert len(levels) == 1
|
||||
assert levels[0][0]["id"] == "b"
|
||||
|
||||
|
||||
def test_trace_ancestors_no_parents_returns_empty():
|
||||
from multi_swarm.dashboard.aquarium import trace_ancestors
|
||||
|
||||
idx = {
|
||||
"solo": {
|
||||
"id": "solo",
|
||||
"parent_ids": [],
|
||||
"fitness": 0.4,
|
||||
"generation": 0,
|
||||
"cognitive_style": "engineer",
|
||||
},
|
||||
}
|
||||
assert trace_ancestors("solo", idx) == []
|
||||
|
||||
|
||||
def test_build_fish_dataset_attaches_ancestors():
|
||||
from multi_swarm.dashboard.aquarium import build_fish_dataset, build_lineage_index
|
||||
|
||||
genomes = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"id": "p",
|
||||
"generation_idx": 0,
|
||||
"generation": 0,
|
||||
"system_prompt": "p",
|
||||
"feature_access": ["close"],
|
||||
"temperature": 0.8,
|
||||
"top_p": 0.9,
|
||||
"model_tier": "C",
|
||||
"lookback_window": 100,
|
||||
"cognitive_style": "physicist",
|
||||
"parent_ids": [],
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
"generation_idx": 1,
|
||||
"generation": 1,
|
||||
"system_prompt": "c",
|
||||
"feature_access": ["close"],
|
||||
"temperature": 0.8,
|
||||
"top_p": 0.9,
|
||||
"model_tier": "C",
|
||||
"lookback_window": 120,
|
||||
"cognitive_style": "biologist",
|
||||
"parent_ids": ["p"],
|
||||
},
|
||||
]
|
||||
)
|
||||
evals = pd.DataFrame(
|
||||
[
|
||||
{"genome_id": "p", "fitness": 0.3, "dsr": 0.0, "sharpe": 0.0,
|
||||
"max_dd": 0.0, "n_trades": 0},
|
||||
{"genome_id": "c", "fitness": 0.6, "dsr": 0.0, "sharpe": 0.0,
|
||||
"max_dd": 0.0, "n_trades": 0},
|
||||
]
|
||||
)
|
||||
lineage = build_lineage_index(genomes, evals)
|
||||
|
||||
active = genomes[genomes["generation_idx"] == 1].merge(
|
||||
evals, left_on="id", right_on="genome_id", how="left"
|
||||
)
|
||||
fish = build_fish_dataset(active, lineage)
|
||||
assert len(fish) == 1
|
||||
assert fish[0]["id"] == "c"
|
||||
assert len(fish[0]["ancestors"]) == 1
|
||||
assert fish[0]["ancestors"][0][0]["id"] == "p"
|
||||
|
||||
Reference in New Issue
Block a user