coredipper's picture
Upload folder using huggingface_hub
a1bc809 verified
"""
Operon Coalgebra — State Machine Explorer (Gradio Demo)
=======================================================
Three-tab demo:
1. Step-by-Step — drive a counter coalgebra, inspect trace
2. Composition — parallel & sequential machines
3. Finite-Trace Equivalence — compare two machines over a supplied input trace
Run locally:
pip install gradio
python space-coalgebra/app.py
"""
import sys
from pathlib import Path
import gradio as gr
# Allow importing operon_ai from the repo root when running locally
_repo_root = Path(__file__).resolve().parent.parent
if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root))
from operon_ai import (
FunctionalCoalgebra,
ParallelCoalgebra,
SequentialCoalgebra,
StateMachine,
BisimulationResult,
check_bisimulation,
)
# ── Coalgebra factories ──────────────────────────────────────────────────
COALGEBRA_TYPES = {
"Counter (state + input)": lambda: FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: s + i,
),
"Doubler (state * 2, ignores input)": lambda: FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: s * 2,
),
"Modular (state + input) mod 10": lambda: FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: (s + i) % 10,
),
"Saturating (clamps at 100)": lambda: FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: min(s + i, 100),
),
}
# ── HTML helpers ─────────────────────────────────────────────────────────
def _trace_table_html(trace) -> str:
"""Render a transition trace as an HTML table."""
if not trace:
return "<p style='color:#888'>No steps recorded yet.</p>"
rows = []
for t in trace:
rows.append(
f"<tr>"
f"<td style='text-align:center'>{t.step}</td>"
f"<td style='text-align:center'>{t.state_before}</td>"
f"<td style='text-align:center'>{t.input}</td>"
f"<td style='text-align:center;font-weight:600'>{t.output}</td>"
f"<td style='text-align:center'>{t.state_after}</td>"
f"</tr>"
)
return (
"<table style='width:100%;border-collapse:collapse'>"
"<tr style='background:#f3f4f6'>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>Step</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>State Before</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>Input</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>Output</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>State After</th>"
"</tr>"
+ "".join(rows)
+ "</table>"
)
def _state_badge(label: str, value, color: str = "#6366f1") -> str:
return (
f'<span style="display:inline-block;padding:4px 12px;border-radius:6px;'
f'background:{color};color:#fff;font-weight:600;margin:2px">'
f"{label}: {value}</span>"
)
def _bisim_result_html(result: BisimulationResult) -> str:
if result.equivalent:
icon = '<span style="color:#22c55e;font-size:1.5em">&#10003;</span>'
verdict = "Equivalent"
color = "#22c55e"
else:
icon = '<span style="color:#ef4444;font-size:1.5em">&#10007;</span>'
verdict = "Not Equivalent"
color = "#ef4444"
html = (
f'<div style="padding:12px;border:2px solid {color};border-radius:8px">'
f'<div style="font-size:1.2em;font-weight:700;margin-bottom:8px">'
f'{icon} {verdict}</div>'
f'<div style="color:#666">{result.message}</div>'
f'<div style="margin-top:6px;color:#888">States explored: {result.states_explored}</div>'
)
if result.witness:
inp, out_a, out_b = result.witness
html += (
f'<div style="margin-top:8px;padding:8px;background:#fef2f2;border-radius:4px">'
f'<strong>Witness:</strong> input={inp}, '
f'output_a={out_a}, output_b={out_b}</div>'
)
html += "</div>"
return html
# ── Persistent state for Tab 1 ──────────────────────────────────────────
_step_machine: StateMachine | None = None
def _reset_machine(initial_state: int) -> tuple[str, str]:
"""Reset the state machine and return (state_badge, trace_html)."""
global _step_machine
coalgebra = FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: s + i,
)
_step_machine = StateMachine(state=int(initial_state), coalgebra=coalgebra)
badge = _state_badge("Current State", _step_machine.state)
return badge, "<p style='color:#888'>Trace cleared. Ready to step.</p>"
def _do_step(input_val: int) -> tuple[str, str, str]:
"""Step the machine once. Returns (readout, state_badge, trace_html)."""
global _step_machine
if _step_machine is None:
_reset_machine(0)
output = _step_machine.step(int(input_val))
readout = _state_badge("Readout (output)", output, "#3b82f6")
badge = _state_badge("Current State", _step_machine.state)
trace_html = _trace_table_html(_step_machine.trace)
return readout, badge, trace_html
def _run_sequence(seq_text: str) -> tuple[str, str, str]:
"""Run comma-separated inputs. Returns (readout, state_badge, trace)."""
global _step_machine
if _step_machine is None:
_reset_machine(0)
try:
inputs = [int(x.strip()) for x in seq_text.split(",") if x.strip()]
except ValueError:
return (
'<span style="color:#ef4444">Invalid input — use comma-separated integers</span>',
_state_badge("Current State", _step_machine.state),
_trace_table_html(_step_machine.trace),
)
if not inputs:
return (
'<span style="color:#888">No inputs provided</span>',
_state_badge("Current State", _step_machine.state),
_trace_table_html(_step_machine.trace),
)
outputs = _step_machine.run(inputs)
readout = _state_badge("Last Readout", outputs[-1], "#3b82f6")
badge = _state_badge("Current State", _step_machine.state)
trace_html = _trace_table_html(_step_machine.trace)
return readout, badge, trace_html
# ── Tab 2: Composition ──────────────────────────────────────────────────
def _run_composition(
mode: str,
init_s1: int,
init_s2: int,
seq_text: str,
) -> tuple[str, str]:
"""Run parallel or sequential composition.
Returns (state_html, trace_html).
"""
counter = FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: s + i,
)
doubler = FunctionalCoalgebra(
readout_fn=lambda s: s,
update_fn=lambda s, i: s * 2,
)
try:
inputs = [int(x.strip()) for x in seq_text.split(",") if x.strip()]
except ValueError:
return (
'<span style="color:#ef4444">Invalid input — use comma-separated integers</span>',
"",
)
if not inputs:
return ('<span style="color:#888">No inputs provided</span>', "")
init_s1 = int(init_s1)
init_s2 = int(init_s2)
if mode == "Parallel":
composed = ParallelCoalgebra(first=counter, second=doubler)
machine = StateMachine(state=(init_s1, init_s2), coalgebra=composed)
else:
composed = SequentialCoalgebra(first=counter, second=doubler)
machine = StateMachine(state=(init_s1, init_s2), coalgebra=composed)
machine.run(inputs)
# Build trace table with dual states
rows = []
for t in machine.trace:
s1_before, s2_before = t.state_before
s1_after, s2_after = t.state_after
if isinstance(t.output, tuple):
o1, o2 = t.output
out_str = f"({o1}, {o2})"
else:
out_str = str(t.output)
rows.append(
f"<tr>"
f"<td style='text-align:center'>{t.step}</td>"
f"<td style='text-align:center'>({s1_before}, {s2_before})</td>"
f"<td style='text-align:center'>{t.input}</td>"
f"<td style='text-align:center;font-weight:600'>{out_str}</td>"
f"<td style='text-align:center'>({s1_after}, {s2_after})</td>"
f"</tr>"
)
trace_html = (
"<table style='width:100%;border-collapse:collapse'>"
"<tr style='background:#f3f4f6'>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>Step</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>(S1, S2) Before</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>Input</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>Output</th>"
"<th style='padding:6px;border-bottom:2px solid #d1d5db'>(S1, S2) After</th>"
"</tr>"
+ "".join(rows)
+ "</table>"
)
final_s1, final_s2 = machine.state
mode_desc = (
"Both machines receive the same input and evolve independently."
if mode == "Parallel"
else "Counter output feeds as input to Doubler."
)
state_html = (
f'<div style="padding:8px">'
f'<div style="margin-bottom:8px;color:#666">{mode_desc}</div>'
f'{_state_badge("Counter (S1)", final_s1, "#6366f1")} '
f'{_state_badge("Doubler (S2)", final_s2, "#f97316")}'
f"</div>"
)
return state_html, trace_html
# ── Tab 3: Bisimulation ─────────────────────────────────────────────────
BISIM_PRESETS = {
"(custom)": {
"desc": "Configure machines and inputs manually.",
"type_a": "Counter (state + input)",
"init_a": 0,
"type_b": "Counter (state + input)",
"init_b": 0,
"inputs": "1,2,3",
},
"Identical machines": {
"desc": "Two counters starting at 0 — should be equivalent.",
"type_a": "Counter (state + input)",
"init_a": 0,
"type_b": "Counter (state + input)",
"init_b": 0,
"inputs": "1,2,3,4,5",
},
"Different initial state": {
"desc": "Same coalgebra but different starting state — diverges immediately.",
"type_a": "Counter (state + input)",
"init_a": 0,
"type_b": "Counter (state + input)",
"init_b": 5,
"inputs": "1,2,3",
},
"Counter vs Doubler": {
"desc": "Different coalgebras — diverges when outputs differ.",
"type_a": "Counter (state + input)",
"init_a": 0,
"type_b": "Doubler (state * 2, ignores input)",
"init_b": 0,
"inputs": "1,2,3,4",
},
"Counter vs Modular": {
"desc": "Counter vs mod-10 counter — equivalent until state exceeds 10.",
"type_a": "Counter (state + input)",
"init_a": 0,
"type_b": "Modular (state + input) mod 10",
"init_b": 0,
"inputs": "1,2,3,4,5",
},
}
def _load_bisim_preset(name: str) -> tuple[str, int, str, int, str]:
p = BISIM_PRESETS.get(name, BISIM_PRESETS["(custom)"])
return p["type_a"], p["init_a"], p["type_b"], p["init_b"], p["inputs"]
def _run_bisimulation(
type_a: str,
init_a: int,
type_b: str,
init_b: int,
seq_text: str,
) -> str:
"""Check bounded observational equivalence and return result HTML."""
coal_a = COALGEBRA_TYPES.get(type_a, list(COALGEBRA_TYPES.values())[0])()
coal_b = COALGEBRA_TYPES.get(type_b, list(COALGEBRA_TYPES.values())[0])()
machine_a = StateMachine(state=int(init_a), coalgebra=coal_a)
machine_b = StateMachine(state=int(init_b), coalgebra=coal_b)
try:
inputs = [int(x.strip()) for x in seq_text.split(",") if x.strip()]
except ValueError:
return '<span style="color:#ef4444">Invalid input — use comma-separated integers</span>'
result = check_bisimulation(machine_a, machine_b, inputs)
return _bisim_result_html(result)
# ── Gradio UI ────────────────────────────────────────────────────────────
def build_app() -> gr.Blocks:
with gr.Blocks(title="Coalgebra Explorer") as app:
gr.Markdown(
"# ⚙️ Coalgebra — State Machine Explorer\n"
"Build, compose, and compare **coalgebraic state machines** "
"with full transition tracing."
)
with gr.Tabs():
# ── Tab 1: Step-by-Step ──────────────────────────────────
with gr.TabItem("Step-by-Step"):
gr.Markdown(
"Drive a **counter coalgebra** (`state + input`) one step "
"at a time or run a whole sequence. The trace table records "
"every transition."
)
with gr.Row():
init_sl = gr.Slider(
-50, 50, value=0, step=1,
label="Initial State",
)
reset_btn = gr.Button("Reset", variant="secondary")
state_html = gr.HTML(
value=_state_badge("Current State", 0),
label="Current State",
)
with gr.Row():
input_sl = gr.Slider(
-10, 10, value=1, step=1,
label="Input Value",
)
step_btn = gr.Button("Step", variant="primary")
readout_html = gr.HTML(label="Readout")
with gr.Row():
seq_tb = gr.Textbox(
label="Run Sequence (comma-separated)",
placeholder="1,2,3,-1,5",
scale=3,
)
seq_btn = gr.Button("Run Sequence", variant="primary", scale=1)
trace_html = gr.HTML(
value="<p style='color:#888'>Press Reset to initialize, then Step or Run Sequence.</p>",
label="Transition Trace",
)
# Wire events
reset_btn.click(
fn=_reset_machine,
inputs=[init_sl],
outputs=[state_html, trace_html],
)
step_btn.click(
fn=_do_step,
inputs=[input_sl],
outputs=[readout_html, state_html, trace_html],
)
seq_btn.click(
fn=_run_sequence,
inputs=[seq_tb],
outputs=[readout_html, state_html, trace_html],
)
# ── Tab 2: Composition ───────────────────────────────────
with gr.TabItem("Composition"):
gr.Markdown(
"Compose a **Counter** (`state + input`) and a **Doubler** "
"(`state * 2`) in parallel or sequentially.\n\n"
"- **Parallel**: both receive the same input, evolve independently\n"
"- **Sequential**: counter's readout feeds as the doubler's input"
)
with gr.Row():
comp_mode = gr.Dropdown(
choices=["Parallel", "Sequential"],
value="Parallel",
label="Composition Mode",
)
comp_s1 = gr.Slider(-20, 20, value=0, step=1, label="Counter Initial State (S1)")
comp_s2 = gr.Slider(-20, 20, value=1, step=1, label="Doubler Initial State (S2)")
with gr.Row():
comp_seq = gr.Textbox(
label="Input Sequence (comma-separated)",
placeholder="1,2,3,4,5",
value="1,2,3,4,5",
scale=3,
)
comp_btn = gr.Button("Run", variant="primary", scale=1)
comp_state_html = gr.HTML(label="Final States")
comp_trace_html = gr.HTML(label="Transition Trace")
comp_btn.click(
fn=_run_composition,
inputs=[comp_mode, comp_s1, comp_s2, comp_seq],
outputs=[comp_state_html, comp_trace_html],
)
# ── Tab 3: Finite-Trace Equivalence ─────────────────────
with gr.TabItem("Finite-Trace Equivalence"):
gr.Markdown(
"Check whether two machines produce **identical outputs** "
"over a given input sequence. This is a **bounded** check, "
"not an all-input proof. If they diverge, the first "
"diverging input is shown as a *witness*."
)
with gr.Row():
bisim_preset = gr.Dropdown(
choices=list(BISIM_PRESETS.keys()),
value="Identical machines",
label="Preset Scenario",
scale=2,
)
with gr.Row():
with gr.Column():
gr.Markdown("### Machine A")
bisim_type_a = gr.Dropdown(
choices=list(COALGEBRA_TYPES.keys()),
value="Counter (state + input)",
label="Coalgebra Type",
)
bisim_init_a = gr.Slider(
-50, 50, value=0, step=1,
label="Initial State",
)
with gr.Column():
gr.Markdown("### Machine B")
bisim_type_b = gr.Dropdown(
choices=list(COALGEBRA_TYPES.keys()),
value="Counter (state + input)",
label="Coalgebra Type",
)
bisim_init_b = gr.Slider(
-50, 50, value=0, step=1,
label="Initial State",
)
with gr.Row():
bisim_seq = gr.Textbox(
label="Input Sequence (comma-separated)",
placeholder="1,2,3,4,5",
value="1,2,3,4,5",
scale=3,
)
bisim_btn = gr.Button("Check Equivalence", variant="primary", scale=1)
bisim_result_html = gr.HTML(label="Result")
bisim_preset.change(
fn=_load_bisim_preset,
inputs=[bisim_preset],
outputs=[bisim_type_a, bisim_init_a, bisim_type_b, bisim_init_b, bisim_seq],
)
bisim_btn.click(
fn=_run_bisimulation,
inputs=[bisim_type_a, bisim_init_a, bisim_type_b, bisim_init_b, bisim_seq],
outputs=[bisim_result_html],
)
return app
if __name__ == "__main__":
app = build_app()
app.launch(theme=gr.themes.Soft())