Consensus

Head slot tracking, justification, finalization, and fork choice analysis for PQ Devnet clients.

This notebook examines:

  • Head slot vs current slot (how far behind each client is)
  • Justified and finalized slot progression
  • Head-to-justified, justified-to-finalized, and head-to-finalized distances
  • Fork choice reorgs
Show code
import json
from pathlib import Path

import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from IPython.display import display

# Set default renderer for static HTML output
import plotly.io as pio
pio.renderers.default = "notebook"
Show code
# Resolve devnet_id
DATA_DIR = Path("../data")

if devnet_id is None:
    devnets_path = DATA_DIR / "devnets.json"
    if devnets_path.exists():
        with open(devnets_path) as f:
            devnets = json.load(f).get("devnets", [])
        if devnets:
            devnet_id = devnets[-1]["id"]
            print(f"Using latest devnet: {devnet_id}")
    else:
        raise ValueError("No devnets.json found. Run 'just detect-devnets' first.")

DEVNET_DIR = DATA_DIR / devnet_id
print(f"Loading data from: {DEVNET_DIR}")
Loading data from: ../data/pqdevnet-20260627T0839Z
Show code
# Load devnet metadata
with open(DATA_DIR / "devnets.json") as f:
    devnets_data = json.load(f)
    devnet_info = next((d for d in devnets_data["devnets"] if d["id"] == devnet_id), None)

if devnet_info:
    print(f"Devnet: {devnet_info['id']}")
    print(f"Duration: {devnet_info['duration_hours']:.1f} hours")
    print(f"Time: {devnet_info['start_time']} to {devnet_info['end_time']}")
    print(f"Slots: {devnet_info['start_slot']} \u2192 {devnet_info['end_slot']}")
    print(f"Clients: {', '.join(devnet_info['clients'])}")
Devnet: pqdevnet-20260627T0839Z
Duration: 0.6 hours
Time: 2026-06-27T08:39:30+00:00 to 2026-06-27T09:12:17+00:00
Slots: 0 β†’ 407
Clients: buildx_buildkit_multiarch0, zeam_0, zeam_1, zeam_10, zeam_11, zeam_12, zeam_13, zeam_14, zeam_15, zeam_16, zeam_17, zeam_18, zeam_19, zeam_2, zeam_20, zeam_21, zeam_22, zeam_23, zeam_24, zeam_25, zeam_26, zeam_27, zeam_28, zeam_29, zeam_3, zeam_30, zeam_31, zeam_4, zeam_5, zeam_6, zeam_7, zeam_8, zeam_9

Load DataΒΆ

Show code
# Unified client list from devnet metadata (includes all containers via cAdvisor)
all_clients = sorted(devnet_info["clients"])
n_cols = min(len(all_clients), 2)
n_rows = -(-len(all_clients) // n_cols)

# Load head slot data
head_df = pd.read_parquet(DEVNET_DIR / "head_slot.parquet")
head_df = head_df.groupby(["client", "metric", "timestamp"], as_index=False)["value"].max()
print(f"Head slot: {len(head_df)} records, clients: {sorted(head_df['client'].unique())}")

# Load finality data
finality_df = pd.read_parquet(DEVNET_DIR / "finality_metrics.parquet")
finality_df = finality_df.groupby(["client", "metric", "timestamp"], as_index=False)["value"].max()
print(f"Finality: {len(finality_df)} records, clients: {sorted(finality_df['client'].unique())}")
print(f"Finality metrics: {sorted(finality_df['metric'].unique())}")

# Load fork choice reorgs
reorgs_df = pd.read_parquet(DEVNET_DIR / "fork_choice_reorgs.parquet")
reorgs_df = reorgs_df.groupby(["client", "timestamp"], as_index=False)["value"].max()
print(f"Reorgs: {len(reorgs_df)} records, clients: {sorted(reorgs_df['client'].unique())}")

print(f"\nAll clients ({len(all_clients)}): {all_clients}")
Head slot: 2104 records, clients: ['zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']
Finality: 2104 records, clients: ['zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']
Finality metrics: ['lean_latest_finalized_slot', 'lean_latest_justified_slot']
Reorgs: 1052 records, clients: ['zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']

All clients (33): ['buildx_buildkit_multiarch0', 'zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']

Head Slot vs Current SlotΒΆ

Comparing each client's head slot (lean_head_slot) against the expected current slot (lean_current_slot). A gap indicates the client is falling behind.

Show code
fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

colors = {"lean_head_slot": "#636EFA", "lean_current_slot": "#EF553B"}
labels = {"lean_head_slot": "head_slot", "lean_current_slot": "current_slot"}
legend_added = set()

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = head_df[head_df["client"] == client]
    has_data = False
    for metric in ["lean_current_slot", "lean_head_slot"]:
        mdf = cdf[cdf["metric"] == metric].sort_values("timestamp")
        if mdf.empty:
            continue
        has_data = True
        label = labels[metric]
        show_legend = metric not in legend_added
        legend_added.add(metric)
        fig.add_trace(
            go.Scatter(
                x=mdf["timestamp"], y=mdf["value"],
                name=label, legendgroup=metric,
                showlegend=show_legend,
                line=dict(color=colors[metric]),
            ),
            row=row, col=col,
        )
    if not has_data:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slot", row=row, col=col)

fig.update_layout(
    title="Head Slot vs Current Slot by Client",
    height=270 * n_rows,
)
fig.show()

Head, Justified & Finalized SlotsΒΆ

Progression of justified and finalized slots over time. With 3SF, both should track closely behind the head slot.

Show code
jf_metrics = ["lean_latest_justified_slot", "lean_latest_finalized_slot"]
jf_df = finality_df[finality_df["metric"].isin(jf_metrics)].copy()

head_only_all = head_df[head_df["metric"] == "lean_head_slot"].copy()
head_only_all["metric"] = "lean_head_slot"

combined = pd.concat([jf_df, head_only_all], ignore_index=True)

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

colors = {
    "lean_head_slot": "#636EFA",
    "lean_latest_justified_slot": "#00CC96",
    "lean_latest_finalized_slot": "#EF553B",
}
labels = {
    "lean_head_slot": "head",
    "lean_latest_justified_slot": "justified",
    "lean_latest_finalized_slot": "finalized",
}
legend_added = set()

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = combined[combined["client"] == client]
    has_data = False
    for metric in ["lean_head_slot", "lean_latest_justified_slot", "lean_latest_finalized_slot"]:
        mdf = cdf[cdf["metric"] == metric].sort_values("timestamp")
        if mdf.empty:
            continue
        has_data = True
        show_legend = metric not in legend_added
        legend_added.add(metric)
        fig.add_trace(
            go.Scatter(
                x=mdf["timestamp"], y=mdf["value"],
                name=labels[metric], legendgroup=metric,
                showlegend=show_legend,
                line=dict(color=colors[metric]),
            ),
            row=row, col=col,
        )
    if not has_data:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slot", row=row, col=col)

fig.update_layout(
    title="Head, Justified & Finalized Slot by Client",
    height=270 * n_rows,
)
fig.show()

Head-to-Finalized DistanceΒΆ

Total distance between head slot and finalized slot. This is the combined gap across justification and finalization, showing the overall finality lag.

finalized
justified
head
current
Show code
head_ts_hf = head_df[head_df["metric"] == "lean_head_slot"][["client", "timestamp", "value"]].rename(columns={"value": "head_slot"})
fin_ts_hf = finality_df[finality_df["metric"] == "lean_latest_finalized_slot"][["client", "timestamp", "value"]].rename(columns={"value": "finalized_slot"})
head_fin_lag = head_ts_hf.merge(fin_ts_hf, on=["client", "timestamp"], how="inner")
head_fin_lag["lag"] = head_fin_lag["head_slot"] - head_fin_lag["finalized_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = head_fin_lag[head_fin_lag["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots", row=row, col=col)

fig.update_layout(
    title="Head-to-Finalized Distance (head_slot - finalized_slot)",
    height=270 * n_rows,
)
fig.show()

Current-to-Head DistanceΒΆ

Difference between current slot and head slot. A value of 0 means the client is fully synced; higher values indicate falling behind.

finalized
justified
head
current
Show code
current_df = head_df[head_df["metric"] == "lean_current_slot"][["client", "timestamp", "value"]].rename(columns={"value": "current_slot"})
head_only = head_df[head_df["metric"] == "lean_head_slot"][["client", "timestamp", "value"]].rename(columns={"value": "head_slot"})
lag_df = current_df.merge(head_only, on=["client", "timestamp"], how="inner")
lag_df["lag"] = lag_df["current_slot"] - lag_df["head_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = lag_df[lag_df["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots behind", row=row, col=col)

fig.update_layout(
    title="Current-to-Head Distance (current_slot - head_slot)",
    height=270 * n_rows,
)
fig.show()

Head-to-Justified DistanceΒΆ

Gap between head slot and justified slot. A growing gap means the client's head is advancing but justification is not keeping up.

finalized
justified
head
current
Show code
head_ts = head_df[head_df["metric"] == "lean_head_slot"][["client", "timestamp", "value"]].rename(columns={"value": "head_slot"})
just_ts = finality_df[finality_df["metric"] == "lean_latest_justified_slot"][["client", "timestamp", "value"]].rename(columns={"value": "justified_slot"})
justification_lag = head_ts.merge(just_ts, on=["client", "timestamp"], how="inner")
justification_lag["lag"] = justification_lag["head_slot"] - justification_lag["justified_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = justification_lag[justification_lag["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots", row=row, col=col)

fig.update_layout(
    title="Head-to-Justified Distance (head_slot - justified_slot)",
    height=270 * n_rows,
)
fig.show()

Justified-to-Finalized DistanceΒΆ

Gap between justified slot and finalized slot. A growing gap means justification is advancing but finalization is stalling.

finalized
justified
head
current
Show code
fin_ts = finality_df[finality_df["metric"] == "lean_latest_finalized_slot"][["client", "timestamp", "value"]].rename(columns={"value": "finalized_slot"})
finality_lag = just_ts.merge(fin_ts, on=["client", "timestamp"], how="inner")
finality_lag["lag"] = finality_lag["justified_slot"] - finality_lag["finalized_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = finality_lag[finality_lag["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots", row=row, col=col)

fig.update_layout(
    title="Justified-to-Finalized Distance (justified_slot - finalized_slot)",
    height=270 * n_rows,
)
fig.show()

Fork Choice ReorgsΒΆ

Cumulative chain reorgs per client. Reorgs occur when the fork choice rule switches to a different chain head, often caused by late-arriving blocks or attestations.

Show code
fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = reorgs_df[reorgs_df["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["value"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Cumulative reorgs", row=row, col=col)

fig.update_layout(
    title="Fork Choice Reorgs by Client",
    height=270 * n_rows,
)
fig.show()

SummaryΒΆ

Show code
summary_rows = []

for client in all_clients:
    row = {"Client": client}

    # Current-to-head distance
    client_lag = lag_df[lag_df["client"] == client]["lag"]
    if not client_lag.empty:
        row["Avg C-H Dist."] = f"{client_lag.mean():.1f}"
        row["Max C-H Dist."] = f"{client_lag.max():.0f}"

    # Head-to-justified distance
    client_just = justification_lag[justification_lag["client"] == client]["lag"]
    if not client_just.empty:
        row["Avg H-J Dist."] = f"{client_just.mean():.1f}"
        row["Max H-J Dist."] = f"{client_just.max():.0f}"

    # Justified-to-finalized distance
    client_fin = finality_lag[finality_lag["client"] == client]["lag"]
    if not client_fin.empty:
        row["Avg J-F Dist."] = f"{client_fin.mean():.1f}"
        row["Max J-F Dist."] = f"{client_fin.max():.0f}"

    # Head-to-finalized distance
    client_hf = head_fin_lag[head_fin_lag["client"] == client]["lag"]
    if not client_hf.empty:
        row["Avg H-F Dist."] = f"{client_hf.mean():.1f}"
        row["Max H-F Dist."] = f"{client_hf.max():.0f}"

    # Reorgs
    client_reorgs = reorgs_df[reorgs_df["client"] == client]["value"]
    if not client_reorgs.empty:
        row["Reorgs"] = f"{client_reorgs.max():.0f}"

    # Final head slot
    client_head = head_df[(head_df["client"] == client) & (head_df["metric"] == "lean_head_slot")]
    if not client_head.empty:
        row["Final Head Slot"] = f"{client_head['value'].max():.0f}"

    # Final finalized slot
    client_finalized = finality_df[(finality_df["client"] == client) & (finality_df["metric"] == "lean_latest_finalized_slot")]
    if not client_finalized.empty:
        row["Final Finalized Slot"] = f"{client_finalized['value'].max():.0f}"

    summary_rows.append(row)

if summary_rows:
    summary_df = pd.DataFrame(summary_rows).set_index("Client").fillna("-")
    display(summary_df)

print(f"\nDevnet: {devnet_id}")
if devnet_info:
    print(f"Duration: {devnet_info['duration_hours']:.1f} hours")
Avg C-H Dist. Max C-H Dist. Avg H-J Dist. Max H-J Dist. Avg J-F Dist. Max J-F Dist. Avg H-F Dist. Max H-F Dist. Reorgs Final Head Slot Final Finalized Slot
Client
buildx_buildkit_multiarch0 - - - - - - - - - - -
zeam_0 4.8 15 116.2 319 69.8 100 185.9 419 31 419 0
zeam_1 5.9 26 111.7 319 70.4 100 182.1 419 33 419 0
zeam_10 8.6 31 114.5 326 66.7 100 181.2 426 31 426 0
zeam_11 3.3 12 115.8 317 70.7 100 186.5 417 33 417 0
zeam_12 3.1 13 114.6 319 70.4 100 185.0 419 34 419 0
zeam_13 10.1 30 109.8 297 69.1 100 178.9 397 15 397 0
zeam_14 5.2 48 115.7 322 68.9 100 184.6 422 21 422 0
zeam_15 6.9 29 112.7 310 71.1 100 183.8 410 26 410 0
zeam_16 3.8 20 113.9 322 70.4 100 184.3 422 29 422 0
zeam_17 13.2 135 109.6 319 71.2 100 180.8 419 18 419 0
zeam_18 11.8 31 133.4 346 43.7 56 177.1 402 10 402 0
zeam_19 8.5 33 109.9 303 68.7 100 178.6 403 26 403 0
zeam_2 3.3 12 115.4 322 71.1 100 186.5 422 28 422 0
zeam_20 4.2 16 114.3 307 68.7 100 183.0 407 30 407 0
zeam_21 9.8 30 135.1 349 44.0 56 179.2 405 13 405 0
zeam_22 4.0 20 117.7 318 72.2 100 189.9 418 40 418 0
zeam_23 9.4 71 109.2 307 70.4 100 179.6 407 24 407 0
zeam_24 7.8 33 110.2 312 70.0 100 180.2 412 22 412 0
zeam_25 9.7 31 139.2 353 45.1 56 184.2 409 15 409 0
zeam_26 4.1 20 113.9 310 70.0 100 184.0 410 29 410 0
zeam_27 25.9 424 97.5 311 66.3 100 163.9 411 14 411 0
zeam_28 6.7 68 112.8 312 71.1 100 184.0 412 25 412 0
zeam_29 5.7 26 113.7 319 68.7 100 182.4 419 39 419 0
zeam_3 5.5 24 114.5 322 69.8 100 184.3 422 45 422 0
zeam_30 5.0 19 112.2 319 70.0 100 182.2 419 29 419 0
zeam_31 9.7 30 111.0 315 67.4 100 178.4 415 13 415 0
zeam_4 9.8 31 112.0 320 68.0 100 180.0 420 16 420 0
zeam_5 15.8 135 104.7 321 69.4 100 174.1 421 22 421 0
zeam_6 4.7 29 116.1 322 72.2 100 188.3 422 34 422 0
zeam_7 5.9 31 112.7 323 70.4 100 183.1 423 35 423 0
zeam_8 9.6 31 136.2 368 44.0 56 180.2 424 12 424 0
zeam_9 9.3 34 114.3 325 67.1 100 181.4 425 23 425 0
Devnet: pqdevnet-20260627T0839Z
Duration: 0.6 hours