Guide states the rubric and size cap from the season, not from prose that went stale
59ee1a0 verified | # -*- coding: utf-8 -*- | |
| """Season registry. Everything that differs between seasons lives here and nowhere else. | |
| Seasons run concurrently: #1 Malaria closes 30 September 2026, #2 Tuberculosis closes 31 | |
| October 2026, and for six weeks both take entries. So "the season" stopped being a | |
| constant and became a lookup, and every read of the ledger is scoped by it. | |
| Season 1 keeps the ledger paths it already had. Its 65 scored records stay exactly where | |
| they are - re-homing live data to make the layout tidy is how records get lost, and there | |
| is nothing wrong with the layout that a prefix cannot solve. Season 2 is namespaced under | |
| s2/, and every season after it gets its own prefix the same way. | |
| The scoring rubric is deliberately repeated per season rather than shared. Season 2 carries | |
| corrections for scorer defects measured during Season 1, and none of those corrections may | |
| reach Season 1, where entrants have already been scored under the published rules. The | |
| defects themselves are tracked outside this repository: naming them here would tell an | |
| entrant which way to push a molecule while Season 1 is still taking submissions. | |
| The same applies to each later season. Where a season's admission limits differ from | |
| Season 1's, the limits are here because the code needs them and the page shows them in a | |
| rejection message anyway; the measurements behind the choice are not. | |
| """ | |
| SEASONS = { | |
| 1: { | |
| "number": 1, | |
| "name": "Open Discovery Challenge", | |
| "topic": "Malaria", | |
| "topic_ko": "말라리아", | |
| "organism": "Plasmodium falciparum", | |
| "target": "PfDHODH", | |
| "counter_target": "human DHODH", | |
| "closes": "2026-09-30", | |
| "prize_usd": 1000, | |
| "weights": {"activity": 30, "binding": 20, "selectivity": 20, | |
| "admet": 15, "novelty": 10, "synthesis": 5}, | |
| # empty prefix: season 1 predates the split and its paths are not moving | |
| "prefix": "", | |
| "anchors": "anchor_scores.json", | |
| "open": True, | |
| }, | |
| 2: { | |
| "number": 2, | |
| "name": "Open Discovery Challenge", | |
| "topic": "Tuberculosis", | |
| "topic_ko": "결핵", | |
| "organism": "Mycobacterium tuberculosis", | |
| # InhA is the target isoniazid and ethionamide act through, it has direct | |
| # (non-prodrug) inhibitors to calibrate against, 1,460 ChEMBL activities, and a | |
| # 269-residue crystallised construct that docks sanely. | |
| "target": "InhA", | |
| # human fatty acid synthase carries the homologous enoyl-reductase chemistry, so | |
| # selectivity keeps the same meaning it had in season 1: hit the pathogen's | |
| # enzyme, not ours | |
| "counter_target": "human FASN (ER domain)", | |
| "closes": "2026-10-31", | |
| "prize_usd": 2000, | |
| # Not the season 1 split. Each axis is weighted by what this season's evidence can | |
| # support, and the tuberculosis whole-cell model supports less than the malaria one | |
| # did, so activity is paid less and the difference moves to binding and selectivity, | |
| # which are measured rather than predicted. The measurements behind the split stay | |
| # outside this repository while the season is taking entries. | |
| "weights": {"activity": 20, "binding": 25, "selectivity": 25, | |
| "admet": 15, "novelty": 10, "synthesis": 5}, | |
| "prefix": "s2/", | |
| "anchors": "anchor_scores_s2.json", | |
| # opened 2026-08-16 after the scorer was measured against its own panel: every | |
| # WHO anti-tubercular scored 20.7-39.2 and every inert control 1.6 or below, and | |
| # triclosan - the reference direct InhA binder - cleared the weak-binding control. | |
| # Both conditions were written down before the panel was run. | |
| "open": True, | |
| }, | |
| 3: { | |
| "number": 3, | |
| "name": "Open Discovery Challenge", | |
| "topic": "Chagas disease", | |
| "topic_ko": "샤가스병", | |
| "organism": "Trypanosoma cruzi", | |
| # sterol 14a-demethylase. This season can validate its own binding axis because the | |
| # target has established direct inhibitors rather than only prodrugs, which is what a | |
| # reference panel needs in order to mean anything | |
| "target": "T. cruzi CYP51", | |
| # the same enzyme in us. Inhibiting it is where the existing drug class gets its side | |
| # effects, so "hit theirs, not ours" is the actual clinical problem here | |
| "counter_target": "human CYP51A1", | |
| # confirmed 2026-08-19. Six weeks after season 2 closes, so the three seasons finish | |
| # a month apart rather than piling onto one scoring queue. | |
| "closes": "2026-11-30", | |
| "prize_usd": 1000, | |
| # Not season 2's split. Three acceptance tests were written down before the | |
| # whole-cell model was trained; two passed and one failed, and the pre-registered | |
| # consequence of a single failure is to lower activity and move the points to binding | |
| # and selectivity. That is what these weights are. The tests, the numbers and which | |
| # one failed are recorded outside this repository while the season runs. | |
| "weights": {"activity": 10, "binding": 30, "selectivity": 30, | |
| "admet": 15, "novelty": 10, "synthesis": 5}, | |
| # Admission limits are per season. Season 3's target turns over a large natural | |
| # substrate and its clinically validated inhibitors are correspondingly large, so a | |
| # cap set for season 1's chemistry would exclude the very compounds this season is | |
| # calibrated against - and a cap that excludes every reference compound is measuring | |
| # drug-likeness in the wrong place. Two PAINS families are likewise not treated as | |
| # rejections here, because they fire on motifs that are ordinary in this season's | |
| # chemistry. The rest of the catalogue is unchanged. | |
| # | |
| # Raising the size cap does not leave size unpoliced: the binding term divides by | |
| # heavy-atom count, so mass bought without affinity is scored down by the ligand | |
| # efficiency term rather than waved through. | |
| # | |
| # Set before the season opened, never during, and checked against live submissions | |
| # from seasons 1 and 2 first - no verdict on an existing entry changed. Which | |
| # compounds motivated each limit is recorded outside this repository. | |
| "gate": {"mw_max": 750.0, "heavy_max": 55, | |
| "pains_allow": ("anil_di_alk", "quinone")}, | |
| "prefix": "s3/", | |
| "anchors": "anchor_scores_s3.json", | |
| # opened 2026-08-19, after a reference panel was scored against conditions written | |
| # down before any of it was measured: compounds with nothing to do with the disease | |
| # have to fall below the established drugs, and the potent reference compound has to | |
| # clear the weak one. Both held. The panel and its scores are shown on the page; the | |
| # conditions and the run that judged them are recorded outside this repository. | |
| "open": True, | |
| }, | |
| } | |
| DEFAULT = 1 | |
| def get(n): | |
| try: | |
| n = int(n) | |
| except (TypeError, ValueError): | |
| n = DEFAULT | |
| return SEASONS.get(n) or SEASONS[DEFAULT] | |
| def public(s): | |
| """What the page is allowed to see. `prefix` and `anchors` are storage details.""" | |
| out = {k: v for k, v in s.items() if k not in ("prefix", "anchors", "gate")} | |
| out["limits"] = limits(s) | |
| return out | |
| def limits(s): | |
| """The admission numbers the page is allowed to state back to an entrant. | |
| The guide used to write these out in prose and they went stale the moment a season | |
| changed them - the Chagas board told entrants the cap was 550 when it was 750. The page | |
| reads them from here instead.""" | |
| g = s.get("gate") or {} | |
| return {"mw_max": g.get("mw_max", 550.0), "heavy_max": g.get("heavy_max", 45)} | |
| def gate(s): | |
| """Per-season gate overrides, as kwargs for gates.check(). Empty means season 1's.""" | |
| return dict(s.get("gate") or {}) | |
| def path(s, name): | |
| """Ledger path for this season, e.g. path(s, "leaderboard.json").""" | |
| return s["prefix"] + name | |
| def listing(): | |
| return [public(SEASONS[n]) for n in sorted(SEASONS)] | |