Release GEOM scaffold substituent LoRA rank-16 checkpoint after 25 epochs (part 2)
Browse files- geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/metrics/tensor.py +336 -0
- geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/rdkit_utils.py +291 -0
- geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/run_config.py +192 -0
- geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/vocab_utils.py +177 -0
- geom/scaffold_murcko_substituent_lora_r16/runtime/uv.lock +0 -0
- geom/scaffold_murcko_substituent_lora_r16/training_config.yaml +479 -0
- geom/scaffold_murcko_substituent_lora_r16/training_source.tar.gz +3 -0
geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/metrics/tensor.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Geometric tensor metrics consumed directly from ``ChemBatch``.
|
| 2 |
+
|
| 3 |
+
These run in every representation (no RDKit, no validity conditioning). Target
|
| 4 |
+
stats (per-element-pair distance histogram, RoG histogram) are computed once at
|
| 5 |
+
preprocessing time on training-set coords in Angstroms and stored in
|
| 6 |
+
``Distributions``. Generated coords are expected to arrive in Angstroms too —
|
| 7 |
+
``sample()`` in the LightningModule already multiplies by ``coordinate_std``
|
| 8 |
+
before returning the final ``ChemBatch``.
|
| 9 |
+
|
| 10 |
+
Also hosts the shared bin edges + histogram accumulators because both this
|
| 11 |
+
module and ``preprocessing.py`` must agree on them exactly.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from torchmetrics import Metric
|
| 18 |
+
|
| 19 |
+
from morph.dataset.core.atomic_data import ChemBatch
|
| 20 |
+
|
| 21 |
+
# Shared bin edges (Angstroms). Used both at preprocessing time to compute
|
| 22 |
+
# target histograms and at metric time to bin generated samples — they must
|
| 23 |
+
# match exactly. Defaults are generous enough for QM9 / GEOM / TMQM; values
|
| 24 |
+
# above the upper bound are counted in an overflow accumulator (see the binning
|
| 25 |
+
# helpers below) so silent clamping is visible at validation time.
|
| 26 |
+
DIST_RANGE = (0.0, 15.0)
|
| 27 |
+
DIST_N_BINS = 150
|
| 28 |
+
RG_RANGE = (0.0, 8.0)
|
| 29 |
+
RG_N_BINS = 80
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True)
|
| 33 |
+
class BatchGeometry:
|
| 34 |
+
"""Geometry shared by all tensor metrics for one generated batch."""
|
| 35 |
+
|
| 36 |
+
num_graphs: int
|
| 37 |
+
node_counts: torch.Tensor
|
| 38 |
+
pair_distances: torch.Tensor
|
| 39 |
+
pair_atom_lo: torch.Tensor
|
| 40 |
+
pair_atom_hi: torch.Tensor
|
| 41 |
+
pair_graph: torch.Tensor
|
| 42 |
+
radii_of_gyration: torch.Tensor
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def compute_batch_geometry(batch: ChemBatch) -> BatchGeometry:
|
| 46 |
+
"""Compute pair distances and radii of gyration once for all metrics."""
|
| 47 |
+
rows, cols = batch.edge_index
|
| 48 |
+
upper = rows < cols
|
| 49 |
+
rows, cols = rows[upper], cols[upper]
|
| 50 |
+
pair_distances = (batch.x[rows] - batch.x[cols]).norm(dim=-1)
|
| 51 |
+
atom_i, atom_j = batch.a[rows], batch.a[cols]
|
| 52 |
+
|
| 53 |
+
counts = batch.num_nodes_per_graph
|
| 54 |
+
centers = batch.x.new_zeros((batch.num_graphs, batch.x.shape[-1]))
|
| 55 |
+
centers.index_add_(0, batch.batch, batch.x)
|
| 56 |
+
centers = centers / counts.clamp(min=1).unsqueeze(1)
|
| 57 |
+
squared_radius = (batch.x - centers[batch.batch]).square().sum(dim=-1)
|
| 58 |
+
mean_squared_radius = batch.x.new_zeros(batch.num_graphs)
|
| 59 |
+
mean_squared_radius.index_add_(0, batch.batch, squared_radius)
|
| 60 |
+
mean_squared_radius = mean_squared_radius / counts.clamp(min=1)
|
| 61 |
+
|
| 62 |
+
return BatchGeometry(
|
| 63 |
+
num_graphs=batch.num_graphs,
|
| 64 |
+
node_counts=counts,
|
| 65 |
+
pair_distances=pair_distances,
|
| 66 |
+
pair_atom_lo=torch.minimum(atom_i, atom_j),
|
| 67 |
+
pair_atom_hi=torch.maximum(atom_i, atom_j),
|
| 68 |
+
pair_graph=batch.batch[rows],
|
| 69 |
+
radii_of_gyration=mean_squared_radius.sqrt(),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def dist_edges() -> torch.Tensor:
|
| 74 |
+
return torch.linspace(DIST_RANGE[0], DIST_RANGE[1], DIST_N_BINS + 1)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def rg_edges() -> torch.Tensor:
|
| 78 |
+
return torch.linspace(RG_RANGE[0], RG_RANGE[1], RG_N_BINS + 1)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def accumulate_pairwise_distance_hist(
|
| 82 |
+
hist: torch.Tensor,
|
| 83 |
+
coord: torch.Tensor,
|
| 84 |
+
atom_idx: torch.Tensor,
|
| 85 |
+
edges: torch.Tensor,
|
| 86 |
+
) -> tuple[int, int]:
|
| 87 |
+
"""Add upper-triangular pair distances to ``hist`` (A, A, B), in place.
|
| 88 |
+
|
| 89 |
+
The pair key is ``(min(a_i, a_j), max(a_i, a_j))`` so the histogram is
|
| 90 |
+
naturally invariant to atom ordering.
|
| 91 |
+
|
| 92 |
+
Returns ``(n_overflow, n_total)`` for the pairs in this molecule, so callers
|
| 93 |
+
can track the fraction of distances that fell beyond the histogram range
|
| 94 |
+
(silent clamping otherwise hides "the cloud is way too large").
|
| 95 |
+
"""
|
| 96 |
+
n = coord.shape[0]
|
| 97 |
+
if n < 2:
|
| 98 |
+
return 0, 0
|
| 99 |
+
i_idx, j_idx = torch.triu_indices(n, n, offset=1, device=coord.device)
|
| 100 |
+
dists = (coord[i_idx] - coord[j_idx]).norm(dim=-1)
|
| 101 |
+
lo = torch.minimum(atom_idx[i_idx], atom_idx[j_idx])
|
| 102 |
+
hi = torch.maximum(atom_idx[i_idx], atom_idx[j_idx])
|
| 103 |
+
edges_d = edges.to(coord.device)
|
| 104 |
+
n_overflow = int((dists >= edges_d[-1]).sum().item())
|
| 105 |
+
bin_idx = torch.bucketize(dists, edges_d).sub_(1).clamp_(0, hist.shape[-1] - 1)
|
| 106 |
+
a = hist.shape[0]
|
| 107 |
+
flat = (lo.long() * a + hi.long()) * hist.shape[-1] + bin_idx
|
| 108 |
+
hist.view(-1).index_add_(
|
| 109 |
+
0, flat, torch.ones_like(flat, dtype=hist.dtype, device=coord.device)
|
| 110 |
+
)
|
| 111 |
+
return n_overflow, int(dists.numel())
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def accumulate_rog_hist(
|
| 115 |
+
hist: torch.Tensor, coord: torch.Tensor, edges: torch.Tensor
|
| 116 |
+
) -> tuple[int, int]:
|
| 117 |
+
"""Add one molecule's radius of gyration into ``hist`` (B,), in place.
|
| 118 |
+
|
| 119 |
+
Returns ``(n_overflow, n_total)`` (0 or 1 each) for the same reason as
|
| 120 |
+
``accumulate_pairwise_distance_hist``.
|
| 121 |
+
"""
|
| 122 |
+
if coord.shape[0] == 0:
|
| 123 |
+
return 0, 0
|
| 124 |
+
centered = coord - coord.mean(dim=0)
|
| 125 |
+
rog = (centered.pow(2).sum(dim=-1).mean()).sqrt()
|
| 126 |
+
edges_d = edges.to(coord.device)
|
| 127 |
+
overflow = int((rog >= edges_d[-1]).item())
|
| 128 |
+
bin_idx = (
|
| 129 |
+
torch.bucketize(rog.unsqueeze(0), edges_d).sub_(1).clamp_(0, hist.shape[0] - 1)
|
| 130 |
+
)
|
| 131 |
+
hist.index_add_(0, bin_idx, torch.ones(1, dtype=hist.dtype, device=coord.device))
|
| 132 |
+
return overflow, 1
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class BatchMetric(Metric):
|
| 136 |
+
"""Base class for metrics that consume a ``ChemBatch`` directly."""
|
| 137 |
+
|
| 138 |
+
def update(self, batch: ChemBatch) -> None:
|
| 139 |
+
"""Accumulate tensor statistics from molecular batches."""
|
| 140 |
+
raise NotImplementedError
|
| 141 |
+
|
| 142 |
+
def compute(self) -> torch.Tensor:
|
| 143 |
+
"""Reduce the accumulated tensor statistics to a metric value."""
|
| 144 |
+
raise NotImplementedError
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class PairwiseDistanceL1(BatchMetric):
|
| 148 |
+
"""Mean per-element-pair L1 distance between gen and target distance histograms.
|
| 149 |
+
|
| 150 |
+
For every ordered (lo, hi) atom-type pair the *target* has meaningful mass
|
| 151 |
+
for, we normalize gen and target slices independently and report
|
| 152 |
+
``|gen - target|.sum()``. The mean over those pairs is a 0-2 metric where 0
|
| 153 |
+
means perfect distributional match.
|
| 154 |
+
|
| 155 |
+
Note: gen-empty pairs are *not* excluded. If the target has C-N distances
|
| 156 |
+
but the model never generates a C-N pair, the L1 against zeros is 1.0 and
|
| 157 |
+
that pair counts in the average — which is what we want.
|
| 158 |
+
"""
|
| 159 |
+
|
| 160 |
+
def __init__(
|
| 161 |
+
self, target_hist: torch.Tensor, min_pair_count: float = 10.0, **kwargs
|
| 162 |
+
):
|
| 163 |
+
super().__init__(**kwargs)
|
| 164 |
+
self.register_buffer("target_hist", target_hist.detach().to(torch.float32))
|
| 165 |
+
self.register_buffer("edges", dist_edges())
|
| 166 |
+
self.min_pair_count = min_pair_count
|
| 167 |
+
self.add_state(
|
| 168 |
+
"gen_hist",
|
| 169 |
+
default=torch.zeros_like(self.target_hist),
|
| 170 |
+
dist_reduce_fx="sum",
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
def update(self, geometry: BatchGeometry) -> None:
|
| 174 |
+
"""Accumulate pair-distance histograms by unordered atom-type pair."""
|
| 175 |
+
distances = geometry.pair_distances
|
| 176 |
+
if distances.numel() == 0:
|
| 177 |
+
return
|
| 178 |
+
edges = self.edges.to(distances.device)
|
| 179 |
+
bins = (
|
| 180 |
+
torch.bucketize(distances, edges)
|
| 181 |
+
.sub_(1)
|
| 182 |
+
.clamp_(0, self.gen_hist.shape[-1] - 1)
|
| 183 |
+
)
|
| 184 |
+
num_atom_types = self.gen_hist.shape[0]
|
| 185 |
+
flat = (
|
| 186 |
+
geometry.pair_atom_lo.long() * num_atom_types + geometry.pair_atom_hi.long()
|
| 187 |
+
) * self.gen_hist.shape[-1] + bins
|
| 188 |
+
self.gen_hist.view(-1).index_add_(
|
| 189 |
+
0, flat, torch.ones_like(flat, dtype=self.gen_hist.dtype)
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
def compute(self) -> torch.Tensor:
|
| 193 |
+
"""Average normalized histogram L1 errors over populated target pairs."""
|
| 194 |
+
gen = self.gen_hist # (A, A, B)
|
| 195 |
+
tgt = self.target_hist
|
| 196 |
+
pair_t = tgt.sum(dim=-1)
|
| 197 |
+
# Target-populated only: a pair the training set knows about counts in
|
| 198 |
+
# the metric even if the model generates zero of them (gen_n = 0 →
|
| 199 |
+
# L1 == tgt_n.sum() == 1, which is maximal pain).
|
| 200 |
+
populated = pair_t >= self.min_pair_count
|
| 201 |
+
if not populated.any():
|
| 202 |
+
return torch.tensor(0.0, device=gen.device)
|
| 203 |
+
gen_n = gen / gen.sum(dim=-1).unsqueeze(-1).clamp(min=1.0)
|
| 204 |
+
tgt_n = tgt / pair_t.unsqueeze(-1).clamp(min=1.0)
|
| 205 |
+
l1 = (gen_n - tgt_n).abs().sum(dim=-1) # (A, A)
|
| 206 |
+
return l1[populated].mean()
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class MinDistanceViolation(BatchMetric):
|
| 210 |
+
"""Fraction of generated molecules whose smallest pairwise distance is below
|
| 211 |
+
a physical threshold (default 0.5 Å). Catches collapse and overlap.
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
def __init__(self, threshold: float = 0.5, **kwargs):
|
| 215 |
+
super().__init__(**kwargs)
|
| 216 |
+
self.threshold = float(threshold)
|
| 217 |
+
self.add_state("violations", default=torch.tensor(0.0), dist_reduce_fx="sum")
|
| 218 |
+
self.add_state("total", default=torch.tensor(0.0), dist_reduce_fx="sum")
|
| 219 |
+
|
| 220 |
+
def update(self, geometry: BatchGeometry) -> None:
|
| 221 |
+
"""Count graphs with an atom pair below the distance threshold."""
|
| 222 |
+
valid = geometry.node_counts >= 2
|
| 223 |
+
minima = geometry.pair_distances.new_full((geometry.num_graphs,), float("inf"))
|
| 224 |
+
minima.scatter_reduce_(
|
| 225 |
+
0, geometry.pair_graph, geometry.pair_distances, reduce="amin"
|
| 226 |
+
)
|
| 227 |
+
self.violations += ((minima < self.threshold) & valid).sum()
|
| 228 |
+
self.total += valid.sum()
|
| 229 |
+
|
| 230 |
+
def compute(self) -> torch.Tensor:
|
| 231 |
+
"""Return the violation fraction among graphs with at least two atoms."""
|
| 232 |
+
if self.total <= 0:
|
| 233 |
+
return torch.tensor(0.0, device=self.violations.device)
|
| 234 |
+
return self.violations / self.total
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
class RangeOverflow(BatchMetric):
|
| 238 |
+
"""Fraction of generated values that landed outside the histogram range.
|
| 239 |
+
|
| 240 |
+
If this is non-trivial (say >1%), the fixed range in this module is too
|
| 241 |
+
small for the dataset and we're silently clamping into the last bin —
|
| 242 |
+
revisit ``DIST_RANGE`` / ``RG_RANGE`` or switch to target-derived edges.
|
| 243 |
+
"""
|
| 244 |
+
|
| 245 |
+
def __init__(self, **kwargs):
|
| 246 |
+
super().__init__(**kwargs)
|
| 247 |
+
self.register_buffer("dist_edges_buf", dist_edges())
|
| 248 |
+
self.register_buffer("rog_edges_buf", rg_edges())
|
| 249 |
+
self.add_state("dist_over", default=torch.tensor(0.0), dist_reduce_fx="sum")
|
| 250 |
+
self.add_state("dist_total", default=torch.tensor(0.0), dist_reduce_fx="sum")
|
| 251 |
+
self.add_state("rog_over", default=torch.tensor(0.0), dist_reduce_fx="sum")
|
| 252 |
+
self.add_state("rog_total", default=torch.tensor(0.0), dist_reduce_fx="sum")
|
| 253 |
+
|
| 254 |
+
def update(self, geometry: BatchGeometry) -> None:
|
| 255 |
+
"""Count distances and radii outside their histogram ranges."""
|
| 256 |
+
self.dist_over += (geometry.pair_distances >= self.dist_edges_buf[-1]).sum()
|
| 257 |
+
self.dist_total += geometry.pair_distances.numel()
|
| 258 |
+
valid = geometry.node_counts > 0
|
| 259 |
+
self.rog_over += (
|
| 260 |
+
(geometry.radii_of_gyration >= self.rog_edges_buf[-1]) & valid
|
| 261 |
+
).sum()
|
| 262 |
+
self.rog_total += valid.sum()
|
| 263 |
+
|
| 264 |
+
def compute(self) -> torch.Tensor:
|
| 265 |
+
"""Return the larger of the distance and radius overflow fractions."""
|
| 266 |
+
d = self.dist_over / self.dist_total.clamp(min=1.0)
|
| 267 |
+
r = self.rog_over / self.rog_total.clamp(min=1.0)
|
| 268 |
+
return torch.maximum(d, r)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
class RoGL1(BatchMetric):
|
| 272 |
+
"""L1 distance between gen and target normalized radius-of-gyration histograms."""
|
| 273 |
+
|
| 274 |
+
def __init__(self, target_hist: torch.Tensor, **kwargs):
|
| 275 |
+
super().__init__(**kwargs)
|
| 276 |
+
self.register_buffer("target_hist", target_hist.detach().to(torch.float32))
|
| 277 |
+
self.register_buffer("edges", rg_edges())
|
| 278 |
+
self.add_state(
|
| 279 |
+
"gen_hist",
|
| 280 |
+
default=torch.zeros_like(self.target_hist),
|
| 281 |
+
dist_reduce_fx="sum",
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
def update(self, geometry: BatchGeometry) -> None:
|
| 285 |
+
"""Accumulate radii of gyration for nonempty graphs."""
|
| 286 |
+
valid = geometry.node_counts > 0
|
| 287 |
+
radii = geometry.radii_of_gyration[valid]
|
| 288 |
+
if radii.numel() == 0:
|
| 289 |
+
return
|
| 290 |
+
bins = (
|
| 291 |
+
torch.bucketize(radii, self.edges.to(radii.device))
|
| 292 |
+
.sub_(1)
|
| 293 |
+
.clamp_(0, self.gen_hist.shape[0] - 1)
|
| 294 |
+
)
|
| 295 |
+
self.gen_hist.index_add_(
|
| 296 |
+
0, bins, torch.ones_like(bins, dtype=self.gen_hist.dtype)
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def compute(self) -> torch.Tensor:
|
| 300 |
+
"""Return the L1 distance between normalized radius histograms."""
|
| 301 |
+
gen = self.gen_hist
|
| 302 |
+
tgt = self.target_hist
|
| 303 |
+
if gen.sum() <= 0 or tgt.sum() <= 0:
|
| 304 |
+
return torch.tensor(0.0, device=gen.device)
|
| 305 |
+
gen_n = gen / gen.sum().clamp(min=1.0)
|
| 306 |
+
tgt_n = tgt / tgt.sum().clamp(min=1.0)
|
| 307 |
+
return (gen_n - tgt_n).abs().sum()
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def build_batch_metrics(distributions) -> dict[str, BatchMetric]:
|
| 311 |
+
"""Construct the geometric batch-side metric set from training target stats.
|
| 312 |
+
|
| 313 |
+
Atom-count / atom-type / edge / charge KLs live in the distribution
|
| 314 |
+
collection (see ``init_metrics``), not here — this set is purely geometric.
|
| 315 |
+
|
| 316 |
+
Returns an empty dict if Distributions lacks either geometric target stat
|
| 317 |
+
(e.g. an older cache built before these were added). Both are checked
|
| 318 |
+
independently in case one is added without the other later.
|
| 319 |
+
"""
|
| 320 |
+
if (
|
| 321 |
+
distributions.pairwise_distance_histogram is None
|
| 322 |
+
or distributions.radius_of_gyration_histogram is None
|
| 323 |
+
):
|
| 324 |
+
return {}
|
| 325 |
+
return {
|
| 326 |
+
"min_dist_violation": MinDistanceViolation(
|
| 327 |
+
threshold=0.5, sync_on_compute=False
|
| 328 |
+
),
|
| 329 |
+
"range_overflow": RangeOverflow(sync_on_compute=False),
|
| 330 |
+
"pair_dist_l1": PairwiseDistanceL1(
|
| 331 |
+
distributions.pairwise_distance_histogram, sync_on_compute=False
|
| 332 |
+
),
|
| 333 |
+
"rog_l1": RoGL1(
|
| 334 |
+
distributions.radius_of_gyration_histogram, sync_on_compute=False
|
| 335 |
+
),
|
| 336 |
+
}
|
geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/rdkit_utils.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RDKit molecule construction, conversion, and checks.
|
| 2 |
+
|
| 3 |
+
Upstream attribution is recorded in docs/development.md.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import threading
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from rdkit import Chem, RDLogger
|
| 10 |
+
from rdkit.Chem import AllChem
|
| 11 |
+
from rdkit.Chem.Descriptors import NumRadicalElectrons
|
| 12 |
+
from scipy.spatial.transform import Rotation
|
| 13 |
+
|
| 14 |
+
# Silence RDKit's noisy validity/kekulize warnings. These fire once per
|
| 15 |
+
# generated molecule during validation and, on a shared Lustre stdout,
|
| 16 |
+
# can block every Python thread for minutes -- including the TorchElastic
|
| 17 |
+
# rendezvous keep-alive thread, which then times out and kills the job.
|
| 18 |
+
RDLogger.DisableLog("rdApp.*")
|
| 19 |
+
|
| 20 |
+
ArrT = np.ndarray
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class PeriodicTable:
|
| 24 |
+
"""Thread-safe singleton wrapper for RDKit periodic-table queries."""
|
| 25 |
+
|
| 26 |
+
_instance = None
|
| 27 |
+
|
| 28 |
+
def __new__(cls):
|
| 29 |
+
"""Return the shared periodic-table wrapper instance."""
|
| 30 |
+
if cls._instance is None:
|
| 31 |
+
cls._instance = super().__new__(cls)
|
| 32 |
+
|
| 33 |
+
return cls._instance
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self._table = Chem.GetPeriodicTable()
|
| 37 |
+
|
| 38 |
+
# Just to be certain that vocab objects are thread safe
|
| 39 |
+
self._pt_lock = threading.Lock()
|
| 40 |
+
|
| 41 |
+
def atomic_from_symbol(self, symbol: str) -> int:
|
| 42 |
+
"""Look up an atomic number under the periodic-table lock."""
|
| 43 |
+
with self._pt_lock:
|
| 44 |
+
symbol = symbol.upper() if len(symbol) == 1 else symbol
|
| 45 |
+
atomic = self._table.GetAtomicNumber(symbol)
|
| 46 |
+
|
| 47 |
+
return atomic
|
| 48 |
+
|
| 49 |
+
def symbol_from_atomic(self, atomic_num: int) -> str:
|
| 50 |
+
"""Look up an element symbol under the periodic-table lock."""
|
| 51 |
+
with self._pt_lock:
|
| 52 |
+
token = self._table.GetElementSymbol(atomic_num)
|
| 53 |
+
|
| 54 |
+
return token
|
| 55 |
+
|
| 56 |
+
def valence(self, atom: str | int) -> int:
|
| 57 |
+
"""Look up the default valence for an element symbol or atomic number."""
|
| 58 |
+
with self._pt_lock:
|
| 59 |
+
valence = self._table.GetDefaultValence(atom)
|
| 60 |
+
|
| 61 |
+
return valence
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
PT = PeriodicTable()
|
| 65 |
+
|
| 66 |
+
IDX_BOND_MAP = {
|
| 67 |
+
0: "NO_BOND",
|
| 68 |
+
1: Chem.BondType.SINGLE,
|
| 69 |
+
2: Chem.BondType.DOUBLE,
|
| 70 |
+
3: Chem.BondType.TRIPLE,
|
| 71 |
+
4: Chem.BondType.AROMATIC,
|
| 72 |
+
}
|
| 73 |
+
BOND_IDX_MAP = {bond: idx for idx, bond in IDX_BOND_MAP.items()}
|
| 74 |
+
|
| 75 |
+
# IDX_CHARGE_MAP = {0: 0, 1: 1, 2: 2, 3: 3, 4: -1, 5: -2, 6: -3}
|
| 76 |
+
# CHARGE_IDX_MAP = {charge: idx for idx, charge in IDX_CHARGE_MAP.items()}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# TODO merge these with check functions in other files
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def tensors_to_rdkit_mol(
|
| 83 |
+
atom_types: list[str],
|
| 84 |
+
coords: np.ndarray,
|
| 85 |
+
charge_types: list[int],
|
| 86 |
+
edge_types: list[Chem.BondType],
|
| 87 |
+
edge_index: list[tuple[int, int]],
|
| 88 |
+
sanitize: bool = True,
|
| 89 |
+
implicit_hydrogens: bool = False,
|
| 90 |
+
):
|
| 91 |
+
mol = Chem.EditableMol(Chem.Mol())
|
| 92 |
+
|
| 93 |
+
# Add atoms with formal charges
|
| 94 |
+
for idx, atomic in enumerate(atom_types):
|
| 95 |
+
atom = Chem.Atom(atomic)
|
| 96 |
+
charge = charge_types[idx]
|
| 97 |
+
atom.SetFormalCharge(charge)
|
| 98 |
+
atom.SetNoImplicit(not implicit_hydrogens)
|
| 99 |
+
mol.AddAtom(atom)
|
| 100 |
+
|
| 101 |
+
mol = mol.GetMol()
|
| 102 |
+
|
| 103 |
+
# Add 3D coords
|
| 104 |
+
conf = Chem.Conformer(len(atom_types))
|
| 105 |
+
|
| 106 |
+
conf.SetPositions(np.ascontiguousarray(coords, dtype=np.float64))
|
| 107 |
+
|
| 108 |
+
mol.AddConformer(conf, assignId=True)
|
| 109 |
+
|
| 110 |
+
mol = Chem.EditableMol(mol)
|
| 111 |
+
for edge, edge_type in zip(edge_index, edge_types, strict=True):
|
| 112 |
+
start, end = edge
|
| 113 |
+
mol.AddBond(start, end, edge_type)
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
mol = mol.GetMol()
|
| 117 |
+
# DetermineBonds(mol)
|
| 118 |
+
for atom in mol.GetAtoms():
|
| 119 |
+
atom.UpdatePropertyCache(strict=False)
|
| 120 |
+
except Exception:
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
if sanitize:
|
| 124 |
+
return sanitize_mol_correctly(mol, implicit_hydrogens=implicit_hydrogens)
|
| 125 |
+
|
| 126 |
+
return mol
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def sanitize_mol_correctly(mol: Chem.Mol, *, implicit_hydrogens: bool | None = False):
|
| 130 |
+
"""Sanitize in place; None preserves each atom's existing hydrogen policy."""
|
| 131 |
+
for a in mol.GetAtoms():
|
| 132 |
+
if implicit_hydrogens is not None:
|
| 133 |
+
a.SetNoImplicit(not implicit_hydrogens)
|
| 134 |
+
if a.HasProp("_MolFileHCount"):
|
| 135 |
+
a.ClearProp("_MolFileHCount")
|
| 136 |
+
|
| 137 |
+
flags = Chem.SanitizeFlags.SANITIZE_ALL
|
| 138 |
+
if all(a.GetNoImplicit() for a in mol.GetAtoms()):
|
| 139 |
+
flags &= ~Chem.SanitizeFlags.SANITIZE_ADJUSTHS
|
| 140 |
+
|
| 141 |
+
# Explicit-H graphs must not acquire extra H; implicit-H graphs need ADJUSTHS.
|
| 142 |
+
err = Chem.SanitizeMol(
|
| 143 |
+
mol,
|
| 144 |
+
sanitizeOps=flags,
|
| 145 |
+
catchErrors=True,
|
| 146 |
+
)
|
| 147 |
+
if err: # nonzero bitmask means some step failed
|
| 148 |
+
return None
|
| 149 |
+
return mol
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def mol_is_valid(
|
| 153 |
+
mol: Chem.Mol, allow_radical: bool = False, allow_charged: bool = False
|
| 154 |
+
) -> bool:
|
| 155 |
+
# Validity is a predicate: do not let sanitization mutate the molecule that
|
| 156 |
+
# subsequent metrics, serialization, or dataset processing will consume.
|
| 157 |
+
if mol is None or mol.GetNumAtoms() == 0:
|
| 158 |
+
return False
|
| 159 |
+
sanitized_mol = sanitize_mol_correctly(Chem.Mol(mol), implicit_hydrogens=None)
|
| 160 |
+
return not (
|
| 161 |
+
sanitized_mol is None
|
| 162 |
+
or len(Chem.GetMolFrags(sanitized_mol)) > 1
|
| 163 |
+
or (not allow_radical and NumRadicalElectrons(sanitized_mol) != 0)
|
| 164 |
+
or (not allow_charged and Chem.GetFormalCharge(sanitized_mol) != 0)
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def calc_energy(mol: Chem.Mol, per_atom: bool = False, level: str = "MMFF94") -> float:
|
| 169 |
+
"""Calculate the energy for an RDKit molecule using the MMFF forcefield
|
| 170 |
+
|
| 171 |
+
Evaluate the first conformer on a copy, leaving the original unchanged.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
mol (Chem.Mol): RDKit molecule
|
| 175 |
+
per_atom (bool): Whether to normalise by the number of atoms.
|
| 176 |
+
level: Force-field variant; currently only "MMFF94" is supported.
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
float: Energy of the molecule or None if the energy could not be calculated
|
| 180 |
+
"""
|
| 181 |
+
|
| 182 |
+
mol_copy = Chem.Mol(mol)
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
if level == "MMFF94":
|
| 186 |
+
mmff_props = AllChem.MMFFGetMoleculeProperties(
|
| 187 |
+
mol_copy, mmffVariant="MMFF94"
|
| 188 |
+
)
|
| 189 |
+
ff = AllChem.MMFFGetMoleculeForceField(mol_copy, mmff_props, confId=0)
|
| 190 |
+
energy = ff.CalcEnergy()
|
| 191 |
+
energy = energy / mol.GetNumAtoms() if per_atom else energy
|
| 192 |
+
except Exception:
|
| 193 |
+
energy = None
|
| 194 |
+
|
| 195 |
+
return energy
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def optimise_mol(
|
| 199 |
+
mol: Chem.rdchem.Mol, level: str = "MMFF94", max_iters: int = 1000
|
| 200 |
+
) -> Chem.rdchem.Mol:
|
| 201 |
+
"""Optimise the conformation of an RDKit molecule
|
| 202 |
+
|
| 203 |
+
Optimize the first conformer on a copy, leaving the original unchanged.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
mol (Chem.Mol): RDKit molecule
|
| 207 |
+
max_iters (int): Maximum iterations for conformer optimization.
|
| 208 |
+
level: Force-field variant; currently only "MMFF94" is supported.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
Chem.Mol: Optimized copy, or None if the force-field call raises an error.
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
mol_copy = Chem.Mol(mol)
|
| 215 |
+
try:
|
| 216 |
+
if level == "MMFF94":
|
| 217 |
+
AllChem.MMFFOptimizeMolecule(mol_copy, maxIters=max_iters)
|
| 218 |
+
except Exception:
|
| 219 |
+
return None
|
| 220 |
+
|
| 221 |
+
return mol_copy
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def conf_distance(
|
| 225 |
+
mol1: Chem.rdchem.Mol, mol2: Chem.rdchem.Mol, fix_order: bool = True
|
| 226 |
+
) -> float:
|
| 227 |
+
"""Approximately align two molecules and then calculate RMSD between them
|
| 228 |
+
|
| 229 |
+
Alignment and distance use the default conformer of each molecule.
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
mol1 (Chem.Mol): First molecule to align
|
| 233 |
+
mol2 (Chem.Mol): Second molecule to align
|
| 234 |
+
fix_order (bool): Whether to fix the atom order of the molecules
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
float: RMSD between molecules after approximate alignment
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
assert len(mol1.GetAtoms()) == len(mol2.GetAtoms())
|
| 241 |
+
|
| 242 |
+
if not fix_order:
|
| 243 |
+
raise NotImplementedError
|
| 244 |
+
|
| 245 |
+
coords1 = np.array(mol1.GetConformer().GetPositions())
|
| 246 |
+
coords2 = np.array(mol2.GetConformer().GetPositions())
|
| 247 |
+
|
| 248 |
+
# Firstly, centre both molecules
|
| 249 |
+
coords1 = coords1 - (coords1.sum(axis=0) / coords1.shape[0])
|
| 250 |
+
coords2 = coords2 - (coords2.sum(axis=0) / coords2.shape[0])
|
| 251 |
+
|
| 252 |
+
if coords1.shape[0] == 1:
|
| 253 |
+
# catch the case where the molecule is a single atom
|
| 254 |
+
return 0.0
|
| 255 |
+
|
| 256 |
+
else:
|
| 257 |
+
# Find the best rotation alignment between the centred mols
|
| 258 |
+
rotation, _ = Rotation.align_vectors(coords1, coords2)
|
| 259 |
+
aligned_coords2 = rotation.apply(coords2)
|
| 260 |
+
|
| 261 |
+
sqrd_dists = (coords1 - aligned_coords2) ** 2
|
| 262 |
+
rmsd = np.sqrt(sqrd_dists.sum(axis=1).mean())
|
| 263 |
+
return rmsd
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def smiles_from_mol(
|
| 267 |
+
mol: Chem.rdchem.Mol, canonical: bool = True, explicit_hs: bool = False
|
| 268 |
+
) -> str | None:
|
| 269 |
+
"""Create a SMILES string from a molecule
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
mol (Chem.Mol): RDKit molecule object
|
| 273 |
+
canonical (bool): Whether to create a canonical SMILES, default True
|
| 274 |
+
explicit_hs (bool): Add explicit hydrogen atoms before creating the SMILES.
|
| 275 |
+
If False, remove hydrogen atoms. Explicit and implicit hydrogen
|
| 276 |
+
representations can produce different strings.
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
str: SMILES string which could be None if the SMILES generation failed
|
| 280 |
+
"""
|
| 281 |
+
|
| 282 |
+
if mol is None:
|
| 283 |
+
return None
|
| 284 |
+
|
| 285 |
+
try:
|
| 286 |
+
mol = Chem.AddHs(mol) if explicit_hs else Chem.RemoveHs(mol)
|
| 287 |
+
smiles = Chem.MolToSmiles(mol, canonical=canonical)
|
| 288 |
+
except Exception:
|
| 289 |
+
smiles = None
|
| 290 |
+
|
| 291 |
+
return smiles
|
geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/run_config.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Runtime settings, Hydra/Lightning configuration, callbacks, and run IDs."""
|
| 2 |
+
|
| 3 |
+
import faulthandler
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
import hydra
|
| 8 |
+
import pytorch_lightning as pl
|
| 9 |
+
import torch
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
import wandb
|
| 12 |
+
from omegaconf import DictConfig
|
| 13 |
+
from pytorch_lightning import Callback
|
| 14 |
+
from pytorch_lightning.callbacks import (
|
| 15 |
+
EarlyStopping,
|
| 16 |
+
ModelCheckpoint,
|
| 17 |
+
)
|
| 18 |
+
from pytorch_lightning.loggers import WandbLogger
|
| 19 |
+
from rdkit import RDLogger
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def configure_runtime(*, seed: int | None = None, diagnostics: bool = False) -> None:
|
| 23 |
+
"""Set arithmetic, chemistry logging, and optional reproducibility diagnostics."""
|
| 24 |
+
torch.set_float32_matmul_precision("medium")
|
| 25 |
+
RDLogger.DisableLog("rdApp.*")
|
| 26 |
+
if seed is not None:
|
| 27 |
+
pl.seed_everything(seed)
|
| 28 |
+
if diagnostics and os.environ.get("MORPH_STALL_DUMP"):
|
| 29 |
+
faulthandler.dump_traceback_later(
|
| 30 |
+
int(os.environ["MORPH_STALL_DUMP"]), repeat=True, file=sys.stderr
|
| 31 |
+
)
|
| 32 |
+
faulthandler.enable(file=sys.stderr, all_threads=True)
|
| 33 |
+
if diagnostics and os.environ.get("MORPH_FORCE_MATH_SDP"):
|
| 34 |
+
torch.backends.cuda.enable_flash_sdp(False)
|
| 35 |
+
torch.backends.cuda.enable_mem_efficient_sdp(False)
|
| 36 |
+
torch.backends.cuda.enable_math_sdp(True)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ManualAccumulationWandbLogger(WandbLogger):
|
| 40 |
+
"""Report manual-optimization updates on W&B's standard step axis."""
|
| 41 |
+
|
| 42 |
+
def __init__(self, *args, accumulation_steps: int = 1, **kwargs):
|
| 43 |
+
if accumulation_steps <= 0:
|
| 44 |
+
raise ValueError("accumulation_steps must be positive")
|
| 45 |
+
self._accumulation_steps = accumulation_steps
|
| 46 |
+
super().__init__(*args, **kwargs)
|
| 47 |
+
|
| 48 |
+
def log_metrics(self, metrics, step=None):
|
| 49 |
+
"""Log metrics against completed updates rather than microbatches."""
|
| 50 |
+
# Lightning's logger counter advances per batch when its Trainer-side
|
| 51 |
+
# accumulation is one. Manual optimization accumulates inside the
|
| 52 |
+
# module, so convert that counter back to completed model updates.
|
| 53 |
+
if step is not None:
|
| 54 |
+
step //= self._accumulation_steps
|
| 55 |
+
return super().log_metrics(metrics, step=step)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def validation_batches_per_rank(
|
| 59 |
+
num_samples: int, batch_size: int, world_size: int
|
| 60 |
+
) -> int:
|
| 61 |
+
"""Return the per-rank batch limit for an exact global sample count."""
|
| 62 |
+
if num_samples <= 0 or batch_size <= 0 or world_size <= 0:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
"validation sample count, batch size and world size must be positive"
|
| 65 |
+
)
|
| 66 |
+
samples_per_distributed_batch = batch_size * world_size
|
| 67 |
+
if num_samples % samples_per_distributed_batch:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
f"num_val_samples ({num_samples}) must be divisible by val batch size "
|
| 70 |
+
f"({batch_size}) * world size ({world_size})"
|
| 71 |
+
)
|
| 72 |
+
return num_samples // samples_per_distributed_batch
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def configure_manual_gradient_accumulation(module, trainer_kwargs: dict) -> None:
|
| 76 |
+
"""Move Lightning's accumulation setting onto a manual-optimization module.
|
| 77 |
+
|
| 78 |
+
Lightning rejects ``Trainer(accumulate_grad_batches != 1)`` for manual
|
| 79 |
+
optimization. The optimizer mixin implements the equivalent accumulation,
|
| 80 |
+
while the Trainer must therefore retain its neutral value of one.
|
| 81 |
+
"""
|
| 82 |
+
steps = int(trainer_kwargs.get("accumulate_grad_batches", 1))
|
| 83 |
+
if steps <= 0:
|
| 84 |
+
raise ValueError("accumulate_grad_batches must be positive")
|
| 85 |
+
if module.automatic_optimization:
|
| 86 |
+
return
|
| 87 |
+
module.manual_gradient_accumulation_steps = steps
|
| 88 |
+
trainer_kwargs["accumulate_grad_batches"] = 1
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def gather_rank_outputs(outputs: list) -> list | None:
|
| 92 |
+
"""Gather picklable rank outputs on rank zero without replicating them."""
|
| 93 |
+
if not (dist.is_available() and dist.is_initialized()):
|
| 94 |
+
return outputs
|
| 95 |
+
|
| 96 |
+
is_global_zero = dist.get_rank() == 0
|
| 97 |
+
gathered = [None] * dist.get_world_size() if is_global_zero else None
|
| 98 |
+
dist.gather_object(outputs, gathered, dst=0)
|
| 99 |
+
if not is_global_zero:
|
| 100 |
+
return None
|
| 101 |
+
return [output for rank_outputs in gathered for output in rank_outputs]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def build_callbacks(cfg: DictConfig) -> list[Callback]:
|
| 105 |
+
callbacks: list[Callback] = []
|
| 106 |
+
|
| 107 |
+
if "early_stopping" in cfg.callbacks:
|
| 108 |
+
hydra.utils.log.info("Adding callback <EarlyStopping>")
|
| 109 |
+
es_cfg = cfg.callbacks.early_stopping
|
| 110 |
+
# stopping_threshold (optional): halt the moment the monitored metric
|
| 111 |
+
# crosses this value (>= for mode=max), independent of the plateau path.
|
| 112 |
+
# Used to stop a run once validity reaches a target (e.g. 0.8).
|
| 113 |
+
callbacks.append(
|
| 114 |
+
EarlyStopping(
|
| 115 |
+
monitor=cfg.callbacks.monitor_metric,
|
| 116 |
+
mode=cfg.callbacks.monitor_metric_mode,
|
| 117 |
+
patience=es_cfg.patience,
|
| 118 |
+
verbose=es_cfg.verbose,
|
| 119 |
+
stopping_threshold=es_cfg.get("stopping_threshold", None),
|
| 120 |
+
)
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if "model_checkpoints" in cfg.callbacks:
|
| 124 |
+
hydra.utils.log.info("Adding callback <ModelCheckpoint>")
|
| 125 |
+
callbacks.append(
|
| 126 |
+
ModelCheckpoint(
|
| 127 |
+
monitor=cfg.callbacks.monitor_metric,
|
| 128 |
+
mode=cfg.callbacks.monitor_metric_mode,
|
| 129 |
+
every_n_epochs=cfg.callbacks.model_checkpoints.get("every_n_epochs", 1),
|
| 130 |
+
save_top_k=cfg.callbacks.model_checkpoints.save_top_k,
|
| 131 |
+
verbose=cfg.callbacks.model_checkpoints.verbose,
|
| 132 |
+
save_last=cfg.callbacks.model_checkpoints.save_last,
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
if "every_n_epochs_checkpoint" in cfg.callbacks:
|
| 137 |
+
cb_cfg = cfg.callbacks.every_n_epochs_checkpoint
|
| 138 |
+
hydra.utils.log.info(
|
| 139 |
+
"Adding callback <ModelCheckpoint> for every "
|
| 140 |
+
f"{cb_cfg.every_n_epochs} epochs"
|
| 141 |
+
)
|
| 142 |
+
# Keep best-by-validation separate from latest-for-resume. Lightning only saves
|
| 143 |
+
# train-epoch checkpoints when save_on_train_epoch_end=True if validation is not
|
| 144 |
+
# run every epoch.
|
| 145 |
+
callbacks.append(
|
| 146 |
+
ModelCheckpoint(
|
| 147 |
+
dirpath="every_n_epochs",
|
| 148 |
+
every_n_epochs=cb_cfg.every_n_epochs,
|
| 149 |
+
save_top_k=cb_cfg.save_top_k,
|
| 150 |
+
verbose=cb_cfg.verbose,
|
| 151 |
+
save_last=cb_cfg.save_last,
|
| 152 |
+
save_on_train_epoch_end=cb_cfg.get("save_on_train_epoch_end", None),
|
| 153 |
+
)
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Task-specific callbacks are built generically: any ``callbacks.<name>`` that
|
| 157 |
+
# carries a ``_target_`` is instantiated as-is. This module therefore imports
|
| 158 |
+
# none of them, which matters because the per-task eval callbacks ship with
|
| 159 |
+
# their own recipe (edit / multimode) and may not be installed at
|
| 160 |
+
# all -- a hard import here would make this core module depend on them.
|
| 161 |
+
# Defaults live on the callback classes, so the config need only override.
|
| 162 |
+
for entry in (cfg.callbacks or {}).values():
|
| 163 |
+
if not isinstance(entry, DictConfig) or "_target_" not in entry:
|
| 164 |
+
continue # the explicitly-handled callbacks above
|
| 165 |
+
hydra.utils.log.info(f"Adding callback <{entry._target_.rsplit('.', 1)[-1]}>")
|
| 166 |
+
callbacks.append(hydra.utils.instantiate(entry, _convert_="all"))
|
| 167 |
+
|
| 168 |
+
return callbacks
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def bootstrap_run_id() -> None:
|
| 172 |
+
"""Set the shared run IDs before Hydra composes ``hydra.run.dir``.
|
| 173 |
+
|
| 174 |
+
A single process generates a W&B-style ID. Multi-process ``torchrun`` must
|
| 175 |
+
receive the ID in its environment so every rank shares one output directory.
|
| 176 |
+
"""
|
| 177 |
+
if os.environ.get("MORPH_RUN_ID"):
|
| 178 |
+
os.environ.setdefault("WANDB_RUN_ID", os.environ["MORPH_RUN_ID"])
|
| 179 |
+
return
|
| 180 |
+
|
| 181 |
+
world_size = int(os.environ.get("WORLD_SIZE", "1"))
|
| 182 |
+
if world_size > 1:
|
| 183 |
+
raise RuntimeError(
|
| 184 |
+
"Multi-process runs require MORPH_RUN_ID in the environment (use the same "
|
| 185 |
+
"value as WANDB_RUN_ID). Example:\n"
|
| 186 |
+
" export MORPH_RUN_ID=<shared-id>\n"
|
| 187 |
+
' export WANDB_RUN_ID="$MORPH_RUN_ID"'
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
rid = wandb.util.generate_id()
|
| 191 |
+
os.environ["MORPH_RUN_ID"] = rid
|
| 192 |
+
os.environ["WANDB_RUN_ID"] = rid
|
geom/scaffold_murcko_substituent_lora_r16/runtime/src/morph/utils/vocab_utils.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Token vocabulary helpers and loss-weight computation."""
|
| 2 |
+
|
| 3 |
+
from copy import deepcopy
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from rdkit import Chem
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def z_to_atom_types(z):
|
| 10 |
+
"""Convert the atomic numbers to atom symbols with rdkit."""
|
| 11 |
+
|
| 12 |
+
table = Chem.GetPeriodicTable()
|
| 13 |
+
return [table.GetElementSymbol(z_i) for z_i in z]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def token_to_index(token_list, token: str):
|
| 17 |
+
return token_list.index(token)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def index_to_token(token_list, index: int):
|
| 21 |
+
return token_list[index]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def compute_token_weights(
|
| 25 |
+
token_list: list[str],
|
| 26 |
+
distribution: torch.Tensor,
|
| 27 |
+
special_token_names: list[str],
|
| 28 |
+
weight_alpha: float = 1.0,
|
| 29 |
+
type_loss_token_weights: str = "uniform",
|
| 30 |
+
) -> torch.Tensor:
|
| 31 |
+
"""
|
| 32 |
+
Compute token weights with special handling for special tokens.
|
| 33 |
+
|
| 34 |
+
This function can be used for both node tokens (atom types) and edge tokens.
|
| 35 |
+
It computes inverse frequency weights, normalizes regular tokens, and assigns
|
| 36 |
+
appropriate weights to special tokens.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
token_list: List of all tokens (e.g., atom types or edge types)
|
| 40 |
+
distribution: Distribution of token types from training data.
|
| 41 |
+
MUST have the same length as token_list and be aligned by index.
|
| 42 |
+
special_token_names: List of special token names to handle specially
|
| 43 |
+
(e.g., ["<NO_BOND>"] for edges)
|
| 44 |
+
weight_alpha: Alpha parameter for weight scaling (default: 1.0)
|
| 45 |
+
type_loss_token_weights: "uniform" or "training" - if "uniform", returns
|
| 46 |
+
uniform weights (all 1.0)
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
Weights tensor with same shape as distribution
|
| 50 |
+
"""
|
| 51 |
+
# Validate inputs
|
| 52 |
+
if len(distribution) != len(token_list):
|
| 53 |
+
raise ValueError(
|
| 54 |
+
f"Distribution length ({len(distribution)}) must match "
|
| 55 |
+
f"token_list length ({len(token_list)})"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Get indices for special tokens (with validation)
|
| 59 |
+
special_token_indices = set()
|
| 60 |
+
for token_name in special_token_names:
|
| 61 |
+
if token_name not in token_list:
|
| 62 |
+
print(f"Warning: Special token '{token_name}' not in token_list, skipping")
|
| 63 |
+
continue
|
| 64 |
+
idx = token_to_index(token_list, token_name)
|
| 65 |
+
special_token_indices.add(idx)
|
| 66 |
+
|
| 67 |
+
# --- Handle uniform weights case early ---
|
| 68 |
+
if type_loss_token_weights == "uniform":
|
| 69 |
+
return torch.ones_like(distribution)
|
| 70 |
+
|
| 71 |
+
# --- 1. Initial Weight Calculation (Inverse Frequency) ---
|
| 72 |
+
epsilon = 1e-8
|
| 73 |
+
weights = 1.0 / (distribution + epsilon)
|
| 74 |
+
weights = weights**weight_alpha # Apply alpha scaling
|
| 75 |
+
|
| 76 |
+
# --- 2. Isolate & Normalize REGULAR Tokens ---
|
| 77 |
+
all_indices = set(range(len(token_list)))
|
| 78 |
+
regular_token_indices = list(all_indices - special_token_indices)
|
| 79 |
+
|
| 80 |
+
if not regular_token_indices:
|
| 81 |
+
return torch.ones_like(weights)
|
| 82 |
+
|
| 83 |
+
# Get weights for only the regular tokens
|
| 84 |
+
regular_weights = weights[regular_token_indices]
|
| 85 |
+
|
| 86 |
+
# Normalize *only* the regular weights to have a mean of 1.0
|
| 87 |
+
mean_regular_weight = regular_weights.mean()
|
| 88 |
+
if mean_regular_weight > 0:
|
| 89 |
+
regular_weights = regular_weights / mean_regular_weight
|
| 90 |
+
|
| 91 |
+
# --- 3. Build Final Weights Tensor ---
|
| 92 |
+
# Start with the full weights (including special tokens computed from distribution)
|
| 93 |
+
final_weights = weights.clone()
|
| 94 |
+
|
| 95 |
+
# Assign normalized regular weights to their correct positions
|
| 96 |
+
final_weights[regular_token_indices] = regular_weights
|
| 97 |
+
|
| 98 |
+
# Handle special tokens: use their actual inverse frequency weight,
|
| 99 |
+
# normalized the same way as regular tokens
|
| 100 |
+
for token_name in special_token_names:
|
| 101 |
+
if token_name not in token_list:
|
| 102 |
+
continue
|
| 103 |
+
token_idx = token_to_index(token_list, token_name)
|
| 104 |
+
special_weight = weights[token_idx]
|
| 105 |
+
if mean_regular_weight > 0:
|
| 106 |
+
special_weight = special_weight / mean_regular_weight
|
| 107 |
+
final_weights[token_idx] = special_weight
|
| 108 |
+
|
| 109 |
+
# set the minimum weight to 1.0
|
| 110 |
+
# final_weights = final_weights / final_weights.min()
|
| 111 |
+
|
| 112 |
+
return final_weights
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def init_uniform_prior(distributions):
|
| 116 |
+
"""Return a new Distributions with uniform atom, edge, and charge type
|
| 117 |
+
distributions.
|
| 118 |
+
|
| 119 |
+
All other fields (n_atoms_distribution, coordinate_std) are copied from the
|
| 120 |
+
original. The original object is not modified.
|
| 121 |
+
"""
|
| 122 |
+
prior = deepcopy(distributions)
|
| 123 |
+
for attr in (
|
| 124 |
+
"atom_type_distribution",
|
| 125 |
+
"edge_type_distribution",
|
| 126 |
+
"charge_type_distribution",
|
| 127 |
+
):
|
| 128 |
+
dist = torch.ones_like(getattr(prior, attr))
|
| 129 |
+
setattr(prior, attr, dist / dist.sum())
|
| 130 |
+
return prior
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
_PRIOR_MODES = ("uniform", "marginal", "absorbing")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def build_token_prior(
|
| 137 |
+
distributions,
|
| 138 |
+
*,
|
| 139 |
+
edge_mode: str = "uniform",
|
| 140 |
+
atom_mode: str = "uniform",
|
| 141 |
+
charge_mode: str = "uniform",
|
| 142 |
+
):
|
| 143 |
+
"""Return a new Distributions with per-token prior modes.
|
| 144 |
+
|
| 145 |
+
Generalises :func:`init_uniform_prior` (``all modes == "uniform"`` reproduces
|
| 146 |
+
it exactly). The prior is the flow-matching *source*: it seeds
|
| 147 |
+
``sample_prior_graph`` and the interpolator, so changing it changes the whole
|
| 148 |
+
transport path the denoiser is trained on.
|
| 149 |
+
|
| 150 |
+
Modes per token channel:
|
| 151 |
+
* ``uniform`` -- maximum entropy (the historical default; for edges this
|
| 152 |
+
is why the prior graph starts massively over-bonded).
|
| 153 |
+
* ``marginal`` -- the data marginal already on ``distributions`` (DiGress-
|
| 154 |
+
style; the source is close to the data, so far fewer edits).
|
| 155 |
+
* ``absorbing`` -- one-hot on token index 0 (``<NO_BOND>`` for edges): an
|
| 156 |
+
empty-graph / masked source with zero deletions to make.
|
| 157 |
+
"""
|
| 158 |
+
prior = deepcopy(distributions)
|
| 159 |
+
for attr, mode in (
|
| 160 |
+
("atom_type_distribution", atom_mode),
|
| 161 |
+
("edge_type_distribution", edge_mode),
|
| 162 |
+
("charge_type_distribution", charge_mode),
|
| 163 |
+
):
|
| 164 |
+
cur = getattr(prior, attr)
|
| 165 |
+
if mode == "uniform":
|
| 166 |
+
dist = torch.ones_like(cur)
|
| 167 |
+
elif mode == "marginal":
|
| 168 |
+
dist = cur.clamp_min(0).clone() # keep the data marginal already present
|
| 169 |
+
elif mode == "absorbing":
|
| 170 |
+
dist = torch.zeros_like(cur)
|
| 171 |
+
dist[0] = 1.0
|
| 172 |
+
else:
|
| 173 |
+
raise ValueError(
|
| 174 |
+
f"prior mode for {attr} must be one of {_PRIOR_MODES}, got {mode!r}"
|
| 175 |
+
)
|
| 176 |
+
setattr(prior, attr, dist / dist.sum())
|
| 177 |
+
return prior
|
geom/scaffold_murcko_substituent_lora_r16/runtime/uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
geom/scaffold_murcko_substituent_lora_r16/training_config.yaml
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
representation: molecule
|
| 2 |
+
seed: 42
|
| 3 |
+
heads:
|
| 4 |
+
heads:
|
| 5 |
+
_target_: morph.model.layers.heads.MultiHeadModule
|
| 6 |
+
output_init_std: ${model.head_initialization.output_std}
|
| 7 |
+
independent_adapters: ${oc.select:model.head_independent_adapters,false}
|
| 8 |
+
heads_configs:
|
| 9 |
+
graph_aggregation: mean
|
| 10 |
+
node_heads:
|
| 11 |
+
atom_type_head:
|
| 12 |
+
input_dim: ${model.head_node_dim}
|
| 13 |
+
output_dim: ${len:${data.vocab.atom_tokens}}
|
| 14 |
+
adapter_dim: ${model.head_node_adapter_dim}
|
| 15 |
+
group: state
|
| 16 |
+
charge_head:
|
| 17 |
+
input_dim: ${model.head_node_dim}
|
| 18 |
+
output_dim: ${len:${data.vocab.charge_tokens}}
|
| 19 |
+
adapter_dim: ${model.head_node_adapter_dim}
|
| 20 |
+
group: state
|
| 21 |
+
ins_rate_head:
|
| 22 |
+
input_dim: ${model.head_node_dim}
|
| 23 |
+
output_dim: 1
|
| 24 |
+
adapter_dim: ${model.head_node_adapter_dim}
|
| 25 |
+
group: edit
|
| 26 |
+
initial_rate: ${model.head_initialization.ins_rate}
|
| 27 |
+
do_del_head:
|
| 28 |
+
input_dim: ${model.head_node_dim}
|
| 29 |
+
output_dim: 1
|
| 30 |
+
adapter_dim: ${model.head_node_adapter_dim}
|
| 31 |
+
group: edit
|
| 32 |
+
initial_probability: ${model.head_initialization.do_del_probability}
|
| 33 |
+
do_sub_a_head:
|
| 34 |
+
input_dim: ${model.head_node_dim}
|
| 35 |
+
output_dim: 1
|
| 36 |
+
adapter_dim: ${model.head_node_adapter_dim}
|
| 37 |
+
group: edit
|
| 38 |
+
initial_probability: ${model.head_initialization.do_sub_a_probability}
|
| 39 |
+
edge_heads:
|
| 40 |
+
edge_type_head:
|
| 41 |
+
input_dim: ${model.head_edge_dim}
|
| 42 |
+
output_dim: ${len:${data.vocab.edge_tokens}}
|
| 43 |
+
adapter_dim: ${model.head_edge_adapter_dim}
|
| 44 |
+
group: state
|
| 45 |
+
do_sub_e_head:
|
| 46 |
+
input_dim: ${model.head_edge_dim}
|
| 47 |
+
output_dim: 1
|
| 48 |
+
adapter_dim: ${model.head_edge_adapter_dim}
|
| 49 |
+
group: state
|
| 50 |
+
initial_probability: ${model.head_initialization.do_sub_e_probability}
|
| 51 |
+
ins_edge_head:
|
| 52 |
+
_target_: morph.model.layers.heads.InsertionEdgeHead
|
| 53 |
+
_recursive_: false
|
| 54 |
+
hidden_dim: ${model.head_node_dim}
|
| 55 |
+
readout_dim: ${model.head_insertion_edge_dim}
|
| 56 |
+
output_init_std: ${model.head_initialization.output_std}
|
| 57 |
+
n_edge_types: ${len:${data.vocab.edge_tokens}}
|
| 58 |
+
n_atom_types: ${len:${data.vocab.atom_tokens}}
|
| 59 |
+
n_charge_types: ${len:${data.vocab.charge_tokens}}
|
| 60 |
+
rbf_embedding_args:
|
| 61 |
+
_target_: morph.model.layers.embedding.RBFEmbedding
|
| 62 |
+
num_rbf: 128
|
| 63 |
+
rbf_dmax: 3.5
|
| 64 |
+
out_dim: 64
|
| 65 |
+
dropout: 0.0
|
| 66 |
+
trainable: false
|
| 67 |
+
insertion_head:
|
| 68 |
+
mode: unimodal
|
| 69 |
+
dimension: 3
|
| 70 |
+
num_components: 1
|
| 71 |
+
model:
|
| 72 |
+
_target_: morph.model.layers.heads.UnimodalInsertionHead
|
| 73 |
+
hidden_dim: ${model.head_node_dim}
|
| 74 |
+
readout_dim: ${model.head_insertion_adapter_dim}
|
| 75 |
+
output_init_std: ${model.head_initialization.output_std}
|
| 76 |
+
independent_adapters: ${oc.select:model.head_independent_adapters,false}
|
| 77 |
+
N_a: ${len:${data.vocab.atom_tokens}}
|
| 78 |
+
N_c: ${len:${data.vocab.charge_tokens}}
|
| 79 |
+
model:
|
| 80 |
+
module:
|
| 81 |
+
optimizer_config:
|
| 82 |
+
implementation: torch
|
| 83 |
+
optimizer:
|
| 84 |
+
execution: ${oc.env:MORPH_MUON_EXECUTION,native}
|
| 85 |
+
lr: 0.001
|
| 86 |
+
momentum: 0.95
|
| 87 |
+
weight_decay: 0.0
|
| 88 |
+
nesterov: true
|
| 89 |
+
ns_steps: 5
|
| 90 |
+
eps: 1.0e-07
|
| 91 |
+
adjust_lr_fn: original
|
| 92 |
+
aux_optimizer:
|
| 93 |
+
lr: 0.0001
|
| 94 |
+
betas:
|
| 95 |
+
- 0.9
|
| 96 |
+
- 0.95
|
| 97 |
+
eps: 1.0e-10
|
| 98 |
+
weight_decay: 0.0
|
| 99 |
+
gradient_clip_val: 1.0
|
| 100 |
+
gradient_clip_algorithm: norm
|
| 101 |
+
scheduler:
|
| 102 |
+
_target_: morph.utils.lr_schedulers.MultiGroupCosineWarmupLR
|
| 103 |
+
warmup_steps: 2000
|
| 104 |
+
total_steps: 200000
|
| 105 |
+
min_lr_fraction: 0.05
|
| 106 |
+
last_epoch: -1
|
| 107 |
+
interval: step
|
| 108 |
+
monitor: null
|
| 109 |
+
loss_weights:
|
| 110 |
+
do_sub_a: 0.2
|
| 111 |
+
sub_a_class: 0.03
|
| 112 |
+
do_sub_e: 0.25
|
| 113 |
+
sub_e_class: 0.5
|
| 114 |
+
do_del: 0.4
|
| 115 |
+
ins_rate: 0.15
|
| 116 |
+
ins_state: 0.01
|
| 117 |
+
ins_e: 0.4
|
| 118 |
+
x: 2.0
|
| 119 |
+
c: 0.5
|
| 120 |
+
ins_e_ii: 0.03
|
| 121 |
+
_target_: morph.model.finetune.scaffold.LightningModuleRatesScaffoldFinetune
|
| 122 |
+
insertion_head_mode: ${insertion_head.mode}
|
| 123 |
+
ins_noise_scale: ${data.ins_noise_scale}
|
| 124 |
+
ema_decay: 0.999
|
| 125 |
+
ema_warmup_steps: 5000
|
| 126 |
+
use_ema_for_eval: true
|
| 127 |
+
time_dist: ${if:${eq:${model.sample_time_strategy}, log}, ${model.beta_sched},
|
| 128 |
+
${model.linear_sched}}
|
| 129 |
+
integrator: ${model.integrator}
|
| 130 |
+
vocab: ${data.vocab}
|
| 131 |
+
use_learnable_loss_weights: false
|
| 132 |
+
use_time_weights: false
|
| 133 |
+
model:
|
| 134 |
+
_target_: morph.model.finetune.scaffold.ScaffoldConditionalDiTBackboneWithHeads
|
| 135 |
+
_recursive_: false
|
| 136 |
+
atom_type_embedding_args: ${embeddings.atom_type_embedding}
|
| 137 |
+
edge_type_embedding_args: ${embeddings.edge_type_embedding}
|
| 138 |
+
time_embedding_args: ${embeddings.time_embedding}
|
| 139 |
+
node_count_embedding_args: ${embeddings.node_count_embedding}
|
| 140 |
+
incident_bond_embedding: false
|
| 141 |
+
backbone_model_args: ${model.backbone}
|
| 142 |
+
heads_args: ${heads.heads}
|
| 143 |
+
insertion_head_args: ${insertion_head.model}
|
| 144 |
+
ins_edge_head_args: ${heads.ins_edge_head}
|
| 145 |
+
xattn_encoder_args: ${model.xattn_encoder}
|
| 146 |
+
use_distance_bias: true
|
| 147 |
+
pretrained_ckpt: /cluster/project/krause/frankem/morph-chem/outputs/hf_geom_qm9_20261009/stage/geom/explicit_h/epoch_100.ckpt
|
| 148 |
+
pretrained_prefer_ema: true
|
| 149 |
+
freeze_pretrained_backbone: true
|
| 150 |
+
freeze_output_heads: false
|
| 151 |
+
exclude_scaffold_from_sub_loss: true
|
| 152 |
+
exclude_scaffold_from_x_loss: false
|
| 153 |
+
freeze_keep_positions: false
|
| 154 |
+
freeze_keep_charges: true
|
| 155 |
+
rotate: true
|
| 156 |
+
n_augmentations: 1
|
| 157 |
+
integrator: ${integrator.integrator}
|
| 158 |
+
beta_sched:
|
| 159 |
+
_target_: torch.distributions.Beta
|
| 160 |
+
concentration1: 2.0
|
| 161 |
+
concentration0: 1.0
|
| 162 |
+
linear_sched:
|
| 163 |
+
_target_: torch.distributions.Uniform
|
| 164 |
+
low: 0.0
|
| 165 |
+
high: 1.0
|
| 166 |
+
sample_time_strategy: linear
|
| 167 |
+
head_node_dim: ${model.backbone.out_node_nf}
|
| 168 |
+
head_edge_dim: ${model.backbone.out_edge_nf}
|
| 169 |
+
head_node_adapter_dim: 256
|
| 170 |
+
head_edge_adapter_dim: 256
|
| 171 |
+
head_insertion_adapter_dim: 256
|
| 172 |
+
head_insertion_edge_dim: 256
|
| 173 |
+
head_independent_adapters: true
|
| 174 |
+
head_initialization:
|
| 175 |
+
output_std: 0.001
|
| 176 |
+
coordinate_output_std: 0.0
|
| 177 |
+
do_del_probability: 0.5
|
| 178 |
+
do_sub_a_probability: 0.5
|
| 179 |
+
do_sub_e_probability: 0.5
|
| 180 |
+
ins_rate: 0.6931471805599453
|
| 181 |
+
token_weighting:
|
| 182 |
+
type_loss_token_weights: uniform
|
| 183 |
+
weight_alpha: 0.4
|
| 184 |
+
lora:
|
| 185 |
+
enabled: true
|
| 186 |
+
rank: 16
|
| 187 |
+
alpha: 16.0
|
| 188 |
+
dropout: 0.0
|
| 189 |
+
targets:
|
| 190 |
+
- qkv
|
| 191 |
+
- proj
|
| 192 |
+
- mlp.gate_value
|
| 193 |
+
- mlp.out_proj
|
| 194 |
+
freeze_base: true
|
| 195 |
+
backbone:
|
| 196 |
+
_target_: morph.model.backbones.conditional_dit.ConditionalDiTBackbone
|
| 197 |
+
_recursive_: false
|
| 198 |
+
d_input: ${embeddings.atom_type_embedding.out_dim}
|
| 199 |
+
d_cond: ${oc.eval:"${embeddings.time_embedding.out_dim} + ${embeddings.node_count_embedding.out_dim}"}
|
| 200 |
+
d_model: 512
|
| 201 |
+
out_node_nf: 256
|
| 202 |
+
out_edge_nf: 256
|
| 203 |
+
num_layers: 8
|
| 204 |
+
nhead: 16
|
| 205 |
+
mlp_ratio: 4.0
|
| 206 |
+
ffn_hidden_dim: null
|
| 207 |
+
ffn_multiple_of: 64
|
| 208 |
+
qk_norm: true
|
| 209 |
+
coordinate_output_init_std: ${model.head_initialization.coordinate_output_std}
|
| 210 |
+
center_coordinate_output: true
|
| 211 |
+
in_edge_nf: ${embeddings.edge_type_embedding.out_dim}
|
| 212 |
+
edge_pair_dim: 256
|
| 213 |
+
pair_bias_hidden_dim: 32
|
| 214 |
+
num_atom_types: ${len:${data.vocab.atom_tokens}}
|
| 215 |
+
lora: ${model.lora}
|
| 216 |
+
rbf_embedding_args:
|
| 217 |
+
_target_: morph.model.layers.embedding.RBFEmbedding
|
| 218 |
+
num_rbf: 128
|
| 219 |
+
rbf_dmax: 3.5
|
| 220 |
+
out_dim: 64
|
| 221 |
+
dropout: 0.0
|
| 222 |
+
trainable: false
|
| 223 |
+
ctx_dim: 128
|
| 224 |
+
use_cross_attn: true
|
| 225 |
+
use_node_cond: true
|
| 226 |
+
use_edge_cond: true
|
| 227 |
+
xattn_encoder:
|
| 228 |
+
_target_: morph.model.finetune.base.LightweightContextEncoder
|
| 229 |
+
num_atom_types: ${len:${data.vocab.atom_tokens}}
|
| 230 |
+
num_charges: ${len:${data.vocab.charge_tokens}}
|
| 231 |
+
atom_embed_dim: 64
|
| 232 |
+
charge_embed_dim: 16
|
| 233 |
+
pos_embed_dim: 32
|
| 234 |
+
ctx_dim: ${model.backbone.ctx_dim}
|
| 235 |
+
num_attn_layers: 4
|
| 236 |
+
nhead: 8
|
| 237 |
+
mlp_ratio: 4.0
|
| 238 |
+
dropout: 0.0
|
| 239 |
+
use_pair_bias: true
|
| 240 |
+
num_edge_types: ${len:${data.vocab.edge_tokens}}
|
| 241 |
+
edge_embed_dim: 32
|
| 242 |
+
num_rbf: 32
|
| 243 |
+
rbf_dmax: 5.0
|
| 244 |
+
pair_bias_hidden_dim: 32
|
| 245 |
+
schedule:
|
| 246 |
+
_target_: morph.flow_matching.schedules.SmoothstepSchedule
|
| 247 |
+
shift: 0.8
|
| 248 |
+
trainer:
|
| 249 |
+
trainer:
|
| 250 |
+
deterministic: warn
|
| 251 |
+
devices: 2
|
| 252 |
+
accelerator: gpu
|
| 253 |
+
strategy: ddp_find_unused_parameters_false
|
| 254 |
+
precision: bf16-mixed
|
| 255 |
+
max_epochs: 50
|
| 256 |
+
accumulate_grad_batches: 1
|
| 257 |
+
num_sanity_val_steps: 0
|
| 258 |
+
gradient_clip_val: null
|
| 259 |
+
gradient_clip_algorithm: norm
|
| 260 |
+
check_val_every_n_epoch: 1
|
| 261 |
+
num_val_samples: 4096
|
| 262 |
+
limit_val_batches: null
|
| 263 |
+
limit_predict_batches: 4
|
| 264 |
+
enable_progress_bar: false
|
| 265 |
+
max_time:
|
| 266 |
+
hours: 0
|
| 267 |
+
seconds: 169927
|
| 268 |
+
do_train: true
|
| 269 |
+
do_validate: false
|
| 270 |
+
do_predict: false
|
| 271 |
+
checkpoint_path: null
|
| 272 |
+
final_checkpoint_path: final.ckpt
|
| 273 |
+
callbacks:
|
| 274 |
+
monitor_metric: ${if:${oc.eval:"'${representation}' in ('geometric_graph', 'molecule')"},val/validity,val/batch/pair_dist_l1}
|
| 275 |
+
monitor_metric_mode: ${if:${oc.eval:"'${representation}' in ('geometric_graph',
|
| 276 |
+
'molecule')"},max,min}
|
| 277 |
+
model_checkpoints:
|
| 278 |
+
save_top_k: 1
|
| 279 |
+
save_last: false
|
| 280 |
+
verbose: false
|
| 281 |
+
every_n_epochs: 5
|
| 282 |
+
every_n_epochs_checkpoint:
|
| 283 |
+
every_n_epochs: 5
|
| 284 |
+
save_top_k: -1
|
| 285 |
+
save_last: link
|
| 286 |
+
verbose: false
|
| 287 |
+
save_on_train_epoch_end: true
|
| 288 |
+
metric_audit:
|
| 289 |
+
_target_: run_audit.ValidationAudit
|
| 290 |
+
expected_samples: 4096
|
| 291 |
+
expected_scaffolds: 512
|
| 292 |
+
logging:
|
| 293 |
+
enabled: true
|
| 294 |
+
project: morph-geom-scaffold-decoration
|
| 295 |
+
name: ${oc.env:MORPH_RUN_ID,base}
|
| 296 |
+
save_dir: .
|
| 297 |
+
offline: false
|
| 298 |
+
group: geom-e100-murcko-original-50ep-20261010
|
| 299 |
+
tags:
|
| 300 |
+
- substituent-insertion
|
| 301 |
+
- rank-16
|
| 302 |
+
- scaffold-decoration
|
| 303 |
+
- bemis-murcko
|
| 304 |
+
- geom-e100
|
| 305 |
+
- lora
|
| 306 |
+
- trainable-heads
|
| 307 |
+
- movable-scaffold
|
| 308 |
+
- jitter-0.1A
|
| 309 |
+
- scaffold-metrics
|
| 310 |
+
integrator:
|
| 311 |
+
integrator:
|
| 312 |
+
_target_: morph.flow_matching.integration.RateIntegrator
|
| 313 |
+
vocab: ${data.vocab}
|
| 314 |
+
distributions: None
|
| 315 |
+
insertion_head_mode: ${insertion_head.mode}
|
| 316 |
+
insertion_dimension: ${insertion_head.dimension}
|
| 317 |
+
insertion_num_components: ${insertion_head.num_components}
|
| 318 |
+
time_strategy: log
|
| 319 |
+
time_beta_alpha: 2.0
|
| 320 |
+
time_beta_beta: 1.0
|
| 321 |
+
num_integration_steps: 100
|
| 322 |
+
ins_noise_scale: ${data.ins_noise_scale}
|
| 323 |
+
renoise_insertions: true
|
| 324 |
+
exact_schedule_integration: true
|
| 325 |
+
del_schedule: ${data.interpolator.del_schedule}
|
| 326 |
+
ins_schedule: ${data.interpolator.ins_schedule}
|
| 327 |
+
sub_schedule: ${data.interpolator.sub_schedule}
|
| 328 |
+
sub_e_schedule: ${data.interpolator.sub_e_schedule}
|
| 329 |
+
data:
|
| 330 |
+
root_path: ${oc.env:PROJECT_ROOT}/data
|
| 331 |
+
preprocess_workers: 10
|
| 332 |
+
preprocess_chunk_size: 128
|
| 333 |
+
statistics_workers: ${data.preprocess_workers}
|
| 334 |
+
hydrogen_mode: explicit
|
| 335 |
+
cache_name: ${data.dataset_name}${if:${eq:${data.hydrogen_mode},implicit},_implicit_h,''}
|
| 336 |
+
source_dir: ${oc.env:GEOM_SOURCE_DIR,null}
|
| 337 |
+
max_atoms: 72
|
| 338 |
+
allowed_atom_symbols:
|
| 339 |
+
- H
|
| 340 |
+
- C
|
| 341 |
+
- 'N'
|
| 342 |
+
- O
|
| 343 |
+
- S
|
| 344 |
+
- F
|
| 345 |
+
- Cl
|
| 346 |
+
- Br
|
| 347 |
+
- P
|
| 348 |
+
- I
|
| 349 |
+
n_atoms_strategy: uniform
|
| 350 |
+
uniform_n_atoms_padding: 5
|
| 351 |
+
optimal_transport: equivariant
|
| 352 |
+
ins_noise_scale: 0.65
|
| 353 |
+
move_noise_scale: 0.5
|
| 354 |
+
allow_charged: true
|
| 355 |
+
preprocessing:
|
| 356 |
+
_target_: morph.dataset.core.preprocessing.Preprocessing
|
| 357 |
+
_recursive_: false
|
| 358 |
+
root: /cluster/project/krause/frankem/morph/data/geom
|
| 359 |
+
num_workers: ${data.statistics_workers}
|
| 360 |
+
train_dataset: ${data.datamodule.datasets.train}
|
| 361 |
+
atom_tokens_path: ${data.preprocessing.root}/processed/atom_tokens.txt
|
| 362 |
+
edge_tokens_path: ${data.preprocessing.root}/processed/edge_tokens.txt
|
| 363 |
+
charge_tokens_path: ${data.preprocessing.root}/processed/charge_tokens.txt
|
| 364 |
+
distributions_path: ${data.preprocessing.root}/processed/distributions.pt
|
| 365 |
+
vocab: null
|
| 366 |
+
interpolator:
|
| 367 |
+
_target_: morph.flow_matching.interpolation.Interpolator
|
| 368 |
+
distributions: null
|
| 369 |
+
optimal_transport: ${data.optimal_transport}
|
| 370 |
+
ins_noise_scale: ${data.ins_noise_scale}
|
| 371 |
+
move_noise_scale: ${data.move_noise_scale}
|
| 372 |
+
atom_type_noise_prob: 0.0
|
| 373 |
+
pre_align_iters: 2
|
| 374 |
+
c_move: 1.0
|
| 375 |
+
c_sub: 0.0
|
| 376 |
+
c_ins: 6.125
|
| 377 |
+
c_del: 6.125
|
| 378 |
+
del_schedule: ${schedule}
|
| 379 |
+
ins_schedule: ${schedule}
|
| 380 |
+
sub_schedule:
|
| 381 |
+
_target_: morph.flow_matching.schedules.SmoothstepSchedule
|
| 382 |
+
shift: 1.5
|
| 383 |
+
sub_e_schedule:
|
| 384 |
+
_target_: morph.flow_matching.schedules.SmoothstepSchedule
|
| 385 |
+
shift: 1.5
|
| 386 |
+
jitter_scale: ${data.jitter_scale}
|
| 387 |
+
insertion_assignment: ${data.insertion_assignment}
|
| 388 |
+
datamodule:
|
| 389 |
+
_target_: morph.dataset.conditional.scaffold.datamodule.ScaffoldLightningDataModule
|
| 390 |
+
vocab: ${data.vocab}
|
| 391 |
+
distributions: null
|
| 392 |
+
representation: ${representation}
|
| 393 |
+
datasets:
|
| 394 |
+
train:
|
| 395 |
+
_target_: ${data.dataset_target}
|
| 396 |
+
root: ${data.preprocessing.root}
|
| 397 |
+
preprocess_workers: ${data.preprocess_workers}
|
| 398 |
+
preprocess_chunk_size: ${data.preprocess_chunk_size}
|
| 399 |
+
hydrogen_mode: ${data.hydrogen_mode}
|
| 400 |
+
source_dir: ${data.source_dir}
|
| 401 |
+
max_atoms: ${data.max_atoms}
|
| 402 |
+
allowed_atom_symbols: ${data.allowed_atom_symbols}
|
| 403 |
+
vocab: ${data.vocab}
|
| 404 |
+
distributions: null
|
| 405 |
+
val:
|
| 406 |
+
- _target_: ${data.dataset_target}
|
| 407 |
+
root: ${data.preprocessing.root}
|
| 408 |
+
preprocess_workers: ${data.preprocess_workers}
|
| 409 |
+
preprocess_chunk_size: ${data.preprocess_chunk_size}
|
| 410 |
+
hydrogen_mode: ${data.hydrogen_mode}
|
| 411 |
+
source_dir: ${data.source_dir}
|
| 412 |
+
max_atoms: ${data.max_atoms}
|
| 413 |
+
allowed_atom_symbols: ${data.allowed_atom_symbols}
|
| 414 |
+
vocab: ${data.vocab}
|
| 415 |
+
distributions: null
|
| 416 |
+
test:
|
| 417 |
+
- _target_: ${data.dataset_target}
|
| 418 |
+
root: ${data.preprocessing.root}
|
| 419 |
+
preprocess_workers: ${data.preprocess_workers}
|
| 420 |
+
preprocess_chunk_size: ${data.preprocess_chunk_size}
|
| 421 |
+
hydrogen_mode: ${data.hydrogen_mode}
|
| 422 |
+
source_dir: ${data.source_dir}
|
| 423 |
+
max_atoms: ${data.max_atoms}
|
| 424 |
+
allowed_atom_symbols: ${data.allowed_atom_symbols}
|
| 425 |
+
vocab: ${data.vocab}
|
| 426 |
+
distributions: null
|
| 427 |
+
interpolator: ${data.interpolator}
|
| 428 |
+
rotate: ${model.rotate}
|
| 429 |
+
n_augmentations: ${model.n_augmentations}
|
| 430 |
+
num_workers:
|
| 431 |
+
train: 20
|
| 432 |
+
val: 0
|
| 433 |
+
test: 0
|
| 434 |
+
batch_size:
|
| 435 |
+
train: 320
|
| 436 |
+
val: 32
|
| 437 |
+
test: 32
|
| 438 |
+
n_atoms_strategy: ${data.n_atoms_strategy}
|
| 439 |
+
uniform_n_atoms_padding: ${data.uniform_n_atoms_padding}
|
| 440 |
+
time_dist: ${model.module.time_dist}
|
| 441 |
+
scaffold_groups_dir: ${data.scaffold_groups_dir}
|
| 442 |
+
scaffold_kind: ${data.scaffold_kind}
|
| 443 |
+
enumerate_slicings: ${data.enumerate_slicings}
|
| 444 |
+
eval_panel: ${data.eval_panel}
|
| 445 |
+
train_prefetch_factor: 1
|
| 446 |
+
dataset_name: geom
|
| 447 |
+
dataset_target: morph.dataset.unconditional.geom.FlowMatchingGEOMDataset
|
| 448 |
+
scaffold_groups_dir: ${data.preprocessing.root}/processed/scaffold_groups
|
| 449 |
+
scaffold_kind: murcko
|
| 450 |
+
jitter_scale: 0.1
|
| 451 |
+
insertion_assignment: substituent
|
| 452 |
+
enumerate_slicings: false
|
| 453 |
+
eval_panel:
|
| 454 |
+
directory: /cluster/project/krause/frankem/morph-chem/logs/training/geom-scaffold-substituent-lora-r16-20261012T095326Z/panels
|
| 455 |
+
n_scaffolds: 512
|
| 456 |
+
samples_per_scaffold: 8
|
| 457 |
+
seed: 42
|
| 458 |
+
embeddings:
|
| 459 |
+
atom_type_embedding:
|
| 460 |
+
_target_: morph.model.layers.embedding.Embedding
|
| 461 |
+
num_embeddings: ${len:${data.vocab.atom_tokens}}
|
| 462 |
+
embedding_dim: 64
|
| 463 |
+
out_dim: 128
|
| 464 |
+
edge_type_embedding:
|
| 465 |
+
_target_: morph.model.layers.embedding.Embedding
|
| 466 |
+
num_embeddings: ${len:${data.vocab.edge_tokens}}
|
| 467 |
+
embedding_dim: 64
|
| 468 |
+
out_dim: 128
|
| 469 |
+
time_embedding:
|
| 470 |
+
_target_: morph.model.layers.embedding.TimeEmbedding
|
| 471 |
+
embedding_dim: 128
|
| 472 |
+
out_dim: 128
|
| 473 |
+
node_count_embedding:
|
| 474 |
+
_target_: morph.model.layers.embedding.ExtrapolatableCountEmbedding
|
| 475 |
+
embedding_dim: 64
|
| 476 |
+
out_dim: 128
|
| 477 |
+
min_period: 128.0
|
| 478 |
+
max_period: 1024.0
|
| 479 |
+
max_count: 96.0
|
geom/scaffold_murcko_substituent_lora_r16/training_source.tar.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2ee1bf919ad17d664f0614252f43bdcf39b2474e5a1069a37429a520adb8a2f
|
| 3 |
+
size 510894
|