IMP logo
IMP Reference Guide  develop.0e3808d870,2026/10/23
The Integrative Modeling Platform
jax.py
1 """@namespace IMP.jax
2  @brief Support for the JAX Python library.
3 
4  IMP currently has rudimentary support for running on a graphics
5  processing unit (GPU) or similar systems such as
6  Tensor Processing Units (TPUs). This support uses the
7  [JAX](https://docs.jax.dev/) Python library.
8 """
9 
10 import numpy as np
11 import jax.numpy as jnp
12 import jax.random
13 import jax.tree_util
14 import IMP
15 
16 
17 class Space:
18  """The space in which restraints are evaluated. See FreeSpace for the
19  default unbounded space, or PeriodicSpace for a space that implements
20  periodic boundary conditions."""
21 
22  def distance(dr):
23  """If given an array of particle-particle vectors, return an array
24  of distances. If given a single particle-particle vector, return
25  a single distance."""
26  pass
27 
28  def shift(r, dr):
29  """Shift r by dr and return new r"""
30  pass
31 
32  def shift_indexes(r, indexes, dr):
33  """Modify r[indexes] in place by adding dr"""
34  pass
35 
36 
37 class FreeSpace(Space):
38  """An unbounded space with no periodic boundary conditions."""
39 
40  @staticmethod
41  def distance(dr):
42  return jnp.linalg.norm(dr, axis=-1)
43 
44  @staticmethod
45  def shift(r, dr):
46  return r + dr
47 
48  @staticmethod
49  def shift_indexes(r, indexes, dr):
50  return r.at[..., indexes, :].add(dr)
51 
52 
54  """A space with periodic boundary conditions.
55 
56  @param side A 3D vector of the periodic boundary box dimensions.
57  """
58 
59  def __init__(self, side):
60  self.side = jnp.asarray(side)
61 
62  def distance(self, dr):
63  p_dr = jnp.mod(dr + self.side * 0.5, self.side) - 0.5 * self.side
64  return jnp.linalg.norm(p_dr, axis=-1)
65 
66  def shift(self, r, dr):
67  return jnp.mod(r + dr, self.side)
68 
69  def shift_indexes(self, r, indexes, dr):
70  newr = jnp.mod(r[..., indexes, :] + dr, self.side)
71  return r.at[..., indexes, :].set(newr)
72 
73 
75  """Get a new JAX random key seeded from IMP's random number generator"""
76  return jax.random.key(IMP.random_number_generator())
77 
78 
79 class _Remap:
80  """A mapping from full particle indexes to and from the compact array.
81  This is used in _CompactArray, below. It is hashed by identity so
82  that it (unlike the arrays it contains) can be used as pytree aux data.
83 
84  @param full_view A NumPy view of the entire IMP Model.
85  @param indexes A NumPy array of indexes of the particles that
86  have the attribute.
87  @param mapping A NumPy array that maps original Particle indexes
88  to indexes into the `data` array.
89  """
90  def __init__(self, full_view, indexes, mapping):
91  self.full_view, self.indexes = full_view, indexes
92  self.mapping = mapping
93 
94 
95 @jax.tree_util.register_pytree_node_class
96 class _CompactArray:
97  """Access an IMP Model Float attribute as a compacted or sparse array.
98  An IMP attribute array (returned by Model.get_numpy()) can be sparsely
99  populated. Compact it down to a flat array of only the particles that
100  have the attribute. Original Particle indexes are mapped to indexes
101  into the compacted array at JAX trace time. This should make JAX code
102  more performant since only the compacted array needs to be transferred
103  to and from the GPU.
104 
105  @param data Compact array of only the used particles.
106  @param remap A _Remap object that maps full particle indexes to and
107  from the compact array.
108  """
109 
110  def __init__(self, data, remap):
111  self.data, self.remap = data, remap
112 
113  shape = property(lambda s: s.remap.full_view.shape)
114 
115  @classmethod
116  def from_model(cls, m, fk):
117  """Create a new CompactArray for the given FloatKey `fk` in the
118  given IMP Model `m`."""
119  full_view = m.get_numpy(fk)
120  # IMP uses infinity to represent particles without the attribute
121  if full_view.ndim == 2:
122  # If we are using multiple attribute sets, get indexes
123  # over all sets
124  indexes = np.unique(np.nonzero(full_view != np.inf)[-1])
125  else:
126  indexes = np.nonzero(full_view != np.inf)[0]
127  # Any particle not in `indexes` is mapped to the last element,
128  # which is inf (just as in the original full view)
129  len_map = full_view.shape[-1]
130  mapping = np.full(len_map, len_map + 1, dtype=np.int32)
131  mapping[indexes] = np.arange(len(indexes), dtype=np.int32)
132  remap = _Remap(full_view, indexes, mapping)
133  infpad = np.array([np.inf])
134  if full_view.ndim == 2:
135  infpad = np.broadcast_to(infpad, (full_view.shape[0], 1))
136  concat = np.concatenate((full_view[..., indexes], infpad), axis=-1)
137  return cls(concat, remap)
138 
139  def tree_flatten(self):
140  # Convert to JAX. Only `data` is sent to the device; everything
141  # else is static (aux data)
142  return (self.data,), self.remap
143 
144  @classmethod
145  def tree_unflatten(cls, aux_data, children):
146  return cls(data=children[0], remap=aux_data)
147 
148  def _map(self, idx):
149  return self.remap.mapping[idx]
150 
151  def _full_map(self, idx):
152  if isinstance(idx, tuple):
153  return idx[:-1] + (self._map(idx[-1]),)
154  else:
155  return self._map(idx)
156 
157  def __getitem__(self, idx):
158  """Lookup by particle index"""
159  return self.data[self._full_map(idx)]
160 
161  @property
162  def at(self):
163  """Like jax.Array.at; allow in-place array modification"""
164  return _AtCompactArray(self)
165 
166  def sync(self):
167  """Copy the JAX data back to the IMP Model"""
168  self.remap.full_view[self.remap.indexes] = self.data[:-1]
169 
170 
171 class _AtCompactArray:
172  """Helper class for _CompactArray.at"""
173  def __init__(self, arr):
174  self.arr = arr
175 
176  def __getitem__(self, idx):
177  return _AtIdxCompactArray(self.arr, self.arr._full_map(idx))
178 
179 
180 class _AtIdxCompactArray:
181  """Helper class for _CompactArray.at[idx]"""
182  def __init__(self, arr, rows):
183  self.arr, self.rows = arr, rows
184 
185  def _new(self, data):
186  return _CompactArray(data, self.arr.remap)
187 
188  def set(self, v):
189  return self._new(self.arr.data.at[self.rows].set(v))
190 
191  def add(self, v):
192  return self._new(self.arr.data.at[self.rows].add(v))
193 
194 
195 @jax.tree_util.register_pytree_node_class
196 class _JAXModel:
197  """JAX equivalent of an IMP Model.
198  This acts like a regular Python dict.
199  @see IMP._jax_util._get_jax_model
200  """
201  def __init__(self, multi_attr_keys=None, **kwargs):
202  self._d = dict(**kwargs)
203  self.multi_attr_keys = frozenset(multi_attr_keys or ())
204 
205  def __getitem__(self, k):
206  return self._d[k]
207 
208  def __setitem__(self, k, v):
209  self._d[k] = v
210 
211  def __contains__(self, k):
212  return k in self._d
213 
214  def keys(self):
215  return self._d.keys()
216 
217  def items(self):
218  return self._d.items()
219 
220  def values(self):
221  return self._d.values()
222 
223  def copy(self):
224  m = _JAXModel(multi_attr_keys=self.multi_attr_keys)
225  m._d = self._d.copy()
226  return m
227 
228  def __str__(self):
229  return str(self._d)
230 
231  def tree_flatten(self):
232  # Treat None as a leaf otherwise we cannot use JAXModel objects
233  # with None values as input to jax.vmap's in_axes:
234  d_leaves, d_treedef = jax.tree.flatten(
235  self._d, is_leaf=lambda x: x is None)
236  return (d_leaves, (d_treedef, self.multi_attr_keys))
237 
238  @classmethod
239  def tree_unflatten(cls, aux_data, children):
240  d_leaves = children
241  d_treedef, multi_attr_keys = aux_data
242  c = cls()
243  c._d = jax.tree.unflatten(d_treedef, d_leaves)
244  c.multi_attr_keys = multi_attr_keys
245  return c
246 
247  def get_vmap_axes(self):
248  """Return a suitable value for jax.vmap's in_axes.
249  If multiple attribute sets are being used, jax.vmap should
250  be used on restraints, scores, etc. to extend the functions
251  (that act on a single attribute set) to work on all sets.
252  This method returns a suitable value for in_axes for the JAX
253  model itself. If no attribute sets are being used, return None."""
254  if not self.multi_attr_keys:
255  return None
256  d = _JAXModel(multi_attr_keys=self.multi_attr_keys)
257  for k, v in self.items():
258  val = 0 if k in self.multi_attr_keys else None
259  # Need to match pytree of the original CompactArray
260  if isinstance(v, _CompactArray):
261  val = _CompactArray(val, v.remap)
262  d[k] = val
263  return d
def shift_indexes
Modify r[indexes] in place by adding dr.
Definition: jax.py:32
def get_random_key
Get a new JAX random key seeded from IMP's random number generator.
Definition: jax.py:74
A space with periodic boundary conditions.
Definition: jax.py:53
The space in which restraints are evaluated.
Definition: jax.py:17
def shift
Shift r by dr and return new r.
Definition: jax.py:28
def distance
If given an array of particle-particle vectors, return an array of distances.
Definition: jax.py:22
RandomNumberGenerator random_number_generator
A shared non-GPU random number generator.