{"title":"evaluate","code":"# /// script\n# requires-python = \">=3.10,<3.14\"\n# dependencies = [\n#     \"marimo>=0.25.1\",\n#     \"constellaration==0.2.6\",\n#     \"pandas\",\n#     \"pyarrow\",\n#     \"matplotlib\",\n# ]\n#\n# [tool.uv]\n# # booz-xform 0.0.9, which constellaration 0.2.6 pins, ships no wheels and\n# # needs a compiler and libnetcdf to build. 0.1.0 has wheels. Both give\n# # identical metrics: see README.md of sciencehub-team/constellaration/evaluate.\n# override-dependencies = [\"booz-xform==0.1.0\"]\n# ///\n\nimport marimo\n\n__generated_with = \"0.25.1\"\napp = marimo.App(width=\"medium\", app_title=\"Evaluate a stellarator boundary\")\n\nwith app.setup:\n    import json\n    import subprocess\n    import sys\n    import tempfile\n    import time\n    from pathlib import Path\n\n    import numpy as np\n    import pandas as pd\n    from constellaration import forward_model\n    from constellaration import initial_guess\n    from constellaration import problems\n    from constellaration.geometry import surface_rz_fourier\n\n    EVALUATOR_VERSION = \"0.2.6\"\n\n    # The constraints in the order constellaration 0.2.6 returns their\n    # normalised violations (problems.py), with the metric each one reads.\n    PROBLEMS = {\n        \"geometrical\": {\n            \"title\": \"Geometrical\",\n            \"evaluator\": problems.GeometricalProblem,\n            \"objective\": (\"max_elongation\", \"minimise\"),\n            \"constraints\": [\n                (\"aspect_ratio\", \"≤\", 4.0),\n                (\"average_triangularity\", \"≤\", -0.5),\n                (\"|edge_rotational_transform_over_n_field_periods|\", \"≥\", 0.3),\n            ],\n        },\n        \"simple_to_build\": {\n            \"title\": \"Simple-to-build QI\",\n            \"evaluator\": problems.SimpleToBuildQIStellarator,\n            \"objective\": (\"minimum_normalized_magnetic_gradient_scale_length\", \"maximise\"),\n            \"constraints\": [\n                (\"aspect_ratio\", \"≤\", 10.0),\n                (\"|edge_rotational_transform_over_n_field_periods|\", \"≥\", 0.25),\n                (\"log10(qi)\", \"≤\", -4.0),\n                (\"edge_magnetic_mirror_ratio\", \"≤\", 0.2),\n                (\"max_elongation\", \"≤\", 5.0),\n            ],\n        },\n        \"mhd_stable\": {\n            \"title\": \"MHD-stable QI\",\n            \"evaluator\": problems.MHDStableQIStellarator,\n            \"objective\": (\"hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)\", \"maximise\"),\n            \"constraints\": [\n                (\"|edge_rotational_transform_over_n_field_periods|\", \"≥\", 0.25),\n                (\"log10(qi)\", \"≤\", -3.5),\n                (\"edge_magnetic_mirror_ratio\", \"≤\", 0.25),\n                (\"flux_compression_in_regions_of_bad_curvature\", \"≤\", 0.9),\n                (\"vacuum_well\", \"≥\", 0.0),\n            ],\n        },\n    }\n    TOLERANCE = 0.01\n    HYPERVOLUME_REFERENCE = (1.0, 20.0)\n    # The largest peak resident memory of one QI evaluation we measured on\n    # x86-64, 3,136 MiB (README.md of sciencehub-team/constellaration/evaluate,\n    # \"Cost of one evaluation\").\n    QI_MEMORY_GIB = 3.1\n\n\n@app.function\ndef load_boundary(source) -> surface_rz_fourier.SurfaceRZFourier:\n    \"\"\"A plasma boundary from a JSON string, a dict, or a path to a JSON file.\n\n    The format is the one the ConStellaration dataset and leaderboard use:\n    `r_cos` and `z_sin` as (m_max + 1) × (2 n_max + 1) arrays, `n_field_periods`,\n    and `is_stellarator_symmetric: true`.\n    \"\"\"\n    if isinstance(source, surface_rz_fourier.SurfaceRZFourier):\n        return source\n    if isinstance(source, dict):\n        return surface_rz_fourier.SurfaceRZFourier.model_validate(source)\n    if isinstance(source, Path) or (isinstance(source, str) and not source.lstrip().startswith(\"{\")):\n        source = Path(source).read_text()\n    return surface_rz_fourier.SurfaceRZFourier.model_validate_json(source)\n\n\n@app.function\ndef rotating_ellipse(aspect_ratio: float, elongation: float, rotational_transform: float, n_field_periods: int) -> surface_rz_fourier.SurfaceRZFourier:\n    \"\"\"The rotating-ellipse boundary the paper's baselines start from, with major radius 1.\"\"\"\n    return initial_guess.generate_rotating_ellipse(\n        aspect_ratio=aspect_ratio,\n        elongation=elongation,\n        rotational_transform=rotational_transform,\n        n_field_periods=n_field_periods,\n    )\n\n\n@app.function\ndef compute_metrics(boundary, problem: str = \"simple_to_build\", fidelity: str = \"high_fidelity\", corrected_flux_compression: bool = False) -> dict:\n    \"\"\"Run the forward model with the settings the official evaluator uses for `problem`.\n\n    Returns the metrics as a dict. The geometrical problem skips the Boozer\n    transform and the QI residual, as the evaluator does; the other two\n    compute everything. VMEC++ raises RuntimeError when the equilibrium does\n    not converge.\n\n    `fidelity` selects the VMEC++ preset: \"high_fidelity\" is what the\n    leaderboard uses; \"low_fidelity\" and \"very_low_fidelity\" are cheaper\n    presets for the inner loop of an optimiser, whose results should be\n    checked at high fidelity. With `corrected_flux_compression` the dict also\n    holds `flux_compression_pest`, the proxy averaged with the field-aligned\n    Jacobian as constellaration 0.3.0 does, from the same equilibrium.\n    \"\"\"\n    settings = forward_model.ConstellarationSettings\n    chosen = settings.default_high_fidelity_skip_qi() if problem == \"geometrical\" else settings.default_high_fidelity()\n    if fidelity != \"high_fidelity\":\n        chosen = chosen.model_copy(update={\"vmec_preset_settings\": chosen.vmec_preset_settings.model_copy(update={\"fidelity\": fidelity})})\n    metrics, equilibrium = forward_model.forward_model(load_boundary(boundary), settings=chosen)\n    out = metrics.model_dump(mode=\"json\")\n    if corrected_flux_compression:\n        out[\"flux_compression_pest\"] = flux_compression_both_weights(equilibrium, chosen.turbulent_settings)[1]\n    return out\n\n\n@app.function\ndef flux_compression_both_weights(equilibrium, settings) -> tuple[float, float]:\n    \"\"\"Flux compression in bad curvature averaged with the VMEC and with the PEST Jacobian.\n\n    The first value is what constellaration 0.2.6 reports. The second changes\n    only the averaging weight, as constellaration 0.3.0 does\n    (proximafusion/constellaration#109), on the same equilibrium.\n    \"\"\"\n    from constellaration.mhd import turbulent_transport, vmec_utils\n    from simsopt import mhd\n\n    vmec = vmec_utils.as_simsopt_vmec(equilibrium)\n    alpha, theta = turbulent_transport._set_up_surface_grid(\n        settings=settings, n_field_periods=equilibrium.nfp, is_stellarator_symmetric=not equilibrium.lasym\n    )\n    data = mhd.vmec_fieldlines(vs=vmec, s=settings.normalized_toroidal_flux, alpha=alpha, theta1d=theta)\n    grad_r_squared = equilibrium.Aminor_p**2 / (4 * settings.normalized_toroidal_flux[:, None, None]) * data.grad_s_dot_grad_s\n    bad_curvature = np.heaviside(data.B_cross_kappa_dot_grad_alpha * np.sign(equilibrium.phipf[-1]), 0)\n\n    def averaged(jacobian):\n        weights = np.abs(jacobian) / np.abs(jacobian).mean(axis=(1, 2))[:, None, None]\n        return float(np.mean(bad_curvature * grad_r_squared * weights, axis=(1, 2))[0])\n\n    return averaged(data.sqrt_g_vmec), averaged(data.edge_toroidal_flux_over_2pi / data.B_sup_theta_pest)\n\n\n@app.function\ndef constraint_report(problem: str, metrics: dict) -> pd.DataFrame:\n    \"\"\"Each constraint of `problem` with its value, bound and normalised violation.\n\n    The violation is (value − bound) / |bound|, signed so that a positive\n    number means the constraint is broken; the evaluator accepts violations up\n    to 0.01. For the vacuum well, whose bound is 0, it divides by 0.1. The\n    numbers come from constellaration's own code.\n    \"\"\"\n    spec = PROBLEMS[problem]\n    evaluator = spec[\"evaluator\"]()\n    typed = forward_model.ConstellarationMetrics.model_validate({k: v for k, v in metrics.items() if k in forward_model.ConstellarationMetrics.model_fields})\n    violations = evaluator._normalized_constraint_violations(typed)\n    rows = []\n    for (name, relation, bound), violation in zip(spec[\"constraints\"], violations):\n        if name.startswith(\"|\"):\n            value = abs(metrics[name.strip(\"|\")])\n        elif name == \"log10(qi)\":\n            value = float(np.log10(metrics[\"qi\"]))\n        else:\n            value = metrics[name]\n        rows.append({\n            \"constraint\": f\"{name} {relation} {bound:g}\",\n            \"value\": value,\n            \"normalised violation\": float(violation),\n            \"satisfied\": bool(violation <= TOLERANCE),\n        })\n    return pd.DataFrame(rows)\n\n\n@app.function\ndef hypervolume(points) -> float:\n    \"\"\"The MHD-stable score of feasible (gradient scale length, aspect ratio) points.\n\n    The area in the plane of −L∇B and A that the points dominate, up to the\n    reference point (−L∇B, A) = (1, 20). Pass only feasible points.\n    \"\"\"\n    if len(points) == 0:\n        return 0.0\n    from pymoo.indicators import hv\n\n    X = np.array([(-float(L), float(A)) for L, A in points])\n    return float(hv.Hypervolume(ref_point=np.array(HYPERVOLUME_REFERENCE))(X))\n\n\n@app.function\ndef added_hypervolume(candidate, front) -> float:\n    \"\"\"How much a feasible (L∇B, A) point would add to the hypervolume of `front`.\n\n    `front` is a list of feasible (L∇B, A) points, for example the leading\n    submission's. Zero when the candidate is dominated.\n    \"\"\"\n    return hypervolume([*front, candidate]) - hypervolume(front)\n\n\n@app.function\ndef evaluate(problem: str, boundary, force: bool = False) -> dict:\n    \"\"\"Score a boundary (or, for \"mhd_stable\", a set of boundaries) as the leaderboard does.\n\n    For \"geometrical\" and \"simple_to_build\" the result holds objective,\n    feasibility, feasible, score, metrics, constraints (a list of dicts as in\n    `constraint_report`) and seconds. For \"mhd_stable\", `boundary` is a list\n    of boundaries or the leaderboard's submission format (a JSON array of\n    boundary JSON strings, as text or as a path), and the result holds one\n    such record per boundary under \"points\" and the set's score, the\n    hypervolume of its feasible points. A boundary that does not parse, or\n    whose evaluation raises, usually because VMEC++ did not converge, gets a\n    record with \"error\" and feasible False, where the leaderboard would reject the whole submission;\n    for the single-objective problems the record has score 0. The calls are\n    those of constellaration 0.2.6's `evaluate`, with one forward-model run\n    per boundary.\n\n    The two QI problems raise MemoryError before any work when the machine\n    has less memory than one QI evaluation needs (`memory_shortfall`), because\n    running out of memory would end the Python process. `force=True` runs\n    them anyway.\n    \"\"\"\n    if problem not in PROBLEMS:\n        raise ValueError(f\"problem must be one of {', '.join(PROBLEMS)}\")\n    refuse_without_memory(problem, force)\n    if problem == \"mhd_stable\":\n        points = [evaluate_single(problem, b) for b in boundary_list(boundary)]\n        return {\"problem\": problem, \"evaluator\": EVALUATOR_VERSION, \"points\": points, \"score\": hypervolume(feasible_points(points))}\n    return {\"problem\": problem, \"evaluator\": EVALUATOR_VERSION, **evaluate_single(problem, boundary)}\n\n\n@app.function\ndef boundary_list(boundaries) -> list:\n    \"\"\"The boundaries of a list, or of the leaderboard's submission format.\n\n    The submission format is a JSON array of boundary JSON strings, given as\n    text or as a path to a file. A single boundary becomes a list of one.\n    \"\"\"\n    if isinstance(boundaries, Path) or (isinstance(boundaries, str) and not boundaries.lstrip().startswith(\"{\")):\n        text = boundaries if isinstance(boundaries, str) and boundaries.lstrip().startswith(\"[\") else Path(boundaries).read_text()\n        boundaries = json.loads(text)\n    if isinstance(boundaries, (str, dict, surface_rz_fourier.SurfaceRZFourier)):\n        return [boundaries]\n    return list(boundaries)\n\n\n@app.function\ndef feasible_points(records) -> list[tuple[float, float]]:\n    \"\"\"The (L∇B, A) points of the feasible records among `records`, as `hypervolume` takes them.\"\"\"\n    return [\n        (r[\"metrics\"][\"minimum_normalized_magnetic_gradient_scale_length\"], r[\"metrics\"][\"aspect_ratio\"])\n        for r in records\n        if r[\"feasible\"]\n    ]\n\n\n@app.function\ndef error_record(problem: str, message: str, seconds: float) -> dict:\n    \"\"\"The record of a boundary whose evaluation failed: not feasible, and score 0 for the single-objective problems.\"\"\"\n    record = {\"error\": message, \"feasible\": False, \"feasibility\": None, \"metrics\": None, \"constraints\": None}\n    if problem != \"mhd_stable\":\n        record.update(objective=None, score=0.0)\n    record[\"seconds\"] = round(seconds, 1)\n    return record\n\n\n@app.function\ndef evaluate_single(problem: str, boundary) -> dict:\n    \"\"\"One boundary's record for `evaluate`: one forward-model run, then the evaluator's own feasibility and score.\"\"\"\n    started = time.perf_counter()\n    try:\n        metrics = compute_metrics(load_boundary(boundary), problem)\n    except Exception as error:\n        return error_record(problem, f\"{type(error).__name__}: {error}\", time.perf_counter() - started)\n    evaluator = PROBLEMS[problem][\"evaluator\"]()\n    typed = forward_model.ConstellarationMetrics.model_validate(metrics)\n    feasible = evaluator.is_feasible(typed)\n    record = {\n        \"feasibility\": evaluator.compute_feasibility(typed),\n        \"feasible\": feasible,\n        \"metrics\": metrics,\n        \"constraints\": constraint_report(problem, metrics).to_dict(\"records\"),\n    }\n    if problem != \"mhd_stable\":\n        record[\"objective\"] = evaluator.get_objective(typed)[0]\n        record[\"score\"] = evaluator._score(typed) if feasible else 0.0\n    record[\"seconds\"] = round(time.perf_counter() - started, 1)\n    return record\n\n\n@app.function\ndef evaluate_many(problem: str, boundaries, jobs: int = 1, timeout: float = 600.0, force: bool = False) -> list[dict]:\n    \"\"\"Score each boundary in its own Python process, `jobs` at a time.\n\n    `boundaries` is a list or the leaderboard's submission format, as\n    `boundary_list` reads it. Returns one record per boundary, in order, as\n    `evaluate_single` makes them, except that \"seconds\" is the wall time of\n    the whole process, imports included. A boundary that does not parse, a\n    process that runs longer than `timeout` seconds, and a process that\n    crashes each give an error record with feasible False; the other\n    boundaries still run. The timeout counts wall time, so keep `jobs` at or\n    below the number of CPUs. For \"mhd_stable\",\n    `hypervolume(feasible_points(records))` is the set's score.\n\n    The process boundary does not protect the caller from running out of\n    memory where one limit covers the caller and its children, as on\n    ScienceHub compute: a child that exceeds the kernel's memory ends the\n    whole kernel. Use `jobs=1` there. For the same reason the two QI problems\n    raise MemoryError, as in `evaluate`, unless `force=True`. Each process\n    imports this file again, so the file must exist on disk.\n    \"\"\"\n    if problem not in PROBLEMS:\n        raise ValueError(f\"problem must be one of {', '.join(PROBLEMS)}\")\n    refuse_without_memory(problem, force)\n    source = globals().get(\"__file__\")\n    if not source or not Path(source).is_file():\n        raise RuntimeError(\"evaluate_many runs this notebook's file in new processes; import it from evaluate.py on disk.\")\n    worker = (\n        \"import importlib.util, json, sys\\n\"\n        \"spec = importlib.util.spec_from_file_location('constellaration_evaluate', sys.argv[1])\\n\"\n        \"module = importlib.util.module_from_spec(spec)\\n\"\n        \"spec.loader.exec_module(module)\\n\"\n        \"task = json.loads(open(sys.argv[2]).read())\\n\"\n        \"record = module.evaluate_single(task['problem'], task['boundary'])\\n\"\n        \"open(sys.argv[3], 'w').write(json.dumps(record))\\n\"\n    )\n    texts, records = [], []\n    for b in boundary_list(boundaries):\n        try:\n            texts.append(load_boundary(b).model_dump_json())\n            records.append(None)\n        except Exception as error:\n            texts.append(None)\n            records.append(error_record(problem, f\"{type(error).__name__}: {error}\", 0.0))\n    waiting = [i for i in reversed(range(len(texts))) if records[i] is None]\n    running = {}\n    with tempfile.TemporaryDirectory() as scratch:\n        scratch = Path(scratch)\n\n        def finished(i, child, seconds):\n            out = scratch / f\"{i}.out\"\n            if child.returncode == 0 and out.is_file():\n                return {**json.loads(out.read_text()), \"seconds\": round(seconds, 1)}\n            if child.returncode < 0:\n                reason = f\"killed by signal {-child.returncode}\"\n                if child.returncode == -9:\n                    reason += \", usually because it ran out of memory\"\n            else:\n                lines = (scratch / f\"{i}.err\").read_text().strip().splitlines()\n                reason = lines[-1] if lines else f\"exit status {child.returncode}\"\n            return error_record(problem, f\"ProcessError: {reason}\", seconds)\n\n        try:\n            while waiting or running:\n                while waiting and len(running) < max(1, jobs):\n                    i = waiting.pop()\n                    (scratch / f\"{i}.json\").write_text(json.dumps({\"problem\": problem, \"boundary\": texts[i]}))\n                    with open(scratch / f\"{i}.err\", \"w\") as err:\n                        command = [sys.executable, \"-c\", worker, str(source), str(scratch / f\"{i}.json\"), str(scratch / f\"{i}.out\")]\n                        running[i] = (subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=err), time.perf_counter())\n                time.sleep(0.2)\n                for i, (child, started) in list(running.items()):\n                    seconds = time.perf_counter() - started\n                    if child.poll() is not None:\n                        records[i] = finished(i, child, seconds)\n                    elif seconds > timeout:\n                        child.kill()\n                        child.wait()\n                        records[i] = error_record(problem, f\"TimeoutError: no result after {timeout:g} s\", seconds)\n                    else:\n                        continue\n                    del running[i]\n        finally:\n            for child, _ in running.values():\n                child.kill()\n                child.wait()\n    return records\n\n\n@app.function\ndef leader_front(path=None) -> list[tuple[float, float]]:\n    \"\"\"The feasible (L∇B, A) points of the leading MHD-stable submission.\n\n    Read from `data/leading-front.json` beside this notebook, or from `path`.\n    The leaderboard notebook's `build_data.py` writes that file from the\n    leaderboard as recorded on 25 October 2026; the points' hypervolume is\n    the leader's score. Pass them to `added_hypervolume` as the front.\n    \"\"\"\n    if path is None:\n        path = Path(globals()[\"__file__\"]).parent / \"data\" / \"leading-front.json\"\n    return [tuple(point) for point in json.loads(Path(path).read_text())[\"points\"]]\n\n\n@app.function\ndef memory_limit_gib() -> float | None:\n    \"\"\"The memory available to this process in GiB, or None when it cannot tell.\n\n    Reads the container's limit from cgroup v2, then cgroup v1, which reports\n    no limit as a number near 2**63. Without a limit there, or without\n    cgroup files, it falls back to MemTotal in /proc/meminfo. macOS has\n    neither and gives None.\n    \"\"\"\n    for name in (\"/sys/fs/cgroup/memory.max\", \"/sys/fs/cgroup/memory/memory.limit_in_bytes\"):\n        try:\n            raw = Path(name).read_text().strip()\n        except OSError:\n            continue\n        if raw != \"max\" and int(raw) < 2**60:\n            return int(raw) / 2**30\n        break\n    try:\n        for line in Path(\"/proc/meminfo\").read_text().splitlines():\n            if line.startswith(\"MemTotal:\"):\n                return int(line.split()[1]) / 2**20\n    except (OSError, ValueError, IndexError):\n        pass\n    return None\n\n\n@app.function\ndef memory_shortfall(problem: str) -> str | None:\n    \"\"\"Why `problem` cannot run in this machine's memory, or None when it can.\n\n    Only the two QI problems need more than a ScienceHub kernel's 1.5 GiB.\n    \"\"\"\n    limit = memory_limit_gib()\n    if problem == \"geometrical\" or limit is None or limit >= QI_MEMORY_GIB:\n        return None\n    return (\n        f\"This machine has {limit:.1f} GiB of memory. The QI residual of constellaration 0.2.6 needs \"\n        f\"about {QI_MEMORY_GIB:g} GiB, so only the geometrical problem runs here. Run the other two on \"\n        \"a computer with at least 4 GiB.\"\n    )\n\n\n@app.function\ndef refuse_without_memory(problem: str, force: bool = False) -> None:\n    \"\"\"Raise MemoryError with `memory_shortfall`'s reason, unless `force` is set.\"\"\"\n    reason = None if force else memory_shortfall(problem)\n    if reason:\n        raise MemoryError(f\"{reason} Pass force=True to run it anyway.\")\n\n\n@app.function\ndef cross_sections(boundary, toroidal_angles=None, n_theta: int = 256):\n    \"\"\"R and Z of the boundary in planes of constant toroidal angle φ.\n\n    Evaluates R = Σ r_cos[m, n] cos(mθ − n N φ) and Z = Σ z_sin[m, n] sin(mθ − n N φ)\n    on the stored coefficients, without solving anything. Returns a list of\n    (φ, R, Z) with φ in radians.\n    \"\"\"\n    b = load_boundary(boundary)\n    nfp = b.n_field_periods\n    if toroidal_angles is None:\n        toroidal_angles = [0.0, np.pi / (2 * nfp), np.pi / nfp]\n    r_cos, z_sin = np.asarray(b.r_cos), np.asarray(b.z_sin)\n    m = np.arange(r_cos.shape[0])[:, None]\n    n = np.arange(r_cos.shape[1])[None, :] - (r_cos.shape[1] - 1) // 2\n    theta = np.linspace(0, 2 * np.pi, n_theta)\n    out = []\n    for phi in toroidal_angles:\n        angle = m[..., None] * theta - (n * nfp * phi)[..., None]\n        out.append((phi, (r_cos[..., None] * np.cos(angle)).sum((0, 1)), (z_sin[..., None] * np.sin(angle)).sum((0, 1))))\n    return out\n\n\n@app.cell\ndef _():\n    import marimo as mo\n    import matplotlib.pyplot as plt\n\n    return mo, plt\n\n\n@app.cell(hide_code=True)\ndef _(mo):\n    mo.md(r\"\"\"\n    # Evaluate a stellarator boundary\n\n    A stellarator confines a fusion plasma in a twisted magnetic field made by\n    external coils alone. Its performance is set mostly by the shape of the\n    outermost magnetic surface, the plasma boundary, and finding a good shape\n    is an optimisation problem with expensive physics in the loop.\n    ConStellaration, released by Proxima Fusion with a dataset of about 180,000\n    boundaries, poses three such problems with fixed evaluation code, so that\n    results from different groups can be compared\n    ([Cadena et al. 2025](https://arxiv.org/abs/2506.19583)).\n\n    This notebook holds that evaluation code, pinned to **constellaration\n    0.2.6**, the version the public leaderboard uses, and exposes it as\n    functions another notebook can call. The booz-xform override in this\n    notebook's header changes no number; `README.md` beside the notebook\n    gives the comparison and the time and memory one evaluation takes. The\n    project's other notebooks look at the dataset and at the leaderboard,\n    where the recorded scores reproduce in the leaderboard's environment.\n    \"\"\")\n    return\n\n\n@app.cell(hide_code=True)\ndef _(mo):\n    mo.md(r\"\"\"\n    ## The boundary\n\n    All three problems take a stellarator-symmetric boundary in cylindrical\n    coordinates (R, φ, Z), written as a Fourier series in the poloidal angle θ\n    and the toroidal angle φ:\n\n    $$R(\\theta, \\varphi) = \\sum_{m,n} r_{mn} \\cos(m\\theta - n N_\\mathrm{fp}\\varphi), \\qquad\n    Z(\\theta, \\varphi) = \\sum_{m,n} z_{mn} \\sin(m\\theta - n N_\\mathrm{fp}\\varphi),$$\n\n    where $N_\\mathrm{fp}$ is the number of field periods. The dataset uses\n    modes up to m = n = 4 and fixes $r_{00} = 1$, which leaves 80 free\n    coefficients; the evaluator accepts more modes, and the leading\n    geometrical entries use poloidal modes up to m = 12. The boundary below is the\n    example Proxima ships with the benchmark, with four field periods.\n    \"\"\")\n    return\n\n\n@app.cell\ndef _(mo):\n    reference = load_boundary(mo.notebook_dir() / \"data\" / \"reference-boundary.json\")\n    reference_set = [load_boundary(s) for s in json.loads((mo.notebook_dir() / \"data\" / \"reference-boundaries.json\").read_text())]\n    return reference, reference_set\n\n\n@app.cell\ndef _(mo, plt, reference):\n    _fig, _ax = plt.subplots(figsize=(5.5, 4))\n    for (_phi, _R, _Z), _color in zip(cross_sections(reference), (\"#1f2328\", \"#0969da\", \"#9a6700\")):\n        _ax.plot(_R, _Z, color=_color, lw=1.6, label=f\"φ = {_phi * reference.n_field_periods / np.pi:.1f} π/N\")\n    _ax.set_aspect(\"equal\")\n    _ax.set_xlabel(\"R\")\n    _ax.set_ylabel(\"Z\")\n    _ax.legend(frameon=False, fontsize=9)\n    for _side in (\"top\", \"right\"):\n        _ax.spines[_side].set_visible(False)\n    _fig.tight_layout()\n    mo.vstack([\n        _fig,\n        mo.md(\n            f\"Cross-sections of the reference boundary at the start, quarter and half of a field period. \"\n            f\"N = {reference.n_field_periods} field periods, {reference.r_cos.shape[0]} × {reference.r_cos.shape[1]} coefficients for R and for Z.\"\n        ),\n    ])\n    return\n\n\n@app.cell(hide_code=True)\ndef _(mo):\n    mo.md(r\"\"\"\n    ## The three problems\n\n    The forward model solves for the ideal-MHD equilibrium inside the boundary\n    with VMEC++, with essentially no pressure and no net toroidal current, at\n    a resolution of at least ten poloidal and toroidal modes that grows with\n    the boundary's own, and computes the quantities the constraints use\n    from it. A boundary is feasible when every normalised constraint violation,\n    (value − bound) / |bound| with the sign chosen so that positive means\n    broken, is at most 0.01.\n\n    **Geometrical.** Minimise the maximum elongation of the cross-sections at\n    aspect ratio at most 4, average triangularity at most −0.5 and edge\n    rotational transform per field period at least 0.3. Score\n    1 − (ε_max − 1)/9, clipped to [0, 1], when feasible. No quasi-isodynamicity is required; this\n    is the warm-up problem.\n\n    **Simple-to-build QI.** Maximise the smallest magnetic gradient scale\n    length on the boundary, $L_{\\nabla B} = B\\sqrt{2}/\\lVert\\nabla \\mathbf{B}\\rVert_F$,\n    normalised as $\\tilde L_{\\nabla B} = N_\\mathrm{fp} L_{\\nabla B}/a$ with $a$\n    the minor radius. It is a proxy for how far the coils can stand from the\n    plasma (Kappel et al. 2024). The field must be quasi-isodynamic (QI):\n    omnigenous, so that trapped particles have no bounce-averaged radial\n    drift, with contours of |B| that close poloidally, which keeps the\n    bootstrap current small. The QI residual of Goodman et al. (2023)\n    measures the distance from such a field. Score $\\tilde L_{\\nabla B}/20$,\n    clipped to [0, 1], when feasible.\n\n    **MHD-stable QI.** The trade-off between $\\tilde L_{\\nabla B}$ and compactness,\n    for QI fields that also have a magnetic well (W ≥ 0, a proxy for ideal-MHD\n    stability) and little flux compression in regions of bad curvature: the\n    flux-surface average at ρ = 0.7 of $|\\nabla r|^2$ where the curvature\n    drift is bad and zero elsewhere, a geometric proxy for turbulent\n    transport. A submission is a set of\n    boundaries; its score is the area its feasible points dominate in the\n    plane of $-\\tilde L_{\\nabla B}$ and aspect ratio, measured up to the reference\n    point (1, 20). The score has no upper bound.\n    \"\"\")\n    return\n\n\n@app.cell\ndef _(mo):\n    _rows = []\n    for _key, _spec in PROBLEMS.items():\n        for _name, _relation, _bound in _spec[\"constraints\"]:\n            _rows.append({\"problem\": _spec[\"title\"], \"objective\": f\"{_spec['objective'][1]} {_spec['objective'][0]}\", \"constraint\": f\"{_name} {_relation} {_bound:g}\"})\n    mo.vstack([mo.md(\"Constraints of constellaration 0.2.6\"), mo.Html(pd.DataFrame(_rows).to_html(index=False, border=0))])\n    return\n\n\n@app.cell(hide_code=True)\ndef _(mo):\n    mo.md(r\"\"\"\n    ## Run the evaluator\n\n    One evaluation is a full equilibrium solve at the leaderboard's\n    resolution. On one x86-64 core of our CI runners it takes about a minute\n    for the example boundary and up to about four minutes for the largest\n    boundaries on the leaderboard. On a ScienceHub kernel the example\n    boundary took 189 s. The two QI problems also compute a Boozer\n    transform and the QI residual, which need about 3 GiB of memory, so on a\n    ScienceHub kernel (1.5 GiB) this notebook refuses them. The geometrical\n    problem needs 1.0 GiB for boundaries of the dataset's size. The largest\n    leaderboard boundaries, with 13 × 25 modes, may not fit: our runs\n    measured at most 1.72 GiB for them, in worker processes that had already\n    run other evaluations, so a single evaluation may need less.\n\n    Choose a problem and start it. The MHD-stable problem evaluates the\n    two-boundary example set and reports each point and the hypervolume of\n    the feasible ones. On the geometrical problem the example boundary\n    breaks its constraints by a normalised violation of 1.23 and scores 0;\n    the constraint table shows by how much each one misses.\n    \"\"\")\n    return\n\n\n@app.cell\ndef _(mo):\n    problem_choice = mo.ui.dropdown(\n        options={spec[\"title\"]: key for key, spec in PROBLEMS.items()},\n        value=\"Geometrical\",\n        label=\"Problem\",\n    )\n    run = mo.ui.run_button(label=\"Evaluate\")\n    mo.hstack([problem_choice, run], justify=\"start\", gap=1)\n    return problem_choice, run\n\n\n@app.cell\ndef _(mo, problem_choice, reference, reference_set, run):\n    mo.stop(not run.value, mo.md(\"The evaluation starts when you press **Evaluate**.\"))\n    _problem = problem_choice.value\n    _shortfall = memory_shortfall(_problem)\n    mo.stop(\n        _shortfall is not None,\n        mo.md(f\"{_shortfall} The section on calling these functions shows how to install the evaluator there.\"),\n    )\n    with mo.status.spinner(title=\"Solving the equilibrium with VMEC++\"):\n        result = evaluate(_problem, reference_set if _problem == \"mhd_stable\" else reference)\n    return (result,)\n\n\n@app.cell\ndef _(mo, result):\n    if result[\"problem\"] == \"mhd_stable\":\n        _points = pd.DataFrame([\n            {\n                \"aspect ratio\": p[\"metrics\"][\"aspect_ratio\"] if p[\"metrics\"] else None,\n                \"L∇B\": p[\"metrics\"][\"minimum_normalized_magnetic_gradient_scale_length\"] if p[\"metrics\"] else None,\n                \"feasibility\": p[\"feasibility\"],\n                \"feasible\": p[\"feasible\"],\n                \"error\": p.get(\"error\"),\n                \"seconds\": p[\"seconds\"],\n            }\n            for p in result[\"points\"]\n        ])\n        _view = mo.vstack([mo.md(f\"Hypervolume score: **{result['score']:.3f}**\"), mo.Html(_points.to_html(index=False, border=0, na_rep=\"\", float_format=lambda _x: f\"{_x:.4g}\"))])\n    elif \"error\" in result:\n        _view = mo.md(f\"The evaluation failed after {result['seconds']:.0f} s and scores 0: `{result['error']}`\")\n    else:\n        _view = mo.vstack([\n            mo.md(\n                f\"Objective **{result['objective']:.4f}**, feasibility {result['feasibility']:.4f} \"\n                f\"({'feasible' if result['feasible'] else 'not feasible'}), score **{result['score']:.4f}**. \"\n                f\"The evaluation took {result['seconds']:.0f} s.\"\n            ),\n            mo.Html(pd.DataFrame(result[\"constraints\"]).to_html(index=False, border=0, float_format=lambda _x: f\"{_x:.4g}\")),\n        ])\n    _view\n    return\n\n\n@app.cell(hide_code=True)\ndef _(mo):\n    mo.md(r\"\"\"\n    ## Calling these functions from another notebook\n\n    **Where each problem runs.** A ScienceHub kernel has one CPU and 1.5 GiB\n    of memory. That is enough for the geometrical problem on boundaries of\n    the dataset's size. The two QI problems need about 3 GiB, and ScienceHub\n    compute has no larger kernel yet, so evaluate them on your own computer\n    or another machine with at least 4 GiB. Every dependency installs there\n    without a compiler on Python 3.10 to 3.13, on Linux x86-64 and on\n    macOS 14 or later with Apple silicon.\n\n    **On your own machine** the `sciencehub` package downloads this notebook\n    with its `data/` folder and imports it. Install the evaluator with the\n    same booz-xform override as this notebook's header, because pip alone\n    would try to build booz-xform 0.0.9 from source:\n\n    ```sh\n    echo \"booz-xform==0.1.0\" > overrides.txt\n    uv pip install sciencehub \"constellaration==0.2.6\" pandas pyarrow --override overrides.txt\n    ```\n\n    ```python\n    import sciencehub as sh\n\n    bench = sh.load(\"sciencehub-team/constellaration/evaluate\")\n    start = bench.rotating_ellipse(aspect_ratio=8, elongation=1.5, rotational_transform=0.4, n_field_periods=3)\n    print(bench.evaluate(\"simple_to_build\", start)[\"constraints\"])\n    ```\n\n    `sh.load` runs each function with the downloaded notebook's folder as\n    the working directory, so a relative path such as `\"my-boundary.json\"`\n    would be looked up there. Pass absolute paths, or boundaries already\n    loaded with `load_boundary`.\n\n    **On ScienceHub compute** a kernel cannot reach sciencehub.sh, so\n    `sh.load` fails there. Copy `evaluate.py` and its `data/` folder into\n    the files of your own notebook, copy the dependencies and the\n    `[tool.uv]` override of this notebook's header into yours, and import\n    the functions with `from evaluate import evaluate_many, leader_front`.\n    The notebook's folder is the kernel's working directory and is on\n    `sys.path`.\n\n    **In an optimisation loop**, `evaluate_many(problem, boundaries, jobs,\n    timeout)` scores each boundary in its own Python process, at most `jobs`\n    at a time. A boundary that does not parse, a run that crashes, and a run\n    that exceeds `timeout` seconds of wall time (default 600; the slowest\n    run we measured took 252 s on its own CPU) each get an error record\n    with feasible False (and score 0 on the single-objective problems), so\n    one bad candidate does not stop the loop. Keep `jobs` at or below the\n    number of CPUs, or slow runs share a CPU and reach the timeout. Each record serialises to JSON and\n    can be appended to a JSON-lines log.\n    `compute_metrics(..., fidelity=\"low_fidelity\")` is cheaper for an inner\n    loop; check what it finds at the leaderboard's fidelity.\n\n    Running out of memory is different on ScienceHub compute. The kernel's\n    1.5 GiB limit covers the kernel and every process it starts, so a run\n    that exceeds it ends the kernel, not only the run. The kernel already\n    holds constellaration's imports, and each run imports them again and\n    peaks at about 1.0 GiB for the geometrical problem; we have not measured\n    the two together. Use `jobs=1` in a kernel. `evaluate` and\n    `evaluate_many` raise MemoryError for the two QI problems when the\n    memory is below what one QI run needs; `force=True` overrides that.\n    Without a container memory\n    limit, as on your own computer, the operating system usually kills only\n    the run, which then gets an error record like any other crash.\n\n    **Against the current leader**, `leader_front()` returns the 688\n    feasible (L∇B, A) points of the best MHD-stable submission as recorded\n    on 25 October 2026, read from `data/leading-front.json`, and\n    `added_hypervolume(point, leader_front())` is the area a new feasible\n    point would add to that front. For a set of records from\n    `evaluate_many`, `feasible_points(records)` gives the points to pass.\n    \"\"\")\n    return\n\n\n@app.cell\ndef _(mo):\n    leading = leader_front(mo.notebook_dir() / \"data\" / \"leading-front.json\")\n    _candidate = (15.0, 10.0)\n    mo.md(\n        f\"The leading front holds {len(leading)} feasible points with a hypervolume of \"\n        f\"**{hypervolume(leading):.2f}**, the leader's recorded score. A feasible point at \"\n        f\"L∇B = {_candidate[0]:g}, A = {_candidate[1]:g} would add **{added_hypervolume(_candidate, leading):.2f}** to it.\"\n    )\n    return\n\n\n@app.cell(hide_code=True)\ndef _(mo):\n    mo.md(r\"\"\"\n    ## Limits and references\n\n    The evaluator is constellaration 0.2.6 unchanged; only booz-xform, the\n    Boozer transform it calls, is 0.1.0 instead of 0.0.9 because 0.0.9 cannot\n    be installed without a compiler. Version 0.3.0 corrects how the flux\n    compression is averaged\n    ([constellaration#109](https://github.com/proximafusion/constellaration/pull/109))\n    and changes which MHD-stable boundaries are feasible; the leaderboard\n    stays on 0.2.6, and so does this notebook. `compute_metrics(...,\n    corrected_flux_compression=True)` adds the corrected proxy alone. A full\n    0.3.0 installation also moves VMEC++ from 0.4.11 to 0.5.4 and other\n    numerical dependencies, so its scores can differ in more than that one\n    quantity. The equilibrium is computed in near-vacuum, and the gradient\n    scale length stands in for coil complexity; neither replaces a finite-β\n    equilibrium or a coil design. The Hugging Face dataset includes VMEC++\n    equilibria at volume-averaged β of 1 to 5 % for many of its boundaries.\n    Optimisers that work at lower VMEC++ resolution should check their\n    results here, because a boundary that converges at low resolution can\n    fail at this one.\n\n    Cadena, S. A., Merlo, A., Laude, E., Bauer, A., Agrawal, A., Pascu, M.,\n    Savtchouk, M., Guiraud, E., Bonauer, L., Hudson, S. R. and Kaiser, M.\n    (2025). ConStellaration: A dataset of QI-like stellarator plasma\n    boundaries and optimization benchmarks. *NeurIPS 2025 Datasets and\n    Benchmarks Track*. [OpenReview NQSbGKlCpx](https://openreview.net/forum?id=NQSbGKlCpx),\n    [arXiv:2506.19583](https://arxiv.org/abs/2506.19583).\n\n    Code: [proximafusion/constellaration](https://github.com/proximafusion/constellaration)\n    at tag v0.2.6, MIT licence, Copyright (c) 2025-present Proxima Fusion GmbH.\n    The two example boundaries in `data/` are `hugging_face_competition/inputs/boundary.json`\n    and `boundaries.json` at that tag, renamed and otherwise unchanged.\n\n    Kappel, J., Landreman, M. and Malhotra, D. (2024). The magnetic gradient\n    scale length explains why certain plasmas require close external magnetic\n    coils. *Plasma Phys. Control. Fusion* 66, 025018.\n\n    Goodman, A. G. et al. (2023). Constructing precisely quasi-isodynamic\n    magnetic fields. *J. Plasma Phys.* 89, 905890504.\n    \"\"\")\n    return\n\n\nif __name__ == \"__main__\":\n    app.run()\n","revision":"2026-10-28T15:24:22.885Z","appConfig":{"width":"medium","app_title":"Evaluate a stellarator boundary"},"firstPaint":{"notebook":{"version":"1","metadata":{"marimo_version":"0.25.2"},"cells":[{"id":"setup","name":"setup","code":"import json\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom constellaration import forward_model\nfrom constellaration import initial_guess\nfrom constellaration import problems\nfrom constellaration.geometry import surface_rz_fourier\n\nEVALUATOR_VERSION = \"0.2.6\"\n\n# The constraints in the order constellaration 0.2.6 returns their\n# normalised violations (problems.py), with the metric each one reads.\nPROBLEMS = {\n    \"geometrical\": {\n        \"title\": \"Geometrical\",\n        \"evaluator\": problems.GeometricalProblem,\n        \"objective\": (\"max_elongation\", \"minimise\"),\n        \"constraints\": [\n            (\"aspect_ratio\", \"≤\", 4.0),\n            (\"average_triangularity\", \"≤\", -0.5),\n            (\"|edge_rotational_transform_over_n_field_periods|\", \"≥\", 0.3),\n        ],\n    },\n    \"simple_to_build\": {\n        \"title\": \"Simple-to-build QI\",\n        \"evaluator\": problems.SimpleToBuildQIStellarator,\n        \"objective\": (\"minimum_normalized_magnetic_gradient_scale_length\", \"maximise\"),\n        \"constraints\": [\n            (\"aspect_ratio\", \"≤\", 10.0),\n            (\"|edge_rotational_transform_over_n_field_periods|\", \"≥\", 0.25),\n            (\"log10(qi)\", \"≤\", -4.0),\n            (\"edge_magnetic_mirror_ratio\", \"≤\", 0.2),\n            (\"max_elongation\", \"≤\", 5.0),\n        ],\n    },\n    \"mhd_stable\": {\n        \"title\": \"MHD-stable QI\",\n        \"evaluator\": problems.MHDStableQIStellarator,\n        \"objective\": (\"hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)\", \"maximise\"),\n        \"constraints\": [\n            (\"|edge_rotational_transform_over_n_field_periods|\", \"≥\", 0.25),\n            (\"log10(qi)\", \"≤\", -3.5),\n            (\"edge_magnetic_mirror_ratio\", \"≤\", 0.25),\n            (\"flux_compression_in_regions_of_bad_curvature\", \"≤\", 0.9),\n            (\"vacuum_well\", \"≥\", 0.0),\n        ],\n    },\n}\nTOLERANCE = 0.01\nHYPERVOLUME_REFERENCE = (1.0, 20.0)\n# The largest peak resident memory of one QI evaluation we measured on\n# x86-64, 3,136 MiB (README.md of sciencehub-team/constellaration/evaluate,\n# \"Cost of one evaluation\").\nQI_MEMORY_GIB = 3.1","code_hash":"2e75152b52afd87d38daf540983b2901","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"Hbol","name":"*load_boundary","code":"def load_boundary(source) -> surface_rz_fourier.SurfaceRZFourier:\n    \"\"\"A plasma boundary from a JSON string, a dict, or a path to a JSON file.\n\n    The format is the one the ConStellaration dataset and leaderboard use:\n    `r_cos` and `z_sin` as (m_max + 1) × (2 n_max + 1) arrays, `n_field_periods`,\n    and `is_stellarator_symmetric: true`.\n    \"\"\"\n    if isinstance(source, surface_rz_fourier.SurfaceRZFourier):\n        return source\n    if isinstance(source, dict):\n        return surface_rz_fourier.SurfaceRZFourier.model_validate(source)\n    if isinstance(source, Path) or (isinstance(source, str) and not source.lstrip().startswith(\"{\")):\n        source = Path(source).read_text()\n    return surface_rz_fourier.SurfaceRZFourier.model_validate_json(source)","code_hash":"cd42294ccff0b6768a4f7a28243a572e","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"MJUe","name":"*rotating_ellipse","code":"def rotating_ellipse(aspect_ratio: float, elongation: float, rotational_transform: float, n_field_periods: int) -> surface_rz_fourier.SurfaceRZFourier:\n    \"\"\"The rotating-ellipse boundary the paper's baselines start from, with major radius 1.\"\"\"\n    return initial_guess.generate_rotating_ellipse(\n        aspect_ratio=aspect_ratio,\n        elongation=elongation,\n        rotational_transform=rotational_transform,\n        n_field_periods=n_field_periods,\n    )","code_hash":"e1e302db7d8ba1e61f63592cb7d6cac5","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"vblA","name":"*compute_metrics","code":"def compute_metrics(boundary, problem: str = \"simple_to_build\", fidelity: str = \"high_fidelity\", corrected_flux_compression: bool = False) -> dict:\n    \"\"\"Run the forward model with the settings the official evaluator uses for `problem`.\n\n    Returns the metrics as a dict. The geometrical problem skips the Boozer\n    transform and the QI residual, as the evaluator does; the other two\n    compute everything. VMEC++ raises RuntimeError when the equilibrium does\n    not converge.\n\n    `fidelity` selects the VMEC++ preset: \"high_fidelity\" is what the\n    leaderboard uses; \"low_fidelity\" and \"very_low_fidelity\" are cheaper\n    presets for the inner loop of an optimiser, whose results should be\n    checked at high fidelity. With `corrected_flux_compression` the dict also\n    holds `flux_compression_pest`, the proxy averaged with the field-aligned\n    Jacobian as constellaration 0.3.0 does, from the same equilibrium.\n    \"\"\"\n    settings = forward_model.ConstellarationSettings\n    chosen = settings.default_high_fidelity_skip_qi() if problem == \"geometrical\" else settings.default_high_fidelity()\n    if fidelity != \"high_fidelity\":\n        chosen = chosen.model_copy(update={\"vmec_preset_settings\": chosen.vmec_preset_settings.model_copy(update={\"fidelity\": fidelity})})\n    metrics, equilibrium = forward_model.forward_model(load_boundary(boundary), settings=chosen)\n    out = metrics.model_dump(mode=\"json\")\n    if corrected_flux_compression:\n        out[\"flux_compression_pest\"] = flux_compression_both_weights(equilibrium, chosen.turbulent_settings)[1]\n    return out","code_hash":"f3dd91e56b98eb7c4e17c0435b354680","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"bkHC","name":"*flux_compression_both_weights","code":"def flux_compression_both_weights(equilibrium, settings) -> tuple[float, float]:\n    \"\"\"Flux compression in bad curvature averaged with the VMEC and with the PEST Jacobian.\n\n    The first value is what constellaration 0.2.6 reports. The second changes\n    only the averaging weight, as constellaration 0.3.0 does\n    (proximafusion/constellaration#109), on the same equilibrium.\n    \"\"\"\n    from constellaration.mhd import turbulent_transport, vmec_utils\n    from simsopt import mhd\n\n    vmec = vmec_utils.as_simsopt_vmec(equilibrium)\n    alpha, theta = turbulent_transport._set_up_surface_grid(\n        settings=settings, n_field_periods=equilibrium.nfp, is_stellarator_symmetric=not equilibrium.lasym\n    )\n    data = mhd.vmec_fieldlines(vs=vmec, s=settings.normalized_toroidal_flux, alpha=alpha, theta1d=theta)\n    grad_r_squared = equilibrium.Aminor_p**2 / (4 * settings.normalized_toroidal_flux[:, None, None]) * data.grad_s_dot_grad_s\n    bad_curvature = np.heaviside(data.B_cross_kappa_dot_grad_alpha * np.sign(equilibrium.phipf[-1]), 0)\n\n    def averaged(jacobian):\n        weights = np.abs(jacobian) / np.abs(jacobian).mean(axis=(1, 2))[:, None, None]\n        return float(np.mean(bad_curvature * grad_r_squared * weights, axis=(1, 2))[0])\n\n    return averaged(data.sqrt_g_vmec), averaged(data.edge_toroidal_flux_over_2pi / data.B_sup_theta_pest)","code_hash":"10f92955487c8157ee0902d1d1cd33df","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"lEQa","name":"*constraint_report","code":"def constraint_report(problem: str, metrics: dict) -> pd.DataFrame:\n    \"\"\"Each constraint of `problem` with its value, bound and normalised violation.\n\n    The violation is (value − bound) / |bound|, signed so that a positive\n    number means the constraint is broken; the evaluator accepts violations up\n    to 0.01. For the vacuum well, whose bound is 0, it divides by 0.1. The\n    numbers come from constellaration's own code.\n    \"\"\"\n    spec = PROBLEMS[problem]\n    evaluator = spec[\"evaluator\"]()\n    typed = forward_model.ConstellarationMetrics.model_validate({k: v for k, v in metrics.items() if k in forward_model.ConstellarationMetrics.model_fields})\n    violations = evaluator._normalized_constraint_violations(typed)\n    rows = []\n    for (name, relation, bound), violation in zip(spec[\"constraints\"], violations):\n        if name.startswith(\"|\"):\n            value = abs(metrics[name.strip(\"|\")])\n        elif name == \"log10(qi)\":\n            value = float(np.log10(metrics[\"qi\"]))\n        else:\n            value = metrics[name]\n        rows.append({\n            \"constraint\": f\"{name} {relation} {bound:g}\",\n            \"value\": value,\n            \"normalised violation\": float(violation),\n            \"satisfied\": bool(violation <= TOLERANCE),\n        })\n    return pd.DataFrame(rows)","code_hash":"9eb3bb347fcc09f4ed3c29b1584792d1","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"PKri","name":"*hypervolume","code":"def hypervolume(points) -> float:\n    \"\"\"The MHD-stable score of feasible (gradient scale length, aspect ratio) points.\n\n    The area in the plane of −L∇B and A that the points dominate, up to the\n    reference point (−L∇B, A) = (1, 20). Pass only feasible points.\n    \"\"\"\n    if len(points) == 0:\n        return 0.0\n    from pymoo.indicators import hv\n\n    X = np.array([(-float(L), float(A)) for L, A in points])\n    return float(hv.Hypervolume(ref_point=np.array(HYPERVOLUME_REFERENCE))(X))","code_hash":"f8bfc9f77ddb9c2be6346399b27b60a6","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"Xref","name":"*added_hypervolume","code":"def added_hypervolume(candidate, front) -> float:\n    \"\"\"How much a feasible (L∇B, A) point would add to the hypervolume of `front`.\n\n    `front` is a list of feasible (L∇B, A) points, for example the leading\n    submission's. Zero when the candidate is dominated.\n    \"\"\"\n    return hypervolume([*front, candidate]) - hypervolume(front)","code_hash":"c1981d8929328592c483783df1917caf","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"SFPL","name":"*evaluate","code":"def evaluate(problem: str, boundary, force: bool = False) -> dict:\n    \"\"\"Score a boundary (or, for \"mhd_stable\", a set of boundaries) as the leaderboard does.\n\n    For \"geometrical\" and \"simple_to_build\" the result holds objective,\n    feasibility, feasible, score, metrics, constraints (a list of dicts as in\n    `constraint_report`) and seconds. For \"mhd_stable\", `boundary` is a list\n    of boundaries or the leaderboard's submission format (a JSON array of\n    boundary JSON strings, as text or as a path), and the result holds one\n    such record per boundary under \"points\" and the set's score, the\n    hypervolume of its feasible points. A boundary that does not parse, or\n    whose evaluation raises, usually because VMEC++ did not converge, gets a\n    record with \"error\" and feasible False, where the leaderboard would reject the whole submission;\n    for the single-objective problems the record has score 0. The calls are\n    those of constellaration 0.2.6's `evaluate`, with one forward-model run\n    per boundary.\n\n    The two QI problems raise MemoryError before any work when the machine\n    has less memory than one QI evaluation needs (`memory_shortfall`), because\n    running out of memory would end the Python process. `force=True` runs\n    them anyway.\n    \"\"\"\n    if problem not in PROBLEMS:\n        raise ValueError(f\"problem must be one of {', '.join(PROBLEMS)}\")\n    refuse_without_memory(problem, force)\n    if problem == \"mhd_stable\":\n        points = [evaluate_single(problem, b) for b in boundary_list(boundary)]\n        return {\"problem\": problem, \"evaluator\": EVALUATOR_VERSION, \"points\": points, \"score\": hypervolume(feasible_points(points))}\n    return {\"problem\": problem, \"evaluator\": EVALUATOR_VERSION, **evaluate_single(problem, boundary)}","code_hash":"3bb038d073647c127c29fd1c6a534ac8","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"BYtC","name":"*boundary_list","code":"def boundary_list(boundaries) -> list:\n    \"\"\"The boundaries of a list, or of the leaderboard's submission format.\n\n    The submission format is a JSON array of boundary JSON strings, given as\n    text or as a path to a file. A single boundary becomes a list of one.\n    \"\"\"\n    if isinstance(boundaries, Path) or (isinstance(boundaries, str) and not boundaries.lstrip().startswith(\"{\")):\n        text = boundaries if isinstance(boundaries, str) and boundaries.lstrip().startswith(\"[\") else Path(boundaries).read_text()\n        boundaries = json.loads(text)\n    if isinstance(boundaries, (str, dict, surface_rz_fourier.SurfaceRZFourier)):\n        return [boundaries]\n    return list(boundaries)","code_hash":"91e9dbde19b24215de5fc27d48de4501","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"RGSE","name":"*feasible_points","code":"def feasible_points(records) -> list[tuple[float, float]]:\n    \"\"\"The (L∇B, A) points of the feasible records among `records`, as `hypervolume` takes them.\"\"\"\n    return [\n        (r[\"metrics\"][\"minimum_normalized_magnetic_gradient_scale_length\"], r[\"metrics\"][\"aspect_ratio\"])\n        for r in records\n        if r[\"feasible\"]\n    ]","code_hash":"7711f386a9942b1c3ec0342b90d47777","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"Kclp","name":"*error_record","code":"def error_record(problem: str, message: str, seconds: float) -> dict:\n    \"\"\"The record of a boundary whose evaluation failed: not feasible, and score 0 for the single-objective problems.\"\"\"\n    record = {\"error\": message, \"feasible\": False, \"feasibility\": None, \"metrics\": None, \"constraints\": None}\n    if problem != \"mhd_stable\":\n        record.update(objective=None, score=0.0)\n    record[\"seconds\"] = round(seconds, 1)\n    return record","code_hash":"0341398aa63aa9b022c7f12196b1a4e8","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"emfo","name":"*evaluate_single","code":"def evaluate_single(problem: str, boundary) -> dict:\n    \"\"\"One boundary's record for `evaluate`: one forward-model run, then the evaluator's own feasibility and score.\"\"\"\n    started = time.perf_counter()\n    try:\n        metrics = compute_metrics(load_boundary(boundary), problem)\n    except Exception as error:\n        return error_record(problem, f\"{type(error).__name__}: {error}\", time.perf_counter() - started)\n    evaluator = PROBLEMS[problem][\"evaluator\"]()\n    typed = forward_model.ConstellarationMetrics.model_validate(metrics)\n    feasible = evaluator.is_feasible(typed)\n    record = {\n        \"feasibility\": evaluator.compute_feasibility(typed),\n        \"feasible\": feasible,\n        \"metrics\": metrics,\n        \"constraints\": constraint_report(problem, metrics).to_dict(\"records\"),\n    }\n    if problem != \"mhd_stable\":\n        record[\"objective\"] = evaluator.get_objective(typed)[0]\n        record[\"score\"] = evaluator._score(typed) if feasible else 0.0\n    record[\"seconds\"] = round(time.perf_counter() - started, 1)\n    return record","code_hash":"994b2256d4779f79689726ccc59776a8","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"Hstk","name":"*evaluate_many","code":"def evaluate_many(problem: str, boundaries, jobs: int = 1, timeout: float = 600.0, force: bool = False) -> list[dict]:\n    \"\"\"Score each boundary in its own Python process, `jobs` at a time.\n\n    `boundaries` is a list or the leaderboard's submission format, as\n    `boundary_list` reads it. Returns one record per boundary, in order, as\n    `evaluate_single` makes them, except that \"seconds\" is the wall time of\n    the whole process, imports included. A boundary that does not parse, a\n    process that runs longer than `timeout` seconds, and a process that\n    crashes each give an error record with feasible False; the other\n    boundaries still run. The timeout counts wall time, so keep `jobs` at or\n    below the number of CPUs. For \"mhd_stable\",\n    `hypervolume(feasible_points(records))` is the set's score.\n\n    The process boundary does not protect the caller from running out of\n    memory where one limit covers the caller and its children, as on\n    ScienceHub compute: a child that exceeds the kernel's memory ends the\n    whole kernel. Use `jobs=1` there. For the same reason the two QI problems\n    raise MemoryError, as in `evaluate`, unless `force=True`. Each process\n    imports this file again, so the file must exist on disk.\n    \"\"\"\n    if problem not in PROBLEMS:\n        raise ValueError(f\"problem must be one of {', '.join(PROBLEMS)}\")\n    refuse_without_memory(problem, force)\n    source = globals().get(\"__file__\")\n    if not source or not Path(source).is_file():\n        raise RuntimeError(\"evaluate_many runs this notebook's file in new processes; import it from evaluate.py on disk.\")\n    worker = (\n        \"import importlib.util, json, sys\\n\"\n        \"spec = importlib.util.spec_from_file_location('constellaration_evaluate', sys.argv[1])\\n\"\n        \"module = importlib.util.module_from_spec(spec)\\n\"\n        \"spec.loader.exec_module(module)\\n\"\n        \"task = json.loads(open(sys.argv[2]).read())\\n\"\n        \"record = module.evaluate_single(task['problem'], task['boundary'])\\n\"\n        \"open(sys.argv[3], 'w').write(json.dumps(record))\\n\"\n    )\n    texts, records = [], []\n    for b in boundary_list(boundaries):\n        try:\n            texts.append(load_boundary(b).model_dump_json())\n            records.append(None)\n        except Exception as error:\n            texts.append(None)\n            records.append(error_record(problem, f\"{type(error).__name__}: {error}\", 0.0))\n    waiting = [i for i in reversed(range(len(texts))) if records[i] is None]\n    running = {}\n    with tempfile.TemporaryDirectory() as scratch:\n        scratch = Path(scratch)\n\n        def finished(i, child, seconds):\n            out = scratch / f\"{i}.out\"\n            if child.returncode == 0 and out.is_file():\n                return {**json.loads(out.read_text()), \"seconds\": round(seconds, 1)}\n            if child.returncode < 0:\n                reason = f\"killed by signal {-child.returncode}\"\n                if child.returncode == -9:\n                    reason += \", usually because it ran out of memory\"\n            else:\n                lines = (scratch / f\"{i}.err\").read_text().strip().splitlines()\n                reason = lines[-1] if lines else f\"exit status {child.returncode}\"\n            return error_record(problem, f\"ProcessError: {reason}\", seconds)\n\n        try:\n            while waiting or running:\n                while waiting and len(running) < max(1, jobs):\n                    i = waiting.pop()\n                    (scratch / f\"{i}.json\").write_text(json.dumps({\"problem\": problem, \"boundary\": texts[i]}))\n                    with open(scratch / f\"{i}.err\", \"w\") as err:\n                        command = [sys.executable, \"-c\", worker, str(source), str(scratch / f\"{i}.json\"), str(scratch / f\"{i}.out\")]\n                        running[i] = (subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=err), time.perf_counter())\n                time.sleep(0.2)\n                for i, (child, started) in list(running.items()):\n                    seconds = time.perf_counter() - started\n                    if child.poll() is not None:\n                        records[i] = finished(i, child, seconds)\n                    elif seconds > timeout:\n                        child.kill()\n                        child.wait()\n                        records[i] = error_record(problem, f\"TimeoutError: no result after {timeout:g} s\", seconds)\n                    else:\n                        continue\n                    del running[i]\n        finally:\n            for child, _ in running.values():\n                child.kill()\n                child.wait()\n    return records","code_hash":"739a81c6b1a9d088aeac47c117000670","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"nWHF","name":"*leader_front","code":"def leader_front(path=None) -> list[tuple[float, float]]:\n    \"\"\"The feasible (L∇B, A) points of the leading MHD-stable submission.\n\n    Read from `data/leading-front.json` beside this notebook, or from `path`.\n    The leaderboard notebook's `build_data.py` writes that file from the\n    leaderboard as recorded on 25 October 2026; the points' hypervolume is\n    the leader's score. Pass them to `added_hypervolume` as the front.\n    \"\"\"\n    if path is None:\n        path = Path(globals()[\"__file__\"]).parent / \"data\" / \"leading-front.json\"\n    return [tuple(point) for point in json.loads(Path(path).read_text())[\"points\"]]","code_hash":"4046f2ac52372863f58feea8f4935784","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"iLit","name":"*memory_limit_gib","code":"def memory_limit_gib() -> float | None:\n    \"\"\"The memory available to this process in GiB, or None when it cannot tell.\n\n    Reads the container's limit from cgroup v2, then cgroup v1, which reports\n    no limit as a number near 2**63. Without a limit there, or without\n    cgroup files, it falls back to MemTotal in /proc/meminfo. macOS has\n    neither and gives None.\n    \"\"\"\n    for name in (\"/sys/fs/cgroup/memory.max\", \"/sys/fs/cgroup/memory/memory.limit_in_bytes\"):\n        try:\n            raw = Path(name).read_text().strip()\n        except OSError:\n            continue\n        if raw != \"max\" and int(raw) < 2**60:\n            return int(raw) / 2**30\n        break\n    try:\n        for line in Path(\"/proc/meminfo\").read_text().splitlines():\n            if line.startswith(\"MemTotal:\"):\n                return int(line.split()[1]) / 2**20\n    except (OSError, ValueError, IndexError):\n        pass\n    return None","code_hash":"67f408e4319fdcb59bd6c506ea6f597a","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"ZHCJ","name":"*memory_shortfall","code":"def memory_shortfall(problem: str) -> str | None:\n    \"\"\"Why `problem` cannot run in this machine's memory, or None when it can.\n\n    Only the two QI problems need more than a ScienceHub kernel's 1.5 GiB.\n    \"\"\"\n    limit = memory_limit_gib()\n    if problem == \"geometrical\" or limit is None or limit >= QI_MEMORY_GIB:\n        return None\n    return (\n        f\"This machine has {limit:.1f} GiB of memory. The QI residual of constellaration 0.2.6 needs \"\n        f\"about {QI_MEMORY_GIB:g} GiB, so only the geometrical problem runs here. Run the other two on \"\n        \"a computer with at least 4 GiB.\"\n    )","code_hash":"87ff2dfed75be51f34df7760784d58a4","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"ROlb","name":"*refuse_without_memory","code":"def refuse_without_memory(problem: str, force: bool = False) -> None:\n    \"\"\"Raise MemoryError with `memory_shortfall`'s reason, unless `force` is set.\"\"\"\n    reason = None if force else memory_shortfall(problem)\n    if reason:\n        raise MemoryError(f\"{reason} Pass force=True to run it anyway.\")","code_hash":"a423ee3a524f6c856a3306612d542766","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"qnkX","name":"*cross_sections","code":"def cross_sections(boundary, toroidal_angles=None, n_theta: int = 256):\n    \"\"\"R and Z of the boundary in planes of constant toroidal angle φ.\n\n    Evaluates R = Σ r_cos[m, n] cos(mθ − n N φ) and Z = Σ z_sin[m, n] sin(mθ − n N φ)\n    on the stored coefficients, without solving anything. Returns a list of\n    (φ, R, Z) with φ in radians.\n    \"\"\"\n    b = load_boundary(boundary)\n    nfp = b.n_field_periods\n    if toroidal_angles is None:\n        toroidal_angles = [0.0, np.pi / (2 * nfp), np.pi / nfp]\n    r_cos, z_sin = np.asarray(b.r_cos), np.asarray(b.z_sin)\n    m = np.arange(r_cos.shape[0])[:, None]\n    n = np.arange(r_cos.shape[1])[None, :] - (r_cos.shape[1] - 1) // 2\n    theta = np.linspace(0, 2 * np.pi, n_theta)\n    out = []\n    for phi in toroidal_angles:\n        angle = m[..., None] * theta - (n * nfp * phi)[..., None]\n        out.append((phi, (r_cos[..., None] * np.cos(angle)).sum((0, 1)), (z_sin[..., None] * np.sin(angle)).sum((0, 1))))\n    return out","code_hash":"35e0c32c7d767a3567ce599734fda194","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"TqIu","name":"_","code":"import marimo as mo\nimport matplotlib.pyplot as plt","code_hash":"7802eb41d8b52c22105f8bbe08d60476","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"Vxnm","name":"_","code":"mo.md(r\"\"\"\n# Evaluate a stellarator boundary\n\nA stellarator confines a fusion plasma in a twisted magnetic field made by\nexternal coils alone. Its performance is set mostly by the shape of the\noutermost magnetic surface, the plasma boundary, and finding a good shape\nis an optimisation problem with expensive physics in the loop.\nConStellaration, released by Proxima Fusion with a dataset of about 180,000\nboundaries, poses three such problems with fixed evaluation code, so that\nresults from different groups can be compared\n([Cadena et al. 2025](https://arxiv.org/abs/2506.19583)).\n\nThis notebook holds that evaluation code, pinned to **constellaration\n0.2.6**, the version the public leaderboard uses, and exposes it as\nfunctions another notebook can call. The booz-xform override in this\nnotebook's header changes no number; `README.md` beside the notebook\ngives the comparison and the time and memory one evaluation takes. The\nproject's other notebooks look at the dataset and at the leaderboard,\nwhere the recorded scores reproduce in the leaderboard's environment.\n\"\"\")","code_hash":"8dae936a43bb0dadb2b0836364e10b84","config":{"column":null,"disabled":false,"hide_code":true,"expand_output":false}},{"id":"DnEU","name":"_","code":"mo.md(r\"\"\"\n## The boundary\n\nAll three problems take a stellarator-symmetric boundary in cylindrical\ncoordinates (R, φ, Z), written as a Fourier series in the poloidal angle θ\nand the toroidal angle φ:\n\n$$R(\\theta, \\varphi) = \\sum_{m,n} r_{mn} \\cos(m\\theta - n N_\\mathrm{fp}\\varphi), \\qquad\nZ(\\theta, \\varphi) = \\sum_{m,n} z_{mn} \\sin(m\\theta - n N_\\mathrm{fp}\\varphi),$$\n\nwhere $N_\\mathrm{fp}$ is the number of field periods. The dataset uses\nmodes up to m = n = 4 and fixes $r_{00} = 1$, which leaves 80 free\ncoefficients; the evaluator accepts more modes, and the leading\ngeometrical entries use poloidal modes up to m = 12. The boundary below is the\nexample Proxima ships with the benchmark, with four field periods.\n\"\"\")","code_hash":"e9f4031fc6cf3c58dfa5cbf96b128de4","config":{"column":null,"disabled":false,"hide_code":true,"expand_output":false}},{"id":"ulZA","name":"_","code":"reference = load_boundary(mo.notebook_dir() / \"data\" / \"reference-boundary.json\")\nreference_set = [load_boundary(s) for s in json.loads((mo.notebook_dir() / \"data\" / \"reference-boundaries.json\").read_text())]","code_hash":"eedc64d42a6b95583c90cfeb7af7fbd8","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"ecfG","name":"_","code":"_fig, _ax = plt.subplots(figsize=(5.5, 4))\nfor (_phi, _R, _Z), _color in zip(cross_sections(reference), (\"#1f2328\", \"#0969da\", \"#9a6700\")):\n    _ax.plot(_R, _Z, color=_color, lw=1.6, label=f\"φ = {_phi * reference.n_field_periods / np.pi:.1f} π/N\")\n_ax.set_aspect(\"equal\")\n_ax.set_xlabel(\"R\")\n_ax.set_ylabel(\"Z\")\n_ax.legend(frameon=False, fontsize=9)\nfor _side in (\"top\", \"right\"):\n    _ax.spines[_side].set_visible(False)\n_fig.tight_layout()\nmo.vstack([\n    _fig,\n    mo.md(\n        f\"Cross-sections of the reference boundary at the start, quarter and half of a field period. \"\n        f\"N = {reference.n_field_periods} field periods, {reference.r_cos.shape[0]} × {reference.r_cos.shape[1]} coefficients for R and for Z.\"\n    ),\n])","code_hash":"f4af439195b432c7b7c66e9b693c8913","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"Pvdt","name":"_","code":"mo.md(r\"\"\"\n## The three problems\n\nThe forward model solves for the ideal-MHD equilibrium inside the boundary\nwith VMEC++, with essentially no pressure and no net toroidal current, at\na resolution of at least ten poloidal and toroidal modes that grows with\nthe boundary's own, and computes the quantities the constraints use\nfrom it. A boundary is feasible when every normalised constraint violation,\n(value − bound) / |bound| with the sign chosen so that positive means\nbroken, is at most 0.01.\n\n**Geometrical.** Minimise the maximum elongation of the cross-sections at\naspect ratio at most 4, average triangularity at most −0.5 and edge\nrotational transform per field period at least 0.3. Score\n1 − (ε_max − 1)/9, clipped to [0, 1], when feasible. No quasi-isodynamicity is required; this\nis the warm-up problem.\n\n**Simple-to-build QI.** Maximise the smallest magnetic gradient scale\nlength on the boundary, $L_{\\nabla B} = B\\sqrt{2}/\\lVert\\nabla \\mathbf{B}\\rVert_F$,\nnormalised as $\\tilde L_{\\nabla B} = N_\\mathrm{fp} L_{\\nabla B}/a$ with $a$\nthe minor radius. It is a proxy for how far the coils can stand from the\nplasma (Kappel et al. 2024). The field must be quasi-isodynamic (QI):\nomnigenous, so that trapped particles have no bounce-averaged radial\ndrift, with contours of |B| that close poloidally, which keeps the\nbootstrap current small. The QI residual of Goodman et al. (2023)\nmeasures the distance from such a field. Score $\\tilde L_{\\nabla B}/20$,\nclipped to [0, 1], when feasible.\n\n**MHD-stable QI.** The trade-off between $\\tilde L_{\\nabla B}$ and compactness,\nfor QI fields that also have a magnetic well (W ≥ 0, a proxy for ideal-MHD\nstability) and little flux compression in regions of bad curvature: the\nflux-surface average at ρ = 0.7 of $|\\nabla r|^2$ where the curvature\ndrift is bad and zero elsewhere, a geometric proxy for turbulent\ntransport. A submission is a set of\nboundaries; its score is the area its feasible points dominate in the\nplane of $-\\tilde L_{\\nabla B}$ and aspect ratio, measured up to the reference\npoint (1, 20). The score has no upper bound.\n\"\"\")","code_hash":"3a1aeae3d7257278df052d6aaf507a52","config":{"column":null,"disabled":false,"hide_code":true,"expand_output":false}},{"id":"ZBYS","name":"_","code":"_rows = []\nfor _key, _spec in PROBLEMS.items():\n    for _name, _relation, _bound in _spec[\"constraints\"]:\n        _rows.append({\"problem\": _spec[\"title\"], \"objective\": f\"{_spec['objective'][1]} {_spec['objective'][0]}\", \"constraint\": f\"{_name} {_relation} {_bound:g}\"})\nmo.vstack([mo.md(\"Constraints of constellaration 0.2.6\"), mo.Html(pd.DataFrame(_rows).to_html(index=False, border=0))])","code_hash":"93f6d5aa4442b3b3d1b26330d6b049e7","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"aLJB","name":"_","code":"mo.md(r\"\"\"\n## Run the evaluator\n\nOne evaluation is a full equilibrium solve at the leaderboard's\nresolution. On one x86-64 core of our CI runners it takes about a minute\nfor the example boundary and up to about four minutes for the largest\nboundaries on the leaderboard. On a ScienceHub kernel the example\nboundary took 189 s. The two QI problems also compute a Boozer\ntransform and the QI residual, which need about 3 GiB of memory, so on a\nScienceHub kernel (1.5 GiB) this notebook refuses them. The geometrical\nproblem needs 1.0 GiB for boundaries of the dataset's size. The largest\nleaderboard boundaries, with 13 × 25 modes, may not fit: our runs\nmeasured at most 1.72 GiB for them, in worker processes that had already\nrun other evaluations, so a single evaluation may need less.\n\nChoose a problem and start it. The MHD-stable problem evaluates the\ntwo-boundary example set and reports each point and the hypervolume of\nthe feasible ones. On the geometrical problem the example boundary\nbreaks its constraints by a normalised violation of 1.23 and scores 0;\nthe constraint table shows by how much each one misses.\n\"\"\")","code_hash":"b3c903110c5dc039d25fe422e2922b5b","config":{"column":null,"disabled":false,"hide_code":true,"expand_output":false}},{"id":"nHfw","name":"_","code":"problem_choice = mo.ui.dropdown(\n    options={spec[\"title\"]: key for key, spec in PROBLEMS.items()},\n    value=\"Geometrical\",\n    label=\"Problem\",\n)\nrun = mo.ui.run_button(label=\"Evaluate\")\nmo.hstack([problem_choice, run], justify=\"start\", gap=1)","code_hash":"8bf1d9a993eabf0ab370f5371bc8e946","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"xXTn","name":"_","code":"mo.stop(not run.value, mo.md(\"The evaluation starts when you press **Evaluate**.\"))\n_problem = problem_choice.value\n_shortfall = memory_shortfall(_problem)\nmo.stop(\n    _shortfall is not None,\n    mo.md(f\"{_shortfall} The section on calling these functions shows how to install the evaluator there.\"),\n)\nwith mo.status.spinner(title=\"Solving the equilibrium with VMEC++\"):\n    result = evaluate(_problem, reference_set if _problem == \"mhd_stable\" else reference)","code_hash":"89ca9ebe8663fc1c03a106523c30feba","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"AjVT","name":"_","code":"if result[\"problem\"] == \"mhd_stable\":\n    _points = pd.DataFrame([\n        {\n            \"aspect ratio\": p[\"metrics\"][\"aspect_ratio\"] if p[\"metrics\"] else None,\n            \"L∇B\": p[\"metrics\"][\"minimum_normalized_magnetic_gradient_scale_length\"] if p[\"metrics\"] else None,\n            \"feasibility\": p[\"feasibility\"],\n            \"feasible\": p[\"feasible\"],\n            \"error\": p.get(\"error\"),\n            \"seconds\": p[\"seconds\"],\n        }\n        for p in result[\"points\"]\n    ])\n    _view = mo.vstack([mo.md(f\"Hypervolume score: **{result['score']:.3f}**\"), mo.Html(_points.to_html(index=False, border=0, na_rep=\"\", float_format=lambda _x: f\"{_x:.4g}\"))])\nelif \"error\" in result:\n    _view = mo.md(f\"The evaluation failed after {result['seconds']:.0f} s and scores 0: `{result['error']}`\")\nelse:\n    _view = mo.vstack([\n        mo.md(\n            f\"Objective **{result['objective']:.4f}**, feasibility {result['feasibility']:.4f} \"\n            f\"({'feasible' if result['feasible'] else 'not feasible'}), score **{result['score']:.4f}**. \"\n            f\"The evaluation took {result['seconds']:.0f} s.\"\n        ),\n        mo.Html(pd.DataFrame(result[\"constraints\"]).to_html(index=False, border=0, float_format=lambda _x: f\"{_x:.4g}\")),\n    ])\n_view","code_hash":"f70131a16fccc6f6c307eac1fa893dca","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"pHFh","name":"_","code":"mo.md(r\"\"\"\n## Calling these functions from another notebook\n\n**Where each problem runs.** A ScienceHub kernel has one CPU and 1.5 GiB\nof memory. That is enough for the geometrical problem on boundaries of\nthe dataset's size. The two QI problems need about 3 GiB, and ScienceHub\ncompute has no larger kernel yet, so evaluate them on your own computer\nor another machine with at least 4 GiB. Every dependency installs there\nwithout a compiler on Python 3.10 to 3.13, on Linux x86-64 and on\nmacOS 14 or later with Apple silicon.\n\n**On your own machine** the `sciencehub` package downloads this notebook\nwith its `data/` folder and imports it. Install the evaluator with the\nsame booz-xform override as this notebook's header, because pip alone\nwould try to build booz-xform 0.0.9 from source:\n\n```sh\necho \"booz-xform==0.1.0\" > overrides.txt\nuv pip install sciencehub \"constellaration==0.2.6\" pandas pyarrow --override overrides.txt\n```\n\n```python\nimport sciencehub as sh\n\nbench = sh.load(\"sciencehub-team/constellaration/evaluate\")\nstart = bench.rotating_ellipse(aspect_ratio=8, elongation=1.5, rotational_transform=0.4, n_field_periods=3)\nprint(bench.evaluate(\"simple_to_build\", start)[\"constraints\"])\n```\n\n`sh.load` runs each function with the downloaded notebook's folder as\nthe working directory, so a relative path such as `\"my-boundary.json\"`\nwould be looked up there. Pass absolute paths, or boundaries already\nloaded with `load_boundary`.\n\n**On ScienceHub compute** a kernel cannot reach sciencehub.sh, so\n`sh.load` fails there. Copy `evaluate.py` and its `data/` folder into\nthe files of your own notebook, copy the dependencies and the\n`[tool.uv]` override of this notebook's header into yours, and import\nthe functions with `from evaluate import evaluate_many, leader_front`.\nThe notebook's folder is the kernel's working directory and is on\n`sys.path`.\n\n**In an optimisation loop**, `evaluate_many(problem, boundaries, jobs,\ntimeout)` scores each boundary in its own Python process, at most `jobs`\nat a time. A boundary that does not parse, a run that crashes, and a run\nthat exceeds `timeout` seconds of wall time (default 600; the slowest\nrun we measured took 252 s on its own CPU) each get an error record\nwith feasible False (and score 0 on the single-objective problems), so\none bad candidate does not stop the loop. Keep `jobs` at or below the\nnumber of CPUs, or slow runs share a CPU and reach the timeout. Each record serialises to JSON and\ncan be appended to a JSON-lines log.\n`compute_metrics(..., fidelity=\"low_fidelity\")` is cheaper for an inner\nloop; check what it finds at the leaderboard's fidelity.\n\nRunning out of memory is different on ScienceHub compute. The kernel's\n1.5 GiB limit covers the kernel and every process it starts, so a run\nthat exceeds it ends the kernel, not only the run. The kernel already\nholds constellaration's imports, and each run imports them again and\npeaks at about 1.0 GiB for the geometrical problem; we have not measured\nthe two together. Use `jobs=1` in a kernel. `evaluate` and\n`evaluate_many` raise MemoryError for the two QI problems when the\nmemory is below what one QI run needs; `force=True` overrides that.\nWithout a container memory\nlimit, as on your own computer, the operating system usually kills only\nthe run, which then gets an error record like any other crash.\n\n**Against the current leader**, `leader_front()` returns the 688\nfeasible (L∇B, A) points of the best MHD-stable submission as recorded\non 25 October 2026, read from `data/leading-front.json`, and\n`added_hypervolume(point, leader_front())` is the area a new feasible\npoint would add to that front. For a set of records from\n`evaluate_many`, `feasible_points(records)` gives the points to pass.\n\"\"\")","code_hash":"ccdc2fda695597ac53bea33682db5232","config":{"column":null,"disabled":false,"hide_code":true,"expand_output":false}},{"id":"NCOB","name":"_","code":"leading = leader_front(mo.notebook_dir() / \"data\" / \"leading-front.json\")\n_candidate = (15.0, 10.0)\nmo.md(\n    f\"The leading front holds {len(leading)} feasible points with a hypervolume of \"\n    f\"**{hypervolume(leading):.2f}**, the leader's recorded score. A feasible point at \"\n    f\"L∇B = {_candidate[0]:g}, A = {_candidate[1]:g} would add **{added_hypervolume(_candidate, leading):.2f}** to it.\"\n)","code_hash":"ff9f48b6cfa56d500b0a81a4b228f18a","config":{"column":null,"disabled":false,"hide_code":false,"expand_output":false}},{"id":"aqbW","name":"_","code":"mo.md(r\"\"\"\n## Limits and references\n\nThe evaluator is constellaration 0.2.6 unchanged; only booz-xform, the\nBoozer transform it calls, is 0.1.0 instead of 0.0.9 because 0.0.9 cannot\nbe installed without a compiler. Version 0.3.0 corrects how the flux\ncompression is averaged\n([constellaration#109](https://github.com/proximafusion/constellaration/pull/109))\nand changes which MHD-stable boundaries are feasible; the leaderboard\nstays on 0.2.6, and so does this notebook. `compute_metrics(...,\ncorrected_flux_compression=True)` adds the corrected proxy alone. A full\n0.3.0 installation also moves VMEC++ from 0.4.11 to 0.5.4 and other\nnumerical dependencies, so its scores can differ in more than that one\nquantity. The equilibrium is computed in near-vacuum, and the gradient\nscale length stands in for coil complexity; neither replaces a finite-β\nequilibrium or a coil design. The Hugging Face dataset includes VMEC++\nequilibria at volume-averaged β of 1 to 5 % for many of its boundaries.\nOptimisers that work at lower VMEC++ resolution should check their\nresults here, because a boundary that converges at low resolution can\nfail at this one.\n\nCadena, S. A., Merlo, A., Laude, E., Bauer, A., Agrawal, A., Pascu, M.,\nSavtchouk, M., Guiraud, E., Bonauer, L., Hudson, S. R. and Kaiser, M.\n(2025). ConStellaration: A dataset of QI-like stellarator plasma\nboundaries and optimization benchmarks. *NeurIPS 2025 Datasets and\nBenchmarks Track*. [OpenReview NQSbGKlCpx](https://openreview.net/forum?id=NQSbGKlCpx),\n[arXiv:2506.19583](https://arxiv.org/abs/2506.19583).\n\nCode: [proximafusion/constellaration](https://github.com/proximafusion/constellaration)\nat tag v0.2.6, MIT licence, Copyright (c) 2025-present Proxima Fusion GmbH.\nThe two example boundaries in `data/` are `hugging_face_competition/inputs/boundary.json`\nand `boundaries.json` at that tag, renamed and otherwise unchanged.\n\nKappel, J., Landreman, M. and Malhotra, D. (2024). The magnetic gradient\nscale length explains why certain plasmas require close external magnetic\ncoils. *Plasma Phys. Control. Fusion* 66, 025018.\n\nGoodman, A. G. et al. (2023). Constructing precisely quasi-isodynamic\nmagnetic fields. *J. Plasma Phys.* 89, 905890504.\n\"\"\")","code_hash":"9ac4bc03d6592fdd30c6b09fb3309d90","config":{"column":null,"disabled":false,"hide_code":true,"expand_output":false}}]},"session":{"version":"1","metadata":{"marimo_version":"0.25.2","script_metadata_hash":"180113b1ab2ca731841b391e97d93382"},"cells":[{"id":"setup","code_hash":"2e75152b52afd87d38daf540983b2901","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[{"type":"stream","name":"stderr","text":"/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/geo/surfaceobjectives.py:1092: SyntaxWarning: invalid escape sequence '\\l'\n  vector-Jacobian product of \\lm^T * dlsqgrad_dcoils, \\lm^T * dlsqgrad_dcurrents:\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/geo/surface.py:328: SyntaxWarning: invalid escape sequence '\\p'\n  This function takes in a cylindrical angle :math:`\\phi` and returns the cross\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/geo/surfacerzfourier.py:928: SyntaxWarning: invalid escape sequence '\\p'\n  cross-section at :math:`\\phi=0` being taller than it is wide.\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/objectives/constrained.py:36: SyntaxWarning: invalid escape sequence '\\m'\n  \\min_x f(x)\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/geo/surfacegarabedian.py:116: SyntaxWarning: invalid escape sequence '\\D'\n  Return a particular :math:`\\Delta_{m,n}` coefficient.\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/geo/surfacegarabedian.py:122: SyntaxWarning: invalid escape sequence '\\D'\n  Set a particular :math:`\\Delta_{m,n}` coefficient.\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/mhd/vmec_diagnostics.py:1554: SyntaxWarning: invalid escape sequence '\\p'\n  plt.xlabel('Standard toroidal angle $\\phi$')\n/opt/marimohub/venv/lib/python3.13/site-packages/simsopt/mhd/profiles.py:109: SyntaxWarning: invalid escape sequence '\\p'\n  Here :math:`s` is defined as :math:`s = \\psi_t/\\psi_{edge}`. Thus,\n/opt/marimohub/venv/lib/python3.13/site-packages/qic/plot.py:615: SyntaxWarning: invalid escape sequence '\\p'\n  ax.xaxis.set_major_formatter(tck.FormatStrFormatter('%g $\\pi$'))\n/opt/marimohub/venv/lib/python3.13/site-packages/qic/plot.py:616: SyntaxWarning: invalid escape sequence '\\p'\n  ax.yaxis.set_major_formatter(tck.FormatStrFormatter('%g $\\pi$'))\n/opt/marimohub/venv/lib/python3.13/site-packages/qic/plot.py:647: SyntaxWarning: invalid escape sequence '\\p'\n  ax.xaxis.set_major_formatter(tck.FormatStrFormatter('%g $\\pi$'))\n/opt/marimohub/venv/lib/python3.13/site-packages/qic/plot.py:648: SyntaxWarning: invalid escape sequence '\\p'\n  ax.yaxis.set_major_formatter(tck.FormatStrFormatter('%g $\\pi$'))\n","mimetype":"text/plain"}]},{"id":"Hbol","code_hash":"cd42294ccff0b6768a4f7a28243a572e","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"MJUe","code_hash":"e1e302db7d8ba1e61f63592cb7d6cac5","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"vblA","code_hash":"f3dd91e56b98eb7c4e17c0435b354680","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"bkHC","code_hash":"10f92955487c8157ee0902d1d1cd33df","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"lEQa","code_hash":"9eb3bb347fcc09f4ed3c29b1584792d1","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"PKri","code_hash":"f8bfc9f77ddb9c2be6346399b27b60a6","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"Xref","code_hash":"c1981d8929328592c483783df1917caf","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"SFPL","code_hash":"3bb038d073647c127c29fd1c6a534ac8","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"BYtC","code_hash":"91e9dbde19b24215de5fc27d48de4501","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"RGSE","code_hash":"7711f386a9942b1c3ec0342b90d47777","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"Kclp","code_hash":"0341398aa63aa9b022c7f12196b1a4e8","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"emfo","code_hash":"994b2256d4779f79689726ccc59776a8","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"Hstk","code_hash":"739a81c6b1a9d088aeac47c117000670","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"nWHF","code_hash":"4046f2ac52372863f58feea8f4935784","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"iLit","code_hash":"67f408e4319fdcb59bd6c506ea6f597a","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"ZHCJ","code_hash":"87ff2dfed75be51f34df7760784d58a4","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"ROlb","code_hash":"a423ee3a524f6c856a3306612d542766","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"qnkX","code_hash":"35e0c32c7d767a3567ce599734fda194","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"TqIu","code_hash":"7802eb41d8b52c22105f8bbe08d60476","outputs":[{"type":"data","data":{"text/html":"<div style='display: flex;flex: 1;flex-direction: column;justify-content: flex-start;align-items: normal;flex-wrap: nowrap;gap: 0.5rem'><marimo-ui-element object-id='TqIu-0' random-id='8de205dd-ad93-5f66-33e9-25526011664f'><marimo-anywidget data-initial-value='{&quot;model_id&quot;:&quot;5bbd06f3970c4aea80a14e0459d29b9e&quot;}' data-label='null' data-model-id='&quot;5bbd06f3970c4aea80a14e0459d29b9e&quot;'></marimo-anywidget></marimo-ui-element></div>"}}],"console":[]},{"id":"Vxnm","code_hash":"8dae936a43bb0dadb2b0836364e10b84","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><h1 id=\"evaluate-a-stellarator-boundary\">Evaluate a stellarator boundary</h1>\n<span class=\"paragraph\">A stellarator confines a fusion plasma in a twisted magnetic field made by\nexternal coils alone. Its performance is set mostly by the shape of the\noutermost magnetic surface, the plasma boundary, and finding a good shape\nis an optimisation problem with expensive physics in the loop.\nConStellaration, released by Proxima Fusion with a dataset of about 180,000\nboundaries, poses three such problems with fixed evaluation code, so that\nresults from different groups can be compared\n(<a href=\"https://arxiv.org/abs/2506.19583\" rel=\"noopener noreferrer\" target=\"_blank\">Cadena et al. 2025</a>).</span>\n<span class=\"paragraph\">This notebook holds that evaluation code, pinned to <strong>constellaration\n0.2.6</strong>, the version the public leaderboard uses, and exposes it as\nfunctions another notebook can call. The booz-xform override in this\nnotebook's header changes no number; <code>README.md</code> beside the notebook\ngives the comparison and the time and memory one evaluation takes. The\nproject's other notebooks look at the dataset and at the leaderboard,\nwhere the recorded scores reproduce in the leaderboard's environment.</span></span>"}}],"console":[]},{"id":"DnEU","code_hash":"e9f4031fc6cf3c58dfa5cbf96b128de4","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"the-boundary\">The boundary</h2>\n<span class=\"paragraph\">All three problems take a stellarator-symmetric boundary in cylindrical\ncoordinates (R, φ, Z), written as a Fourier series in the poloidal angle θ\nand the toroidal angle φ:</span>\n<marimo-tex class=\"arithmatex\">||[R(\\theta, \\varphi) = \\sum_{m,n} r_{mn} \\cos(m\\theta - n N_\\mathrm{fp}\\varphi), \\qquad\nZ(\\theta, \\varphi) = \\sum_{m,n} z_{mn} \\sin(m\\theta - n N_\\mathrm{fp}\\varphi),||]</marimo-tex><span class=\"paragraph\">where <marimo-tex class=\"arithmatex\">||(N_\\mathrm{fp}||)</marimo-tex> is the number of field periods. The dataset uses\nmodes up to m = n = 4 and fixes <marimo-tex class=\"arithmatex\">||(r_{00} = 1||)</marimo-tex>, which leaves 80 free\ncoefficients; the evaluator accepts more modes, and the leading\ngeometrical entries use poloidal modes up to m = 12. The boundary below is the\nexample Proxima ships with the benchmark, with four field periods.</span></span>"}}],"console":[]},{"id":"ulZA","code_hash":"eedc64d42a6b95583c90cfeb7af7fbd8","outputs":[{"type":"data","data":{"text/plain":""}}],"console":[]},{"id":"ecfG","code_hash":"f4af439195b432c7b7c66e9b693c8913","outputs":[{"type":"data","data":{"text/html":"<div style='display: flex;flex: 1;flex-direction: column;justify-content: flex-start;align-items: normal;flex-wrap: nowrap;gap: 0.5rem'><marimo-mime-renderer data-mime='&quot;application/vnd.marimo+mimebundle&quot;' data-data='&quot;{&#92;&quot;image/png&#92;&quot;: &#92;&quot;https://nuobzamne4zxrprj5btgelybtf.notebooks.sciencehubusercontent.com/_outputs/eyJuYW1lc3BhY2VJZCI6Im5zLXdicjZya2tkOWZqMG1neGUiLCJwcm9qZWN0SWQiOiJwcm9qLWs4ams4ZnQ4dDAzamc1NnEiLCJub3RlYm9va0lkIjoibmItbmNiMnNnM3ZjZDloZHIxMiIsImhhc2giOiI4MjAyODNjMDA0OTBjMjA1NzI1MGM3ZTUxM2U5MTI5MiIsInNoYXJlZCI6dHJ1ZX0.0.k83yI62qKa3gQTqgJRh0ZFzrjEnagewHsNSSnr2aWS8&#92;&quot;, &#92;&quot;__metadata__&#92;&quot;: {&#92;&quot;image/png&#92;&quot;: {&#92;&quot;width&#92;&quot;: 453, &#92;&quot;height&#92;&quot;: 390}}}&quot;'></marimo-mime-renderer><span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">Cross-sections of the reference boundary at the start, quarter and half of a field period. N = 4 field periods, 5 × 9 coefficients for R and for Z.</span></span></div>"}}],"console":[]},{"id":"Pvdt","code_hash":"3a1aeae3d7257278df052d6aaf507a52","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"the-three-problems\">The three problems</h2>\n<span class=\"paragraph\">The forward model solves for the ideal-MHD equilibrium inside the boundary\nwith VMEC++, with essentially no pressure and no net toroidal current, at\na resolution of at least ten poloidal and toroidal modes that grows with\nthe boundary's own, and computes the quantities the constraints use\nfrom it. A boundary is feasible when every normalised constraint violation,\n(value − bound) / |bound| with the sign chosen so that positive means\nbroken, is at most 0.01.</span>\n<span class=\"paragraph\"><strong>Geometrical.</strong> Minimise the maximum elongation of the cross-sections at\naspect ratio at most 4, average triangularity at most −0.5 and edge\nrotational transform per field period at least 0.3. Score\n1 − (ε_max − 1)/9, clipped to [0, 1], when feasible. No quasi-isodynamicity is required; this\nis the warm-up problem.</span>\n<span class=\"paragraph\"><strong>Simple-to-build QI.</strong> Maximise the smallest magnetic gradient scale\nlength on the boundary, <marimo-tex class=\"arithmatex\">||(L_{\\nabla B} = B\\sqrt{2}/\\lVert\\nabla \\mathbf{B}\\rVert_F||)</marimo-tex>,\nnormalised as <marimo-tex class=\"arithmatex\">||(\\tilde L_{\\nabla B} = N_\\mathrm{fp} L_{\\nabla B}/a||)</marimo-tex> with <marimo-tex class=\"arithmatex\">||(a||)</marimo-tex>\nthe minor radius. It is a proxy for how far the coils can stand from the\nplasma (Kappel et al. 2024). The field must be quasi-isodynamic (QI):\nomnigenous, so that trapped particles have no bounce-averaged radial\ndrift, with contours of |B| that close poloidally, which keeps the\nbootstrap current small. The QI residual of Goodman et al. (2023)\nmeasures the distance from such a field. Score <marimo-tex class=\"arithmatex\">||(\\tilde L_{\\nabla B}/20||)</marimo-tex>,\nclipped to [0, 1], when feasible.</span>\n<span class=\"paragraph\"><strong>MHD-stable QI.</strong> The trade-off between <marimo-tex class=\"arithmatex\">||(\\tilde L_{\\nabla B}||)</marimo-tex> and compactness,\nfor QI fields that also have a magnetic well (W ≥ 0, a proxy for ideal-MHD\nstability) and little flux compression in regions of bad curvature: the\nflux-surface average at ρ = 0.7 of <marimo-tex class=\"arithmatex\">||(|\\nabla r|^2||)</marimo-tex> where the curvature\ndrift is bad and zero elsewhere, a geometric proxy for turbulent\ntransport. A submission is a set of\nboundaries; its score is the area its feasible points dominate in the\nplane of <marimo-tex class=\"arithmatex\">||(-\\tilde L_{\\nabla B}||)</marimo-tex> and aspect ratio, measured up to the reference\npoint (1, 20). The score has no upper bound.</span></span>"}}],"console":[]},{"id":"ZBYS","code_hash":"93f6d5aa4442b3b3d1b26330d6b049e7","outputs":[{"type":"data","data":{"text/html":"<div style='display: flex;flex: 1;flex-direction: column;justify-content: flex-start;align-items: normal;flex-wrap: nowrap;gap: 0.5rem'><span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">Constraints of constellaration 0.2.6</span></span><table class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th>problem</th>\n      <th>objective</th>\n      <th>constraint</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>Geometrical</td>\n      <td>minimise max_elongation</td>\n      <td>aspect_ratio ≤ 4</td>\n    </tr>\n    <tr>\n      <td>Geometrical</td>\n      <td>minimise max_elongation</td>\n      <td>average_triangularity ≤ -0.5</td>\n    </tr>\n    <tr>\n      <td>Geometrical</td>\n      <td>minimise max_elongation</td>\n      <td>|edge_rotational_transform_over_n_field_periods| ≥ 0.3</td>\n    </tr>\n    <tr>\n      <td>Simple-to-build QI</td>\n      <td>maximise minimum_normalized_magnetic_gradient_scale_length</td>\n      <td>aspect_ratio ≤ 10</td>\n    </tr>\n    <tr>\n      <td>Simple-to-build QI</td>\n      <td>maximise minimum_normalized_magnetic_gradient_scale_length</td>\n      <td>|edge_rotational_transform_over_n_field_periods| ≥ 0.25</td>\n    </tr>\n    <tr>\n      <td>Simple-to-build QI</td>\n      <td>maximise minimum_normalized_magnetic_gradient_scale_length</td>\n      <td>log10(qi) ≤ -4</td>\n    </tr>\n    <tr>\n      <td>Simple-to-build QI</td>\n      <td>maximise minimum_normalized_magnetic_gradient_scale_length</td>\n      <td>edge_magnetic_mirror_ratio ≤ 0.2</td>\n    </tr>\n    <tr>\n      <td>Simple-to-build QI</td>\n      <td>maximise minimum_normalized_magnetic_gradient_scale_length</td>\n      <td>max_elongation ≤ 5</td>\n    </tr>\n    <tr>\n      <td>MHD-stable QI</td>\n      <td>maximise hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)</td>\n      <td>|edge_rotational_transform_over_n_field_periods| ≥ 0.25</td>\n    </tr>\n    <tr>\n      <td>MHD-stable QI</td>\n      <td>maximise hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)</td>\n      <td>log10(qi) ≤ -3.5</td>\n    </tr>\n    <tr>\n      <td>MHD-stable QI</td>\n      <td>maximise hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)</td>\n      <td>edge_magnetic_mirror_ratio ≤ 0.25</td>\n    </tr>\n    <tr>\n      <td>MHD-stable QI</td>\n      <td>maximise hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)</td>\n      <td>flux_compression_in_regions_of_bad_curvature ≤ 0.9</td>\n    </tr>\n    <tr>\n      <td>MHD-stable QI</td>\n      <td>maximise hypervolume of (minimum_normalized_magnetic_gradient_scale_length, aspect_ratio)</td>\n      <td>vacuum_well ≥ 0</td>\n    </tr>\n  </tbody>\n</table></div>"}}],"console":[]},{"id":"aLJB","code_hash":"b3c903110c5dc039d25fe422e2922b5b","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"run-the-evaluator\">Run the evaluator</h2>\n<span class=\"paragraph\">One evaluation is a full equilibrium solve at the leaderboard's\nresolution. On one x86-64 core of our CI runners it takes about a minute\nfor the example boundary and up to about four minutes for the largest\nboundaries on the leaderboard. On a ScienceHub kernel the example\nboundary took 189 s. The two QI problems also compute a Boozer\ntransform and the QI residual, which need about 3 GiB of memory, so on a\nScienceHub kernel (1.5 GiB) this notebook refuses them. The geometrical\nproblem needs 1.0 GiB for boundaries of the dataset's size. The largest\nleaderboard boundaries, with 13 × 25 modes, may not fit: our runs\nmeasured at most 1.72 GiB for them, in worker processes that had already\nrun other evaluations, so a single evaluation may need less.</span>\n<span class=\"paragraph\">Choose a problem and start it. The MHD-stable problem evaluates the\ntwo-boundary example set and reports each point and the hypervolume of\nthe feasible ones. On the geometrical problem the example boundary\nbreaks its constraints by a normalised violation of 1.23 and scores 0;\nthe constraint table shows by how much each one misses.</span></span>"}}],"console":[]},{"id":"nHfw","code_hash":"8bf1d9a993eabf0ab370f5371bc8e946","outputs":[{"type":"data","data":{"text/html":"<div style='display: flex;flex: 1;flex-direction: row;justify-content: flex-start;align-items: normal;flex-wrap: nowrap;gap: 1rem'><marimo-ui-element object-id='nHfw-0' random-id='18db36a4-2764-d942-90ad-40dc83c0bb03'><marimo-dropdown data-initial-value='[&quot;Geometrical&quot;]' data-label='&quot;&#92;u003cspan class=&#92;&quot;markdown prose dark:prose-invert contents&#92;&quot;&#92;u003e&#92;u003cspan class=&#92;&quot;paragraph&#92;&quot;&#92;u003eProblem&#92;u003c/span&#92;u003e&#92;u003c/span&#92;u003e&quot;' data-options='[&quot;Geometrical&quot;,&quot;Simple-to-build QI&quot;,&quot;MHD-stable QI&quot;]' data-allow-select-none='false' data-searchable='false' data-full-width='false' data-disabled='false'></marimo-dropdown></marimo-ui-element><marimo-ui-element object-id='nHfw-1' random-id='1ad94b47-9c27-a442-d6ae-d7d31030bd7c'><marimo-button data-initial-value='0' data-label='&quot;&#92;u003cspan class=&#92;&quot;markdown prose dark:prose-invert contents&#92;&quot;&#92;u003e&#92;u003cspan class=&#92;&quot;paragraph&#92;&quot;&#92;u003eEvaluate&#92;u003c/span&#92;u003e&#92;u003c/span&#92;u003e&quot;' data-kind='&quot;neutral&quot;' data-disabled='false' data-full-width='false'></marimo-button></marimo-ui-element></div>"}}],"console":[]},{"id":"xXTn","code_hash":"89ca9ebe8663fc1c03a106523c30feba","outputs":[{"type":"data","data":{"text/html":"<div style='display: flex;flex: 1;flex-direction: column;justify-content: flex-start;align-items: normal;flex-wrap: nowrap;gap: 0.5rem'><marimo-progress data-title='&quot;Solving the equilibrium with VMEC++&quot;' data-progress='true'></marimo-progress></div>"}}],"console":[]},{"id":"AjVT","code_hash":"f70131a16fccc6f6c307eac1fa893dca","outputs":[{"type":"error","ename":"ancestor-stopped","evalue":"This cell wasn't run because an ancestor was stopped with `mo.stop`: ","traceback":[]}],"console":[]},{"id":"pHFh","code_hash":"ccdc2fda695597ac53bea33682db5232","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"calling-these-functions-from-another-notebook\">Calling these functions from another notebook</h2>\n<span class=\"paragraph\"><strong>Where each problem runs.</strong> A ScienceHub kernel has one CPU and 1.5 GiB\nof memory. That is enough for the geometrical problem on boundaries of\nthe dataset's size. The two QI problems need about 3 GiB, and ScienceHub\ncompute has no larger kernel yet, so evaluate them on your own computer\nor another machine with at least 4 GiB. Every dependency installs there\nwithout a compiler on Python 3.10 to 3.13, on Linux x86-64 and on\nmacOS 14 or later with Apple silicon.</span>\n<span class=\"paragraph\"><strong>On your own machine</strong> the <code>sciencehub</code> package downloads this notebook\nwith its <code>data/</code> folder and imports it. Install the evaluator with the\nsame booz-xform override as this notebook's header, because pip alone\nwould try to build booz-xform 0.0.9 from source:</span>\n<div class=\"language-sh codehilite\"><pre><span></span><code><span class=\"nb\">echo</span><span class=\"w\"> </span><span class=\"s2\">\"booz-xform==0.1.0\"</span><span class=\"w\"> </span>&gt;<span class=\"w\"> </span>overrides.txt\nuv<span class=\"w\"> </span>pip<span class=\"w\"> </span>install<span class=\"w\"> </span>sciencehub<span class=\"w\"> </span><span class=\"s2\">\"constellaration==0.2.6\"</span><span class=\"w\"> </span>pandas<span class=\"w\"> </span>pyarrow<span class=\"w\"> </span>--override<span class=\"w\"> </span>overrides.txt\n</code></pre></div>\n<div class=\"language-python codehilite\"><pre><span></span><code><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">sciencehub</span><span class=\"w\"> </span><span class=\"k\">as</span><span class=\"w\"> </span><span class=\"nn\">sh</span>\n\n<span class=\"n\">bench</span> <span class=\"o\">=</span> <span class=\"n\">sh</span><span class=\"o\">.</span><span class=\"n\">load</span><span class=\"p\">(</span><span class=\"s2\">\"sciencehub-team/constellaration/evaluate\"</span><span class=\"p\">)</span>\n<span class=\"n\">start</span> <span class=\"o\">=</span> <span class=\"n\">bench</span><span class=\"o\">.</span><span class=\"n\">rotating_ellipse</span><span class=\"p\">(</span><span class=\"n\">aspect_ratio</span><span class=\"o\">=</span><span class=\"mi\">8</span><span class=\"p\">,</span> <span class=\"n\">elongation</span><span class=\"o\">=</span><span class=\"mf\">1.5</span><span class=\"p\">,</span> <span class=\"n\">rotational_transform</span><span class=\"o\">=</span><span class=\"mf\">0.4</span><span class=\"p\">,</span> <span class=\"n\">n_field_periods</span><span class=\"o\">=</span><span class=\"mi\">3</span><span class=\"p\">)</span>\n<span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"n\">bench</span><span class=\"o\">.</span><span class=\"n\">evaluate</span><span class=\"p\">(</span><span class=\"s2\">\"simple_to_build\"</span><span class=\"p\">,</span> <span class=\"n\">start</span><span class=\"p\">)[</span><span class=\"s2\">\"constraints\"</span><span class=\"p\">])</span>\n</code></pre></div>\n<span class=\"paragraph\"><code>sh.load</code> runs each function with the downloaded notebook's folder as\nthe working directory, so a relative path such as <code>\"my-boundary.json\"</code>\nwould be looked up there. Pass absolute paths, or boundaries already\nloaded with <code>load_boundary</code>.</span>\n<span class=\"paragraph\"><strong>On ScienceHub compute</strong> a kernel cannot reach sciencehub.sh, so\n<code>sh.load</code> fails there. Copy <code>evaluate.py</code> and its <code>data/</code> folder into\nthe files of your own notebook, copy the dependencies and the\n<code>[tool.uv]</code> override of this notebook's header into yours, and import\nthe functions with <code>from evaluate import evaluate_many, leader_front</code>.\nThe notebook's folder is the kernel's working directory and is on\n<code>sys.path</code>.</span>\n<span class=\"paragraph\"><strong>In an optimisation loop</strong>, <code>evaluate_many(problem, boundaries, jobs,\ntimeout)</code> scores each boundary in its own Python process, at most <code>jobs</code>\nat a time. A boundary that does not parse, a run that crashes, and a run\nthat exceeds <code>timeout</code> seconds of wall time (default 600; the slowest\nrun we measured took 252 s on its own CPU) each get an error record\nwith feasible False (and score 0 on the single-objective problems), so\none bad candidate does not stop the loop. Keep <code>jobs</code> at or below the\nnumber of CPUs, or slow runs share a CPU and reach the timeout. Each record serialises to JSON and\ncan be appended to a JSON-lines log.\n<code>compute_metrics(..., fidelity=\"low_fidelity\")</code> is cheaper for an inner\nloop; check what it finds at the leaderboard's fidelity.</span>\n<span class=\"paragraph\">Running out of memory is different on ScienceHub compute. The kernel's\n1.5 GiB limit covers the kernel and every process it starts, so a run\nthat exceeds it ends the kernel, not only the run. The kernel already\nholds constellaration's imports, and each run imports them again and\npeaks at about 1.0 GiB for the geometrical problem; we have not measured\nthe two together. Use <code>jobs=1</code> in a kernel. <code>evaluate</code> and\n<code>evaluate_many</code> raise MemoryError for the two QI problems when the\nmemory is below what one QI run needs; <code>force=True</code> overrides that.\nWithout a container memory\nlimit, as on your own computer, the operating system usually kills only\nthe run, which then gets an error record like any other crash.</span>\n<span class=\"paragraph\"><strong>Against the current leader</strong>, <code>leader_front()</code> returns the 688\nfeasible (L∇B, A) points of the best MHD-stable submission as recorded\non 25 October 2026, read from <code>data/leading-front.json</code>, and\n<code>added_hypervolume(point, leader_front())</code> is the area a new feasible\npoint would add to that front. For a set of records from\n<code>evaluate_many</code>, <code>feasible_points(records)</code> gives the points to pass.</span></span>"}}],"console":[]},{"id":"NCOB","code_hash":"ff9f48b6cfa56d500b0a81a4b228f18a","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">The leading front holds 688 feasible points with a hypervolume of <strong>249.14</strong>, the leader's recorded score. A feasible point at L∇B = 15, A = 10 would add <strong>3.65</strong> to it.</span></span>"}}],"console":[]},{"id":"aqbW","code_hash":"9ac4bc03d6592fdd30c6b09fb3309d90","outputs":[{"type":"data","data":{"text/markdown":"<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"limits-and-references\">Limits and references</h2>\n<span class=\"paragraph\">The evaluator is constellaration 0.2.6 unchanged; only booz-xform, the\nBoozer transform it calls, is 0.1.0 instead of 0.0.9 because 0.0.9 cannot\nbe installed without a compiler. Version 0.3.0 corrects how the flux\ncompression is averaged\n(<a href=\"https://github.com/proximafusion/constellaration/pull/109\" rel=\"noopener noreferrer\" target=\"_blank\">constellaration#109</a>)\nand changes which MHD-stable boundaries are feasible; the leaderboard\nstays on 0.2.6, and so does this notebook. <code>compute_metrics(...,\ncorrected_flux_compression=True)</code> adds the corrected proxy alone. A full\n0.3.0 installation also moves VMEC++ from 0.4.11 to 0.5.4 and other\nnumerical dependencies, so its scores can differ in more than that one\nquantity. The equilibrium is computed in near-vacuum, and the gradient\nscale length stands in for coil complexity; neither replaces a finite-β\nequilibrium or a coil design. The Hugging Face dataset includes VMEC++\nequilibria at volume-averaged β of 1 to 5 % for many of its boundaries.\nOptimisers that work at lower VMEC++ resolution should check their\nresults here, because a boundary that converges at low resolution can\nfail at this one.</span>\n<span class=\"paragraph\">Cadena, S. A., Merlo, A., Laude, E., Bauer, A., Agrawal, A., Pascu, M.,\nSavtchouk, M., Guiraud, E., Bonauer, L., Hudson, S. R. and Kaiser, M.\n(2025). ConStellaration: A dataset of QI-like stellarator plasma\nboundaries and optimization benchmarks. <em>NeurIPS 2025 Datasets and\nBenchmarks Track</em>. <a href=\"https://openreview.net/forum?id=NQSbGKlCpx\" rel=\"noopener noreferrer\" target=\"_blank\">OpenReview NQSbGKlCpx</a>,\n<a href=\"https://arxiv.org/abs/2506.19583\" rel=\"noopener noreferrer\" target=\"_blank\">arXiv:2506.19583</a>.</span>\n<span class=\"paragraph\">Code: <a href=\"https://github.com/proximafusion/constellaration\" rel=\"noopener noreferrer\" target=\"_blank\">proximafusion/constellaration</a>\nat tag v0.2.6, MIT licence, Copyright (c) 2025-present Proxima Fusion GmbH.\nThe two example boundaries in <code>data/</code> are <code>hugging_face_competition/inputs/boundary.json</code>\nand <code>boundaries.json</code> at that tag, renamed and otherwise unchanged.</span>\n<span class=\"paragraph\">Kappel, J., Landreman, M. and Malhotra, D. (2024). The magnetic gradient\nscale length explains why certain plasmas require close external magnetic\ncoils. <em>Plasma Phys. Control. Fusion</em> 66, 025018.</span>\n<span class=\"paragraph\">Goodman, A. G. et al. (2023). Constructing precisely quasi-isodynamic\nmagnetic fields. <em>J. Plasma Phys.</em> 89, 905890504.</span></span>"}}],"console":[]}]},"appConfig":null,"layout":null,"renderedOn":"compute","formulas":{"||[R(\\theta, \\varphi) = \\sum_{m,n} r_{mn} \\cos(m\\theta - n N_\\mathrm{fp}\\varphi), \\qquad\nZ(\\theta, \\varphi) = \\sum_{m,n} z_{mn} \\sin(m\\theta - n N_\\mathrm{fp}\\varphi),||]":"<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1em;vertical-align:-0.25em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.0077em;\">R</span><span class=\"mopen\">(</span><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">θ</span><span class=\"mpunct\">,</span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mord mathnormal\">φ</span><span class=\"mclose\">)</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:2.4361em;vertical-align:-1.3861em;\"></span><span class=\"mop op-limits\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.05em;\"><span style=\"top:-1.9em;margin-left:0em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathnormal mtight\">m</span><span class=\"mpunct mtight\">,</span><span class=\"mord mathnormal mtight\">n</span></span></span></span><span style=\"top:-3.05em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span><span class=\"mop op-symbol large-op\">∑</span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.3861em;\"><span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">r</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.1514em;\"><span style=\"top:-2.55em;margin-left:-0.0278em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathnormal mtight\">mn</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mop\">cos</span><span class=\"mopen\">(</span><span class=\"mord mathnormal\">m</span><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">θ</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">−</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0361em;vertical-align:-0.2861em;\"></span><span class=\"mord mathnormal\">n</span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.109em;\">N</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3361em;\"><span style=\"top:-2.55em;margin-left:-0.109em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">fp</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2861em;\"><span></span></span></span></span></span></span><span class=\"mord mathnormal\">φ</span><span class=\"mclose\">)</span><span class=\"mpunct\">,</span><span class=\"mspace\" style=\"margin-right:2em;\"></span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.0715em;\">Z</span><span class=\"mopen\">(</span><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">θ</span><span class=\"mpunct\">,</span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mord mathnormal\">φ</span><span class=\"mclose\">)</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:2.4361em;vertical-align:-1.3861em;\"></span><span class=\"mop op-limits\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.05em;\"><span style=\"top:-1.9em;margin-left:0em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathnormal mtight\">m</span><span class=\"mpunct mtight\">,</span><span class=\"mord mathnormal mtight\">n</span></span></span></span><span style=\"top:-3.05em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span><span class=\"mop op-symbol large-op\">∑</span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.3861em;\"><span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.044em;\">z</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.1514em;\"><span style=\"top:-2.55em;margin-left:-0.044em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathnormal mtight\">mn</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mop\">sin</span><span class=\"mopen\">(</span><span class=\"mord mathnormal\">m</span><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">θ</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">−</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0361em;vertical-align:-0.2861em;\"></span><span class=\"mord mathnormal\">n</span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.109em;\">N</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3361em;\"><span style=\"top:-2.55em;margin-left:-0.109em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">fp</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2861em;\"><span></span></span></span></span></span></span><span class=\"mord mathnormal\">φ</span><span class=\"mclose\">)</span><span class=\"mpunct\">,</span></span></span></span></span>","||(N_\\mathrm{fp}||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:0.9694em;vertical-align:-0.2861em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.109em;\">N</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3361em;\"><span style=\"top:-2.55em;margin-left:-0.109em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">fp</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2861em;\"><span></span></span></span></span></span></span></span></span></span>","||(r_{00} = 1||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:0.5806em;vertical-align:-0.15em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">r</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3011em;\"><span style=\"top:-2.55em;margin-left:-0.0278em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">00</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:0.6444em;\"></span><span class=\"mord\">1</span></span></span></span>","||(L_{\\nabla B} = B\\sqrt{2}/\\lVert\\nabla \\mathbf{B}\\rVert_F||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:0.8333em;vertical-align:-0.15em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">L</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">∇</span><span class=\"mord mathnormal mtight\" style=\"margin-right:0.0502em;\">B</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.1572em;vertical-align:-0.25em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.0502em;\">B</span><span class=\"mord sqrt\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.9072em;\"><span class=\"svg-align\" style=\"top:-3em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"mord\" style=\"padding-left:0.833em;\"><span class=\"mord\">2</span></span></span><span style=\"top:-2.8672em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"hide-tail\" style=\"min-width:0.853em;height:1.08em;\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400em\" height=\"1.08em\" viewBox=\"0 0 400000 1080\" preserveAspectRatio=\"xMinYMin slice\"><path d=\"M95,702\nc-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14\nc0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54\nc44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10\ns173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429\nc69,-144,104.5,-217.7,106.5,-221\nl0 -0\nc5.3,-9.3,12,-14,20,-14\nH400000v40H845.2724\ns-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7\nc-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z\nM834 80h400000v40h-400000z\"/></svg></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.1328em;\"><span></span></span></span></span></span><span class=\"mord\">/</span><span class=\"mopen\">∥</span><span class=\"mord\">∇</span><span class=\"mord mathbf\">B</span><span class=\"mclose\"><span class=\"mclose\">∥</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mathnormal mtight\" style=\"margin-right:0.1389em;\">F</span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span></span></span></span>","||(\\tilde L_{\\nabla B} = N_\\mathrm{fp} L_{\\nabla B}/a||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0702em;vertical-align:-0.15em;\"></span><span class=\"mord\"><span class=\"mord katex-accent\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.9202em;\"><span style=\"top:-3em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"mord mathnormal\">L</span></span><span style=\"top:-3.6023em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"accent-body\" style=\"left:-0.2222em;\"><span class=\"mord\">~</span></span></span></span></span></span></span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">∇</span><span class=\"mord mathnormal mtight\" style=\"margin-right:0.0502em;\">B</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0361em;vertical-align:-0.2861em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.109em;\">N</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3361em;\"><span style=\"top:-2.55em;margin-left:-0.109em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">fp</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2861em;\"><span></span></span></span></span></span></span><span class=\"mord\"><span class=\"mord mathnormal\">L</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">∇</span><span class=\"mord mathnormal mtight\" style=\"margin-right:0.0502em;\">B</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mord\">/</span><span class=\"mord mathnormal\">a</span></span></span></span>","||(a||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">a</span></span></span></span>","||(\\tilde L_{\\nabla B}/20||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.1702em;vertical-align:-0.25em;\"></span><span class=\"mord\"><span class=\"mord katex-accent\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.9202em;\"><span style=\"top:-3em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"mord mathnormal\">L</span></span><span style=\"top:-3.6023em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"accent-body\" style=\"left:-0.2222em;\"><span class=\"mord\">~</span></span></span></span></span></span></span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">∇</span><span class=\"mord mathnormal mtight\" style=\"margin-right:0.0502em;\">B</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span><span class=\"mord\">/20</span></span></span></span>","||(\\tilde L_{\\nabla B}||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0702em;vertical-align:-0.15em;\"></span><span class=\"mord\"><span class=\"mord katex-accent\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.9202em;\"><span style=\"top:-3em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"mord mathnormal\">L</span></span><span style=\"top:-3.6023em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"accent-body\" style=\"left:-0.2222em;\"><span class=\"mord\">~</span></span></span></span></span></span></span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">∇</span><span class=\"mord mathnormal mtight\" style=\"margin-right:0.0502em;\">B</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span></span></span></span>","||(|\\nabla r|^2||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0641em;vertical-align:-0.25em;\"></span><span class=\"mord\">∣∇</span><span class=\"mord mathnormal\" style=\"margin-right:0.0278em;\">r</span><span class=\"mord\"><span class=\"mord\">∣</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8141em;\"><span style=\"top:-3.063em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span>","||(-\\tilde L_{\\nabla B}||)":"<span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"katex-base\"><span class=\"katex-strut\" style=\"height:1.0702em;vertical-align:-0.15em;\"></span><span class=\"mord\">−</span><span class=\"mord\"><span class=\"mord katex-accent\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.9202em;\"><span style=\"top:-3em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"mord mathnormal\">L</span></span><span style=\"top:-3.6023em;\"><span class=\"pstrut\" style=\"height:3em;\"></span><span class=\"accent-body\" style=\"left:-0.2222em;\"><span class=\"mord\">~</span></span></span></span></span></span></span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3283em;\"><span style=\"top:-2.55em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"katex-sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">∇</span><span class=\"mord mathnormal mtight\" style=\"margin-right:0.0502em;\">B</span></span></span></span></span><span class=\"vlist-s\">​</span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"><span></span></span></span></span></span></span></span></span></span>"}},"files":[{"path":"data/leading-front.json","size":28356,"url":"/sciencehub-team/constellaration/raw/data/leading-front.json"},{"path":"data/reference-boundaries.json","size":3961,"url":"/sciencehub-team/constellaration/raw/data/reference-boundaries.json"},{"path":"data/reference-boundary.json","size":3362,"url":"/sciencehub-team/constellaration/raw/data/reference-boundary.json"},{"path":"README.md","size":5440,"url":"/sciencehub-team/constellaration/raw/README.md"},{"path":"UPSTREAM-LICENSE","size":1109,"url":"/sciencehub-team/constellaration/raw/UPSTREAM-LICENSE"}]}