CoolFace
Apppublic

pschenone/defense_app

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
app.py331 linesDownload Raw Back to root
1"""2defense_app.py — Gradio UI for the Defensive Positioning Model (v2)3Upload as "app.py" to your Hugging Face Space.4"""5 6import io7import os8import tempfile9from copy import deepcopy10 11import gradio as gr12import numpy as np13import pandas as pd14 15import run_defense as rd16 17# ─────────────────────────────────────────────────────────────────────────────18# Presets19# ─────────────────────────────────────────────────────────────────────────────20 21EFFORT_PRESETS = {22    "Preview — fast sanity check":    {"n_seeds": 3,  "n_steps": 600,  "refine_n": 100},23    "Standard — good interactive run":{"n_seeds": 8,  "n_steps": 3000, "refine_n": 500},24    "Research — slower, more thorough":{"n_seeds":14, "n_steps": 8000, "refine_n": 1200},25}26 27AREA_OPTIONS = {28    "Central — ball through the middle":     "central",29    "Wide — ball on right wing":             "wide_right",30    "Wide — ball on left wing":              "wide_left",31    "System view — all three areas jointly": "system",32}33 34 35# ─────────────────────────────────────────────────────────────────────────────36# Core run function37# ─────────────────────────────────────────────────────────────────────────────38 39def run_defense(area_label, danger_ui, d_min, effort_label, possession_file, seed):40    area_key = AREA_OPTIONS[area_label]41    params   = deepcopy(rd.PARAMS_DEFAULT)42    params.update(EFFORT_PRESETS[effort_label])43 44    # Danger concentration: slider 0–100 maps to k = 0–2.045    # k=1 → box 2.7× more important than halfway (good default)46    # k=2 → box 7.4× more important (strong emphasis)47    params["danger_concentration"] = float(danger_ui) / 50.048    params["d_min"]                = float(d_min)49 50    # Load possession coordinates if provided51    possession = None52    if possession_file is not None:53        try:54            possession = rd.load_possession_csv(possession_file.name)55            params["transition_weight"] = 0.356        except Exception:57            pass58    else:59        params["transition_weight"] = 0.060 61    tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)62    tmp.close()63 64    if area_key == "system":65        system = rd.optimise_system(params, seed=int(seed))66        rd.plot_system(system, savepath=tmp.name)67        import matplotlib.pyplot as plt68        plt.close("all")69        md = _system_markdown(system, params)70        csv_str = _system_csv(system)71    else:72        result = rd.optimise(rd.AREAS[area_key], params, seed=int(seed))73 74        # Compute possession→defensive transition and inject into result so75        # plot_on_pitch() draws the arrows and the markdown reports distances.76        if possession is not None:77            col_ind, max_d, total_d = rd.bottleneck_matching(78                possession, result["positions"],79                use_possession_transform=True,80            )81            result["transition"] = {82                "assignment":           col_ind.tolist(),83                "max_displacement_m":   round(max_d,   2),84                "total_displacement_m": round(total_d, 2),85            }86 87        rd.plot_on_pitch(result, possession=possession, savepath=tmp.name)88        import matplotlib.pyplot as plt89        plt.close("all")90        md      = _single_markdown(result, area_key, params)91        csv_str = _single_csv(result)92 93    return md, tmp.name, csv_str94 95 96# ─────────────────────────────────────────────────────────────────────────────97# Markdown helpers98# ─────────────────────────────────────────────────────────────────────────────99 100def _single_markdown(result, area_key, params):101    ucr   = result["unweighted_covering_radius_m"]102    wcr   = result["weighted_covering_radius"]103    ms    = result["min_spacing_m"]104    worst = result["worst_gap_location"]105    k     = params["danger_concentration"]106    wg_pitch = float(rd._def_to_pitch_y(worst[1]))107 108    transition_section = ""109    if "transition" in result:110        t = result["transition"]111        transition_section = f"""112 113### Transition from possession shape114 115| Metric | Value |116|---|---|117| Maximum individual displacement | **{t["max_displacement_m"]:.1f} m** |118| Total collective displacement | {t["total_displacement_m"]:.1f} m |119 120*Arrows on the plot show each player moving from their possession position121(shown at the halfway line) to their assigned defensive slot.122The number on each arrow is the distance in metres.*123"""124 125    return f"""126## Result — {result["area"]["label"]}127 128*{result["area"]["info"]}*129 130### Coverage131 132| Metric | Value | Meaning |133|---|---|---|134| **Largest gap** | **{ucr:.1f} m** | Max distance from any point in the defended area to the nearest defender. Lower = tighter block. |135| Weighted covering radius | {wcr:.2f} | Same gap weighted by danger. The quantity the optimiser minimised. |136| Min spacing | {ms:.1f} m | Closest pair of defenders. |137| Worst gap at | x={worst[0]:.1f} m, y={wg_pitch:.1f} m | Full-pitch coordinates. The red × on the plot. |138| Runtime | {result["runtime_s"]:.1f} s | |139 140*Danger concentration k = {k:.2f}:141box edge is {rd.np.exp(k):.1f}× more important than the halfway line.*142{transition_section}"""143 144 145def _system_markdown(system, params):146    k   = params["danger_concentration"]147    tr  = system["transitions"]148    wr  = system["wide_right"]149    wl  = system["wide_left"]150    c   = system["central"]151 152    return f"""153## System result — all three areas154 155The central shape was optimised jointly with the two wide shapes,156minimising the worst-case transition to either flank simultaneously.157 158### Coverage by area159 160| Area | Largest gap (m) | Min spacing (m) |161|---|---|---|162| Central | {c['unweighted_covering_radius_m']:.1f} | {c['min_spacing_m']:.1f} |163| Wide right | {wr['unweighted_covering_radius_m']:.1f} | {wr['min_spacing_m']:.1f} |164| Wide left | {wl['unweighted_covering_radius_m']:.1f} | {wl['min_spacing_m']:.1f} |165 166### Transitions from central shape167 168| Shift | Max individual run (m) | Total collective run (m) |169|---|---|---|170| Central → Wide right | **{tr['central_to_wide_right']['max_displacement_m']:.1f}** | {tr['central_to_wide_right']['total_displacement_m']:.1f} |171| Central → Wide left  | **{tr['central_to_wide_left']['max_displacement_m']:.1f}** | {tr['central_to_wide_left']['total_displacement_m']:.1f} |172| **Worst case** | **{tr['worst_case_transition_m']:.1f}** | — |173 174*The worst-case transition tells you how far the hardest-working player runs175when your team shifts from the central block to either wide block.176A lower number means the two shapes are more compatible.*177 178*Danger concentration k = {k:.2f}: box edge is {rd.np.exp(k):.1f}× more important than halfway.*179"""180 181 182def _single_csv(result):183    pos = result["positions"]184    buf = io.StringIO()185    pd.DataFrame([{"player": i+1, "x": float(pos[i,0]), "y": float(pos[i,1])}186                  for i in range(rd.N_PLAYERS)]).to_csv(buf, index=False)187    return buf.getvalue()188 189 190def _system_csv(system):191    rows = []192    for area_key in ("central", "wide_right", "wide_left"):193        pos = system[area_key]["positions"]194        for i in range(rd.N_PLAYERS):195            rows.append({"area": area_key, "player": i+1,196                         "x": float(pos[i,0]), "y": float(pos[i,1])})197    buf = io.StringIO()198    pd.DataFrame(rows).to_csv(buf, index=False)199    return buf.getvalue()200 201 202# ─────────────────────────────────────────────────────────────────────────────203# UI text204# ─────────────────────────────────────────────────────────────────────────────205 206INTRO = """207# Defensive Shape Optimiser208 209Finds a 10-player out-of-possession block that minimises the worst spatial210gap in the area that needs defending — with space near your own box211weighted as more costly to leave empty than space near the halfway line.212 213The **System view** solves all three shapes jointly: the central block is214optimised to transition efficiently to either wing when the opponent switches.215 216*Companion to the possession shape optimiser.*217"""218 219HOW_TO_READ = """220---221## How to read the results222 223**The plot** shows your half-pitch (goal at the bottom, halfway at the top).224The yellow-bordered rectangle is the defended area.225Inside it, the background colour shows the weighted gap:226red = a large gap relative to danger level, green = well covered.227The red × marks the worst gap — the point the model most wants to close.228 229**Largest gap (m)** is the bluntest number: the furthest any point in the230defended area is from the nearest defender. Think of it as the diameter231of the largest hole in the net. 8–12 m is typical for a compact block232of 10 players in these areas.233 234**Danger concentration** controls how tightly the block packs toward the235box vs. spreading across the full 36 m depth.236- **Low (≤ 20):** the optimiser spreads players evenly from halfway to box edge.237  Good for a high line that defends the whole corridor.238- **Mid (40–60):** moderate emphasis on the box end.239  Players in the bottom half are closer together; the halfway-line end is thinner.240- **High (≥ 80):** strong packing near the box. The block is very tight241  in the last 15 m but leaves a lot of space between halfway and the top of the area.242 243**System view / transitions:** the maximum individual displacement tells you244how far the hardest-working player must run when you shift from central245to wide. Below 15 m is a comfortable transition; above 25 m suggests the246two shapes are poorly matched.247"""248 249 250# ─────────────────────────────────────────────────────────────────────────────251# Gradio layout252# ─────────────────────────────────────────────────────────────────────────────253 254with gr.Blocks(title="Defensive Shape Optimiser") as demo:255    gr.Markdown(INTRO)256 257    with gr.Row():258        # ── Controls ─────────────────────────────────────────────────────────259        with gr.Column(scale=1):260 261            gr.Markdown("### Scenario")262            area_radio = gr.Radio(263                choices=list(AREA_OPTIONS.keys()),264                value="System view — all three areas jointly",265                label="Ball position / view",266                info="'System view' jointly optimises all three shapes and shows transition costs.",267            )268 269            gr.Markdown("### Defensive philosophy")270            danger_slider = gr.Slider(271                0, 100, value=40, step=5,272                label="Danger concentration near box",273                info=(274                    "How much more important is space near your box than space near the halfway line? "275                    "0 = treat all depth equally (spread block). "276                    "40 = box is ~2.2× more important (recommended starting point). "277                    "100 = box is 7× more important (very compact near box, thin near halfway)."278                ),279            )280            d_min_slider = gr.Slider(281                3.0, 9.0, value=5.0, step=0.5,282                label="Minimum spacing between defenders (m)",283                info="Prevents two defenders from occupying the same zone. 5 m is a sensible default.",284            )285 286            gr.Markdown("### Run settings")287            effort_radio = gr.Radio(288                choices=list(EFFORT_PRESETS.keys()),289                value="Standard — good interactive run",290                label="Search effort",291            )292            seed_box = gr.Number(293                value=20260608, precision=0, label="Random seed",294                info="Change to explore different solutions with the same settings.",295            )296 297            gr.Markdown("### Transition analysis (optional)")298            possession_upload = gr.File(299                label="Possession-model coordinates CSV",300                file_types=[".csv"],301            )302            gr.Markdown(303                "_Upload `v21_full_optimized_coordinates.csv` from the possession model "304                "to see transition arrows from attacking positions._"305            )306 307            run_btn = gr.Button("Run model", variant="primary")308 309        # ── Results ───────────────────────────────────────────────────────────310        with gr.Column(scale=2):311            result_md  = gr.Markdown("Run the model to see results.")312            shape_img  = gr.Image(label="Defensive shape on pitch", type="filepath")313            with gr.Accordion("Coordinates (CSV)", open=False):314                coord_text = gr.Textbox(315                    label="Player positions",316                    lines=13,317                    info="player, x, y — in defensive-frame coordinates.",318                )319 320    run_btn.click(321        run_defense,322        inputs=[area_radio, danger_slider, d_min_slider,323                effort_radio, possession_upload, seed_box],324        outputs=[result_md, shape_img, coord_text],325    )326 327    gr.Markdown(HOW_TO_READ)328 329if __name__ == "__main__":330    demo.queue(default_concurrency_limit=1).launch()331