| import json |
| from pathlib import Path |
| import numpy as np |
| import pytest |
| from dooable.chemistry import ( |
| read_catalog, |
| reactions, |
| build_graph, |
| replay, |
| descriptor_rewards, |
| ) |
| from dooable.graph import Graph, toy_graph |
| from dooable.exact import solve, sample, endpoint_distribution |
| from dooable.learning import train, load_model |
| from dooable.properties import scaffold_split |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
|
|
| def test_chemical_route_replay_and_serialization(tmp_path): |
| ex = ROOT / "data/examples" |
| g = build_graph( |
| read_catalog(ex / "parents.csv"), |
| read_catalog(ex / "reagents.csv"), |
| reactions(ex / "reactions.json"), |
| 2, |
| ) |
| g.save(tmp_path / "graph.json") |
| gg = Graph.load(tmp_path / "graph.json") |
| assert len(gg.nodes) == len(g.nodes) |
| sol = solve(gg, descriptor_rewards(gg), 0.7) |
| rows = sample(gg, sol.forward, 200, 7) |
| assert all(replay(r, 2) for r in rows) |
| for y, v in gg.terminals.items(): |
| assert gg.nodes[v].outcome == y |
|
|
|
|
| def test_neural_training_and_checkpoint(tmp_path): |
| g = toy_graph(4) |
| m, h = train( |
| g, {"A": 0.0, "B": 0.0}, steps=600, batch_size=32, seed=11, output=tmp_path |
| ) |
| loaded, _ = load_model(tmp_path) |
| np.testing.assert_allclose(m.probabilities(), loaded.probabilities(), atol=1e-8) |
| assert h[-1]["endpoint_tv"] < 0.06 |
|
|
|
|
| def test_scaffold_split_separation(): |
| from rdkit.Chem.Scaffolds import MurckoScaffold |
|
|
| smi = [ |
| "CC", |
| "CCC", |
| "CCO", |
| "c1ccccc1", |
| "Cc1ccccc1", |
| "c1ccncc1", |
| "C1CCCCC1", |
| "CC1CCCCC1", |
| "c1ccoc1", |
| ] |
| tr, te = scaffold_split(smi, 3, 0.7) |
| scaff = lambda inds: { |
| MurckoScaffold.MurckoScaffoldSmiles(smiles=smi[i]) for i in inds |
| } |
| assert not scaff(tr) & scaff(te) |
|
|
|
|
| def test_catalog_conflicting_identifiers(tmp_path): |
| f = tmp_path / "parents.csv" |
| f.write_text("id,smiles\na,CC\na,CCC\n") |
| with pytest.raises(ValueError, match="Conflicting"): |
| read_catalog(f) |
|
|
|
|
| def test_resumed_training_matches_uninterrupted(tmp_path): |
| g = toy_graph(3) |
| rewards = {"A": 0.0, "B": 0.0} |
| full, _ = train(g, rewards, steps=30, seed=2, batch_size=16) |
| train(g, rewards, steps=12, seed=2, batch_size=16, output=tmp_path) |
| resumed, _ = train(g, rewards, steps=30, seed=2, batch_size=16, resume=tmp_path) |
| np.testing.assert_allclose(full.probabilities(), resumed.probabilities(), atol=1e-9) |
|
|
|
|
| def test_replay_requires_parent_and_stop(): |
| assert replay( |
| { |
| "actions": [{"kind": "parent", "smiles": "CC"}, {"kind": "stop"}], |
| "outcome": "CC", |
| }, |
| 0, |
| ) |
| assert not replay( |
| {"actions": [{"kind": "parent", "smiles": "CC"}], "outcome": "CC"}, 0 |
| ) |
| assert not replay( |
| { |
| "actions": [ |
| {"kind": "parent", "smiles": "CC"}, |
| {"kind": "parent", "smiles": "CC"}, |
| {"kind": "stop"}, |
| ], |
| "outcome": "CC", |
| }, |
| 2, |
| ) |
|
|
|
|
| def test_terminal_merging_ablation_changes_endpoint_mass(): |
| from dooable.ablations import duplicate_endpoint_policy |
|
|
| g = toy_graph(5) |
| r = {"A": 0.0, "B": 0.0} |
| p = endpoint_distribution(g, duplicate_endpoint_policy(g, r, 0.7)) |
| assert p["A"] == pytest.approx(5 / 6) |
|
|