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