2 @brief Support for the JAX Python library.
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.
11 import jax.numpy
as jnp
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."""
23 return jnp.linalg.norm(dr, axis=-1)
25 return jnp.sqrt(jnp.maximum(jnp.sum(jnp.square(dr), axis=-1),
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."""
35 """If given an array of particle-particle vectors, return an array
36 of distances. If given a single particle-particle vector, return
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
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."""
54 """Shift r by dr and return new r"""
58 """Modify r[indexes] in place by adding dr"""
62 class FreeSpace(Space):
63 """An unbounded space with no periodic boundary conditions."""
67 return _vector_length(dr, min_dist)
79 return r.at[..., indexes, :].add(dr)
83 """A space with periodic boundary conditions.
85 @param side A 3D vector of the periodic boundary box dimensions.
88 def __init__(self, side):
89 self.side = jnp.asarray(side)
91 def distance(self, dr, min_dist=None):
95 return jnp.mod(dr + self.side * 0.5, self.side) - 0.5 * self.side
97 def shift(self, r, dr):
98 return jnp.mod(r + dr, self.side)
101 newr = jnp.mod(r[..., indexes, :] + dr, self.side)
102 return r.at[..., indexes, :].set(newr)
106 """Get a new JAX random key seeded from IMP's random number generator"""
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.
115 @param full_view A NumPy view of the entire IMP Model.
116 @param indexes A NumPy array of indexes of the particles that
118 @param mapping A NumPy array that maps original Particle indexes
119 to indexes into the `data` array.
121 def __init__(self, full_view, indexes, mapping):
122 self.full_view, self.indexes = full_view, indexes
123 self.mapping = mapping
126 @jax.tree_util.register_pytree_node_class
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
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.
141 def __init__(self, data, remap):
142 self.data, self.remap = data, remap
144 shape = property(
lambda s: s.remap.full_view.shape)
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)
152 if full_view.ndim == 2:
155 indexes = np.unique(np.nonzero(full_view != np.inf)[-1])
157 indexes = np.nonzero(full_view != np.inf)[0]
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)
170 def tree_flatten(self):
173 return (self.data,), self.remap
176 def tree_unflatten(cls, aux_data, children):
177 return cls(data=children[0], remap=aux_data)
180 return self.remap.mapping[idx]
182 def _full_map(self, idx):
183 if isinstance(idx, tuple):
184 return idx[:-1] + (self._map(idx[-1]),)
186 return self._map(idx)
188 def __getitem__(self, idx):
189 """Lookup by particle index"""
190 return self.data[self._full_map(idx)]
194 """Like jax.Array.at; allow in-place array modification"""
195 return _AtCompactArray(self)
198 """Copy the JAX data back to the IMP Model"""
199 self.remap.full_view[self.remap.indexes] = self.data[:-1]
202 class _AtCompactArray:
203 """Helper class for _CompactArray.at"""
204 def __init__(self, arr):
207 def __getitem__(self, idx):
208 return _AtIdxCompactArray(self.arr, self.arr._full_map(idx))
211 class _AtIdxCompactArray:
212 """Helper class for _CompactArray.at[idx]"""
213 def __init__(self, arr, rows):
214 self.arr, self.rows = arr, rows
216 def _new(self, data):
217 return _CompactArray(data, self.arr.remap)
220 return self._new(self.arr.data.at[self.rows].set(v))
223 return self._new(self.arr.data.at[self.rows].add(v))
226 @jax.tree_util.register_pytree_node_class
228 """JAX equivalent of an IMP Model.
229 This acts like a regular Python dict.
230 @see IMP._jax_util._get_jax_model
232 def __init__(self, multi_attr_keys=None, **kwargs):
233 self._d = dict(**kwargs)
234 self.multi_attr_keys = frozenset(multi_attr_keys
or ())
236 def __getitem__(self, k):
239 def __setitem__(self, k, v):
242 def __contains__(self, k):
246 return self._d.keys()
249 return self._d.items()
252 return self._d.values()
255 m = _JAXModel(multi_attr_keys=self.multi_attr_keys)
256 m._d = self._d.copy()
262 def tree_flatten(self):
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))
270 def tree_unflatten(cls, aux_data, children):
272 d_treedef, multi_attr_keys = aux_data
274 c._d = jax.tree.unflatten(d_treedef, d_leaves)
275 c.multi_attr_keys = multi_attr_keys
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:
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
291 if isinstance(v, _CompactArray):
292 val = _CompactArray(val, v.remap)
def shift_indexes
Modify r[indexes] in place by adding dr.
def get_random_key
Get a new JAX random key seeded from IMP's random number generator.
A space with periodic boundary conditions.
The space in which restraints are evaluated.
def shift
Shift r by dr and return new r.
def distance
If given an array of particle-particle vectors, return an array of distances.
def displacement
If given an array of particle-particle vectors, return an array of the corresponding displacement vec...
RandomNumberGenerator random_number_generator
A shared non-GPU random number generator.