← All skills

mdarena

data

Access MDArena, 50 containerised molecular dynamics tasks — system preparation, parameterization, trajectory analysis, free energy, enhanced sampling — used to score coding agents. Map the catalogue and the per-task resource contract, check the force-field and PDB provenance, and find which 43 of the 50 run from a bare clone. Reads metadata only and never task content.

MDArena

Fifty molecular dynamics workflows, containerised and scored. Each task hands an agent a real system — a receptor in a bilayer, a trajectory, an alchemical setup — and a written objective, and grades what comes back twice over — a deterministic correctness check on the outputs, and a language-model judge scoring how the work was done. The tasks came out of working computational chemistry projects rather than being written as exercises, and the software is what that work actually uses.

This skill is the index and access layer. It gets you the catalogue, the per-task resource contract, the third-party data provenance, the map of what you can and cannot run, and the runner. It never touches a task's instructions, verifier, rubric or reference solution, and the reason is the next section.

Do not load this skill inside an MDArena evaluation run

A skill is loaded into an agent's context. An agent being scored on MDArena that also carries a document about MDArena is contaminated — not because anything here reveals an answer, but because knowing the shape of the benchmark, the categories, the resource limits and the fact that a judge scores the process is itself information the measurement assumes the agent does not have.

So: use this to plan, choose and audit a sweep, from outside. Do not install it into the agent under test. No other page in this registry carries that risk as sharply, because no other page is about the benchmark its reader may be sitting inside.

The project asks for the same thing in the other direction. CANARY.md at the repository root asks that everything under tasks/ — prompts, inputs, verifiers, rubrics, reference outputs, reference solutions — stay out of language-model training corpora, and embeds two marker GUIDs per file so corpus filters can drop them. That request is narrow and it is worth honouring exactly as written: it covers the task material, not the runner, not the install, not the submission flow. Read it at CANARY.md; do not reproduce either GUID anywhere, including here. Copying that string into an unrelated public document is what destroys the attribution it exists to provide.

What you need, and when

To do this You need
Everything down to Get the files — catalogue, schema, provenance, gating map git 2.37+, Python 3.11+, and network. No account, no container, no key
Install the runner and inspect it Python 3.12 or newer
Build the images and run a scored task a container runtime, an amd64 host or emulation, and an Anthropic credential
Task 38_pept2_qm_cluster_orca an ORCA distribution, obtained through FACCTs registration

The judge is not optional. All fifty tasks declare CLAUDE_CODE_OAUTH_TOKEN under [verifier.env], so end-to-end scoring needs either a subscription token or ANTHROPIC_API_KEY for metered billing. Harbor can be told to skip verification, but then nothing is graded, and the submission checker reports any such trial as a verifier-disabled finding — so there is no scored path around the credential. That is stated here, before anything that runs a task, because it is the requirement most likely to be discovered late.

No GPU is required, which is not what the base image suggests. See The task.toml contract below — the number is checked, not assumed.

Get the index without the 222 MB clone

A full clone is roughly 222 MB packed and about half a gigabyte checked out, because the tasks ship trajectories and structures. You do not need any of that to plan a sweep. A blobless sparse clone pulls the metadata layer in a couple of seconds.

set -euo pipefail

# Blobless, sparse, pinned to the tagged release. Pulls the 50 task.toml files,
# the 50 per-task Dockerfiles, the repository's own tooling and its top-level
# documents — and none of the instructions, verifiers or reference solutions.
git -c advice.detachedHead=false clone --quiet \
    --branch v0.1 --depth 1 --filter=blob:none --sparse \
    https://github.com/weitse-hsu/MDArena.git mdarena-meta
git -C mdarena-meta sparse-checkout set --no-cone \
    'tasks/*/task.toml' 'tasks/*/environment/Dockerfile' \
    'docs/**' 'scripts/**' '/*.md' '/LICENSE' '/no-github-access.yaml'

echo "commit      $(git -C mdarena-meta rev-parse HEAD)"
echo "task.toml   $(ls -d mdarena-meta/tasks/*/task.toml | wc -l | tr -d ' ')"
echo "Dockerfile  $(ls -d mdarena-meta/tasks/*/environment/Dockerfile | wc -l | tr -d ' ')"
echo "on disk     $(du -sh mdarena-meta | cut -f1)"

Run 2026-08-29, about three seconds:

commit      d50547af07a4de9720c95ae42450ee3d49b8ae14
task.toml   50
Dockerfile  50
on disk     1.1M

Three things about that command are deliberate. --filter=blob:none with --sparse fetches file contents lazily and checks out nothing until the pattern is set, so the trajectories are never transferred. --no-cone is required because cone mode matches whole directories and the point here is to take one file out of each task directory — it arrived in git 2.37, and a pattern naming a single file wants a leading slash or git warns. --branch v0.1 pins the release the published results were produced against; today main and v0.1 are the same commit, with zero commits between them, so the pin costs nothing and will matter the moment that stops being true.

The sparse pattern is also the boundary this skill respects, expressed as a checkout rather than as a promise: instruction.md, tests/ and solution/ are simply not on disk.

Licence — unmodified MIT, and why GitHub says otherwise

GitHub reports NOASSERTION for this repository, which reads as "no usable licence" and is wrong. The LICENSE file is the canonical MIT text with a THIRD-PARTY MATERIAL section appended after a horizontal rule, and the appended section is what defeats the classifier. Check it rather than taking either GitHub's word or this paragraph's.

import json, pathlib, re, urllib.request

LICENSE = pathlib.Path("mdarena-meta/LICENSE").read_text()

# Split at the horizontal rule and test the grant on its own against SPDX's
# canonical text, with the two placeholders relaxed to patterns.
grant, _, third_party = LICENSE.partition("\n---\n")

with urllib.request.urlopen("https://spdx.org/licenses/MIT.json", timeout=60) as r:
    canonical = json.load(r)["licenseText"]

flat = lambda text: re.sub(r"\s+", " ", text).strip()
pattern = re.escape(flat(canonical))
pattern = pattern.replace(re.escape("<year>"), r"\d{4}")
pattern = pattern.replace(re.escape("<copyright holders>"), r".+?")

holder = re.search(r"Copyright \(c\) \d{4} .+", grant)
print(f"grant matches the SPDX MIT text verbatim  : "
      f"{re.fullmatch(pattern, flat(grant)) is not None}")
print(f"copyright line                            : {holder.group(0)}")
print(f"appended section                          : "
      f"{third_party.strip().splitlines()[0]}, {len(third_party.split())} words")
print(f"appended section disclaims rather than grants: "
      f"{'beyond what its upstream terms allow' in third_party}")

Run 2026-08-29:

grant matches the SPDX MIT text verbatim  : True
copyright line                            : Copyright (c) 2026 Wei-Tse Hsu
appended section                          : THIRD-PARTY MATERIAL, 75 words
appended section disclaims rather than grants: True

So the code and the task scaffolding are MIT. The appended section does not narrow that grant; it says the scientific data redistributed alongside it keeps its own upstream terms, which is a correct statement rather than a restriction on the licence. Those upstream terms are itemised, and they are checked further down under Third-party data.

The copyright is held by a named individual rather than by an organisation. For a benchmark you are running that changes nothing; if you intend to redistribute the text, that is the fact to take to whoever decides such things.

The 50 tasks

Twelve categories over three difficulty levels. The grid is worth reading before choosing a subset, because the corpus is not balanced and a "random ten tasks" sample will not represent it.

import collections, pathlib, tomllib

ROOT = pathlib.Path("mdarena-meta")

# `task.description` is a one-line summary that, for several tasks, states the
# finding the task is asking for or names the defect planted in its inputs. It is
# evaluation content wearing a metadata label, so it is dropped on load and nothing
# downstream can print it by accident.
ANSWER_BEARING = ("description",)

def load(path):
    """tomllib parses values only, so the canary comment block at the head of every
    task.toml never enters the returned object. Parse these files; do not cat them."""
    with open(path, "rb") as fh:
        cfg = tomllib.load(fh)
    cfg["task"] = {k: v for k, v in cfg["task"].items() if k not in ANSWER_BEARING}
    return cfg

tasks = {p.parent.name: load(p) for p in sorted(ROOT.glob("tasks/*/task.toml"))}
assert all("description" not in t["task"] for t in tasks.values()), "projection failed"

grid = collections.defaultdict(collections.Counter)
for cfg in tasks.values():
    grid[cfg["metadata"]["category"]][cfg["metadata"]["difficulty"]] += 1

print(f"{'category':<38}{'easy':>6}{'medium':>8}{'hard':>6}{'total':>7}")
for cat in sorted(grid, key=lambda c: (-sum(grid[c].values()), c)):
    row = grid[cat]
    print(f"{cat:<38}{row['easy']:>6}{row['medium']:>8}{row['hard']:>6}"
          f"{sum(row.values()):>7}")
tot = collections.Counter(t["metadata"]["difficulty"] for t in tasks.values())
print(f"{'ALL (' + str(len(tasks)) + ' tasks, ' + str(len(grid)) + ' categories)':<38}"
      f"{tot['easy']:>6}{tot['medium']:>8}{tot['hard']:>6}{sum(tot.values()):>7}")

Read 2026-08-29 at v0.1:

category                                easy  medium  hard  total
trajectory_analysis                        5       6     2     13
system_preparation                         2       4     1      7
enhanced_sampling                          0       4     2      6
free_energy_planning                       0       6     0      6
membrane_protein_system_preparation        0       0     5      5
parameterization                           1       1     3      5
structure_analysis                         2       0     1      3
free_energy_analysis                       0       0     1      1
free_energy_setup                          0       1     0      1
quantum_chemistry                          0       0     1      1
simulation_validation                      0       1     0      1
troubleshooting                            1       0     0      1
ALL (50 tasks, 12 categories)             11      23    16     50

Twelve categories, five of them holding a single task. A per-category score is therefore an n of one for five of the twelve, and three of the names — free_energy_planning, free_energy_setup, free_energy_analysis — divide eight tasks between them along a line only their authors can arbitrate. Report by task or by difficulty, and treat category as a browsing aid.

The difficulty distribution is more usable: 11 easy, 23 medium, 16 hard, and it is not uniform across categories. Every membrane-protein preparation task is hard; every free energy planning task is medium. So a cheap subset picked on difficulty alone is skewed — five of the eleven easy tasks are trajectory analysis, and none is a membrane setup or a free energy calculation.

One field in task.toml is not metadata

The repository's own tree listing labels task.toml "metadata: resource limits, timeouts, declared artifacts". It is that, plus one field that is not.

[task].description is a single sentence per task, and it is written for a maintainer reading the tree rather than for an agent. At least five of the fifty state outright the conclusion the task is asking the agent to reach, or name the defect deliberately planted in its inputs, and several more come close enough that the only safe rule is to treat the whole field as evaluation content. A catalogue that dumps task.toml verbatim therefore ships part of the answer key in a column labelled "description", and nothing about the file warns you.

That is why the loader above projects the field out at parse time rather than remembering not to print it. The same reflex handles the canary: tomllib reads values and discards comments, so the marker block at the head of every task.toml never reaches the parsed object. Parse these files; do not cat them into a log, an issue, or a model's context.

The task.toml contract

Every task declares the same schema. What varies, and what does not, decides how you size a sweep.

import collections, pathlib, tomllib

ROOT = pathlib.Path("mdarena-meta")
# No projection here because nothing below reads `task.description` — every
# accessor is named explicitly. That is the other safe pattern: project it out,
# or never reach for it.
tasks = {p.parent.name: tomllib.load(open(p, "rb"))
         for p in sorted(ROOT.glob("tasks/*/task.toml"))}

def spread(get):
    c = collections.Counter(get(cfg) for cfg in tasks.values())
    numeric = all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in c)
    return dict(sorted(c.items(), key=(lambda kv: kv[0]) if numeric
                                  else (lambda kv: str(kv[0]))))

env = lambda k: spread(lambda c: c["environment"][k])
print("schema_version        ", spread(lambda c: c["schema_version"]))
print("artifacts             ", spread(lambda c: tuple(c["artifacts"])))
print()
print("environment.gpus      ", env("gpus"))
print("environment.cpus      ", env("cpus"))
print("environment.memory_mb ", env("memory_mb"))
print("environment.storage_mb", env("storage_mb"))
print("build_timeout_sec     ", env("build_timeout_sec"))
print("allow_internet        ", env("allow_internet"))
print()
print("agent.timeout_sec     ", spread(lambda c: c["agent"]["timeout_sec"]))
print("verifier.timeout_sec  ", spread(lambda c: c["verifier"]["timeout_sec"]))
print("verifier.env keys     ", spread(lambda c: tuple(sorted(c["verifier"]["env"]))))
print()
print("metadata key sets     ", spread(lambda c: tuple(sorted(c["metadata"]))))
odd = sorted(t for t, c in tasks.items() if c["task"]["name"] != f"mdarena/{t}")
print(f"task.name != mdarena/<dir>:  {len(odd)} of {len(tasks)}, "
      f"tasks {odd[0][:2]}-{odd[-1][:2]}")
kw = collections.Counter(k for c in tasks.values() for k in c["task"]["keywords"])
print(f"keywords: {len(kw)} distinct, {sum(v == 1 for v in kw.values())} used once, "
      f"case-variant duplicates {sorted(k for k in kw if k.lower() != k and k.lower() in kw)}")

Read 2026-08-29 at v0.1:

schema_version         {'1.2': 50}
artifacts              {('/app/outputs', '/app/final_answer.txt'): 50}

environment.gpus       {0: 50}
environment.cpus       {2: 46, 4: 4}
environment.memory_mb  {8192: 50}
environment.storage_mb {10240: 49, 20480: 1}
build_timeout_sec      {3600.0: 50}
allow_internet         {True: 50}

agent.timeout_sec      {900.0: 30, 3600.0: 6, 4500.0: 7, 7200.0: 6, 9000.0: 1}
verifier.timeout_sec   {600.0: 1, 900.0: 47, 3600.0: 1, 9000.0: 1}
verifier.env keys      {('CLAUDE_CODE_OAUTH_TOKEN',): 50}

metadata key sets      {('category', 'difficulty', 'protocol', 'system', 'task_id'): 2, ('category', 'difficulty', 'task_id'): 48}
task.name != mdarena/<dir>:  31 of 50, tasks 20-50
keywords: 101 distinct, 69 used once, case-variant duplicates ['ABFE', 'GROMACS', 'PCA', 'PepT2']

Five things in that output change what you do.

gpus = 0 on every task, on a CUDA image. The shared base builds GROMACS 2026.2 from source with -DGMX_GPU=CUDA on top of nvidia/cuda:12.4.1-devel-ubuntu22.04, which reads like a GPU requirement and is not one. Every task asks Harbor for zero GPUs, so a GPU host buys you nothing at run time. The CUDA base is a build-time cost — several gigabytes of image and a from-source GROMACS compile — with build_timeout_sec set to a full hour accordingly. Budget an hour and about 8 GB of RAM per concurrent trial, not accelerator time. A value that is the same in all fifty files could just as easily be a default nobody set; Are those numbers declared, or defaulted? below settles that, and they are declared.

allow_internet = true on every task, and it is load-bearing on both sides. The agent gets network access, which is the entire reason the network-mediation overlay further down exists. Less obviously, the verifier needs it too: several tasks' checkers resolve extra Python packages from PyPI on every grading call, uncached, so a network blip during grading fails the check rather than the task. The maintainers log this as a known fragility.

Agent timeouts span fifteen minutes to two and a half hours. Thirty of the fifty get the 15-minute floor; twenty get between one and two and a half hours. Summed, one replicate of every task allows 36.8 hours of agent time plus 15.4 hours of verifier time, so a full sweep is days of wall clock at modest concurrency, not an afternoon. A flat --timeout-multiplier also stretches the long tasks in absolute terms far more than the short ones.

task.name drops the numeric prefix from task 20 onward. Tasks 01–19 are named mdarena/<directory>; tasks 20–50 are named mdarena/<directory-without-the-number>. Harbor records the name, not the path, so joining run output back to a directory by name silently fails for 31 of 50 tasks. Join on metadata.task_id, which does equal the directory name on all fifty.

keywords is not a usable index. 101 distinct terms across 50 tasks, 69 of them used exactly once, and four appear in two casings each. Filter on category, difficulty or task_id; treat keywords as prose.

Two tasks additionally carry system and protocol keys that the other 48 do not. Nothing reads them, but code that assumes a fixed metadata key set will trip over them.

Three base images, not one

The quickstart says every task's container is built on top of one image. Two tasks say otherwise, and both failure modes appear only when you run those tasks.

import collections, pathlib, re

ROOT = pathlib.Path("mdarena-meta")
FROM_RE = re.compile(r"^\s*FROM\s+(?:--\S+\s+)*(\S+)", re.M)
COPY_RE = re.compile(r"^\s*COPY\s+(\S+)", re.M)

bases, external = collections.defaultdict(list), {}
for df in sorted(ROOT.glob("tasks/*/environment/Dockerfile")):
    tid, text = df.parents[1].name, df.read_text()
    bases[FROM_RE.search(text).group(1)].append(tid)
    for src in COPY_RE.findall(text):
        if src.startswith("inputs/"):
            continue
        arg = re.search(rf"^\s*ARG\s+{re.escape(src.strip('${}'))}=(\S+)", text, re.M)
        external[tid] = arg.group(1) if arg else src

for base, ids in sorted(bases.items(), key=lambda kv: -len(kv[1])):
    who = "" if len(ids) > 3 else "   " + ", ".join(ids)
    print(f"{len(ids):>3}  FROM {base}{who}")
print()
for tid, artefact in sorted(external.items()):
    print(f"{tid} also needs an artefact dropped into its environment/ directory:")
    print(f"      {artefact}")

Read 2026-08-29 at v0.1:

 48  FROM mdarena:latest
  1  FROM mambaorg/micromamba:latest   25_mglu5_membrane_setup
  1  FROM mdarena-plumed-lambda:latest   48_cb7_10_alchemical_metadynamics

38_pept2_qm_cluster_orca also needs an artefact dropped into its environment/ directory:
      orca_6_1_1_linux_x86-64_shared_openmpi418_nodmrg.tar.xz

mdarena-plumed-lambda:latest is a second image the quickstart does not build. It exists because GROMACS's native PLUMED interface, which the main image uses, does not support interaction with GROMACS energy — no lambda dynamics, no replica exchange — so the alchemical metadynamics task needs a classically patched build instead. Its own Dockerfile carries the build command:

docker build -f environment/Dockerfile.plumed-lambda -t mdarena-plumed-lambda:latest environment

Skip it and that one task fails at image build.

One task does not use an MDArena image at all. 25_mglu5_membrane_setup builds from mambaorg/micromamba:latest, pulled from a public registry, unpinned — the only task whose base does not come from this repository. Its recorded task digest can therefore be identical across two runs whose containers are not. (The shared base is not perfectly reproducible either — it pulls unpinned upstream layers — but at least the recipe is versioned here.)

And one task needs a distribution you have to register for. 38_pept2_qm_cluster_orca copies an ORCA tarball into its build context; ORCA is free for academic use but is distributed through FACCTs registration and cannot be redistributed here or fetched automatically. That is a restriction on one option among fifty, not on the only path, so it is a disclosure rather than a blocker — and the maintainers have it logged for removal in a later dataset version.

What runs from a bare clone, and what does not

Seven tasks cannot be run from a fresh checkout. Six are missing inputs on purpose, and the repository declares exactly which, in a file its own validator keeps honest.

import pathlib, re, tomllib

ROOT = pathlib.Path("mdarena-meta")
tasks = sorted(p.parent.name for p in ROOT.glob("tasks/*/task.toml"))

# Inputs that are real but deliberately not committed. This is an explicit, reviewed
# exemption from the rule that every /app/inputs path a task references must be
# tracked in git — the task validator fails if an entry names a task that is gone, or
# a file that has since been committed, so the list cannot rot quietly.
withheld = tomllib.load(open(ROOT / "scripts/external_inputs.toml", "rb"))
assert set(withheld) <= set(tasks), "external_inputs.toml names a task that is gone"

SIZE = re.compile(r"(\d+(?:\.\d+)?)\s*(GB|MB)")
def biggest(notes):
    mb = [float(v) * (1000 if u == "GB" else 1) for n in notes for v, u in SIZE.findall(n)]
    return max(mb) if mb else 0.0

print(f"{'task':<40}{'entries':>8}{'globs':>7}{'largest size named':>20}")
for tid in sorted(withheld):
    entries = withheld[tid]
    print(f"{tid:<40}{len(entries):>8}{sum('*' in k for k in entries):>7}"
          f"{biggest(entries.values()) / 1000:>17.1f} GB")
print()

ORCA = "38_pept2_qm_cluster_orca"          # from the Dockerfile sweep above
gated = sorted(set(withheld) | {ORCA})
print(f"gated tasks                 {len(gated)}   {', '.join(t[:2] for t in gated)}")
print(f"run from a bare clone       {len(tasks) - len(gated)} of {len(tasks)}")

Read 2026-08-29 at v0.1:

task                                     entries  globs  largest size named
19_traj_ana_open                               3      0              0.1 GB
20_fep_abfe_dhdl_analysis                      3      3              0.5 GB
22_traj_ana_pept2_pca_cv_suitability           1      0              4.8 GB
28_traj_ana_pept2_pca_plumed_setup             1      0              4.8 GB
31_traj_ana_na_conductance_6nq0                1      0              5.2 GB
39_fep_gmx_d3_abfe                            24      0              0.1 GB

gated tasks                 7   19, 20, 22, 28, 31, 38, 39
run from a bare clone       43 of 50

The size column reads the figures out of the declarations themselves, so it reports what the maintainers wrote rather than a measurement — and for tasks 20 and 39 the figure is the whole withheld set, not one file. It still separates the two causes cleanly.

Forty-three of fifty run from git clone and nothing else. That is the number to plan against, and it is the number to report alongside any score — a sweep of "MDArena" that silently skipped seven tasks is not comparable to one that ran them.

The six withheld tasks split into two causes, and the file says which is which. Three ship trajectories of 4.8 to 5.2 GB, over the 2 GiB per-object limit the file cites. Three more were small enough to distribute but together came to two thirds of the packed repository, so they were held back to keep a clone tractable. All six are declared as "contact the task authors", which is a case-by-case route rather than a published download — a restriction on six options out of fifty rather than on the only path, which is why it is a disclosure here and not a reason to leave the benchmark alone. All six are slated for subsampled inputs in a later version, at which point the entries disappear.

An entry is not a file. 20_fep_abfe_dhdl_analysis shows three entries and all three are globs; between them they cover 44 individual free-energy output files. The other five tasks list literal paths. Counting declared keys therefore under-counts the missing data by an order of magnitude for that one task, which is what the globs column exists to flag.

Third-party data, and what it obliges you to do

The MIT grant covers MDArena's own material. The force fields, parameter sets and coordinates it redistributes keep their own terms, and docs/DATA_PROVENANCE.md itemises every one. Nothing there is non-commercial, and the only copyleft is the LGPL-2.1 on one force field, which is weak and reaches neither MDArena nor your results:

Component Terms
CHARMM36 (July 2022), used by two tasks upstream force-field files MIT; the GROMACS port BSD-3-Clause
AMBER99SB*-ILDN with mutation support citation requested in the bundled documentation
OPLS-AA/L with decane parameters ships with GROMACS, LGPL-2.1
CHARMM-GUI-style toppar trees and lipid parameters, across 15 tasks inherit the CHARMM36 terms above
Ligand coordinates generated locally; the compounds come from published medicinal chemistry and are cited per task
One ligand from PubChem public domain
Experimental structures from the PDB unrestricted redistribution; the obligation is citation, and it carries to derivatives

That last row is the one worth checking rather than trusting, because a provenance table is exactly the kind of document that drifts from the thing it describes. Every PDB id in the file resolves, and the citation each row claims is the citation the PDB itself records:

import json, pathlib, re, urllib.request

PROV = pathlib.Path("mdarena-meta/docs/DATA_PROVENANCE.md").read_text()

# A PDB id is a digit plus three alphanumerics — a pattern that also matches years,
# journal page numbers and DOI fragments, all of which this file is full of. Without
# the isalpha guard the scan returns 33 ids instead of 12 and the first lookup 404s
# on "2012".
CODES = [c for c in dict.fromkeys(re.findall(r"\b([0-9][A-Z0-9]{3})\b", PROV))
         if any(ch.isalpha() for ch in c)]

def rcsb(code):
    with urllib.request.urlopen(
            f"https://data.rcsb.org/rest/v1/core/entry/{code}", timeout=60) as r:
        d = json.load(r)
    cit = d.get("rcsb_primary_citation") or {}
    return (d["rcsb_entry_info"]["experimental_method"],
            d["rcsb_accession_info"]["initial_release_date"][:10],
            f"{cit.get('journal_abbrev')} {cit.get('year')}",
            d["struct"]["title"])

print(f"PDB ids named in docs/DATA_PROVENANCE.md: {len(CODES)}\n")
print(f"{'id':<6}{'method':<8}{'released':<12}{'primary citation':<26}title")
for c in CODES:
    method, released, cite, title = rcsb(c)
    print(f"{c:<6}{method:<8}{released:<12}{cite:<26}{title[:40]}")

Read 2026-08-29 against the RCSB data API:

PDB ids named in docs/DATA_PROVENANCE.md: 12

id    method  released    primary citation          title
3PWH  X-ray   2011-09-07  Structure 2011            Thermostabilised Adenosine A2A Receptor
6X8F  X-ray   2020-11-25  J.Med.Chem. 2020          Crystal structure of TYK2 with Compound 
1A6M  X-ray   1999-04-06  Biophys.J. 1999           OXY-MYOGLOBIN, ATOMIC RESOLUTION
4S0V  X-ray   2015-01-14  Nature 2015               Crystal structure of the human OX2 orexi
5WQC  X-ray   2017-11-29  Structure 2018            Crystal structure of human orexin 2 rece
5WS3  X-ray   2017-12-13  Structure 2018            Crystal structures of human orexin 2 rec
4EIY  X-ray   2012-07-25  Science 2012              Crystal structure of the chimeric protei
5CGC  X-ray   2015-08-12  J.Med.Chem. 2015          Structure of the human class C GPCR meta
3PBL  X-ray   2010-11-03  Science 2010              Structure of the human dopamine D3 recep
6GT3  X-ray   2019-06-26  J Immunother Cancer 2020  Crystal Structure of the A2A-StaR2-bRIL5
9BIS  EM      2024-07-24  Nat Commun 2024           Cryo-EM structure of the mammalian pepti
6NQ0  EM      2019-03-27  Elife 2019                Cryo-EM structure of human TPC2 channel 

Eleven of the twelve agree with the provenance table on journal and year. Ten X-ray structures and two cryo-EM maps, released across twenty-five years, and the file's citations are the entries' own primary citations rather than a secondary source. That is a table you can cite from.

The twelfth, 6NQ0, is named in prose below the table with no citation attached, because only derived coordinates are shipped for it. The file states in its own words that the citation obligation applies equally to processed derivatives, so the two tasks built on it need the eLife 2019 paper cited even though the table does not carry it. The maintainers have completing that table logged as outstanding work.

Do not treat one component's licence as a licence on everything in a task directory. The table runs from MIT and BSD-3-Clause through LGPL-2.1 to public domain and unrestricted-with-citation, and each applies only to what its row names. The LGPL-2.1 entry is the one to read carefully if you redistribute anything: it is weak copyleft on those force-field files, not on MDArena and not on your results. And the compound coordinates carry citation obligations attached to the medicinal chemistry papers they came from, which the provenance file identifies by the papers' own compound numbering rather than by an accession you can resolve automatically.

Get the files

The catalogue, on disk — one row per task, carrying the columns you would actually filter on — plus the provenance document, which is what you cite from. No answer-bearing field is written.

import csv, os, pathlib, re, tomllib

ROOT, OUT = pathlib.Path("mdarena-meta"), pathlib.Path("Data/mdarena")
OUT.mkdir(parents=True, exist_ok=True)

withheld = tomllib.load(open(ROOT / "scripts/external_inputs.toml", "rb"))
FROM_RE = re.compile(r"^\s*FROM\s+(?:--\S+\s+)*(\S+)", re.M)
ARG_RE = re.compile(r"^\s*ARG\s+\w+=(\S+)", re.M)

rows = []
for path in sorted(ROOT.glob("tasks/*/task.toml")):
    tid = path.parent.name
    cfg = tomllib.load(open(path, "rb"))
    docker = (path.parent / "environment/Dockerfile").read_text()
    artefact = ARG_RE.search(docker)
    gate = ("inputs not committed" if tid in withheld else
            "external artefact required" if artefact else "")
    rows.append({
        "task_id": tid,
        # `task.description` is deliberately absent — see the projection above.
        "category": cfg["metadata"]["category"],
        "difficulty": cfg["metadata"]["difficulty"],
        "keywords": ";".join(cfg["task"]["keywords"]),
        "base_image": FROM_RE.search(docker).group(1),
        "cpus": cfg["environment"]["cpus"],
        "storage_mb": cfg["environment"]["storage_mb"],
        "agent_timeout_sec": int(cfg["agent"]["timeout_sec"]),
        "verifier_timeout_sec": int(cfg["verifier"]["timeout_sec"]),
        "runs_from_bare_clone": int(not gate),
        "gate": gate,
    })

with open(OUT / "tasks.tsv", "w", newline="") as fh:
    w = csv.DictWriter(fh, rows[0].keys(), delimiter="\t")
    w.writeheader()
    w.writerows(rows)

(OUT / "DATA_PROVENANCE.md").write_bytes((ROOT / "docs/DATA_PROVENANCE.md").read_bytes())

print(f"{OUT}/tasks.tsv            {len(rows)} rows, {len(rows[0])} columns")
print(f"{OUT}/DATA_PROVENANCE.md   {os.path.getsize(OUT / 'DATA_PROVENANCE.md')} bytes")
print(f"runnable from a bare clone {sum(r['runs_from_bare_clone'] for r in rows)}")
print(f"no answer-bearing field written: {'description' not in rows[0]}")

Run 2026-08-29:

Data/mdarena/tasks.tsv            50 rows, 11 columns
Data/mdarena/DATA_PROVENANCE.md   3106 bytes
runnable from a bare clone 43
no answer-bearing field written: True

That table is enough to choose a subset, size the wall clock, and record what you ran. To actually run anything you need the full checkout, the images and the credential — the rest of this page.

Validate the tree yourself

The repository ships its own structural validator, and it is standard-library only — no container, no GPU, no credential. It is the cheapest way to confirm a checkout is intact before spending an hour on an image build, and the fastest way to see the invariants the project enforces on itself.

set -euo pipefail
# The full clone. About 222 MB packed; the working tree is roughly half a gigabyte
# because tasks ship trajectories and structures. There is no Git LFS here — the
# repository says so in .gitattributes and asks that it not be reintroduced.
git -c advice.detachedHead=false clone --quiet --branch v0.1 --depth 1 \
    https://github.com/weitse-hsu/MDArena.git mdarena
du -sh mdarena mdarena/.git

python3 mdarena/scripts/validate_tasks.py

Run 2026-08-29:

711M	mdarena
224M	mdarena/.git
Validated 50 task(s).

Time elapsed: 1.8 second(s)

What it checks is worth knowing even if you never run it: that every task directory holds the seven required files, that task.toml's fields are well formed, that every /app/inputs path an instruction or a rubric references is either tracked in git or declared in the withheld-inputs file, that the canary markers are present in each file that is supposed to carry one, and that the task root contains nothing outside an allowlist. It runs in the project's CI on every push, so a green result here means your checkout matches what CI saw.

Running a task

MDArena is driven by Harbor, an Apache-2.0 evaluation runner distributed on PyPI as harbor. It starts the container, hands the agent the task's prompt, runs the verifier, and writes a jobs directory. Each task's verifier entry point in turn invokes Reward Kit, a small grading toolkit resolved into its own isolated environment.

Before running anything, re-read What you need at the top. A container runtime and an Anthropic credential are both required, and the second one is the one people discover late, because a run starts happily and fails at the verifier.

Install the runner. Nothing here needs the container yet.

set -euo pipefail
python3 -m venv .venv                       # Harbor requires Python 3.12 or newer
./.venv/bin/pip -q --disable-pip-version-check install "harbor>=0.22"
./.venv/bin/harbor --version
./.venv/bin/python -c "
from harbor.models.agent.name import AgentName
names = sorted(a.value for a in AgentName)
control = sorted({'oracle', 'nop'} & set(names))
print(f'agent adapters registered : {len(names)}')
print(f'control adapters          : {control}')
print(f'vendor CLI adapters       : {len(names) - len(control)} (harbor run -h lists them)')
"

Run 2026-08-29, about two minutes for the install:

0.22.0
agent adapters registered : 43
control adapters          : ['nop', 'oracle']
vendor CLI adapters       : 41 (harbor run -h lists them)

Are those numbers declared, or defaulted?

A field that carries the same value in all fifty task files is either a decision every author made, or a default nobody touched — and the two mean opposite things. Harbor is the only place that answers it, which is why this check waits until it is installed.

set -euo pipefail
./.venv/bin/python -c "
from harbor.models.task.config import EnvironmentConfig
for name in ('gpus', 'cpus', 'memory_mb', 'storage_mb',
             'build_timeout_sec', 'allow_internet'):
    print(f'{name:20} Harbor default {EnvironmentConfig.model_fields[name].default!r}')
"

Run 2026-08-29 against Harbor 0.22.0:

gpus                 Harbor default None
cpus                 Harbor default None
memory_mb            Harbor default None
storage_mb           Harbor default None
build_timeout_sec    Harbor default 600.0
allow_internet       Harbor default None

Five of the six are unset by default, and MDArena sets all six in all fifty tasks. So gpus = 0 is a positive statement that these tasks do not want a GPU, not a field left blank — which is what makes "no GPU required" safe to act on. The sixth is the sharpest: build_timeout_sec defaults to 600 seconds and every task raises it to 3600, which is the project telling you the image build is a six-times-the-default job.

The two control adapters

Start with these two. Neither costs an inference call. oracle is Harbor's default and executes a task's own reference solution inside the container, which is how you confirm a task's verifier works at all before trusting a score from it. nop does nothing, which is the negative control confirming a task scores near zero for no work. Between them they establish a task's scoring range before any model is involved.

oracle needs a reference solution, and a reference solution is optional. Find out which tasks have one without pulling one onto your disk:

set -euo pipefail

# `ls-tree` reads the tree object, not the blobs. In a blobless clone that means
# you can enumerate exactly what each task ships without any of it landing on
# disk — which is how you answer "can the oracle run this?" without fetching a
# reference solution.
git -C mdarena-meta ls-tree -r --name-only HEAD -- tasks \
  | awk -F/ '$3 == "solution" { print $2 }' | sort -u > with-solution.txt
git -C mdarena-meta ls-tree -r --name-only HEAD -- tasks \
  | awk -F/ 'NF > 2 { print $2 }' | sort -u > all-tasks.txt

echo "tasks                       $(wc -l < all-tasks.txt | tr -d ' ')"
echo "ship a reference solution   $(wc -l < with-solution.txt | tr -d ' ')"
echo "oracle cannot run these     $(comm -23 all-tasks.txt with-solution.txt | wc -l | tr -d ' ')"
echo "lowest-numbered task the oracle CAN run   $(head -1 with-solution.txt)"

Read 2026-08-29 at v0.1. Nothing under solution/ lands on disk; the working tree still holds the same 123 files the sparse pattern selected:

tasks                       50
ship a reference solution   31
oracle cannot run these     19
lowest-numbered task the oracle CAN run   05_traj_ana_hbonds

Nineteen tasks have no reference solution, so on those there is no way to confirm the verifier works without spending a real agent run. The quickstart's own oracle example names a task that is one of the nineteen, so copying it produces a failure that looks like a broken install. Point the oracle at a task that ships one.

Then build the shared image and run something. Neither of the next two blocks was executed here — see verified.unverified_reason. They are the upstream quickstart's steps 3 and 4, transcribed with the two corrections this page establishes.

# Paths are relative to the working directory used throughout this page, which
# now holds ./mdarena (the full clone) and ./.venv (Harbor).
#
# Roughly an hour: a CUDA devel base plus a from-source GROMACS build, which is
# what build_timeout_sec = 3600 is sized for. amd64 only.
docker build -t mdarena:latest mdarena/environment

# The second base image, needed by 48_cb7_10_alchemical_metadynamics and by
# nothing else. The quickstart does not mention building it.
docker build -f mdarena/environment/Dockerfile.plumed-lambda \
             -t mdarena-plumed-lambda:latest mdarena/environment
export CLAUDE_CODE_OAUTH_TOKEN=...     # or ANTHROPIC_API_KEY for metered billing

# Reference solution, no model involved: does this task's verifier work?
# `oracle` is the default agent, so omitting -a selects it. This task is one of
# the 31 that ship a solution — see the block above before substituting another.
./.venv/bin/harbor run -p mdarena/tasks/05_traj_ana_hbonds

# Negative control: does it correctly score near zero for no work?
./.venv/bin/harbor run -p mdarena/tasks/05_traj_ana_hbonds -a nop

# A scored run. `-a` names an agent adapter and `-m` its model; read both
# from the Harbor documentation for the version you installed rather than
# from any write-up, this one included.
./.venv/bin/harbor run -p mdarena/tasks/05_traj_ana_hbonds -a <adapter> -m <model-id> \
    --extra-docker-compose mdarena/no-github-access.yaml

./.venv/bin/harbor view ./jobs

Four things about that invocation are worth stating.

The overlay is not optional for a reportable run. --extra-docker-compose no-github-access.yaml is what stops a browsing-enabled agent from reading the verifier for the task it is being scored on. The next section is about what it does and does not achieve.

Never name a model from memory. The identifiers move, and a stale one silently changes what a number means. Read them from the runner's own documentation at the version you installed.

-p takes a path, --config takes a batch file. Sweeping 50 tasks one harbor run at a time works and is slower than it needs to be; the batch config takes a list of tasks and a list of agents with per-agent concurrency.

Replicates matter more than they look. Agent runs are high variance, and a single trial per task produces a number that will not reproduce. Report how many replicates you ran; the submission checker has a --min-trials flag precisely because one is a common mistake.

Contamination control, which is the most distinctive thing here

MDArena is a public repository, and every task runs with allow_internet = true. An agent that can browse can, in principle, fetch the verifier for the task it is being scored on. The project addresses that with three layers, and is unusually candid that none of them is a guarantee.

One: block the obvious routes. no-github-access.yaml is a compose overlay that points GitHub's hosts at 127.0.0.1 inside the container — and not only GitHub. It also covers the CDNs that re-serve arbitrary files from any public repository by path, the repository-to-text services that return a whole repository as one document, a generic URL-fetch proxy, and three code-search engines that would surface a verifier's contents without fetching anything. That list is the interesting part: blocking github.com alone leaves at least ten other services that reach the same content.

Its own header says what it cannot do, and the limits are structural rather than oversights. /etc/hosts has no wildcard syntax, so every hostname must be listed literally and any mirror not named is not covered. An agent that connects by IP bypasses it entirely. And it cannot be scoped to this repository alone — HTTPS exposes the hostname through SNI and never the path, so blocking one repository means blocking the whole host.

Two run-time consequences the overlay's own header records. 49_slco2a1_physical_validation is the one task whose instructions point the agent at a package's source repository, so under the overlay that install route fails; the package is on PyPI and PyPI stays reachable, so the task remains solvable, but the agent has to work that out unaided and what you measure there is partly recovery from a dead URL. And one agent adapter — the header names which — installs itself by bootstrapping from a blocked content host, so it fails before the task starts; bake that CLI into the image, or run that adapter without the overlay.

Two: audit what the run actually did. Since no hostname list is complete, the repository ships a scanner that reads every trajectory after the fact and reports any that referenced this repository or requested a repository-lookup address. Both it and the submission checker are standard-library only and run against the sparse checkout.

set -euo pipefail
mkdir -p jobs

# Both fail closed on an empty or mis-pointed jobs directory rather than reporting
# a clean run — check this before wiring either into a release gate.
python3 mdarena-meta/scripts/scan_exec_trajs.py jobs || echo "  exit $?"
python3 mdarena-meta/scripts/verify_submission.py jobs --tag v0.1 || echo "  exit $?"
python3 mdarena-meta/scripts/verify_submission.py jobs --tag v9.9 || echo "  exit $?"

Run 2026-08-29, with no runs to check:

No trajectory.json files found. Point this at a Harbor jobs directory.

Time elapsed: 0.0 second(s)
  exit 1
No trials found under jobs.

Time elapsed: 0.0 second(s)
  exit 1
No manifest for 'v9.9'. Available: v0.1

Time elapsed: 0.0 second(s)
  exit 1

The scanner makes two distinctions that keep its output usable, and both matter when you read a report. It separates hits in the task prompt from hits by the agent, because one task legitimately names a repository URL in its own instructions and a hit there says nothing about behaviour. And it separates a URL in a tool call's arguments — the agent asked for that address — from one in an observation, which usually means the string appeared in something the agent read, and MD software is full of repository URLs in licence headers and docstrings. Only request-side matches are reported by default.

Three: tie a number to a release. verify_submission.py compares every trial's recorded task digest, agent, model, timeout multiplier, verifier state and compose-overlay digest against a published manifest, and folds the trajectory scan in. It exits non-zero on any finding. Useful flags are --min-trials for replicate counts, --require-mediation to fail trials that ran without the overlay, and --strict-lookups to fail on any repository-lookup address rather than only this repository's.

The manifest lives in the repository as one file per release, generated from the tagged tree rather than hand-written. Only v0.1 exists today, which the checker will tell you if you name any other tag.

Be clear about what that establishes. Everything checked is a file the submitter produced, so a fabricated bundle passes. What it catches is a run against edited tasks, a disabled verifier, an inflated timeout, an agent given extra network access, missing trajectories, and an agent that looked the benchmark up — misconfiguration and casual tampering, not fraud. There is a separate opt-in pass that asks a language model whether a trial's outputs were computed or invented; it never changes pass or fail, and the repository's own guidance is to use a judge from a different vendor than the agent under review, through a plain completion API rather than an agent CLI that could read the verifier it is auditing.

And the canary. Two GUIDs are embedded per file in task.toml, checks.py, judge.toml and solve.sh, plus a CANARY.md in every task directory — per file rather than only at the repository root, because crawlers ingest files without repository context. The task validator enforces their presence, so a task cannot ship unmarked. One GUID is the widely used shared benchmark canary, which existing corpus filters may already drop; the other is specific to MDArena, which is what gives it attribution — a model reproducing it points at this benchmark rather than at any other. instruction.md is deliberately left unmarked, because a canary in the prompt would tell the agent mid-task that it is being evaluated. That is a considered design, not an oversight, and it is the clearest signal that the request is about corpora and not about description.

Pitfalls

  • task.toml is labelled metadata and one of its fields is not. [task].description gives the answer away for at least five of the fifty. Project it out at load, as every block on this page does, rather than relying on remembering.
  • Grading needs the network, not just the agent. Several tasks' verifiers resolve Python packages from PyPI on every call, uncached, so a transient network failure during grading reads as a failed task rather than a failed check. Re-run before believing a zero.
  • The repository keeps an open defect log, TODOS.md, naming specific tasks with underspecified prompts, permissive verifiers, or scoring that credits a failed run. Several are deliberately deferred rather than fixed, because v0.1 has to keep matching the published results. Read it before reporting a per-task number. This page does not restate which tasks or what the defects are, for the reason in the first section.
  • main and v0.1 are the same commit today. Zero commits between them, verified. That makes an unpinned clone accidentally correct right now and wrong later — and the submission checker's own documentation says a sweep where every trial fails the digest usually means the run used a different dataset version rather than edited tasks. Pin the tag.
  • The base image is amd64. The CUDA base is linux/amd64; on Apple Silicon you are emulating, and the verifier dependency that failed to build during the maintainers' own local testing failed for exactly that reason. Build and run on Linux amd64.
  • harbor needs Python 3.12 or newer. An older interpreter reports an unsatisfiable requirement rather than a version error, which reads as a broken package.
  • The oracle agent is not available for every task, and the quickstart's own oracle example is one of the nineteen it cannot run. Check before substituting a task.
  • Do not cat a file under tasks/. Every task.toml, checks.py, judge.toml and solve.sh carries a canary comment block, and printing one into a log, an issue or a model's context is how the marker ends up somewhere it means nothing. Parse; never print.
  • A score is not comparable without its scaffold. Which 43 or 50 tasks ran, how many replicates, which agent adapter and model, whether the mediation overlay was on, and which release tag. Every one of those moves the number, and the submission checker exists because they are all recorded and all easy to omit.

Try it

Data. The MDArena repository at tag v0.1, MIT, public, no account and no Git LFS — this block takes about a megabyte of it. Last confirmed reachable 2026-08-29. The block reads task.toml and scripts/external_inputs.toml and prints only counts and invariants, never a task's own text.

Run. In a fresh empty directory, with nothing installed but git 2.37 or newer — that is where sparse-checkout set --no-cone arrived — and Python 3.11 or newer for tomllib:

set -euo pipefail

git -c advice.detachedHead=false clone --quiet \
    --branch v0.1 --depth 1 --filter=blob:none --sparse \
    https://github.com/weitse-hsu/MDArena.git mdarena-tryit
git -C mdarena-tryit sparse-checkout set --no-cone \
    'tasks/*/task.toml' '/scripts/external_inputs.toml'

python3 - <<'PY'
import collections, pathlib, tomllib

ROOT = pathlib.Path("mdarena-tryit")
paths = sorted(ROOT.glob("tasks/*/task.toml"))

# Project the answer-bearing field out at load, so nothing below can print it.
def load(p):
    cfg = tomllib.load(open(p, "rb"))
    cfg["task"].pop("description", None)
    return cfg

tasks = {p.parent.name: load(p) for p in paths}
assert len(tasks) == 50, f"expected 50 tasks, found {len(tasks)}"
assert all("description" not in c["task"] for c in tasks.values()), "projection failed"

# The invariants a sweep depends on.
assert {c["schema_version"] for c in tasks.values()} == {"1.2"}, "schema moved"
assert all(c["environment"]["gpus"] == 0 for c in tasks.values()), "a task now wants a GPU"
assert all(c["environment"]["allow_internet"] for c in tasks.values()), "network policy moved"
assert all(c["verifier"]["env"] for c in tasks.values()), "a task declares no verifier credential"
assert all(t == c["metadata"]["task_id"] for t, c in tasks.items()), "task_id != directory"

# The trap: task.name is NOT the directory name for the second half of the corpus.
odd = [t for t, c in tasks.items() if c["task"]["name"] != f"mdarena/{t}"]
assert len(odd) == 31, f"expected 31 name mismatches, found {len(odd)}"

withheld = tomllib.load(open(ROOT / "scripts/external_inputs.toml", "rb"))
assert set(withheld) <= set(tasks), "external_inputs.toml names a task that is gone"

diff = collections.Counter(c["metadata"]["difficulty"] for c in tasks.values())
cats = {c["metadata"]["category"] for c in tasks.values()}
print(f"tasks / categories        : {len(tasks)} / {len(cats)}")
print(f"difficulty                : {dict(sorted(diff.items()))}")
print(f"gpus declared, all tasks  : {sorted({c['environment']['gpus'] for c in tasks.values()})}")
print(f"tasks with withheld inputs: {len(withheld)}")
print(f"task.name != directory    : {len(odd)}")
print("all assertions passed")
PY

Expect.

Invariants — a failure here means this page is wrong, not that upstream moved. Fifty task directories, each with a parseable task.toml; metadata.task_id equal to the directory name on every one; gpus = 0 and allow_internet = true on every one; a verifier credential declared on every one; the projection removing description from every one; and every task named in the withheld-inputs file still existing.

Observed at v0.1 (commit d50547a), read 2026-08-29 — these move when the maintainers publish a new dataset version, and a mismatch is drift to investigate rather than a bug:

tasks / categories        : 50 / 12
difficulty                : {'easy': 11, 'hard': 16, 'medium': 23}
gpus declared, all tasks  : [0]
tasks with withheld inputs: 6
task.name != directory    : 31
all assertions passed

The task.name assertion is the one that earns its place. It asserts a defect — 31 of 50 task names do not match their directory — because a reader joining run output back to a directory by name will lose those 31 silently, and if a later release fixes it, this assertion is what tells you the join changed.

Where this sits

MDArena is a leaf, indexed nowhere else in this registry. The biomedarena skill maps a harness registering 155 biomedical benchmarks; this is not one of them, checked 2026-08-29, so there is no index entry to defer to. It is this registry's first computational-chemistry benchmark.

Against the neighbours it shares a runner or a shape with: biomnibench-da is 50 biomedical data-analysis tasks graded on the analytical trajectory against expert rubrics and driven by the same runner, bioagent-bench is ten bioinformatics pipelines each graded on one deliverable file, latch-bench is 45 single-cell and spatial evaluations graded by deterministic comparison, and bixbench3 is twenty study-scale rebuilds. This one is fifty molecular simulation workflows graded by a deterministic correctness check and a process-level judge. Different science, different graders, different maintainers — a score on one says nothing about another.

Sources

  • MDArena — Mouroug Anand, Hsu, Vaccaro, Gage, Colburn, Phan, Seo, Guan & Biggin, MDArena — Evaluating Coding Agents on Realistic Molecular Dynamics Workflows, arXiv 2608.02642 (2026), 10.48550/arXiv.2608.02642. Published results were produced with the v0.1 release.
  • Repository — weitse-hsu/MDArena at tag v0.1, commit d50547a. MIT, © 2026 Wei-Tse Hsu, with third-party scientific data carved out and itemised in docs/DATA_PROVENANCE.md.
  • Canary statement — CANARY.md. Read it before doing anything with the task material; this page deliberately reproduces neither marker GUID.
  • Runner — harbor-framework/harbor, Apache-2.0, on PyPI as harbor. Version 0.22.0 was the one installed and inspected here.
  • Structures — the RCSB PDB data API, used above to confirm the provenance table. wwPDB releases structural data without copyright restriction; the obligation is citation, and it applies to processed derivatives too.

The text of this skill is original and CC-BY-4.0. It documents MDArena's interfaces, metadata and access surface; it reproduces none of its evaluation material.