You inherit a feature-blind baseline: for every molecule it predicts the training-set mean
HOMO-LUMO gap, completely ignoring the smiles column it was given. Your goal is to lower the
mean absolute error (MAE, in eV) of your predicted gap against the true DFT-computed gap; a
sealed verifier re-trains and re-runs your solver on a HIDDEN, disjoint batch of 15,000 molecules
and scores it (lower is better, 0 = every prediction exact).
Hard Constraints
- You may only edit code under
/app/methods/main/; you may add sibling.pymodules. - Keep the entrypoint signatures
train(train_records) -> Noneandpredict(test_records) -> list[float], where the returned list has one entry per row oftest_records, in the same order astest_records(positional, not keyed by any ID column), each entry a finitefloat(the predicted gap in eV). numpy+pandas+scikit-learn+scipy+rdkit+ the standard library only -- no other third-party packages (in particular, notorch/dgl/xgboost/lightgbm/catboost-- the verifier image does not have them). Any other import makes the submission score 0.- Your solver must be deterministic given the shipped train/test data -- the verifier calls
train()thenpredict()fresh in its own process; unseeded randomness makes your local self-check unrepresentative of the graded run (seed every model you fit). - Do not fetch chemistry databases or any external data at run time. Only the data shipped in this container may be used. Treat any outbound request as against the spirit of the task even if it happens not to be blocked.
- Grading budget (declared so you can size your method): your
train()+predict()are re-run once, in a fresh process, under a 3600-second wall-clock budget on 8 CPU cores and 8 GiB of memory, CPU only, no network. The graded run re-trains on 80,000 labelled rows (the same count as your visible/app/data/train.csv) and predicts 15,000 held-out rows -- about 1.2x the total rows your local self-check touches. Over-budget, OOM-killed, crashed or wrongly-shaped output scores 0, so leave real margin rather than tuning to the wall. /dev/shmis only 64 MiB (the container default, here and in the grader) and cannot be enlarged. A shared segment larger than that is created successfully and only faults on the first write, killing the process with SIGBUS and no traceback -- no output, so the submission scores 0. If you parallelise withmultiprocessing.shared_memory, or hand very large arrays tojoblib/lokyworkers, keep the data in ordinary heap memory or point the temp folder at a normal filesystem path.- Your submitted
methods/main/must contain only small.pysource files. The verifier rejects (reward 0) any submission that does not look like code: any non-.pyfile, any symlink, or a total submission size over the guard's byte cap is rejected outright.
What You Have
/app/data/train.csv: 80,000 labelled molecules. Each row hasROW_ID,smiles(a SMILES string), andhomolumogap(the true HOMO-LUMO gap in eV, computed by DFT on the molecule's relaxed 3D geometry -- your training target). No other columns, no 3D coordinates, no precomputed features -- you featurizesmilesyourself./app/methods/main/solver.py: the baseline solver (predicts the training-set mean gap for every molecule) -- this directory is what gets graded. Improve it in place or rewrite the algorithm entirely. Matching the baseline earns nothing./app/force_score.py: the exact MAE formula the verifier uses (the formula is public; only the true held-out gaps are sealed). Read it to see precisely how you are scored./app/selfcheck.py: a free, unlimited local dry-run (python /app/selfcheck.py) that fits on an internal 80/20 split of the visible rows and prints the proxy MAE. It is a proxy only -- the real held-out set is a different, disjoint batch of molecules, so do not overfit to this split.
What You Submit
Edit /app/methods/main/solver.py, keeping the contract:
def train(train_records: list[dict]) -> None:
# Called once, on the labelled re-training rows (same columns as /app/data/train.csv, string
# values -- csv.DictReader output). Fit whatever state you need and stash it (module globals are
# fine).
...
def predict(test_records: list[dict]) -> list[float]:
# Called once, on FEATURE-ONLY rows (ROW_ID + smiles, no homolumogap column). Return one
# predicted gap (float, eV) per row, same row order as test_records.
...
There is no submit step and no per-attempt feedback on the real held-out set -- iterate against
selfcheck.py, then leave your best solver.py in place; it is graded once at the end.
How It Is Judged
After your run, the grader copies your methods/main/ into a sealed verifier, spawns a fresh process
that imports it and calls train() on the sealed re-training rows, then predict() on a HIDDEN,
disjoint batch of 15,000 molecules you never saw, and scores the mean absolute error of your predicted
gap against the true gap (lower is better):
score(y_true, y_pred) = mean_i( |y_true[i] - y_pred[i]| )
Your score improves monotonically as MAE falls, so driving MAE down is always the goal -- 0 (exact prediction) is a genuine floor, and there is real headroom between the shipped baseline and a well-tuned solution. Any crash, wrong output length, or a non-finite prediction scores the whole submission 0.