"""Copyright (c) Meta Platforms, Inc. and affiliates.""" from __future__ import annotations import warnings from functools import partial from typing import Any, Literal import hydra import pytorch_lightning as pl import torch from geoopt.manifolds.euclidean import Euclidean from geoopt.manifolds.product import ProductManifold from omegaconf import DictConfig from pytorch_lightning.utilities.warnings import PossibleUserWarning from torch.func import jvp, vjp from torch_geometric.data import Data from torchmetrics import MeanMetric, MinMetric from diffcsp.common.data_utils import lattices_to_params_shape from manifm.ema import EMA from manifm.model_pl import ManifoldFMLitModule, div_fn, output_and_div from manifm.solvers import projx_integrator, projx_integrator_return_last from flowmm.model.arch import CSPNet, ProjectedConjugatedCSPNet from flowmm.model.solvers import ( projx_cond_integrator_return_last, projx_integrate_xt_to_x1, ) from flowmm.model.standardize import get_affine_stats from flowmm.rfm.manifold_getter import Dims, ManifoldGetter from flowmm.rfm.manifolds.spd import SPDGivenN, SPDNonIsotropicRandom from flowmm.rfm.vmap import VMapManifolds from omegaconf import OmegaConf def output_and_div( vecfield: callable[[torch.Tensor], torch.Tensor], x: torch.Tensor, v: torch.Tensor | None = None, div_mode: Literal["exact", "rademacher"] = "exact", ) -> tuple[torch.Tensor, torch.Tensor]: if div_mode == "exact": dx = vecfield(x) div = div_fn(vecfield)(x) else: dx, vjpfunc = vjp(vecfield, x) vJ = vjpfunc(v)[0] div = torch.sum(vJ * v, dim=-1) return dx, div class MaterialsRFMLitModule(ManifoldFMLitModule): def __init__(self, cfg: DictConfig): pl.LightningModule.__init__(self) self.cfg = cfg self.save_hyperparameters() self.manifold_getter = ManifoldGetter( atom_type_manifold=cfg.model.manifold_getter.atom_type_manifold, coord_manifold=cfg.model.manifold_getter.coord_manifold, lattice_manifold=cfg.model.manifold_getter.lattice_manifold, dataset=cfg.data.dataset_name, analog_bits_scale=cfg.model.manifold_getter.get("analog_bits_scale", None), length_inner_coef=cfg.model.manifold_getter.get("length_inner_coef", None), ) if "null" in cfg.model.manifold_getter.atom_type_manifold: cost_type = 0.0 cost_cross_ent = 0.0 warnings.warn( f"since {cfg.model.manifold_getter.atom_type_manifold=}, set {cost_type=} & {cost_cross_ent=}", PossibleUserWarning, ) else: cost_type = cfg.model.cost_type cost_cross_ent = cfg.model.cost_cross_ent self.costs = { "loss_a": cost_type, "loss_f": cfg.model.cost_coord, "loss_l": cfg.model.cost_lattice, "loss_ce": cost_cross_ent, } if cfg.model.affine_combine_costs: total_cost = sum([v for v in self.costs.values()]) self.costs = {k: v / total_cost for k, v in self.costs.items()} model: CSPNet = hydra.utils.instantiate( self.cfg.vectorfield, _convert_="partial" ) # Model of the vector field. cspnet = ProjectedConjugatedCSPNet( cspnet=model, manifold_getter=self.manifold_getter, lattice_affine_stats=get_affine_stats( cfg.data.dataset_name, self.manifold_getter.lattice_manifold ), coord_affine_stats=get_affine_stats( cfg.data.dataset_name, self.manifold_getter.coord_manifold ), ) if cfg.optim.get("ema_decay", None) is None: self.model = cspnet else: self.model = EMA( cspnet, cfg.optim.ema_decay, ) # use separate metric instance for train, val and test step # to ensure a proper reduction over the epoch self.train_metrics = { "loss": MeanMetric(), "loss_a": MeanMetric(), "loss_f": MeanMetric(), "loss_l": MeanMetric(), "loss_ce": MeanMetric(), "unscaled/loss_a": MeanMetric(), "unscaled/loss_f": MeanMetric(), "unscaled/loss_l": MeanMetric(), "unscaled/loss_ce": MeanMetric(), } self.val_metrics = { "loss": MeanMetric(), "loss_a": MeanMetric(), "loss_f": MeanMetric(), "loss_l": MeanMetric(), "loss_ce": MeanMetric(), "unscaled/loss_a": MeanMetric(), "unscaled/loss_f": MeanMetric(), "unscaled/loss_l": MeanMetric(), "unscaled/loss_ce": MeanMetric(), } self.test_metrics = { "loss": MeanMetric(), "loss_a": MeanMetric(), "loss_f": MeanMetric(), "loss_l": MeanMetric(), "loss_ce": MeanMetric(), "unscaled/loss_a": MeanMetric(), "unscaled/loss_f": MeanMetric(), "unscaled/loss_l": MeanMetric(), "unscaled/loss_ce": MeanMetric(), "nll": MeanMetric(), } # for logging best so far validation accuracy self.val_metrics_best = { "loss": MinMetric(), "loss_a": MinMetric(), "loss_f": MinMetric(), "loss_l": MinMetric(), "loss_ce": MinMetric(), "unscaled/loss_a": MinMetric(), "unscaled/loss_f": MinMetric(), "unscaled/loss_l": MinMetric(), "unscaled/loss_ce": MinMetric(), } if self.cfg.val.compute_nll: self.val_metrics["nll"] = MeanMetric() self.val_metrics_best["nll"] = MinMetric() def on_fit_start(self): super().on_fit_start() for stage in ["train", "val", "test"]: metrics = getattr(self, f"{stage}_metrics") for v in metrics.values(): v = v.to(self.device) @staticmethod def _annealing_schedule( t: torch.Tensor, slope: float, intercept: float ) -> torch.Tensor: return slope * torch.nn.functional.relu(t - intercept) + 1.0 @torch.no_grad() def sample( self, batch: Data, x0: torch.Tensor = None, num_steps: int = 1_000, entire_traj: bool = False, ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: ( x1, manifold, a_manifold, f_manifold, l_manifold, dims, mask_a_or_f, ) = self.manifold_getter( batch.batch, batch.atom_types, batch.frac_coords, batch.lengths, batch.angles, split_manifold=True, ) if x0 is None: if self.cfg.base_distribution_from_data: x0 = self.manifold_getter( batch.batch, batch.atom_types_initial, batch.frac_coords_initial, batch.lengths_initial, batch.angles_initial, split_manifold=True, )[0] else: x0 = manifold.random(*x1.shape, dtype=x1.dtype, device=x1.device) else: x0 = x0.to(x1) return self.finish_sampling( x0=x0, manifold=manifold, a_manifold=a_manifold, f_manifold=f_manifold, l_manifold=l_manifold, dims=dims, num_atoms=batch.num_atoms, node2graph=batch.batch, mask_a_or_f=mask_a_or_f, num_steps=num_steps, entire_traj=entire_traj, ) @torch.no_grad() def gen_sample( self, node2graph: torch.LongTensor, dim_coords: int, x0: torch.Tensor = None, num_steps: int = 1_000, entire_traj: bool = False, ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: ( shape, manifold, a_manifold, f_manifold, l_manifold, dims, mask_a_or_f, ) = self.manifold_getter.from_empty_batch( node2graph, dim_coords, split_manifold=True ) num_atoms = self.manifold_getter._get_num_atoms(mask_a_or_f) if x0 is None: assert not self.cfg.base_distribution_from_data, "Need to sample from the base distribution" x0 = manifold.random(*shape, device=node2graph.device) else: x0 = x0.to(device=node2graph.device) return self.finish_sampling( x0=x0, manifold=manifold, a_manifold=a_manifold, f_manifold=f_manifold, l_manifold=l_manifold, dims=dims, num_atoms=num_atoms, node2graph=node2graph, mask_a_or_f=mask_a_or_f, num_steps=num_steps, entire_traj=entire_traj, ) @torch.no_grad() def pred_sample( self, node2graph: torch.LongTensor, atom_types: torch.LongTensor, dim_coords: int, x0: torch.Tensor = None, num_steps: int = 1_000, entire_traj: bool = False, ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: ( shape, manifold, a_manifold, f_manifold, l_manifold, dims, mask_a_or_f, ) = self.manifold_getter.from_only_atom_types( node2graph, atom_types, dim_coords, split_manifold=True ) num_atoms = self.manifold_getter._get_num_atoms(mask_a_or_f) if x0 is None: assert not self.cfg.base_distribution_from_data, "Need to sample from the base distribution" x0 = manifold.random(*shape, device=node2graph.device) else: x0 = x0.to(device=node2graph.device) return self.finish_sampling( x0=x0, manifold=manifold, a_manifold=a_manifold, f_manifold=f_manifold, l_manifold=l_manifold, dims=dims, num_atoms=num_atoms, node2graph=node2graph, mask_a_or_f=mask_a_or_f, num_steps=num_steps, entire_traj=entire_traj, ) @torch.no_grad() def finish_sampling( self, x0: torch.Tensor, manifold: VMapManifolds, a_manifold: VMapManifolds, f_manifold: VMapManifolds, l_manifold: VMapManifolds, dims: Dims, num_atoms: torch.LongTensor, node2graph: torch.LongTensor, # aka batch.batch mask_a_or_f: torch.BoolTensor, num_steps: int, entire_traj: bool, ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: vecfield = partial( self.vecfield, num_atoms=num_atoms, node2graph=node2graph, dims=dims, mask_a_or_f=mask_a_or_f, ) compute_traj_velo_norms = self.cfg.integrate.get( "compute_traj_velo_norms", False ) c = self.cfg.integrate.get("inference_anneal_slope", 0.0) b = self.cfg.integrate.get("inference_anneal_offset", 0.0) anneal_types = self.cfg.integrate.get("inference_anneal_types", False) anneal_coords = self.cfg.integrate.get("inference_anneal_coords", True) anneal_lattice = self.cfg.integrate.get("inference_anneal_lattice", False) print( "Anneal Types:", anneal_types, "Anneal Coords:", anneal_coords, "Anneal Lattice:", anneal_lattice, ) def scheduled_fn_to_integrate( t: torch.Tensor, x: torch.Tensor, cond: torch.Tensor | None = None ) -> torch.Tensor: anneal_factor = self._annealing_schedule(t, c, b) out = vecfield( t=torch.atleast_2d(t), x=torch.atleast_2d(x), manifold=manifold, cond=torch.atleast_2d(cond) if isinstance(cond, torch.Tensor) else cond, ) if anneal_types: out[:, : dims.a].mul_(anneal_factor) if anneal_coords: out[:, dims.a : -dims.l].mul_(anneal_factor) if anneal_lattice: out[:, -dims.l :].mul_(anneal_factor) return out if self.cfg.model.get("self_cond", False): x1 = projx_cond_integrator_return_last( manifold, scheduled_fn_to_integrate, x0, t=torch.linspace(0, 1, num_steps + 1).to(x0.device), method=self.cfg.integrate.get("method", "euler"), projx=True, local_coords=False, pbar=True, ) return x1 elif entire_traj or compute_traj_velo_norms: xs, vs = projx_integrator( manifold, scheduled_fn_to_integrate, x0, t=torch.linspace(0, 1, num_steps + 1).to(x0.device), method=self.cfg.integrate.get("method", "euler"), projx=True, pbar=True, ) else: x1 = projx_integrator_return_last( manifold, scheduled_fn_to_integrate, x0, t=torch.linspace(0, 1, num_steps + 1).to(x0.device), method=self.cfg.integrate.get("method", "euler"), projx=True, local_coords=False, pbar=True, ) return x1 if compute_traj_velo_norms: s = 0 e = dims.a norm_a = a_manifold.inner( xs[..., s:e], vs[..., s:e], vs[..., s:e], data_in_dim=1 ) s = e e += dims.f norm_f = f_manifold.inner( xs[..., s:e], vs[..., s:e], vs[..., s:e], data_in_dim=1 ) s = e e += dims.l norm_l = l_manifold.inner( xs[..., s:e], vs[..., s:e], vs[..., s:e], data_in_dim=1 ) if entire_traj and compute_traj_velo_norms: return xs, norm_a, norm_f, norm_l elif entire_traj and not compute_traj_velo_norms: return xs elif not entire_traj and compute_traj_velo_norms: return xs[0], norm_a, norm_f, norm_l else: # this should happen due to logic above return xs[0] @torch.no_grad() def compute_exact_loglikelihood( self, batch: Data, stage: str, t1: float = 1.0, return_projx_error: bool = False, num_steps: int = 1_000, ): """Computes the negative log-likelihood of a batch of data.""" x1, manifold, dims, mask_a_or_f = self.manifold_getter( batch.batch, batch.atom_types, batch.frac_coords, batch.lengths, batch.angles, split_manifold=False, ) dim = sum(dims) nfe = [0] div_mode = self.cfg.integrate.get( "div_mode", "rademacher" ) # alternative: exact v = None if div_mode == "rademacher": v = torch.randint(low=0, high=2, size=x1.shape).to(x1) * 2 - 1 vecfield = partial( self.vecfield, num_atoms=batch.num_atoms, node2graph=batch.batch, dims=dims, mask_a_or_f=mask_a_or_f, ) def odefunc(t, tensor): nfe[0] += 1 t = t.to(tensor) x = tensor[..., :dim] l_vecfield = lambda x: vecfield( t=torch.atleast_2d(t), x=torch.atleast_2d(x), manifold=manifold ) dx, div = output_and_div(l_vecfield, x, v=v, div_mode=div_mode) if hasattr(manifold, "logdetG"): corr = jvp(manifold.logdetG, (x,), (dx,))[1] div = div + 0.5 * corr.to(div) div = div.reshape(-1, 1) del t, x return torch.cat([dx, div], dim=-1) # Solve ODE on the product manifold of data manifold x euclidean. prod_manis = [ ProductManifold((m, dim), (Euclidean(), 1)) for m in manifold.manifolds ] product_man = VMapManifolds(prod_manis) state1 = torch.cat([x1, torch.zeros_like(x1[..., :1])], dim=-1) with torch.no_grad(): state0 = projx_integrator_return_last( product_man, odefunc, state1, t=torch.linspace(t1, 0, num_steps + 1).to(x1), method=self.cfg.integrate.get("method", "euler"), projx=True, local_coords=False, pbar=True, ) # log number of function evaluations # self.log( # f"{stage}/nfe", # float(nfe[0]), # prog_bar=True, # logger=True, # batch_size=batch.batch_size, # ) x0, logdetjac = state0[..., :dim], state0[..., -1] x0_ = x0 x0 = manifold.projx(x0) # log how close the final solution is to the manifold. integ_error = (x0[..., :dim] - x0_[..., :dim]).abs().max() # self.log(f"{stage}/integ_error", integ_error, batch_size=batch.batch_size) logp0 = manifold.base_logprob(x0) logp1 = logp0 + logdetjac if self.cfg.get("normalize_loglik", True): logp1 = logp1 / batch.num_atoms # Mask out those that left the manifold masked_logp1 = logp1 if isinstance(manifold, (SPDGivenN, SPDNonIsotropicRandom)): mask = integ_error < 1e-5 # self.log( # f"{stage}/frac_within_manifold", # mask.sum() / mask.nelement(), # batch_size=batch.batch_size, # ) masked_logp1 = logp1[mask] if return_projx_error: return logp1, integ_error else: return masked_logp1 def loss_fn(self, batch: Data, *args, **kwargs) -> dict[str, torch.Tensor]: return self.rfm_loss_fn(batch, *args, **kwargs) def rfm_loss_fn( self, batch: Data, x0: torch.Tensor = None ) -> dict[str, torch.Tensor]: ( x1, manifold, a_manifold, f_manifold, l_manifold, dims, mask_a_or_f, ) = self.manifold_getter( batch.batch, batch.atom_types, batch.frac_coords, batch.lengths, batch.angles, split_manifold=True, ) if x0 is None: if self.cfg.base_distribution_from_data: x0 = self.manifold_getter( batch.batch, batch.atom_types_initial, batch.frac_coords_initial, batch.lengths_initial, batch.angles_initial, split_manifold=True, )[0] else: x0 = manifold.random(*x1.shape, dtype=x1.dtype, device=x1.device) vecfield = partial( self.vecfield, num_atoms=batch.num_atoms, node2graph=batch.batch, dims=dims, mask_a_or_f=mask_a_or_f, ) N = x1.shape[0] t = torch.rand(N, dtype=x1.dtype, device=x1.device).reshape(-1, 1) x1 = manifold.projx(x1) x_t, u_t = manifold.cond_u(x0, x1, t) x_t = x_t.reshape(N, x0.shape[-1]) u_t = u_t.reshape(N, x0.shape[-1]) # this projects out the mean from the tangent vectors # our model cannot predict it, so keeping it in inflates the loss u_t = manifold.proju(x_t, u_t) cond = None if self.cfg.model.self_cond: with torch.no_grad(): if torch.rand((1)) < 0.5: cond = projx_integrate_xt_to_x1( manifold, lambda t, x: vecfield( t=torch.atleast_2d(t), x=torch.atleast_2d(x), manifold=manifold, ), x_t, t, ).detach_() u_t_pred = vecfield(t=t, x=x_t, manifold=manifold, cond=cond) diff = u_t_pred - u_t max_num_atoms = mask_a_or_f.size(-1) dim_a_per_atom = dims.a / max_num_atoms dim_f_per_atom = dims.f / max_num_atoms if self.costs["loss_ce"] > 0.0: x1_pred_t = projx_integrate_xt_to_x1( manifold, None, # already computed velocity x_t, t, u_t_pred, ) a1_pred_t = x1_pred_t[:, : dims.a].reshape(N * max_num_atoms, -1) a1 = x1[:, : dims.a].reshape(N * max_num_atoms, -1) if self.manifold_getter.atom_type_manifold == "analog_bits": loss_ce = torch.einsum("bi,bi->b", a1_pred_t, a1).reshape( N, max_num_atoms ) loss_ce = torch.nn.functional.logsigmoid(loss_ce) * mask_a_or_f elif self.manifold_getter.atom_type_manifold == "simplex": a1 = self.manifold_getter._inverse_atomic_one_hot(a1) loss_ce = torch.nn.functional.cross_entropy( a1_pred_t, a1, reduce=False, ).reshape(N, max_num_atoms) loss_ce = loss_ce * mask_a_or_f else: raise ValueError( f"{self.manifold_getter.atom_type_manifold=} cannot do cross entropy" ) loss_ce = (loss_ce.sum(dim=-1) / mask_a_or_f.sum(dim=-1)).mean() else: loss_ce = torch.Tensor([0.0]).squeeze().to(diff) s = 0 e = dims.a loss_a = ( a_manifold.inner(x_t[:, s:e], diff[:, s:e], diff[:, s:e]).mean() / dim_a_per_atom ) # per dim, already per atom s = e e += dims.f loss_f = ( f_manifold.inner(x_t[:, s:e], diff[:, s:e], diff[:, s:e]).mean() / dim_f_per_atom ) # per dim, already per atom s = e e += dims.l loss_l = ( l_manifold.inner(x_t[:, s:e], diff[:, s:e], diff[:, s:e]).mean() / dims.l ) # per dim loss = ( self.costs["loss_a"] * loss_a + self.costs["loss_f"] * loss_f + self.costs["loss_l"] * loss_l + self.costs["loss_ce"] * loss_ce ) return { "loss": loss, "loss_a": self.costs["loss_a"] * loss_a, "loss_f": self.costs["loss_f"] * loss_f, "loss_l": self.costs["loss_l"] * loss_l, "loss_ce": self.costs["loss_ce"] * loss_ce, "unscaled/loss_a": loss_a, "unscaled/loss_f": loss_f, "unscaled/loss_l": loss_l, "unscaled/loss_ce": loss_ce, } def training_step(self, batch: Data, batch_idx: int): loss_dict = self.loss_fn(batch) if torch.isfinite(loss_dict["loss"]): # log train metrics for k, v in loss_dict.items(): self.log( f"train/{k}", v, batch_size=batch.batch_size, sync_dist = True ) self.train_metrics[k].update(v) else: # skip step if loss is NaN. print(f"Skipping iteration because loss is {loss_dict['loss'].item()}.") return None # we can return here dict with any tensors # and then read it in some callback or in `training_epoch_end()` below # remember to always return loss from `training_step()` or else backpropagation will fail! return loss_dict def training_epoch_end(self, outputs: list[Any]): # `outputs` is a list of dicts returned from `training_step()` for train_metric in self.train_metrics.values(): train_metric.reset() def shared_eval_step( self, batch: Data, batch_idx: int, stage: Literal["val", "test"], compute_loss: bool, compute_nll: bool, ) -> dict[str, torch.Tensor]: if stage not in ["val", "test"]: raise ValueError("stage must be 'val' or 'test'.") metrics = getattr(self, f"{stage}_metrics") out = {} if compute_loss: loss_dict = self.loss_fn(batch) for k, v in loss_dict.items(): self.log( f"{stage}/{k}", v, on_epoch=True, prog_bar=True, batch_size=batch.batch_size, sync_dist = True ) metrics[k].update(v) out.update(loss_dict) if compute_nll: nll_dict = {} logprob = self.compute_exact_loglikelihood( batch, stage, num_steps=self.cfg.integrate.get("num_steps", 1_000), ) nll = -logprob.mean() nll_dict[f"{stage}/nll"] = nll nll_dict[f"{stage}/nll_num_steps"] = self.cfg.integrate.num_steps # self.log( # f"{stage}/nll", # nll, # prog_bar=True, # batch_size=batch.batch_size, # ) # self.log( # f"{stage}/nll_num_steps", # self.cfg.integrate.num_steps, # prog_bar=True, # batch_size=batch.batch_size, # ) self.logger.log_metrics(nll_dict, step=self.global_step) metrics["nll"].update(nll) out.update(nll_dict) return out def compute_reconstruction( self, batch: Data, num_steps: int = 1_000, ) -> dict[str, torch.Tensor | Data]: *_, dims, mask_a_or_f = self.manifold_getter( batch.batch, batch.atom_types, batch.frac_coords, batch.lengths, batch.angles, split_manifold=False, ) if self.cfg.base_distribution_from_data: x0 = self.manifold_getter( batch.batch, batch.atom_types_initial, batch.frac_coords_initial, batch.lengths_initial, batch.angles_initial, split_manifold=True, )[0] else: x0 = None if self.cfg.integrate.get("compute_traj_velo_norms", False): recon, norms_a, norms_f, norms_l = self.sample( batch, num_steps=num_steps, x0=x0 ) norms = {"norms_a": norms_a, "norms_f": norms_f, "norms_l": norms_l} else: recon = self.sample(batch, num_steps=num_steps, x0=x0) norms = {} atom_types, frac_coords, lattices = self.manifold_getter.flatrep_to_crystal( recon, dims, mask_a_or_f ) lengths, angles = lattices_to_params_shape(lattices) out = { "atom_types": atom_types, "frac_coords": frac_coords, "lattices": lattices, "lengths": lengths, "angles": angles, "num_atoms": batch.num_atoms, "input_data_batch": batch, } out.update(norms) return out def compute_recon_trajectory( self, batch: Data, num_steps: int = 1_000, ) -> dict[str, torch.Tensor | Data]: *_, dims, mask_a_or_f = self.manifold_getter( batch.batch, batch.atom_types, batch.frac_coords, batch.lengths, batch.angles, split_manifold=False, ) if self.cfg.integrate.get("compute_traj_velo_norms", False): recon, norms_a, norms_f, norms_l = self.sample( batch, num_steps=num_steps, entire_traj=True ) norms = {"norms_a": norms_a, "norms_f": norms_f, "norms_l": norms_l} else: recon = self.sample(batch, num_steps=num_steps, entire_traj=True) norms = {} atom_types, frac_coords, lattices = [], [], [] lengths, angles = [], [] for recon_step in recon: a, f, l = self.manifold_getter.flatrep_to_crystal( recon_step, dims, mask_a_or_f ) lns, angs = lattices_to_params_shape(l) atom_types.append(a) frac_coords.append(f) lattices.append(l) lengths.append(lns) angles.append(angs) out = { "atom_types": torch.stack(atom_types, dim=0), "frac_coords": torch.stack(frac_coords, dim=0), "lattices": torch.stack(lattices, dim=0), "lengths": torch.stack(lengths, dim=0), "angles": torch.stack(angles, dim=0), "num_atoms": batch.num_atoms, "input_data_batch": batch, } out.update(norms) return out def compute_generation( self, batch: Data, dim_coords: int = 3, num_steps: int = 1_000, ) -> dict[str, torch.Tensor | Data]: *_, dims, mask_a_or_f = self.manifold_getter.from_empty_batch( batch.batch, dim_coords, split_manifold=False ) if self.cfg.base_distribution_from_data: x0 = self.manifold_getter( batch.batch, batch.atom_types_initial, batch.frac_coords_initial, batch.lengths_initial, batch.angles_initial, split_manifold=True, )[0] else: x0 = None if self.cfg.integrate.get("compute_traj_velo_norms", False): recon, norms_a, norms_f, norms_l = self.gen_sample( batch.batch, dim_coords, num_steps=num_steps, x0=x0 ) norms = {"norms_a": norms_a, "norms_f": norms_f, "norms_l": norms_l} else: recon = self.gen_sample( batch.batch, dim_coords, num_steps=num_steps, x0=x0 ) norms = {} atom_types, frac_coords, lattices = self.manifold_getter.flatrep_to_crystal( recon, dims, mask_a_or_f ) lengths, angles = lattices_to_params_shape(lattices) out = { "atom_types": atom_types, "frac_coords": frac_coords, "lattices": lattices, "lengths": lengths, "angles": angles, "num_atoms": batch.num_atoms, "input_data_batch": batch, } out.update(norms) return out def compute_gen_trajectory( self, batch: Data, dim_coords: int = 3, num_steps: int = 1_000, ) -> dict[str, torch.Tensor | Data]: *_, dims, mask_a_or_f = self.manifold_getter.from_empty_batch( batch.batch, dim_coords, split_manifold=False ) if self.cfg.integrate.get("compute_traj_velo_norms", False): recon, norms_a, norms_f, norms_l = self.gen_sample( batch.batch, dim_coords, num_steps=num_steps, entire_traj=True, ) norms = {"norms_a": norms_a, "norms_f": norms_f, "norms_l": norms_l} else: recon = self.gen_sample( batch.batch, dim_coords, num_steps=num_steps, entire_traj=True ) norms = {} atom_types, frac_coords, lattices = [], [], [] lengths, angles = [], [] for recon_step in recon: a, f, l = self.manifold_getter.flatrep_to_crystal( recon_step, dims, mask_a_or_f ) lns, angs = lattices_to_params_shape(l) atom_types.append(a) frac_coords.append(f) lattices.append(l) lengths.append(lns) angles.append(angs) out = { "atom_types": torch.stack(atom_types, dim=0), "frac_coords": torch.stack(frac_coords, dim=0), "lattices": torch.stack(lattices, dim=0), "lengths": torch.stack(lengths, dim=0), "angles": torch.stack(angles, dim=0), "num_atoms": batch.num_atoms, "input_data_batch": batch, } out.update(norms) return out def compute_prediction( self, batch: Data, dim_coords: int = 3, num_steps: int = 1_000, ) -> dict[str, torch.Tensor | Data]: *_, dims, mask_a_or_f = self.manifold_getter.from_only_atom_types( batch.batch, batch.atom_types, dim_coords, split_manifold=False ) if self.cfg.integrate.get("compute_traj_velo_norms", False): recon, norms_a, norms_f, norms_l = self.pred_sample( batch.batch, batch.atom_types, dim_coords, num_steps=num_steps ) norms = {"norms_a": norms_a, "norms_f": norms_f, "norms_l": norms_l} else: recon = self.pred_sample( batch.batch, batch.atom_types, dim_coords, num_steps=num_steps ) norms = {} atom_types, frac_coords, lattices = self.manifold_getter.flatrep_to_crystal( recon, dims, mask_a_or_f ) lengths, angles = lattices_to_params_shape(lattices) out = { "atom_types": atom_types, "frac_coords": frac_coords, "lattices": lattices, "lengths": lengths, "angles": angles, "num_atoms": batch.num_atoms, "input_data_batch": batch, } out.update(norms) return out def validation_step(self, batch: Data, batch_idx: int): return self.shared_eval_step( batch, batch_idx, stage="val", compute_loss=True, compute_nll=self.cfg.val.compute_nll, ) def validation_epoch_end(self, outputs: list[Any]): out = {} for key, val_metric in self.val_metrics.items(): val_metric_value = ( val_metric.compute() ) # get val accuracy from current epoch val_metric_best = self.val_metrics_best[key] val_metric_best.update(val_metric_value) self.log( f"val/best/{key}", val_metric_best.compute(), on_epoch=True, prog_bar=True, sync_dist = True ) val_metric.reset() out[key] = val_metric_value return out def test_step(self, batch: Any, batch_idx: int): return self.shared_eval_step( batch, batch_idx, stage="test", compute_loss=self.cfg.test.get("compute_loss", False), compute_nll=self.cfg.test.get("compute_nll", False), ) def test_epoch_end(self, outputs: list[Any]): for test_metric in self.test_metrics.values(): test_metric.reset() def predict_step(self, batch: Any, batch_idx: int): if not hasattr(batch, "frac_coords"): if "null" in self.cfg.model.manifold_getter.atom_type_manifold: if self.cfg.integrate.get("entire_traj", False): raise NotImplementedError() else: return self.compute_prediction( batch, dim_coords=self.cfg.data.get("dim_coords", 3), num_steps=self.cfg.integrate.get("num_steps", 1_000), ) else: if self.cfg.integrate.get("entire_traj", False): return self.compute_gen_trajectory( batch, dim_coords=self.cfg.data.get("dim_coords", 3), num_steps=self.cfg.integrate.get("num_steps", 1_000), ) else: return self.compute_generation( batch, dim_coords=self.cfg.data.get("dim_coords", 3), num_steps=self.cfg.integrate.get("num_steps", 1_000), ) else: # not generating or predicting new structures if self.cfg.integrate.get("entire_traj", False): return self.compute_recon_trajectory( batch, num_steps=self.cfg.integrate.get("num_steps", 1_000), ) else: return self.compute_reconstruction( batch, num_steps=self.cfg.integrate.get("num_steps", 1_000), ) def configure_optimizers(self): if not self.cfg.optim.optimizer.lr_diff: OmegaConf.set_struct(self.cfg.optim.optimizer, False) # 禁用结构模式 self.cfg.optim.optimizer.pop("lr_diff") self.cfg.optim.optimizer.pop("lr_backbone") self.cfg.optim.optimizer.pop("lr_head") OmegaConf.set_struct(self.cfg.optim.optimizer, True) # 可选:重新启用结构模式 optimizer = hydra.utils.instantiate( self.cfg.optim.optimizer, params=self.parameters(), _convert_="partial", ) else: backbone_param = [] head_param = [] for name, param in self.named_parameters(): if "out" in name: head_param.append(param) else: backbone_param.append(param) param_groups = [ {"params": backbone_param, "lr": self.cfg.optim.optimizer.lr_backbone}, {"params": head_param, "lr": self.cfg.optim.optimizer.lr_head} ] optimizer = torch.optim.AdamW( params = param_groups, weight_decay = self.cfg.optim.optimizer.weight_decay ) if self.cfg.optim.get("lr_scheduler", None) is not None: lr_scheduler = hydra.utils.instantiate( self.cfg.optim.lr_scheduler, optimizer, ) if isinstance(lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): return { "optimizer": optimizer, "lr_scheduler": { "scheduler": lr_scheduler, "interval": "epoch", "monitor": self.cfg.optim.monitor, "frequency": self.cfg.optim.frequency, }, } elif isinstance(lr_scheduler, torch.optim.lr_scheduler.CosineAnnealingLR): return { "optimizer": optimizer, "lr_scheduler": { "scheduler": lr_scheduler, "interval": self.cfg.optim.interval, }, } else: raise NotImplementedError("unsuported lr_scheduler") else: return {"optimizer": optimizer}