From 44b5b79e430b44d658a2d451299f8ceeb143b39c Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Sun, 9 Jun 2024 05:19:41 +0200 Subject: [PATCH 01/21] nws function in utils, DB case with variable number of wall layers only DB working currently, WIP --- cases/db.py | 29 +++++++++++++++++------------ jax_sph/case_setup.py | 12 ++++++------ jax_sph/defaults.py | 2 ++ jax_sph/utils.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/cases/db.py b/cases/db.py index 908e738..89f33f4 100644 --- a/cases/db.py +++ b/cases/db.py @@ -33,21 +33,26 @@ def __init__(self, cfg: DictConfig): if self.case.r0_type == "relaxed": self._load_only_fluid = True - def _box_size2D(self): + def _box_size2D(self, n_walls): dx, bo = self.case.dx, self.special.box_offset return np.array( - [self.special.L_wall + 6 * dx + bo, self.special.H_wall + 6 * dx + bo] + [ + self.special.L_wall + 2 * n_walls * dx + bo, + self.special.H_wall + 2 * n_walls * dx + bo, + ] ) - def _box_size3D(self): + def _box_size3D(self, n_walls): dx, bo = self.case.dx, self.box_offset sp = self.special - return np.array([sp.L_wall + 6 * dx + bo, sp.H_wall + 6 * dx + bo, sp.W]) + return np.array( + [sp.L_wall + 2 * n_walls * dx + bo, sp.H_wall + 2 * n_walls * dx + bo, sp.W] + ) - def _init_pos2D(self, box_size, dx): + def _init_pos2D(self, box_size, dx, n_walls): sp = self.special if self.case.r0_type == "cartesian": - r_fluid = 3 * dx + pos_init_cartesian_2d(np.array([sp.L, sp.H]), dx) + r_fluid = n_walls * dx + pos_init_cartesian_2d(np.array([sp.L, sp.H]), dx) else: r_fluid = self._get_relaxed_r0(None, dx) @@ -67,12 +72,12 @@ def _init_pos3D(self, box_size, dx): r_xyz = np.vstack([xy_ext * [1, 1, z] for z in zs]) return r_xyz - def _tag2D(self, r): - dx3 = 3 * self.case.dx - mask_left = jnp.where(r[:, 0] < dx3, True, False) - mask_bottom = jnp.where(r[:, 1] < dx3, True, False) - mask_right = jnp.where(r[:, 0] > self.special.L_wall + dx3, True, False) - mask_top = jnp.where(r[:, 1] > self.special.H_wall + dx3, True, False) + def _tag2D(self, r, n_walls): + dxn = n_walls * self.case.dx + mask_left = jnp.where(r[:, 0] < dxn, True, False) + mask_bottom = jnp.where(r[:, 1] < dxn, True, False) + mask_right = jnp.where(r[:, 0] > self.special.L_wall + dxn, True, False) + mask_top = jnp.where(r[:, 1] > self.special.H_wall + dxn, True, False) mask_wall = mask_left + mask_bottom + mask_right + mask_top diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index a4246c1..d066f74 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -122,13 +122,13 @@ def initialize(self): # initialize box and positions of particles if dim == 2: - box_size = self._box_size2D() - r = self._init_pos2D(box_size, dx) - tag = self._tag2D(r) + box_size = self._box_size2D(cfg.solver.n_walls) + r = self._init_pos2D(box_size, dx, cfg.solver.n_walls) + tag = self._tag2D(r, cfg.solver.n_walls) elif dim == 3: - box_size = self._box_size3D() - r = self._init_pos3D(box_size, dx) - tag = self._tag3D(r) + box_size = self._box_size3D(cfg.solver.n_walls) + r = self._init_pos3D(box_size, dx, cfg.solver.n_walls) + tag = self._tag3D(r, cfg.solver.n_walls) displacement_fn, shift_fn = space.periodic(side=box_size) num_particles = len(r) diff --git a/jax_sph/defaults.py b/jax_sph/defaults.py index 4e2223d..857d0eb 100644 --- a/jax_sph/defaults.py +++ b/jax_sph/defaults.py @@ -86,6 +86,8 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: cfg.solver.eta_limiter = 3 # previously: eta-limiter # Thermal conductivity (non-dimensional) cfg.solver.kappa = 0 # previously: kappa + # Number of wall boundary particle layers + cfg.solver.n_walls = 3 # Whether to apply the heat conduction term cfg.solver.heat_conduction = False # previously: heat-conduction # Whether to apply boundaty conditions diff --git a/jax_sph/utils.py b/jax_sph/utils.py index 1e9190c..a505cf4 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -120,6 +120,40 @@ def get_stats(state: Dict, props: list, dx: float): return res +def get_nws(dx, dim, r, rho, m, tag, neighbors, displacement_fn): + """Computes the wall normal vectors at boundaries""" + + N = len(r) + i_s, j_s = neighbors.idx + dr_ij = vmap(displacement_fn)(r[i_s], r[j_s]) + dist = space.distance(dr_ij) + wall_mask = jnp.where(jnp.isin(tag, wall_tags), 1.0, 0.0) + kernel_fn = QuinticKernel(h=dx, dim=dim) + + def wall_phi_vec(rho_j, m_j, dr_ij, dist, tag_j, tag_i): + # Compute unit vector, above eq. (6), Zhang (2017) + e_ij_w = dr_ij / (dist + EPS) + + # Compute kernel gradient + kernel_grad = kernel_fn.grad_w(dist) * (e_ij_w) + + # compute phi eq. (15), Zhang (2017) + phi = -1.0 * m_j / rho_j * kernel_grad * tag_j * tag_i + + return phi + + temp = vmap(wall_phi_vec)( + rho[j_s], m[j_s], dr_ij, dist, wall_mask[j_s], wall_mask[i_s] + ) + phi = ops.segment_sum(temp, i_s, N) + n_w = ( + phi / (jnp.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] * wall_mask[:, None] + ) + n_w = jnp.where(jnp.absolute(n_w) < EPS, 0.0, n_w) + + return n_w + + class Logger: """Logger for printing stats to stdout.""" From 49256ba0bb785093ef4b2c9058b31af4e1583a25 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 03:12:00 +0200 Subject: [PATCH 02/21] jax-md independent code - first iteration --- jax_sph/case_setup.py | 2 +- jax_sph/jax_md/LICENSE_JAX_MD.txt | 202 ++++++ jax_sph/jax_md/dataclasses.py | 81 +++ jax_sph/jax_md/partition.py | 1078 ++++++++++++++++++++++++++++ jax_sph/jax_md/space.py | 452 ++++++++++++ jax_sph/jax_md/util.py | 43 ++ jax_sph/partition.py | 7 +- jax_sph/simulate.py | 2 +- jax_sph/solver.py | 2 +- jax_sph/utils.py | 2 +- notebooks/iclr24_inverse.ipynb | 4 +- notebooks/iclr24_sitl.ipynb | 5 +- notebooks/iclr24_sitl.py | 4 +- notebooks/misc/dirichlet_energy.py | 4 +- notebooks/tutorial.ipynb | 6 +- poetry.lock | 547 +------------- pyproject.toml | 8 +- tests/test_neighbors.py | 2 +- 18 files changed, 1884 insertions(+), 567 deletions(-) create mode 100644 jax_sph/jax_md/LICENSE_JAX_MD.txt create mode 100644 jax_sph/jax_md/dataclasses.py create mode 100644 jax_sph/jax_md/partition.py create mode 100644 jax_sph/jax_md/space.py create mode 100644 jax_sph/jax_md/util.py diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index d066f74..e17ef26 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -9,10 +9,10 @@ import jax.numpy as jnp import numpy as np from jax import vmap -from jax_md import space from jax_sph.eos import RIEMANNEoS, TaitEoS from jax_sph.io_state import read_h5 +from jax_sph.jax_md import space from jax_sph.utils import ( Tag, get_noise_masked, diff --git a/jax_sph/jax_md/LICENSE_JAX_MD.txt b/jax_sph/jax_md/LICENSE_JAX_MD.txt new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/jax_sph/jax_md/LICENSE_JAX_MD.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/jax_sph/jax_md/dataclasses.py b/jax_sph/jax_md/dataclasses.py new file mode 100644 index 0000000..af37fdd --- /dev/null +++ b/jax_sph/jax_md/dataclasses.py @@ -0,0 +1,81 @@ +# Source: https://github.com/jax-md/jax-md +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for defining dataclasses that can be used with jax transformations. + +This code was copied and adapted from https://github.com/google/flax/struct.py. + +Accessed on 04/29/2020. +""" + +import dataclasses +import jax + + +def dataclass(clz): + """Create a class which can be passed to functional transformations. + + Jax transformations such as `jax.jit` and `jax.grad` require objects that are + immutable and can be mapped over using the `jax.tree_util` methods. + + The `dataclass` decorator makes it easy to define custom classes that can be + passed safely to Jax. + + Args: + clz: the class that will be transformed by the decorator. + Returns: + The new class. + """ + clz.set = lambda self, **kwargs: dataclasses.replace(self, **kwargs) + data_clz = dataclasses.dataclass(frozen=True)(clz) + meta_fields = [] + data_fields = [] + for name, field_info in data_clz.__dataclass_fields__.items(): + is_static = field_info.metadata.get('static', False) + if is_static: + meta_fields.append(name) + else: + data_fields.append(name) + + def iterate_clz(x): + meta = tuple(getattr(x, name) for name in meta_fields) + data = tuple(getattr(x, name) for name in data_fields) + return data, meta + + def clz_from_iterable(meta, data): + meta_args = tuple(zip(meta_fields, meta)) + data_args = tuple(zip(data_fields, data)) + kwargs = dict(meta_args + data_args) + return data_clz(**kwargs) + + jax.tree_util.register_pytree_node(data_clz, + iterate_clz, + clz_from_iterable) + + return data_clz + + +def static_field(): + return dataclasses.field(metadata={'static': True}) + +replace = dataclasses.replace +asdict = dataclasses.asdict +astuple = dataclasses.astuple +is_dataclass = dataclasses.is_dataclass +fields = dataclasses.fields +field = dataclasses.field +def unpack(dc) -> tuple: + return tuple(getattr(dc, field.name) for field in dataclasses.fields(dc)) \ No newline at end of file diff --git a/jax_sph/jax_md/partition.py b/jax_sph/jax_md/partition.py new file mode 100644 index 0000000..9dd6617 --- /dev/null +++ b/jax_sph/jax_md/partition.py @@ -0,0 +1,1078 @@ +# Source: https://github.com/jax-md/jax-md +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Code to transform functions on individual tuples of particles to sets.""" + +from enum import Enum, IntEnum +from functools import partial, reduce +from operator import mul +from typing import Any, Callable, Dict, Generator, Optional, Tuple, Union + +import jax.numpy as jnp +import jraph +import numpy as onp +from absl import logging +from jax import eval_shape, jit, lax, ops, tree_map, vmap +from jax.core import ShapedArray + +from jax_sph.jax_md import dataclasses, space, util + +# Types + + +Array = util.Array +PyTree = Any +f32 = util.f32 +f64 = util.f64 + +i32 = util.i32 +i64 = util.i64 + +Box = space.Box +DisplacementOrMetricFn = space.DisplacementOrMetricFn +MetricFn = space.MetricFn +MaskFn = Callable[[Array], Array] + + +# Cell List + + +@dataclasses.dataclass +class CellList: + """Stores the spatial partition of a system into a cell list. + + See :meth:`cell_list` for details on the construction / specification. + Cell list buffers all have a common shape, S, where + * `S = [cell_count_x, cell_count_y, cell_capacity]` + * `S = [cell_count_x, cell_count_y, cell_count_z, cell_capacity]` + in two- and three-dimensions respectively. It is assumed that each cell has + the same capacity. + + Attributes: + position_buffer: An ndarray of floating point positions with shape + `S + [spatial_dimension]`. + id_buffer: An ndarray of int32 particle ids of shape `S`. Note that empty + slots are specified by `id = N` where `N` is the number of particles in + the system. + named_buffer: A dictionary of ndarrays of shape `S + [...]`. This contains + side data placed into the cell list. + did_buffer_overflow: A boolean specifying whether or not the cell list + exceeded the maximum allocated capacity. + cell_capacity: An integer specifying the maximum capacity of each cell in + the cell list. + update_fn: A function that updates the cell list at a fixed capacity. + """ + position_buffer: Array + id_buffer: Array + named_buffer: Dict[str, Array] + + did_buffer_overflow: Array + + cell_capacity: int = dataclasses.static_field() + cell_size: float = dataclasses.static_field() + + update_fn: Callable[..., 'CellList'] = \ + dataclasses.static_field() + + def update(self, position: Array, **kwargs) -> 'CellList': + cl_data = (self.cell_capacity, self.did_buffer_overflow, self.update_fn) + return self.update_fn(position, cl_data, **kwargs) + + @property + def kwarg_buffers(self): + logging.warning('kwarg_buffers renamed to named_buffer. The name ' + 'kwarg_buffers will be depricated.') + return self.named_buffer + + +@dataclasses.dataclass +class CellListFns: + allocate: Callable[..., CellList] = dataclasses.static_field() + update: Callable[[Array, Union[CellList, int]], + CellList] = dataclasses.static_field() + + def __iter__(self): + return iter((self.allocate, self.update)) + + +def _cell_dimensions(spatial_dimension: int, + box_size: Box, + minimum_cell_size: float) -> Tuple[Box, Array, Array, int]: + """Compute the number of cells-per-side and total number of cells in a box.""" + if isinstance(box_size, int) or isinstance(box_size, float): + box_size = float(box_size) + + # NOTE(schsam): Should we auto-cast based on box_size? I can't imagine a case + # in which the box_size would not be accurately represented by an f32. + if (isinstance(box_size, onp.ndarray) and + (box_size.dtype == i32 or box_size.dtype == i64)): + box_size = float(box_size) + + cells_per_side = onp.floor(box_size / minimum_cell_size) + cell_size = box_size / cells_per_side + cells_per_side = onp.array(cells_per_side, dtype=i32) + + if isinstance(box_size, (onp.ndarray, jnp.ndarray)): + if box_size.ndim == 1 or box_size.ndim == 2: + assert box_size.size == spatial_dimension + flat_cells_per_side = onp.reshape(cells_per_side, (-1,)) + for cells in flat_cells_per_side: + if cells < 3: + msg = ('Box must be at least 3x the size of the grid spacing in each ' + 'dimension.') + raise ValueError(msg) + cell_count = reduce(mul, flat_cells_per_side, 1) + elif box_size.ndim == 0: + cell_count = cells_per_side ** spatial_dimension + else: + raise ValueError(('Box must be either: a scalar, a vector, or a matrix. ' + f'Found {box_size}.')) + else: + cell_count = cells_per_side ** spatial_dimension + + return box_size, cell_size, cells_per_side, int(cell_count) + + +def count_cell_filling(position: Array, + box_size: Box, + minimum_cell_size: float) -> Array: + """Counts the number of particles per-cell in a spatial partition.""" + dim = int(position.shape[1]) + box_size, cell_size, cells_per_side, cell_count = \ + _cell_dimensions(dim, box_size, minimum_cell_size) + + hash_multipliers = _compute_hash_constants(dim, cells_per_side) + + particle_index = jnp.array(position / cell_size, dtype=i32) + particle_hash = jnp.sum(particle_index * hash_multipliers, axis=1) + + filling = ops.segment_sum(jnp.ones_like(particle_hash), + particle_hash, + cell_count) + return filling + + +def _compute_hash_constants(spatial_dimension: int, + cells_per_side: Array) -> Array: + if cells_per_side.size == 1: + return jnp.array([[cells_per_side ** d for d in range(spatial_dimension)]], + dtype=i32) + elif cells_per_side.size == spatial_dimension: + one = jnp.array([[1]], dtype=i32) + cells_per_side = jnp.concatenate((one, cells_per_side[:, :-1]), axis=1) + return jnp.array(jnp.cumprod(cells_per_side), dtype=i32) + else: + raise ValueError() + + +def _neighboring_cells(dimension: int) -> Generator[onp.ndarray, None, None]: + for dindex in onp.ndindex(*([3] * dimension)): + yield onp.array(dindex, dtype=i32) - 1 + + +def _estimate_cell_capacity(position: Array, + box_size: Box, + cell_size: float, + buffer_size_multiplier: float) -> int: + cell_capacity = onp.max(count_cell_filling(position, box_size, cell_size)) + return int(cell_capacity * buffer_size_multiplier) + + +def shift_array(arr: Array, dindex: Array) -> Array: + if len(dindex) == 2: + dx, dy = dindex + dz = 0 + elif len(dindex) == 3: + dx, dy, dz = dindex + + if dx < 0: + arr = jnp.concatenate((arr[1:], arr[:1])) + elif dx > 0: + arr = jnp.concatenate((arr[-1:], arr[:-1])) + + if dy < 0: + arr = jnp.concatenate((arr[:, 1:], arr[:, :1]), axis=1) + elif dy > 0: + arr = jnp.concatenate((arr[:, -1:], arr[:, :-1]), axis=1) + + if dz < 0: + arr = jnp.concatenate((arr[:, :, 1:], arr[:, :, :1]), axis=2) + elif dz > 0: + arr = jnp.concatenate((arr[:, :, -1:], arr[:, :, :-1]), axis=2) + + return arr + + +def unflatten_cell_buffer(arr: Array, + cells_per_side: Array, + dim: int) -> Array: + if (isinstance(cells_per_side, int) or + isinstance(cells_per_side, float) or + (util.is_array(cells_per_side) and not cells_per_side.shape)): + cells_per_side = (int(cells_per_side),) * dim + elif util.is_array(cells_per_side) and len(cells_per_side.shape) == 1: + cells_per_side = tuple([int(x) for x in cells_per_side[::-1]]) + elif util.is_array(cells_per_side) and len(cells_per_side.shape) == 2: + cells_per_side = tuple([int(x) for x in cells_per_side[0][::-1]]) + else: + raise ValueError() + return jnp.reshape(arr, cells_per_side + (-1,) + arr.shape[1:]) + + +def cell_list(box_size: Box, + minimum_cell_size: float, + buffer_size_multiplier: float = 1.25 + ) -> CellListFns: + r"""Returns a function that partitions point data spatially. + + Given a set of points :math:`\{x_i \in R^d\}` with associated data + :math:`\{k_i \in R^m\}` it is often useful to partition the points / data + spatially. A simple partitioning that can be implemented efficiently within + XLA is a dense partition into a uniform grid called a cell list. + + Since XLA requires that shapes be statically specified inside of a JIT block, + the cell list code can operate in two modes: allocation and update. + + Allocation creates a new cell list that uses a set of input positions to + estimate the capacity of the cell list. This capacity can be adjusted by + setting the `buffer_size_multiplier` or setting the `extra_capacity`. + Allocation cannot be JIT. + + Updating takes a previously allocated cell list and places a new set of + particles in the cells. Updating cannot resize the cell list and is therefore + compatible with JIT. However, if the configuration has changed substantially + it is possible that the existing cell list won't be large enough to + accommodate all of the particles. In this case the `did_buffer_overflow` bit + will be set to True. + + Args: + box_size: A float or an ndarray of shape `[spatial_dimension]` specifying + the size of the system. Note, this code is written for the case where the + boundaries are periodic. If this is not the case, then the current code + will be slightly less efficient. + minimum_cell_size: A float specifying the minimum side length of each cell. + Cells are enlarged so that they exactly fill the box. + buffer_size_multiplier: A floating point multiplier that multiplies the + estimated cell capacity to allow for fluctuations in the maximum cell + occupancy. + Returns: + A `CellListFns` object that contains two methods, one to allocate the cell + list and one to update the cell list. The update function can be called + with either a cell list from which the capacity can be inferred or with + an explicit integer denoting the capacity. Note that an existing cell list + can also be updated by calling `cell_list.update(position)`. + """ + + if util.is_array(box_size): + box_size = onp.array(box_size) + if len(box_size.shape) == 1: + box_size = onp.reshape(box_size, (1, -1)) + + if util.is_array(minimum_cell_size): + minimum_cell_size = onp.array(minimum_cell_size) + + def cell_list_fn(position: Array, + capacity_overflow_update: Optional[ + Tuple[int, bool, Callable[..., CellList]]] = None, + extra_capacity: int = 0, **kwargs) -> CellList: + N = position.shape[0] + dim = position.shape[1] + + if dim != 2 and dim != 3: + # NOTE(schsam): Do we want to check this in compute_fn as well? + raise ValueError( + f'Cell list spatial dimension must be 2 or 3. Found {dim}.') + + _, cell_size, cells_per_side, cell_count = \ + _cell_dimensions(dim, box_size, minimum_cell_size) + + if capacity_overflow_update is None: + cell_capacity = _estimate_cell_capacity(position, box_size, cell_size, + buffer_size_multiplier) + cell_capacity += extra_capacity + overflow = False + update_fn = cell_list_fn + else: + cell_capacity, overflow, update_fn = capacity_overflow_update + + hash_multipliers = _compute_hash_constants(dim, cells_per_side) + + # Create cell list data. + particle_id = lax.iota(i32, N) + # NOTE(schsam): We use the convention that particles that are successfully, + # copied have their true id whereas particles empty slots have id = N. + # Then when we copy data back from the grid, copy it to an array of shape + # [N + 1, output_dimension] and then truncate it to an array of shape + # [N, output_dimension] which ignores the empty slots. + cell_position = jnp.zeros((cell_count * cell_capacity, dim), + dtype=position.dtype) + cell_id = N * jnp.ones((cell_count * cell_capacity, 1), dtype=i32) + + # It might be worth adding an occupied mask. However, that will involve + # more compute since often we will do a mask for species that will include + # an occupancy test. It seems easier to design around this empty_data_value + # for now and revisit the issue if it comes up later. + empty_kwarg_value = 10 ** 5 + cell_kwargs = {} + # pytype: disable=attribute-error + for k, v in kwargs.items(): + if not util.is_array(v): + raise ValueError((f'Data must be specified as an ndarray. Found "{k}" ' + f'with type {type(v)}.')) + if v.shape[0] != position.shape[0]: + raise ValueError(('Data must be specified per-particle (an ndarray ' + f'with shape ({N}, ...)). Found "{k}" with ' + f'shape {v.shape}.')) + kwarg_shape = v.shape[1:] if v.ndim > 1 else (1,) + cell_kwargs[k] = empty_kwarg_value * jnp.ones( + (cell_count * cell_capacity,) + kwarg_shape, v.dtype) + # pytype: enable=attribute-error + indices = jnp.array(position / cell_size, dtype=i32) + hashes = jnp.sum(indices * hash_multipliers, axis=1) + + # Copy the particle data into the grid. Here we use a trick to allow us to + # copy into all cells simultaneously using a single lax.scatter call. To do + # this we first sort particles by their cell hash. We then assign each + # particle to have a cell id = hash * cell_capacity + grid_id where + # grid_id is a flat list that repeats 0, .., cell_capacity. So long as + # there are fewer than cell_capacity particles per cell, each particle is + # guaranteed to get a cell id that is unique. + sort_map = jnp.argsort(hashes) + sorted_position = position[sort_map] + sorted_hash = hashes[sort_map] + sorted_id = particle_id[sort_map] + + sorted_kwargs = {} + for k, v in kwargs.items(): + sorted_kwargs[k] = v[sort_map] + + sorted_cell_id = jnp.mod(lax.iota(i32, N), cell_capacity) + sorted_cell_id = sorted_hash * cell_capacity + sorted_cell_id + + cell_position = cell_position.at[sorted_cell_id].set(sorted_position) + sorted_id = jnp.reshape(sorted_id, (N, 1)) + cell_id = cell_id.at[sorted_cell_id].set(sorted_id) + cell_position = unflatten_cell_buffer(cell_position, cells_per_side, dim) + cell_id = unflatten_cell_buffer(cell_id, cells_per_side, dim) + + for k, v in sorted_kwargs.items(): + if v.ndim == 1: + v = jnp.reshape(v, v.shape + (1,)) + cell_kwargs[k] = cell_kwargs[k].at[sorted_cell_id].set(v) + cell_kwargs[k] = unflatten_cell_buffer( + cell_kwargs[k], cells_per_side, dim) + + occupancy = ops.segment_sum(jnp.ones_like(hashes), hashes, cell_count) + max_occupancy = jnp.max(occupancy) + overflow = overflow | (max_occupancy > cell_capacity) + + return CellList(cell_position, cell_id, cell_kwargs, + overflow, cell_capacity, cell_size, update_fn) # pytype: disable=wrong-arg-count + + def allocate_fn(position: Array, extra_capacity: int = 0, **kwargs + ) -> CellList: + return cell_list_fn(position, extra_capacity=extra_capacity, **kwargs) + + def update_fn(position: Array, cl_or_capacity: Union[CellList, int], **kwargs + ) -> CellList: + if isinstance(cl_or_capacity, int): + capacity = int(cl_or_capacity) + return cell_list_fn(position, (capacity, False, cell_list_fn), **kwargs) + cl = cl_or_capacity + cl_data = (cl.cell_capacity, cl.did_buffer_overflow, cl.update_fn) + return cell_list_fn(position, cl_data, **kwargs) + + return CellListFns(allocate_fn, update_fn) # pytype: disable=wrong-arg-count + + +# Neighbor Lists + + +class PartitionErrorCode(IntEnum): + """An enum specifying different error codes. + + Attributes: + NONE: Means that no error was encountered during simulation. + NEIGHBOR_LIST_OVERFLOW: Indicates that the neighbor list was not large + enough to contain all of the particles. This should indicate that it is + necessary to allocate a new neighbor list. + CELL_LIST_OVERFLOW: Indicates that the cell list was not large enough to + contain all of the particles. This should indicate that it is necessary + to allocate a new cell list. + CELL_SIZE_TOO_SMALL: Indicates that the size of cells in a cell list was + not large enough to properly capture particle interactions. This + indicates that it is necessary to allcoate a new cell list with larger + cells. + MALFORMED_BOX: Indicates that a box matrix was not properly upper + triangular. + """ + NONE = 0 + NEIGHBOR_LIST_OVERFLOW = 1 << 0 + CELL_LIST_OVERFLOW = 1 << 1 + CELL_SIZE_TOO_SMALL = 1 << 2 + MALFORMED_BOX = 1 << 3 +PEC = PartitionErrorCode + + +@dataclasses.dataclass +class PartitionError: + """A struct containing error codes while building / updating neighbor lists. + + Attributes: + code: An array storing the error code. See `PartitionErrorCode` for + details. + """ + code: Array + + def update(self, bit: bytes, pred: Array) -> Array: + """Possibly adds an error based on a predicate.""" + zero = jnp.zeros((), jnp.uint8) + bit = jnp.array(bit, dtype=jnp.uint8) + return PartitionError(self.code | jnp.where(pred, bit, zero)) + + def __str__(self) -> str: + """Produces a string representation of the error code.""" + if not jnp.any(self.code): + return '' + + if jnp.any(self.code & PEC.NEIGHBOR_LIST_OVERFLOW): + return 'Partition Error: Neighbor list buffer overflow.' + + if jnp.any(self.code & PEC.CELL_LIST_OVERFLOW): + return 'Partition Error: Cell list buffer overflow' + + if jnp.any(self.code & PEC.CELL_SIZE_TOO_SMALL): + return 'Partition Error: Cell size too small' + + if jnp.any(self.code & PEC.MALFORMED_BOX): + return ('Partition Error: Incorrect box format. Expecting upper ' + 'triangular.') + + raise ValueError(f'Unexpected Error Code {self.code}.') + + __repr__ = __str__ + + + +def _displacement_or_metric_to_metric_sq( + displacement_or_metric: DisplacementOrMetricFn) -> MetricFn: + """Checks whether or not a displacement or metric was provided.""" + for dim in range(1, 4): + try: + R = ShapedArray((dim,), f32) + dR_or_dr = eval_shape(displacement_or_metric, R, R, t=0) + if len(dR_or_dr.shape) == 0: + return lambda Ra, Rb, **kwargs: \ + displacement_or_metric(Ra, Rb, **kwargs) ** 2 + else: + return lambda Ra, Rb, **kwargs: space.square_distance( + displacement_or_metric(Ra, Rb, **kwargs)) + except TypeError: + continue + except ValueError: + continue + raise ValueError( + 'Canonicalize displacement not implemented for spatial dimension larger' + 'than 4.') + + +def _cell_size(box, minimum_cell_size) -> Array: + cells_per_side = jnp.floor(box / minimum_cell_size) + return box / cells_per_side + + +def _fractional_cell_size(box, cutoff): + if jnp.isscalar(box) or box.ndim == 0: + return cutoff / box + elif box.ndim == 1: + return cutoff / jnp.min(box) + elif box.ndim == 2: + if box.shape[0] == 1: + return 1 / jnp.floor(box[0, 0] / cutoff) + elif box.shape[0] == 2: + xx = box[0, 0] + yy = box[1, 1] + xy = box[0, 1] / yy + + nx = xx / jnp.sqrt(1 + xy**2) + ny = yy + + nmin = jnp.floor(jnp.min(jnp.array([nx, ny])) / cutoff) + nmin = jnp.where(nmin == 0, 1, nmin) + return 1 / nmin + elif box.shape[0] == 3: + xx = box[0, 0] + yy = box[1, 1] + zz = box[2, 2] + xy = box[0, 1] / yy + xz = box[0, 2] / zz + yz = box[1, 2] / zz + + nx = xx / jnp.sqrt(1 + xy**2 + (xy * yz - xz)**2) + ny = yy / jnp.sqrt(1 + yz**2) + nz = zz + + nmin = jnp.floor(jnp.min(jnp.array([nx, ny, nz])) / cutoff) + nmin = jnp.where(nmin == 0, 1, nmin) + return 1 / nmin + else: + raise ValueError('Expected box to be either 1-, 2-, or 3-dimensional ' + f'found {box.shape[0]}') + else: + raise ValueError('Expected box to be either a scalar, a vector, or a ' + f'matrix. Found {type(box)}.') + + +class NeighborListFormat(Enum): + """An enum listing the different neighbor list formats. + + Attributes: + Dense: A dense neighbor list where the ids are a square matrix + of shape `(N, max_neighbors_per_atom)`. Here the capacity of the neighbor + list must scale with the highest connectivity neighbor. + Sparse: A sparse neighbor list where the ids are a rectangular + matrix of shape `(2, max_neighbors)` specifying the start / end particle + of each neighbor pair. + OrderedSparse: A sparse neighbor list whose format is the same as `Sparse` + where only bonds with i < j are included. + """ + Dense = 0 + Sparse = 1 + OrderedSparse = 2 + + +def is_sparse(fmt: NeighborListFormat) -> bool: + return (fmt is NeighborListFormat.Sparse or + fmt is NeighborListFormat.OrderedSparse) + + +def is_format_valid(fmt: NeighborListFormat): + if fmt not in list(NeighborListFormat): + raise ValueError(( + 'Neighbor list format must be a member of NeighborListFormat' + f' found {fmt}.')) + + +def is_box_valid(box: Array) -> bool: + if jnp.isscalar(box) or box.ndim == 0 or box.ndim == 1: + return True + if box.ndim == 2: + return jnp.triu(box) == box + return False + + +@dataclasses.dataclass +class NeighborList: + """A struct containing the state of a Neighbor List. + + Attributes: + idx: For an N particle system this is an `[N, max_occupancy]` array of + integers such that `idx[i, j]` is the j-th neighbor of particle i. + reference_position: The positions of particles when the neighbor list was + constructed. This is used to decide whether the neighbor list ought to be + updated. + error: An error code that is used to identify errors that occured during + neighbor list construction. See `PartitionError` and `PartitionErrorCode` + for details. + cell_list_capacity: An optional integer specifying the capacity of the cell + list used as an intermediate step in the creation of the neighbor list. + max_occupancy: A static integer specifying the maximum size of the + neighbor list. Changing this will invoke a recompilation. + format: A NeighborListFormat enum specifying the format of the neighbor + list. + cell_size: A float specifying the current minimum size of the cells used + in cell list construction. + cell_list_fn: The function used to construct the cell list. + update_fn: A static python function used to update the neighbor list. + """ + idx: Array + reference_position: Array + error: PartitionError + cell_list_capacity: Optional[int] = dataclasses.static_field() + max_occupancy: int = dataclasses.static_field() + + format: NeighborListFormat = dataclasses.static_field() + cell_size: Optional[float] = dataclasses.static_field() + cell_list_fn: Callable[[Array, CellList], + CellList] = dataclasses.static_field() + update_fn: Callable[[Array, 'NeighborList'], + 'NeighborList'] = dataclasses.static_field() + + def update(self, position: Array, **kwargs) -> 'NeighborList': + return self.update_fn(position, self, **kwargs) + + @property + def did_buffer_overflow(self) -> bool: + return self.error.code & (PEC.NEIGHBOR_LIST_OVERFLOW | + PEC.CELL_LIST_OVERFLOW) + + @property + def cell_size_too_small(self) -> bool: + return self.error.code & PEC.CELL_SIZE_TOO_SMALL + + @property + def malformed_box(self) -> bool: + return self.error.code & PEC.MALFORMED_BOX + + +@dataclasses.dataclass +class NeighborListFns: + """A struct containing functions to allocate and update neighbor lists. + + Attributes: + allocate: A function to allocate a new neighbor list. This function cannot + be compiled, since it uses the values of positions to infer the shapes. + update: A function to update a neighbor list given a new set of positions + and a previously allocated neighbor list. + """ + allocate: Callable[..., NeighborList] = dataclasses.static_field() + update: Callable[[Array, NeighborList], + NeighborList] = dataclasses.static_field() + + def __call__(self, + position: Array, + neighbors: Optional[NeighborList] = None, + extra_capacity: int = 0, + **kwargs) -> NeighborList: + """A function for backward compatibility with previous neighbor lists. + + Args: + position: An `(N, dim)` array of particle positions. + neighbors: An optional neighbor list object. If it is provided then + the function updates the neighbor list, otherwise it allocates a new + neighbor list. + extra_capacity: Extra capacity to add if allocating the neighbor list. + Returns: + A neighbor list object. + """ + logging.warning('Using a deprecated code path to create / update neighbor ' + 'lists. It will be removed in a later version of JAX MD. ' + 'Using `neighbor_fn.allocate` and `neighbor_fn.update` ' + 'is preferred.') + if neighbors is None: + return self.allocate(position, extra_capacity, **kwargs) + return self.update(position, neighbors, **kwargs) + + def __iter__(self): + return iter((self.allocate, self.update)) + + +NeighborFn = Callable[[Array, Optional[NeighborList], Optional[int]], + NeighborList] + + +def neighbor_list(displacement_or_metric: DisplacementOrMetricFn, + box: Box, + r_cutoff: float, + dr_threshold: float = 0.0, + capacity_multiplier: float = 1.25, + disable_cell_list: bool = False, + mask_self: bool = True, + custom_mask_function: Optional[MaskFn] = None, + fractional_coordinates: bool = False, + format: NeighborListFormat = NeighborListFormat.Dense, + **static_kwargs) -> NeighborFn: + """Returns a function that builds a list neighbors for collections of points. + + Neighbor lists must balance the need to be jit compatible with the fact that + under a jit the maximum number of neighbors cannot change (owing to static + shape requirements). To deal with this, our `neighbor_list` returns a + `NeighborListFns` object that contains two functions: 1) + `neighbor_fn.allocate` create a new neighbor list and 2) `neighbor_fn.update` + updates an existing neighbor list. Neighbor lists themselves additionally + have a convenience `update` member function. + + Note that allocation of a new neighbor list cannot be jit compiled since it + uses the positions to infer the maximum number of neighbors (along with + additional space specified by the `capacity_multiplier`). Updating the + neighbor list can be jit compiled; if the neighbor list capacity is not + sufficient to store all the neighbors, the `did_buffer_overflow` bit + will be set to `True` and a new neighbor list will need to be reallocated. + + Here is a typical example of a simulation loop with neighbor lists: + + .. code-block:: python + + init_fn, apply_fn = simulate.nve(energy_fn, shift, 1e-3) + exact_init_fn, exact_apply_fn = simulate.nve(exact_energy_fn, shift, 1e-3) + + nbrs = neighbor_fn.allocate(R) + state = init_fn(random.PRNGKey(0), R, neighbor_idx=nbrs.idx) + + def body_fn(i, state): + state, nbrs = state + nbrs = nbrs.update(state.position) + state = apply_fn(state, neighbor_idx=nbrs.idx) + return state, nbrs + + step = 0 + for _ in range(20): + new_state, nbrs = lax.fori_loop(0, 100, body_fn, (state, nbrs)) + if nbrs.did_buffer_overflow: + nbrs = neighbor_fn.allocate(state.position) + else: + state = new_state + step += 1 + + Args: + displacement: A function `d(R_a, R_b)` that computes the displacement + between pairs of points. + box: Either a float specifying the size of the box, an array of + shape `[spatial_dim]` specifying the box size for a cubic box in each + spatial dimension, or a matrix of shape `[spatial_dim, spatial_dim]` that + is _upper triangular_ and specifies the lattice vectors of the box. + r_cutoff: A scalar specifying the neighborhood radius. + dr_threshold: A scalar specifying the maximum distance particles can move + before rebuilding the neighbor list. + capacity_multiplier: A floating point scalar specifying the fractional + increase in maximum neighborhood occupancy we allocate compared with the + maximum in the example positions. + disable_cell_list: An optional boolean. If set to `True` then the neighbor + list is constructed using only distances. This can be useful for + debugging but should generally be left as `False`. + mask_self: An optional boolean. Determines whether points can consider + themselves to be their own neighbors. + custom_mask_function: An optional function. Takes the neighbor array + and masks selected elements. Note: The input array to the function is + `(n_particles, m)` where the index of particle 1 is in index in the first + dimension of the array, the index of particle 2 is given by the value in + the array + fractional_coordinates: An optional boolean. Specifies whether positions + will be supplied in fractional coordinates in the unit cube, :math:`[0, 1]^d`. + If this is set to True then the `box_size` will be set to `1.0` and the + cell size used in the cell list will be set to `cutoff / box_size`. + format: The format of the neighbor list; see the :meth:`NeighborListFormat` enum + for details about the different choices for formats. Defaults to `Dense`. + **static_kwargs: kwargs that get threaded through the calculation of + example positions. + Returns: + A NeighborListFns object that contains a method to allocate a new neighbor + list and a method to update an existing neighbor list. + """ + is_format_valid(format) + box = lax.stop_gradient(box) + r_cutoff = lax.stop_gradient(r_cutoff) + dr_threshold = lax.stop_gradient(dr_threshold) + + box = f32(box) + + cutoff = r_cutoff + dr_threshold + cutoff_sq = cutoff ** 2 + threshold_sq = (dr_threshold / f32(2)) ** 2 + metric_sq = _displacement_or_metric_to_metric_sq(displacement_or_metric) + + @partial(jit, static_argnums=0) + def candidate_fn(positionShape) -> Array: + candidates = jnp.arange(positionShape[0]) + return jnp.broadcast_to(candidates[None, :], + (positionShape[0], positionShape[0])) + + @partial(jit, static_argnums=1) + def cell_list_candidate_fn(cl_id_buffer, positionShape) -> Array: + N, dim = positionShape + + idx = cl_id_buffer + + cell_idx = [idx] + + for dindex in _neighboring_cells(dim): + if onp.all(dindex == 0): + continue + cell_idx += [shift_array(idx, dindex)] + + cell_idx = jnp.concatenate(cell_idx, axis=-2) + cell_idx = cell_idx[..., jnp.newaxis, :, :] + cell_idx = jnp.broadcast_to(cell_idx, idx.shape[:-1] + cell_idx.shape[-2:]) + + def copy_values_from_cell(value, cell_value, cell_id): + scatter_indices = jnp.reshape(cell_id, (-1,)) + cell_value = jnp.reshape(cell_value, (-1,) + cell_value.shape[-2:]) + return value.at[scatter_indices].set(cell_value) + + neighbor_idx = jnp.zeros((N + 1,) + cell_idx.shape[-2:], i32) + neighbor_idx = copy_values_from_cell(neighbor_idx, cell_idx, idx) + return neighbor_idx[:-1, :, 0] + + @jit + def mask_self_fn(idx: Array) -> Array: + self_mask = idx == jnp.reshape(jnp.arange(idx.shape[0], dtype=i32), + (idx.shape[0], 1)) + return jnp.where(self_mask, idx.shape[0], idx) + + @jit + def prune_neighbor_list_dense(position: Array, idx: Array, **kwargs + ) -> Array: + d = partial(metric_sq, **kwargs) + d = space.map_neighbor(d) + + N = position.shape[0] + neigh_position = position[idx] + dR = d(position, neigh_position) + + mask = (dR < cutoff_sq) & (idx < N) + out_idx = N * jnp.ones(idx.shape, i32) + + cumsum = jnp.cumsum(mask, axis=1) + index = jnp.where(mask, cumsum - 1, idx.shape[1] - 1) + p_index = jnp.arange(idx.shape[0])[:, None] + out_idx = out_idx.at[p_index, index].set(idx) + max_occupancy = jnp.max(cumsum[:, -1]) + + return out_idx, max_occupancy + + @jit + def prune_neighbor_list_sparse(position: Array, idx: Array, **kwargs + ) -> Array: + d = partial(metric_sq, **kwargs) + d = space.map_bond(d) + + N = position.shape[0] + sender_idx = jnp.broadcast_to(jnp.arange(N)[:, None], idx.shape) + + sender_idx = jnp.reshape(sender_idx, (-1,)) + receiver_idx = jnp.reshape(idx, (-1,)) + dR = d(position[sender_idx], position[receiver_idx]) + + mask = (dR < cutoff_sq) & (receiver_idx < N) + if format is NeighborListFormat.OrderedSparse: + mask = mask & (receiver_idx < sender_idx) + + out_idx = N * jnp.ones(receiver_idx.shape, i32) + + cumsum = jnp.cumsum(mask) + index = jnp.where(mask, cumsum - 1, len(receiver_idx) - 1) + receiver_idx = out_idx.at[index].set(receiver_idx) + sender_idx = out_idx.at[index].set(sender_idx) + max_occupancy = cumsum[-1] + + return jnp.stack((receiver_idx, sender_idx)), max_occupancy + + def neighbor_list_fn(position: Array, + neighbors = None, + extra_capacity: int = 0, + **kwargs) -> NeighborList: + def neighbor_fn(position_and_error, max_occupancy=None): + position, err = position_and_error + N = position.shape[0] + + cl_fn = None + cl = None + cell_size = None + if not disable_cell_list: + if neighbors is None: + _box = kwargs.get('box', box) + cell_size = cutoff + if fractional_coordinates: + err = err.update(PEC.MALFORMED_BOX, is_box_valid(_box)) + cell_size = _fractional_cell_size(_box, cutoff) + _box = 1.0 + if jnp.all(cell_size < _box / 3.): + cl_fn = cell_list(_box, cell_size, capacity_multiplier) + cl = cl_fn.allocate(position, extra_capacity=extra_capacity) + else: + cell_size = neighbors.cell_size + cl_fn = neighbors.cell_list_fn + if cl_fn is not None: + cl = cl_fn.update(position, neighbors.cell_list_capacity) + + if cl is None: + cl_capacity = None + idx = candidate_fn(position.shape) + else: + err = err.update(PEC.CELL_LIST_OVERFLOW, cl.did_buffer_overflow) + idx = cell_list_candidate_fn(cl.id_buffer, position.shape) + cl_capacity = cl.cell_capacity + + if mask_self: + idx = mask_self_fn(idx) + if custom_mask_function is not None: + idx = custom_mask_function(idx) + + if is_sparse(format): + idx, occupancy = prune_neighbor_list_sparse(position, idx, **kwargs) + else: + idx, occupancy = prune_neighbor_list_dense(position, idx, **kwargs) + + if max_occupancy is None: + _extra_capacity = (extra_capacity if not is_sparse(format) + else N * extra_capacity) + max_occupancy = int(occupancy * capacity_multiplier + _extra_capacity) + if max_occupancy > idx.shape[-1]: + max_occupancy = idx.shape[-1] + if not is_sparse(format): + capacity_limit = N - 1 if mask_self else N + elif format is NeighborListFormat.Sparse: + capacity_limit = N * (N - 1) if mask_self else N**2 + else: + capacity_limit = N * (N - 1) // 2 + if max_occupancy > capacity_limit: + max_occupancy = capacity_limit + idx = idx[:, :max_occupancy] + update_fn = (neighbor_list_fn if neighbors is None else + neighbors.update_fn) + return NeighborList( + idx, + position, + err.update(PEC.NEIGHBOR_LIST_OVERFLOW, occupancy > max_occupancy), + cl_capacity, + max_occupancy, + format, + cell_size, + cl_fn, + update_fn) # pytype: disable=wrong-arg-count + + nbrs = neighbors + if nbrs is None: + return neighbor_fn((position, PartitionError(jnp.zeros((), jnp.uint8)))) + + neighbor_fn = partial(neighbor_fn, max_occupancy=nbrs.max_occupancy) + + # If the box has been updated, then check that fractional coordinates are + # enabled and that the cell list has big enough cells. + if 'box' in kwargs and not disable_cell_list: + if not fractional_coordinates: + raise ValueError('Neighbor list cannot accept a box keyword argument ' + 'if fractional_coordinates is not enabled.') + # `cell_size` is really the minimum cell size. + cur_cell_size = _cell_size(1.0, nbrs.cell_size) + new_cell_size = _cell_size(1.0, + _fractional_cell_size(kwargs['box'], cutoff)) + err = nbrs.error.update(PEC.CELL_SIZE_TOO_SMALL, + new_cell_size > cur_cell_size) + err = err.update(PEC.MALFORMED_BOX, is_box_valid(kwargs['box'])) + nbrs = dataclasses.replace(nbrs, error=err) + + d = partial(metric_sq, **kwargs) + d = vmap(d) + return lax.cond( + jnp.any(d(position, nbrs.reference_position) > threshold_sq), + (position, nbrs.error), neighbor_fn, + nbrs, lambda x: x) + + def allocate_fn(position: Array, extra_capacity: int = 0, **kwargs + ): + return neighbor_list_fn(position, extra_capacity=extra_capacity, **kwargs) + + def update_fn(position: Array, neighbors, **kwargs + ): + return neighbor_list_fn(position, neighbors, **kwargs) + + return NeighborListFns(allocate_fn, update_fn) # pytype: disable=wrong-arg-count + + +def neighbor_list_mask(neighbor: NeighborList, mask_self: bool = False + ) -> Array: + """Compute a mask for neighbor list.""" + if is_sparse(neighbor.format): + mask = neighbor.idx[0] < len(neighbor.reference_position) + if mask_self: + mask = mask & (neighbor.idx[0] != neighbor.idx[1]) + return mask + + mask = neighbor.idx < len(neighbor.idx) + if mask_self: + N = len(neighbor.reference_position) + self_mask = neighbor.idx != jnp.reshape(jnp.arange(N, dtype=i32), (N, 1)) + mask = mask & self_mask + return mask + + +def to_jraph(neighbor: NeighborList, + mask: Optional[Array] = None, + nodes: Optional[PyTree] = None, + edges: Optional[PyTree] = None, + globals: Optional[PyTree] = None + ) -> jraph.GraphsTuple: + """Convert a sparse neighbor list to a `jraph.GraphsTuple`. + + As in jraph, padding here is accomplished by adding a ficticious graph with a + single node. + + Args: + neighbor: A neighbor list that we will convert to the jraph format. Must be + sparse. + mask: An optional mask on the edges. + + Returns: + A `jraph.GraphsTuple` that contains the topology of the neighbor list. + """ + if not is_sparse(neighbor.format): + raise ValueError('Cannot convert a dense neighbor list to jraph format. ' + 'Please use either NeighborListFormat.Sparse or ' + 'NeighborListFormat.OrderedSparse.') + + receivers, senders = neighbor.idx + N = len(neighbor.reference_position) + + _mask = neighbor_list_mask(neighbor) + + # Pad the nodes to add one fictitious node. + def pad(x): + padding = jnp.zeros((1,) + x.shape[1:], dtype=x.dtype) + return jnp.concatenate((x, padding), axis=0) + nodes = tree_map(pad, nodes) + + # Pad the globals to add one fictitious global. + globals = tree_map(pad, globals) + + # If there is an additional mask, reorder the edges. + if mask is not None: + _mask = _mask & mask + cumsum = jnp.cumsum(_mask) + index = jnp.where(_mask, cumsum - 1, len(receivers)) + ordered = N * jnp.ones((len(receivers) + 1,), i32) + receivers = ordered.at[index].set(receivers)[:-1] + senders = ordered.at[index].set(senders)[:-1] + def reorder_edges(x): + return jnp.zeros_like(x).at[index].set(x) + edges = tree_map(reorder_edges, edges) + mask = receivers < N + + return jraph.GraphsTuple( + nodes=nodes, + edges=edges, + receivers=receivers, + senders=senders, + globals=globals, + n_node=jnp.array([N, 1]), + n_edge=jnp.array([jnp.sum(_mask), jnp.sum(~_mask)]), + ) + + +def to_dense(neighbor: NeighborList) -> Array: + """Converts a sparse neighbor list to dense ids. Cannot be JIT.""" + if neighbor.format is not Sparse: + raise ValueError('Can only convert sparse neighbor lists to dense ones.') + + receivers, senders = neighbor.idx + mask = neighbor_list_mask(neighbor) + + receivers = receivers[mask] + senders = senders[mask] + + N = len(neighbor.reference_position) + count = ops.segment_sum(jnp.ones(len(receivers), i32), receivers, N) + max_count = jnp.max(count) + offset = jnp.tile(jnp.arange(max_count), N)[:len(senders)] + hashes = senders * max_count + offset + dense_idx = N * jnp.ones((N * max_count,), i32) + dense_idx = dense_idx.at[hashes].set(receivers).reshape((N, max_count)) + return dense_idx + + +Dense = NeighborListFormat.Dense +Sparse = NeighborListFormat.Sparse +OrderedSparse = NeighborListFormat.OrderedSparse \ No newline at end of file diff --git a/jax_sph/jax_md/space.py b/jax_sph/jax_md/space.py new file mode 100644 index 0000000..22bba01 --- /dev/null +++ b/jax_sph/jax_md/space.py @@ -0,0 +1,452 @@ +# Source: https://github.com/jax-md/jax-md +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Spaces in which particles are simulated. + +Spaces are pairs of functions containing: + `displacement_fn(Ra, Rb, **kwargs)`: + Computes displacements between pairs of particles. `Ra` and `Rb` should + be ndarrays of shape `[spatial_dim]`. Returns an ndarray of shape `[spatial_dim]`. + To compute the displacement over more than one particle at a time see the + :meth:`map_product`, :meth:`map_bond`, and :meth:`map_neighbor` functions. + `shift_fn(R, dR, **kwargs)`: + Moves points at position `R` by an amount `dR`. + +Spaces can accept keyword arguments allowing the space to be changed over the +course of a simulation. For an example of this use see :meth:`periodic_general`. + +Although displacement functions are compute the displacement between two +points, it is often useful to compute displacements between multiple particles +in a vectorized fashion. To do this we provide three functions: `map_product`, +`map_bond`, and `map_neighbor`: + map_product: + Computes displacements between all pairs of points such that if + `Ra` has shape `[n, spatial_dim]` and `Rb` has shape `[m, spatial_dim]` then the + output has shape `[n, m, spatial_dim]`. + map_bond: + Computes displacements between all points in a list such that if + `Ra` has shape `[n, spatial_dim]` and `Rb` has shape `[m, spatial_dim]` then the + output has shape `[n, spatial_dim]`. + map_neighbor: + Computes displacements between points and all of their + neighbors such that if `Ra` has shape `[n, spatial_dim]` and `Rb` has shape + `[n, neighbors, spatial_dim]` then the output has shape + `[n, neighbors, spatial_dim]`. +""" + +from typing import Callable, Optional, Tuple, Union + +import jax.numpy as jnp +from jax import custom_jvp, eval_shape, vmap +from jax.core import ShapedArray + +from jax_sph.jax_md.util import Array, f32, safe_mask + +# Types + + +DisplacementFn = Callable[[Array, Array], Array] +MetricFn = Callable[[Array, Array], float] +DisplacementOrMetricFn = Union[DisplacementFn, MetricFn] + +ShiftFn = Callable[[Array, Array], Array] + +Space = Tuple[DisplacementFn, ShiftFn] +Box = Array + + +# Exceptions + + +class UnexpectedBoxException(Exception): + pass + + +# Primitive Spatial Transforms + + +def inverse(box: Box) -> Box: + """Compute the inverse of an affine transformation.""" + if jnp.isscalar(box) or box.size == 1: + return 1 / box + elif box.ndim == 1: + return 1 / box + elif box.ndim == 2: + return jnp.linalg.inv(box) + raise ValueError(('Box must be either: a scalar, a vector, or a matrix. ' + f'Found {box}.')) + + +def _get_free_indices(n: int) -> str: + return ''.join([chr(ord('a') + i) for i in range(n)]) + + +def raw_transform(box: Box, R: Array) -> Array: + """Apply an affine transformation to positions. + + See `periodic_general` for a description of the semantics of `box`. + + Args: + box: An affine transformation described in `periodic_general`. + R: Array of positions. Should have shape `(..., spatial_dimension)`. + + Returns: + A transformed array positions of shape `(..., spatial_dimension)`. + """ + if jnp.isscalar(box) or box.size == 1: + return R * box + elif box.ndim == 1: + indices = _get_free_indices(R.ndim - 1) + 'i' + return jnp.einsum(f'i,{indices}->{indices}', box, R) + elif box.ndim == 2: + free_indices = _get_free_indices(R.ndim - 1) + left_indices = free_indices + 'j' + right_indices = free_indices + 'i' + return jnp.einsum(f'ij,{left_indices}->{right_indices}', box, R) + raise ValueError(('Box must be either: a scalar, a vector, or a matrix. ' + f'Found {box}.')) + + +@custom_jvp +def transform(box: Box, R: Array) -> Array: + """Apply an affine transformation to positions. + + See `periodic_general` for a description of the semantics of `box`. + + Args: + box: An affine transformation described in `periodic_general`. + R: Array of positions. Should have shape `(..., spatial_dimension)`. + + Returns: + A transformed array positions of shape `(..., spatial_dimension)`. + """ + return raw_transform(box, R) + + +@transform.defjvp +def transform_jvp(primals, tangents): + box, R = primals + dbox, dR = tangents + return (transform(box, R), dR + transform(dbox, R)) + + +def pairwise_displacement(Ra: Array, Rb: Array) -> Array: + """Compute a matrix of pairwise displacements given two sets of positions. + + Args: + Ra: Vector of positions; `ndarray(shape=[spatial_dim])`. + Rb: Vector of positions; `ndarray(shape=[spatial_dim])`. + + Returns: + Matrix of displacements; `ndarray(shape=[spatial_dim])`. + """ + if len(Ra.shape) != 1: + msg = ( + 'Can only compute displacements between vectors. To compute ' + 'displacements between sets of vectors use vmap or TODO.' + ) + raise ValueError(msg) + + if Ra.shape != Rb.shape: + msg = 'Can only compute displacement between vectors of equal dimension.' + raise ValueError(msg) + + return Ra - Rb + + +def periodic_displacement(side: Box, dR: Array) -> Array: + """Wraps displacement vectors into a hypercube. + + Args: + side: Specification of hypercube size. Either, + (a) float if all sides have equal length. + (b) ndarray(spatial_dim) if sides have different lengths. + dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. + Returns: + Matrix of wrapped displacements; `ndarray(shape=[..., spatial_dim])`. + """ + return jnp.mod(dR + side * f32(0.5), side) - f32(0.5) * side + + +def square_distance(dR: Array) -> Array: + """Computes square distances. + + Args: + dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. + Returns: + Matrix of squared distances; `ndarray(shape=[...])`. + """ + return jnp.sum(dR ** 2, axis=-1) + + +def distance(dR: Array) -> Array: + """Computes distances. + + Args: + dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. + Returns: + Matrix of distances; `ndarray(shape=[...])`. + """ + dr = square_distance(dR) + return safe_mask(dr > 0, jnp.sqrt, dr) + + +def periodic_shift(side: Box, R: Array, dR: Array) -> Array: + """Shifts positions, wrapping them back within a periodic hypercube.""" + return jnp.mod(R + dR, side) + + +""" Spaces """ + + +def free() -> Space: + """Free boundary conditions.""" + def displacement_fn(Ra: Array, Rb: Array, perturbation: Optional[Array]=None, + **unused_kwargs) -> Array: + dR = pairwise_displacement(Ra, Rb) + if perturbation is not None: + dR = raw_transform(perturbation, dR) + return dR + def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: + return R + dR + return displacement_fn, shift_fn + + +def periodic(side: Box, wrapped: bool=True) -> Space: + """Periodic boundary conditions on a hypercube of sidelength side. + + Args: + side: Either a float or an ndarray of shape [spatial_dimension] specifying + the size of each side of the periodic box. + wrapped: A boolean specifying whether or not particle positions are + remapped back into the box after each step + Returns: + `(displacement_fn, shift_fn)` tuple. + """ + def displacement_fn(Ra: Array, Rb: Array, + perturbation: Optional[Array] = None, + **unused_kwargs) -> Array: + if 'box' in unused_kwargs: + raise UnexpectedBoxException(('`space.periodic` does not accept a box ' + 'argument. Perhaps you meant to use ' + '`space.periodic_general`?')) + dR = periodic_displacement(side, pairwise_displacement(Ra, Rb)) + if perturbation is not None: + dR = raw_transform(perturbation, dR) + return dR + if wrapped: + def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: + if 'box' in unused_kwargs: + raise UnexpectedBoxException(('`space.periodic` does not accept a box ' + 'argument. Perhaps you meant to use ' + '`space.periodic_general`?')) + + return periodic_shift(side, R, dR) + else: + def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: + if 'box' in unused_kwargs: + raise UnexpectedBoxException(('`space.periodic` does not accept a box ' + 'argument. Perhaps you meant to use ' + '`space.periodic_general`?')) + return R + dR + return displacement_fn, shift_fn + + +def periodic_general(box: Box, + fractional_coordinates: bool=True, + wrapped: bool=True) -> Space: + """Periodic boundary conditions on a parallelepiped. + + This function defines a simulation on a parallelepiped, :math:`X`, formed by + applying an affine transformation, :math:`T`, to the unit hypercube + :math:`U = [0, 1]^d` along with periodic boundary conditions across all + of the faces. + + Formally, the space is defined such that :math:`X = {Tu : u \in [0, 1]^d}`. + + The affine transformation, :math:`T`, can be specified in a number of different + ways. For a parallelepiped that is: 1) a cube of side length :math:`L`, the affine + transformation can simply be a scalar; 2) an orthorhombic unit cell can be + specified by a vector `[Lx, Ly, Lz]` of lengths for each axis; 3) a general + triclinic cell can be specified by an upper triangular matrix. + + There are a number of ways to parameterize a simulation on :math:`X`. + `periodic_general` supports two parametrizations of :math:`X` that can be selected + using the `fractional_coordinates` keyword argument. + + 1) When `fractional_coordinates=True`, particle positions are stored in the + unit cube, :math:`u\in U`. Here, the displacement function computes the + displacement between :math:`x, y \in X` as :math:`d_X(x, y) = Td_U(u, v)` where + :math:`d_U` is the displacement function on the unit cube, :math:`U`, :math:`x = Tu`, and + :math:`v = Tv` with :math:`u, v \in U`. The derivative of the displacement function + is defined so that derivatives live in :math:`X` (as opposed to being + backpropagated to :math:`U`). The shift function, `shift_fn(R, dR)` is defined + so that :math:`R` is expected to lie in :math:`U` while :math:`dR` should lie in :math:`X`. This + combination enables code such as `shift_fn(R, force_fn(R))` to work as + intended. + + 2) When `fractional_coordinates=False`, particle positions are stored in + the parallelepiped :math:`X`. Here, for :math:`x, y \in X`, the displacement function + is defined as :math:`d_X(x, y) = Td_U(T^{-1}x, T^{-1}y)`. Since there is an + extra multiplication by :math:`T^{-1}`, this parameterization is typically + slower than `fractional_coordinates=False`. As in 1), the displacement + function is defined to compute derivatives in :math:`X`. The shift function + is defined so that :math:`R` and :math:`dR` should both lie in :math:`X`. + + Example: + + .. code-block:: python + + from jax import random + side_length = 10.0 + disp_frac, shift_frac = periodic_general(side_length, + fractional_coordinates=True) + disp_real, shift_real = periodic_general(side_length, + fractional_coordinates=False) + + # Instantiate random positions in both parameterizations. + R_frac = random.uniform(random.PRNGKey(0), (4, 3)) + R_real = side_length * R_frac + + # Make some shift vectors. + dR = random.normal(random.PRNGKey(0), (4, 3)) + + disp_real(R_real[0], R_real[1]) == disp_frac(R_frac[0], R_frac[1]) + transform(side_length, shift_frac(R_frac, 1.0)) == shift_real(R_real, 1.0) + + It is often desirable to deform a simulation cell either: using a finite + deformation during a simulation, or using an infinitesimal deformation while + computing elastic constants. To do this using fractional coordinates, we can + supply a new affine transformation as `displacement_fn(Ra, Rb, box=new_box)`. + When using real coordinates, we can specify positions in a space :math:`X` defined + by an affine transformation :math:`T` and compute displacements in a deformed space + :math:`X'` defined by an affine transformation :math:`T'`. This is done by writing + `displacement_fn(Ra, Rb, new_box=new_box)`. + + There are a few caveats when using `periodic_general`. `periodic_general` + uses the minimum image convention, and so it will fail for potentials whose + cutoff is longer than the half of the side-length of the box. It will also + fail to find the correct image when the box is too deformed. We hope to add a + more robust box for small simulations soon (TODO) along with better error + checking. In the meantime caution is recommended. + + Args: + box: A `(spatial_dim, spatial_dim)` affine transformation. + fractional_coordinates: A boolean specifying whether positions are stored + in the parallelepiped or the unit cube. + wrapped: A boolean specifying whether or not particle positions are + remapped back into the box after each step + Returns: + `(displacement_fn, shift_fn)` tuple. + """ + inv_box = inverse(box) + + def displacement_fn(Ra, Rb, perturbation=None, **kwargs): + _box, _inv_box = box, inv_box + + if 'box' in kwargs: + _box = kwargs['box'] + + if not fractional_coordinates: + _inv_box = inverse(_box) + + if 'new_box' in kwargs: + _box = kwargs['new_box'] + + if not fractional_coordinates: + Ra = transform(_inv_box, Ra) + Rb = transform(_inv_box, Rb) + + dR = periodic_displacement(f32(1.0), pairwise_displacement(Ra, Rb)) + dR = transform(_box, dR) + + if perturbation is not None: + dR = raw_transform(perturbation, dR) + + return dR + + def u(R, dR): + if wrapped: + return periodic_shift(f32(1.0), R, dR) + return R + dR + + def shift_fn(R, dR, **kwargs): + if not fractional_coordinates and not wrapped: + return R + dR + + _box, _inv_box = box, inv_box + if 'box' in kwargs: + _box = kwargs['box'] + _inv_box = inverse(_box) + + if 'new_box' in kwargs: + _box = kwargs['new_box'] + + dR = transform(_inv_box, dR) + if not fractional_coordinates: + R = transform(_inv_box, R) + + R = u(R, dR) + + if not fractional_coordinates: + R = transform(_box, R) + return R + + return displacement_fn, shift_fn + + +def metric(displacement: DisplacementFn) -> MetricFn: + """Takes a displacement function and creates a metric.""" + return lambda Ra, Rb, **kwargs: distance(displacement(Ra, Rb, **kwargs)) + + +def map_product(metric_or_displacement: DisplacementOrMetricFn + ) -> DisplacementOrMetricFn: + """Vectorizes a metric or displacement function over all pairs.""" + return vmap(vmap(metric_or_displacement, (0, None), 0), (None, 0), 0) + + +def map_bond(metric_or_displacement: DisplacementOrMetricFn + ) -> DisplacementOrMetricFn: + """Vectorizes a metric or displacement function over bonds.""" + return vmap(metric_or_displacement, (0, 0), 0) + + +def map_neighbor(metric_or_displacement: DisplacementOrMetricFn + ) -> DisplacementOrMetricFn: + """Vectorizes a metric or displacement function over neighborhoods.""" + def wrapped_fn(Ra, Rb, **kwargs): + return vmap(vmap(metric_or_displacement, (0, None)))(Rb, Ra, **kwargs) + return wrapped_fn + + +def canonicalize_displacement_or_metric(displacement_or_metric): + """Checks whether or not a displacement or metric was provided.""" + for dim in range(1, 4): + try: + R = ShapedArray((dim,), f32) + dR_or_dr = eval_shape(displacement_or_metric, R, R, t=0) + if len(dR_or_dr.shape) == 0: + return displacement_or_metric + else: + return metric(displacement_or_metric) + except TypeError: + continue + except ValueError: + continue + raise ValueError( + 'Canonicalize displacement not implemented for spatial dimension larger' + 'than 4.') diff --git a/jax_sph/jax_md/util.py b/jax_sph/jax_md/util.py new file mode 100644 index 0000000..f74bb07 --- /dev/null +++ b/jax_sph/jax_md/util.py @@ -0,0 +1,43 @@ +# Source: https://github.com/jax-md/jax-md +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines utility functions.""" + +from functools import partial +from typing import Any + +import jax.numpy as jnp +import numpy as onp +from jax import jit + +Array = Any +PyTree = Any + +i16 = jnp.int16 +i32 = jnp.int32 +i64 = jnp.int64 + +f32 = jnp.float32 +f64 = jnp.float64 + + +@partial(jit, static_argnums=(1,)) +def safe_mask(mask, fn, operand, placeholder=0): + masked = jnp.where(mask, operand, 0) + return jnp.where(mask, fn(masked), placeholder) + +def is_array(x: Any) -> bool: + return isinstance(x, (jnp.ndarray, onp.ndarray)) \ No newline at end of file diff --git a/jax_sph/partition.py b/jax_sph/partition.py index d0256db..a2b33f5 100644 --- a/jax_sph/partition.py +++ b/jax_sph/partition.py @@ -9,8 +9,9 @@ import numpy as np import numpy as onp from jax import jit -from jax_md import space -from jax_md.partition import ( + +from jax_sph.jax_md import space +from jax_sph.jax_md.partition import ( MaskFn, NeighborFn, NeighborList, @@ -25,7 +26,7 @@ is_sparse, shift_array, ) -from jax_md.partition import neighbor_list as vmap_neighbor_list +from jax_sph.jax_md.partition import neighbor_list as vmap_neighbor_list PEC = PartitionErrorCode diff --git a/jax_sph/simulate.py b/jax_sph/simulate.py index 01e3263..0895d6c 100644 --- a/jax_sph/simulate.py +++ b/jax_sph/simulate.py @@ -5,13 +5,13 @@ import numpy as np from jax import jit -from jax_md.partition import Sparse from omegaconf import DictConfig, OmegaConf from jax_sph import partition from jax_sph.case_setup import load_case, set_relaxation from jax_sph.integrator import si_euler from jax_sph.io_state import io_setup, write_state +from jax_sph.jax_md.partition import Sparse from jax_sph.solver import WCSPH from jax_sph.utils import Logger, Tag diff --git a/jax_sph/solver.py b/jax_sph/solver.py index d5fb9d3..b96f086 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -4,9 +4,9 @@ import jax.numpy as jnp from jax import ops, vmap -from jax_md import space from jax_sph.eos import RIEMANNEoS, TaitEoS +from jax_sph.jax_md import space from jax_sph.kernel import ( CubicKernel, GaussianKernel, diff --git a/jax_sph/utils.py b/jax_sph/utils.py index a505cf4..fbff28b 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -7,11 +7,11 @@ import jax.numpy as jnp import numpy as np from jax import ops, vmap -from jax_md import partition, space from numpy import array from omegaconf import DictConfig from jax_sph.io_state import read_h5 +from jax_sph.jax_md import partition, space from jax_sph.kernel import QuinticKernel EPS = jnp.finfo(float).eps diff --git a/notebooks/iclr24_inverse.ipynb b/notebooks/iclr24_inverse.ipynb index a80ab31..fd7f4d7 100644 --- a/notebooks/iclr24_inverse.ipynb +++ b/notebooks/iclr24_inverse.ipynb @@ -46,8 +46,6 @@ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from jax import jit\n", - "from jax_md import space\n", - "from jax_md.partition import Sparse\n", "from omegaconf import OmegaConf\n", "\n", "from jax_sph import partition\n", @@ -55,6 +53,8 @@ "from jax_sph.defaults import defaults\n", "from jax_sph.integrator import si_euler\n", "from jax_sph.io_state import read_h5, write_h5\n", + "from jax_sph.jax_md import space\n", + "from jax_sph.jax_md.partition import Sparse\n", "from jax_sph.simulate import simulate\n", "from jax_sph.solver import WCSPH\n", "from jax_sph.utils import Tag\n" diff --git a/notebooks/iclr24_sitl.ipynb b/notebooks/iclr24_sitl.ipynb index 44d3571..19f680d 100644 --- a/notebooks/iclr24_sitl.ipynb +++ b/notebooks/iclr24_sitl.ipynb @@ -126,7 +126,8 @@ "import numpy as np\n", "import pyvista as pv\n", "from jax import vmap\n", - "from jax_md import space" + "\n", + "from jax_sph.jax_md import space" ] }, { @@ -235,7 +236,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/notebooks/iclr24_sitl.py b/notebooks/iclr24_sitl.py index ec0fe54..f5f449b 100644 --- a/notebooks/iclr24_sitl.py +++ b/notebooks/iclr24_sitl.py @@ -14,8 +14,6 @@ import jmp import numpy as np from jax import config -from jax_md import space -from jax_md.partition import Sparse from lagrangebench import GNS, Trainer, case_builder, infer from lagrangebench.defaults import defaults from lagrangebench.evaluate import averaged_metrics @@ -24,6 +22,8 @@ from jax_sph import partition from jax_sph.eos import TaitEoS +from jax_sph.jax_md import space +from jax_sph.jax_md.partition import Sparse from jax_sph.kernel import QuinticKernel from jax_sph.solver import WCSPH from jax_sph.utils import Tag diff --git a/notebooks/misc/dirichlet_energy.py b/notebooks/misc/dirichlet_energy.py index 9af2d06..8f94c7e 100644 --- a/notebooks/misc/dirichlet_energy.py +++ b/notebooks/misc/dirichlet_energy.py @@ -8,12 +8,12 @@ import jax.numpy as jnp import numpy as np from jax import ops, vmap -from jax_md import space -from jax_md.partition import Sparse from omegaconf import OmegaConf from jax_sph import partition from jax_sph.io_state import read_h5 +from jax_sph.jax_md import space +from jax_sph.jax_md.partition import Sparse from jax_sph.kernel import QuinticKernel, WendlandC2Kernel from jax_sph.utils import Tag, pos_init_cartesian_2d diff --git a/notebooks/tutorial.ipynb b/notebooks/tutorial.ipynb index 16110b5..87153dd 100644 --- a/notebooks/tutorial.ipynb +++ b/notebooks/tutorial.ipynb @@ -51,7 +51,6 @@ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from jax import jit\n", - "from jax_md.partition import Sparse\n", "from omegaconf import DictConfig, OmegaConf\n", "\n", "from jax_sph import partition\n", @@ -59,9 +58,10 @@ "from jax_sph.defaults import defaults\n", "from jax_sph.integrator import si_euler\n", "from jax_sph.io_state import io_setup, read_h5, write_state\n", + "from jax_sph.jax_md.partition import Sparse\n", "from jax_sph.solver import WCSPH\n", "from jax_sph.utils import Logger, Tag\n", - "from jax_sph.visualize import plt_ekin" + "from jax_sph.visualize import plt_ekin\n" ] }, { @@ -853,7 +853,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/poetry.lock b/poetry.lock index a8bf95d..9156892 100644 --- a/poetry.lock +++ b/poetry.lock @@ -82,25 +82,6 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - [[package]] name = "babel" version = "2.15.0" @@ -300,25 +281,6 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[[package]] -name = "chex" -version = "0.1.86" -description = "Chex: Testing made fun, in JAX!" -optional = false -python-versions = ">=3.9" -files = [ - {file = "chex-0.1.86-py3-none-any.whl", hash = "sha256:251c20821092323a3d9c28e1cf80e4a58180978bec368f531949bd9847eee568"}, - {file = "chex-0.1.86.tar.gz", hash = "sha256:e8b0f96330eba4144659e1617c0f7a57b161e8cbb021e55c6d5056c7378091d1"}, -] - -[package.dependencies] -absl-py = ">=0.9.0" -jax = ">=0.4.16" -jaxlib = ">=0.1.37" -numpy = ">=1.24.1" -toolz = ">=0.9.0" -typing-extensions = ">=4.2.0" - [[package]] name = "colorama" version = "0.4.6" @@ -347,17 +309,6 @@ traitlets = ">=4" [package.extras] test = ["pytest"] -[[package]] -name = "contextlib2" -version = "21.6.0" -description = "Backports and enhancements for the contextlib module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, - {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, -] - [[package]] name = "contourpy" version = "1.2.1" @@ -567,27 +518,6 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] -[[package]] -name = "dm-haiku" -version = "0.0.12" -description = "Haiku is a library for building neural networks in JAX." -optional = false -python-versions = "*" -files = [ - {file = "dm-haiku-0.0.12.tar.gz", hash = "sha256:ba0b3acf71433156737fe342c486da11727e5e6c9e054245f4f9b8f0b53eb608"}, - {file = "dm_haiku-0.0.12-py3-none-any.whl", hash = "sha256:7448a43a6486bff95253f84e18eacc607d9c1256592573117a9d1d23e2780706"}, -] - -[package.dependencies] -absl-py = ">=0.7.1" -flax = ">=0.7.1" -jmp = ">=0.0.2" -numpy = ">=1.18.0" -tabulate = ">=0.8.9" - -[package.extras] -jax = ["jax (>=0.4.24)", "jaxlib (>=0.4.24)"] - [[package]] name = "docutils" version = "0.18.1" @@ -599,38 +529,6 @@ files = [ {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, ] -[[package]] -name = "e3nn-jax" -version = "0.20.6" -description = "Equivariant convolutional neural networks for the group E(3) of 3 dimensional rotations, translations, and mirrors." -optional = false -python-versions = ">=3.9" -files = [ - {file = "e3nn-jax-0.20.6.tar.gz", hash = "sha256:c8cbff68826d78209418341766f6177240505b3b5d38d0c7b793b76b53626a07"}, - {file = "e3nn_jax-0.20.6-py3-none-any.whl", hash = "sha256:0f4dcd124695274608270a8a99599141c542c2317f70921ee0bdf35818a87c20"}, -] - -[package.dependencies] -attrs = "*" -jax = "*" -jaxlib = "*" -numpy = "*" -sympy = "*" - -[package.extras] -dev = ["dm-haiku", "equinox", "flax", "jraph", "kaleido", "nox", "optax", "plotly", "pytest", "s2fft", "tqdm"] - -[[package]] -name = "einops" -version = "0.8.0" -description = "A new flavour of deep learning operations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "einops-0.8.0-py3-none-any.whl", hash = "sha256:9572fb63046264a862693b0a87088af3bdc8c068fde03de63453cbbde245465f"}, - {file = "einops-0.8.0.tar.gz", hash = "sha256:63486517fed345712a8385c100cb279108d9d47e6ae59099b07657e983deae85"}, -] - [[package]] name = "equinox" version = "0.11.4" @@ -647,43 +545,6 @@ jax = ">=0.4.13" jaxtyping = ">=0.2.20" typing-extensions = ">=4.5.0" -[[package]] -name = "etils" -version = "1.5.2" -description = "Collection of common python utils" -optional = false -python-versions = ">=3.9" -files = [ - {file = "etils-1.5.2-py3-none-any.whl", hash = "sha256:6dc882d355e1e98a5d1a148d6323679dc47c9a5792939b9de72615aa4737eb0b"}, - {file = "etils-1.5.2.tar.gz", hash = "sha256:ba6a3e1aff95c769130776aa176c11540637f5dd881f3b79172a5149b6b1c446"}, -] - -[package.dependencies] -fsspec = {version = "*", optional = true, markers = "extra == \"epath\""} -importlib_resources = {version = "*", optional = true, markers = "extra == \"epath\""} -typing_extensions = {version = "*", optional = true, markers = "extra == \"epy\""} -zipp = {version = "*", optional = true, markers = "extra == \"epath\""} - -[package.extras] -all = ["etils[array-types]", "etils[eapp]", "etils[ecolab]", "etils[edc]", "etils[enp]", "etils[epath-gcs]", "etils[epath-s3]", "etils[epath]", "etils[epy]", "etils[etqdm]", "etils[etree-dm]", "etils[etree-jax]", "etils[etree-tf]", "etils[etree]"] -array-types = ["etils[enp]"] -dev = ["chex", "dataclass_array", "optree", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-subtests", "pytest-xdist", "torch"] -docs = ["etils[all,dev]", "sphinx-apitree[ext]"] -eapp = ["absl-py", "etils[epy]", "simple_parsing"] -ecolab = ["etils[enp]", "etils[epy]", "jupyter", "mediapy", "numpy", "packaging"] -edc = ["etils[epy]"] -enp = ["etils[epy]", "numpy"] -epath = ["etils[epy]", "fsspec", "importlib_resources", "typing_extensions", "zipp"] -epath-gcs = ["etils[epath]", "gcsfs"] -epath-s3 = ["etils[epath]", "s3fs"] -epy = ["typing_extensions"] -etqdm = ["absl-py", "etils[epy]", "tqdm"] -etree = ["etils[array-types]", "etils[enp]", "etils[epy]", "etils[etqdm]"] -etree-dm = ["dm-tree", "etils[etree]"] -etree-jax = ["etils[etree]", "jax[cpu]"] -etree-tf = ["etils[etree]", "tensorflow"] -lazy-imports = ["etils[ecolab]"] - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -728,35 +589,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "flax" -version = "0.8.4" -description = "Flax: A neural network library for JAX designed for flexibility" -optional = false -python-versions = ">=3.9" -files = [ - {file = "flax-0.8.4-py3-none-any.whl", hash = "sha256:785707e3a48f782a1bec17aa665697b7618c113a357d5f975791dcb090d818d8"}, - {file = "flax-0.8.4.tar.gz", hash = "sha256:968683f850198e1aa5eb2d9d1e20bead880ef7423c14f042db9d60848cb1c90b"}, -] - -[package.dependencies] -jax = ">=0.4.19" -msgpack = "*" -numpy = [ - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, - {version = ">=1.22", markers = "python_version < \"3.11\""}, -] -optax = "*" -orbax-checkpoint = "*" -PyYAML = ">=5.4.1" -rich = ">=11.1" -tensorstore = "*" -typing-extensions = ">=4.2" - -[package.extras] -all = ["matplotlib"] -testing = ["black[jupyter] (==23.7.0)", "clu", "clu (<=0.0.9)", "einops", "gymnasium[accept-rom-license,atari]", "jaxlib", "jraph (>=0.0.6dev0)", "ml-collections", "mypy", "nbstripout", "opencv-python", "penzai", "pytest", "pytest-cov", "pytest-custom-exit-code", "pytest-xdist", "pytype", "sentencepiece", "tensorflow", "tensorflow-datasets", "tensorflow-text (>=2.11.0)", "torch"] - [[package]] name = "fonttools" version = "4.53.0" @@ -822,45 +654,6 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.1.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] -[[package]] -name = "fsspec" -version = "2024.6.0" -description = "File-system specification" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fsspec-2024.6.0-py3-none-any.whl", hash = "sha256:58d7122eb8a1a46f7f13453187bfea4972d66bf01618d37366521b1998034cee"}, - {file = "fsspec-2024.6.0.tar.gz", hash = "sha256:f579960a56e6d8038a9efc8f9c77279ec12e6299aa86b0769a7e9c46b94527c2"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] -tqdm = ["tqdm"] - [[package]] name = "h5py" version = "3.11.0" @@ -1082,35 +875,6 @@ cuda12-pip = ["jaxlib (==0.4.28+cuda12.cudnn89)", "nvidia-cublas-cu12 (>=12.1.3. minimum-jaxlib = ["jaxlib (==0.4.27)"] tpu = ["jaxlib (==0.4.28)", "libtpu-nightly (==0.1.dev20240508)", "requests"] -[[package]] -name = "jax-md" -version = "0.2.8" -description = "Differentiable, Hardware Accelerated, Molecular Dynamics" -optional = false -python-versions = ">=3.9" -files = [] -develop = false - -[package.dependencies] -absl-py = "*" -dataclasses = "*" -dm-haiku = "*" -e3nn-jax = "*" -einops = "*" -flax = "*" -jax = "*" -jaxlib = "*" -jraph = "*" -ml_collections = "*" -numpy = "*" -optax = "*" - -[package.source] -type = "git" -url = "https://github.com/jax-md/jax-md.git" -reference = "c451353f6ddcab031f660befda256d8a4f657855" -resolved_reference = "c451353f6ddcab031f660befda256d8a4f657855" - [[package]] name = "jaxlib" version = "0.4.28" @@ -1215,23 +979,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jmp" -version = "0.0.4" -description = "JMP is a Mixed Precision library for JAX." -optional = false -python-versions = "*" -files = [ - {file = "jmp-0.0.4-py3-none-any.whl", hash = "sha256:6aa7adbddf2bd574b28c7faf6e81a735eb11f53386447896909c6968dc36807d"}, - {file = "jmp-0.0.4.tar.gz", hash = "sha256:5dfeb0fd7c7a9f72a70fff0aab9d0cbfae32a809c02f4037ff3485ceb33e1730"}, -] - -[package.dependencies] -numpy = ">=1.19.5" - -[package.extras] -jax = ["jax (>=0.2.20)", "jaxlib (>=0.1.71)"] - [[package]] name = "jraph" version = "0.0.6.dev0" @@ -1436,30 +1183,6 @@ files = [ {file = "looseversion-1.3.0.tar.gz", hash = "sha256:ebde65f3f6bb9531a81016c6fef3eb95a61181adc47b7f949e9c0ea47911669e"}, ] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "2.1.5" @@ -1636,33 +1359,6 @@ cli = ["argcomplete"] docs = ["atomman", "jupytext", "myst_nb", "nglview", "nglview (==3.0.8)", "numpydoc", "ovito", "pydata-sphinx-theme", "sphinx", "sphinx_copybutton", "sphinx_rtd_theme", "sphinxcontrib-spelling"] test = ["atomman", "ovito", "pytest", "pytest-subtests", "sympy"] -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "ml-collections" -version = "0.1.1" -description = "ML Collections is a library of Python collections designed for ML usecases." -optional = false -python-versions = ">=2.6" -files = [ - {file = "ml_collections-0.1.1.tar.gz", hash = "sha256:3fefcc72ec433aa1e5d32307a3e474bbb67f405be814ea52a2166bfc9dbe68cc"}, -] - -[package.dependencies] -absl-py = "*" -contextlib2 = "*" -PyYAML = "*" -six = "*" - [[package]] name = "ml-dtypes" version = "0.4.0" @@ -1699,89 +1395,6 @@ numpy = [ [package.extras] dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"] -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "msgpack" -version = "1.0.8" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1882,57 +1495,6 @@ numpy = ">=1.7" docs = ["numpydoc", "sphinx (==1.2.3)", "sphinx-rtd-theme", "sphinxcontrib-napoleon"] tests = ["pytest", "pytest-cov", "pytest-pep8"] -[[package]] -name = "optax" -version = "0.2.2" -description = "A gradient processing and optimisation library in JAX." -optional = false -python-versions = ">=3.9" -files = [ - {file = "optax-0.2.2-py3-none-any.whl", hash = "sha256:411c414a76aae259f4191a60b712663968741a5163ca92fc250b5d5c7d36fb57"}, - {file = "optax-0.2.2.tar.gz", hash = "sha256:f09bf790ef4b09fb9c35f79a07594c6196a719919985f542dc84b0bf97812e0e"}, -] - -[package.dependencies] -absl-py = ">=0.7.1" -chex = ">=0.1.86" -jax = ">=0.1.55" -jaxlib = ">=0.1.37" -numpy = ">=1.18.0" - -[package.extras] -docs = ["flax", "ipython (>=8.8.0)", "matplotlib (>=3.5.0)", "myst-nb (>=1.0.0)", "sphinx (>=6.0.0)", "sphinx-autodoc-typehints", "sphinx-book-theme (>=1.0.1)", "sphinx-collections (>=0.0.1)", "sphinx-gallery (>=0.14.0)", "sphinx_contributors", "sphinxcontrib-katex", "tensorflow (>=2.4.0)", "tensorflow-datasets (>=4.2.0)"] -dp-accounting = ["absl-py (>=1.0.0)", "attrs (>=21.4.0)", "mpmath (>=1.2.1)", "numpy (>=1.21.4)", "scipy (>=1.7.1)"] -examples = ["dp_accounting (>=0.4)", "flax", "tensorflow (>=2.4.0)", "tensorflow-datasets (>=4.2.0)"] -test = ["dm-tree (>=0.1.7)", "flax (>=0.5.3)"] - -[[package]] -name = "orbax-checkpoint" -version = "0.5.15" -description = "Orbax Checkpoint" -optional = false -python-versions = ">=3.9" -files = [ - {file = "orbax_checkpoint-0.5.15-py3-none-any.whl", hash = "sha256:658dd89bc925cecc584d89eaa19af9a7e16e3371377907eb713fbd59b85262e4"}, - {file = "orbax_checkpoint-0.5.15.tar.gz", hash = "sha256:15195e8d1b381b56f23a62a25599a3644f5d08655fa64f60bb1b938b8ffe7ef3"}, -] - -[package.dependencies] -absl-py = "*" -etils = {version = "*", extras = ["epath", "epy"]} -jax = ">=0.4.9" -jaxlib = "*" -msgpack = "*" -nest_asyncio = "*" -numpy = "*" -protobuf = "*" -pyyaml = "*" -tensorstore = ">=0.1.51" -typing_extensions = "*" - -[package.extras] -testing = ["flax", "google-cloud-logging", "mock", "pytest", "pytest-xdist"] - [[package]] name = "ott-jax" version = "0.4.6" @@ -2238,26 +1800,6 @@ files = [ [package.dependencies] wcwidth = "*" -[[package]] -name = "protobuf" -version = "5.27.1" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "protobuf-5.27.1-cp310-abi3-win32.whl", hash = "sha256:3adc15ec0ff35c5b2d0992f9345b04a540c1e73bfee3ff1643db43cc1d734333"}, - {file = "protobuf-5.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:25236b69ab4ce1bec413fd4b68a15ef8141794427e0b4dc173e9d5d9dffc3bcd"}, - {file = "protobuf-5.27.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4e38fc29d7df32e01a41cf118b5a968b1efd46b9c41ff515234e794011c78b17"}, - {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:917ed03c3eb8a2d51c3496359f5b53b4e4b7e40edfbdd3d3f34336e0eef6825a"}, - {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:ee52874a9e69a30271649be88ecbe69d374232e8fd0b4e4b0aaaa87f429f1631"}, - {file = "protobuf-5.27.1-cp38-cp38-win32.whl", hash = "sha256:7a97b9c5aed86b9ca289eb5148df6c208ab5bb6906930590961e08f097258107"}, - {file = "protobuf-5.27.1-cp38-cp38-win_amd64.whl", hash = "sha256:f6abd0f69968792da7460d3c2cfa7d94fd74e1c21df321eb6345b963f9ec3d8d"}, - {file = "protobuf-5.27.1-cp39-cp39-win32.whl", hash = "sha256:dfddb7537f789002cc4eb00752c92e67885badcc7005566f2c5de9d969d3282d"}, - {file = "protobuf-5.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:39309898b912ca6febb0084ea912e976482834f401be35840a008da12d189340"}, - {file = "protobuf-5.27.1-py3-none-any.whl", hash = "sha256:4ac7249a1530a2ed50e24201d6630125ced04b30619262f06224616e0030b6cf"}, - {file = "protobuf-5.27.1.tar.gz", hash = "sha256:df5e5b8e39b7d1c25b186ffdf9f44f40f810bbcc9d2b71d9d3156fee5a9adf15"}, -] - [[package]] name = "psutil" version = "5.9.8" @@ -2643,24 +2185,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "ruff" version = "0.4.8" @@ -2957,64 +2481,6 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] -[[package]] -name = "sympy" -version = "1.12.1" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sympy-1.12.1-py3-none-any.whl", hash = "sha256:9b2cbc7f1a640289430e13d2a56f02f867a1da0190f2f99d8968c2f74da0e515"}, - {file = "sympy-1.12.1.tar.gz", hash = "sha256:2877b03f998cd8c08f07cd0de5b767119cd3ef40d09f41c30d722f6686b0fb88"}, -] - -[package.dependencies] -mpmath = ">=1.1.0,<1.4.0" - -[[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[package.extras] -widechars = ["wcwidth"] - -[[package]] -name = "tensorstore" -version = "0.1.60" -description = "Read and write large, multi-dimensional arrays" -optional = false -python-versions = ">=3.9" -files = [ - {file = "tensorstore-0.1.60-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9e210c24b0cfcdd86f69e1592f3c76833939c1488506f33d8c9119ecb614e935"}, - {file = "tensorstore-0.1.60-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51d09d44c7f66fd714a728131784a71f4e8e00194e926a1cdd8dc8fc6c1ae483"}, - {file = "tensorstore-0.1.60-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b6a5ddd0b1f00c7b2ee6c490e55bebb2e93f39de742e89f264d6b7604d1a9a"}, - {file = "tensorstore-0.1.60-cp310-cp310-win_amd64.whl", hash = "sha256:5c9c7516f9369b3e1dd4ea10e05538d8c47927f169906568cd988604ea61d58c"}, - {file = "tensorstore-0.1.60-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c42177c2147861c233d0c09f9c16c24fd70e1cfbdf7e9193dcaa53a580b8f689"}, - {file = "tensorstore-0.1.60-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:944977cacedced54d9598f043bb6aa33ce2326ccc888a1cb0b60dd7b45dc438f"}, - {file = "tensorstore-0.1.60-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef59df52fd86b3cccf0061f19da37f9fab385641a330933cbce4c7aaf9b5baf3"}, - {file = "tensorstore-0.1.60-cp311-cp311-win_amd64.whl", hash = "sha256:8869a2ba9147f4ac36ede707a0251a95e4da093fc07508c4eba96088de0be4d7"}, - {file = "tensorstore-0.1.60-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:65677e21304fcf272557f195c597704f4ccf55b75314e68ece17bb1784cb59f7"}, - {file = "tensorstore-0.1.60-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:725d1f70c17838815704805d2853c636bb2d680424e81f91677a7defea68373b"}, - {file = "tensorstore-0.1.60-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c477a0e6948326c414ed1bcdab2949e975f0b4e7e449cce39e0fec14b273e1b2"}, - {file = "tensorstore-0.1.60-cp312-cp312-win_amd64.whl", hash = "sha256:32cba3cf0ae6dd03d504162b8ea387f140050e279cf23e7eced68d3c845693da"}, - {file = "tensorstore-0.1.60-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0919e69380904575314b05669319881d4fcfb8e7711fedf7df2b32929675a8ef"}, - {file = "tensorstore-0.1.60-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f6bfd4bf6de8415efce00baeedce8cec79ed568dfe9c1a93ab40fb054f025314"}, - {file = "tensorstore-0.1.60-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af95ea0f036f13145bb33068e623b0114cd7731c8847ace590757e6ac6b8995"}, - {file = "tensorstore-0.1.60-cp39-cp39-win_amd64.whl", hash = "sha256:4c1fd8ed823cd9e395860fb82c1602b5aba44866eb2bc0c9a358a750c6bd6df3"}, - {file = "tensorstore-0.1.60.tar.gz", hash = "sha256:88da8f1978982101b8dbb144fd29ee362e4e8c97fc595c4992d555f80ce62a79"}, -] - -[package.dependencies] -ml-dtypes = ">=0.3.1" -numpy = ">=1.16.0" - [[package]] name = "toml" version = "0.10.2" @@ -3037,17 +2503,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "toolz" -version = "0.12.1" -description = "List processing tools and functional utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, -] - [[package]] name = "tornado" version = "6.4.1" @@ -3227,4 +2682,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.11" -content-hash = "c5b1bbcfbb18730f6e573f9bbd35ee80e2be5e905618a17c3a465d58b0aa04ac" +content-hash = "98afca7167130fab482743ab1792bc6393190a7a4967eadc4449ed001b33e58d" diff --git a/pyproject.toml b/pyproject.toml index 051ecc5..fd2b88f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ pandas = ">=2.1.4" # for validation pyvista = ">=0.42.2" # for visualization jax = {version = "0.4.28", extras = ["cpu"]} jaxlib = "0.4.28" -jax-md = {git = "https://github.com/jax-md/jax-md.git", rev = "c451353f6ddcab031f660befda256d8a4f657855"} omegaconf = "^2.3.0" [tool.poetry.group.dev.dependencies] @@ -37,6 +36,11 @@ sphinx-exec-code = "0.12" sphinx-rtd-theme = "1.3.0" toml = "^0.10.2" +[tool.poetry.group.jaxmd.dependencies] +dataclasses = "0.6" +jraph = "^0.0.6.dev0" +absl-py = "^2.1.0" + [tool.ruff] ignore = ["F821", "E402"] exclude = [ @@ -59,7 +63,7 @@ select = [ [tool.pytest.ini_options] testpaths = "tests/" -addopts = "--cov=jax_sph --cov-fail-under=50" +addopts = "--cov=jax_sph --cov-fail-under=50 --ignore=jax_sph/jax_md" filterwarnings = [ # ignore all deprecation warnings except from jax-sph "ignore::DeprecationWarning:^(?!.*jax_sph).*" diff --git a/tests/test_neighbors.py b/tests/test_neighbors.py index 9e2ca71..d84fbdb 100644 --- a/tests/test_neighbors.py +++ b/tests/test_neighbors.py @@ -6,9 +6,9 @@ config.update("jax_enable_x64", True) import jax.numpy as jnp from jax import jit -from jax_md import space from jax_sph import partition +from jax_sph.jax_md import space @jit From 3866123223b043d2eb12f1c164b2e2826ef67827 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 03:19:10 +0200 Subject: [PATCH 03/21] lint --- jax_sph/jax_md/dataclasses.py | 88 +- jax_sph/jax_md/partition.py | 1925 +++++++++++++++++---------------- jax_sph/jax_md/space.py | 646 +++++------ jax_sph/jax_md/util.py | 9 +- 4 files changed, 1379 insertions(+), 1289 deletions(-) diff --git a/jax_sph/jax_md/dataclasses.py b/jax_sph/jax_md/dataclasses.py index af37fdd..3e973dc 100644 --- a/jax_sph/jax_md/dataclasses.py +++ b/jax_sph/jax_md/dataclasses.py @@ -1,5 +1,5 @@ # Source: https://github.com/jax-md/jax-md -# +# # Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,54 +22,54 @@ """ import dataclasses + import jax def dataclass(clz): - """Create a class which can be passed to functional transformations. - - Jax transformations such as `jax.jit` and `jax.grad` require objects that are - immutable and can be mapped over using the `jax.tree_util` methods. - - The `dataclass` decorator makes it easy to define custom classes that can be - passed safely to Jax. - - Args: - clz: the class that will be transformed by the decorator. - Returns: - The new class. - """ - clz.set = lambda self, **kwargs: dataclasses.replace(self, **kwargs) - data_clz = dataclasses.dataclass(frozen=True)(clz) - meta_fields = [] - data_fields = [] - for name, field_info in data_clz.__dataclass_fields__.items(): - is_static = field_info.metadata.get('static', False) - if is_static: - meta_fields.append(name) - else: - data_fields.append(name) - - def iterate_clz(x): - meta = tuple(getattr(x, name) for name in meta_fields) - data = tuple(getattr(x, name) for name in data_fields) - return data, meta - - def clz_from_iterable(meta, data): - meta_args = tuple(zip(meta_fields, meta)) - data_args = tuple(zip(data_fields, data)) - kwargs = dict(meta_args + data_args) - return data_clz(**kwargs) - - jax.tree_util.register_pytree_node(data_clz, - iterate_clz, - clz_from_iterable) - - return data_clz + """Create a class which can be passed to functional transformations. + + Jax transformations such as `jax.jit` and `jax.grad` require objects that are + immutable and can be mapped over using the `jax.tree_util` methods. + + The `dataclass` decorator makes it easy to define custom classes that can be + passed safely to Jax. + + Args: + clz: the class that will be transformed by the decorator. + Returns: + The new class. + """ + clz.set = lambda self, **kwargs: dataclasses.replace(self, **kwargs) + data_clz = dataclasses.dataclass(frozen=True)(clz) + meta_fields = [] + data_fields = [] + for name, field_info in data_clz.__dataclass_fields__.items(): + is_static = field_info.metadata.get("static", False) + if is_static: + meta_fields.append(name) + else: + data_fields.append(name) + + def iterate_clz(x): + meta = tuple(getattr(x, name) for name in meta_fields) + data = tuple(getattr(x, name) for name in data_fields) + return data, meta + + def clz_from_iterable(meta, data): + meta_args = tuple(zip(meta_fields, meta)) + data_args = tuple(zip(data_fields, data)) + kwargs = dict(meta_args + data_args) + return data_clz(**kwargs) + + jax.tree_util.register_pytree_node(data_clz, iterate_clz, clz_from_iterable) + + return data_clz def static_field(): - return dataclasses.field(metadata={'static': True}) + return dataclasses.field(metadata={"static": True}) + replace = dataclasses.replace asdict = dataclasses.asdict @@ -77,5 +77,7 @@ def static_field(): is_dataclass = dataclasses.is_dataclass fields = dataclasses.fields field = dataclasses.field + + def unpack(dc) -> tuple: - return tuple(getattr(dc, field.name) for field in dataclasses.fields(dc)) \ No newline at end of file + return tuple(getattr(dc, field.name) for field in dataclasses.fields(dc)) diff --git a/jax_sph/jax_md/partition.py b/jax_sph/jax_md/partition.py index 9dd6617..97cc911 100644 --- a/jax_sph/jax_md/partition.py +++ b/jax_sph/jax_md/partition.py @@ -1,5 +1,5 @@ # Source: https://github.com/jax-md/jax-md -# +# # Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -52,1027 +52,1088 @@ @dataclasses.dataclass class CellList: - """Stores the spatial partition of a system into a cell list. - - See :meth:`cell_list` for details on the construction / specification. - Cell list buffers all have a common shape, S, where - * `S = [cell_count_x, cell_count_y, cell_capacity]` - * `S = [cell_count_x, cell_count_y, cell_count_z, cell_capacity]` - in two- and three-dimensions respectively. It is assumed that each cell has - the same capacity. - - Attributes: - position_buffer: An ndarray of floating point positions with shape - `S + [spatial_dimension]`. - id_buffer: An ndarray of int32 particle ids of shape `S`. Note that empty - slots are specified by `id = N` where `N` is the number of particles in - the system. - named_buffer: A dictionary of ndarrays of shape `S + [...]`. This contains - side data placed into the cell list. - did_buffer_overflow: A boolean specifying whether or not the cell list - exceeded the maximum allocated capacity. - cell_capacity: An integer specifying the maximum capacity of each cell in - the cell list. - update_fn: A function that updates the cell list at a fixed capacity. - """ - position_buffer: Array - id_buffer: Array - named_buffer: Dict[str, Array] - - did_buffer_overflow: Array - - cell_capacity: int = dataclasses.static_field() - cell_size: float = dataclasses.static_field() - - update_fn: Callable[..., 'CellList'] = \ - dataclasses.static_field() - - def update(self, position: Array, **kwargs) -> 'CellList': - cl_data = (self.cell_capacity, self.did_buffer_overflow, self.update_fn) - return self.update_fn(position, cl_data, **kwargs) - - @property - def kwarg_buffers(self): - logging.warning('kwarg_buffers renamed to named_buffer. The name ' - 'kwarg_buffers will be depricated.') - return self.named_buffer + """Stores the spatial partition of a system into a cell list. + + See :meth:`cell_list` for details on the construction / specification. + Cell list buffers all have a common shape, S, where + * `S = [cell_count_x, cell_count_y, cell_capacity]` + * `S = [cell_count_x, cell_count_y, cell_count_z, cell_capacity]` + in two- and three-dimensions respectively. It is assumed that each cell has + the same capacity. + + Attributes: + position_buffer: An ndarray of floating point positions with shape + `S + [spatial_dimension]`. + id_buffer: An ndarray of int32 particle ids of shape `S`. Note that empty + slots are specified by `id = N` where `N` is the number of particles in + the system. + named_buffer: A dictionary of ndarrays of shape `S + [...]`. This contains + side data placed into the cell list. + did_buffer_overflow: A boolean specifying whether or not the cell list + exceeded the maximum allocated capacity. + cell_capacity: An integer specifying the maximum capacity of each cell in + the cell list. + update_fn: A function that updates the cell list at a fixed capacity. + """ + + position_buffer: Array + id_buffer: Array + named_buffer: Dict[str, Array] + + did_buffer_overflow: Array + + cell_capacity: int = dataclasses.static_field() + cell_size: float = dataclasses.static_field() + + update_fn: Callable[..., "CellList"] = dataclasses.static_field() + + def update(self, position: Array, **kwargs) -> "CellList": + cl_data = (self.cell_capacity, self.did_buffer_overflow, self.update_fn) + return self.update_fn(position, cl_data, **kwargs) + + @property + def kwarg_buffers(self): + logging.warning( + "kwarg_buffers renamed to named_buffer. The name " + "kwarg_buffers will be depricated." + ) + return self.named_buffer @dataclasses.dataclass class CellListFns: - allocate: Callable[..., CellList] = dataclasses.static_field() - update: Callable[[Array, Union[CellList, int]], - CellList] = dataclasses.static_field() - - def __iter__(self): - return iter((self.allocate, self.update)) - - -def _cell_dimensions(spatial_dimension: int, - box_size: Box, - minimum_cell_size: float) -> Tuple[Box, Array, Array, int]: - """Compute the number of cells-per-side and total number of cells in a box.""" - if isinstance(box_size, int) or isinstance(box_size, float): - box_size = float(box_size) - - # NOTE(schsam): Should we auto-cast based on box_size? I can't imagine a case - # in which the box_size would not be accurately represented by an f32. - if (isinstance(box_size, onp.ndarray) and - (box_size.dtype == i32 or box_size.dtype == i64)): - box_size = float(box_size) - - cells_per_side = onp.floor(box_size / minimum_cell_size) - cell_size = box_size / cells_per_side - cells_per_side = onp.array(cells_per_side, dtype=i32) - - if isinstance(box_size, (onp.ndarray, jnp.ndarray)): - if box_size.ndim == 1 or box_size.ndim == 2: - assert box_size.size == spatial_dimension - flat_cells_per_side = onp.reshape(cells_per_side, (-1,)) - for cells in flat_cells_per_side: - if cells < 3: - msg = ('Box must be at least 3x the size of the grid spacing in each ' - 'dimension.') - raise ValueError(msg) - cell_count = reduce(mul, flat_cells_per_side, 1) - elif box_size.ndim == 0: - cell_count = cells_per_side ** spatial_dimension + allocate: Callable[..., CellList] = dataclasses.static_field() + update: Callable[ + [Array, Union[CellList, int]], CellList + ] = dataclasses.static_field() + + def __iter__(self): + return iter((self.allocate, self.update)) + + +def _cell_dimensions( + spatial_dimension: int, box_size: Box, minimum_cell_size: float +) -> Tuple[Box, Array, Array, int]: + """Compute the number of cells-per-side and total number of cells in a box.""" + if isinstance(box_size, (int, float)): + box_size = float(box_size) + + # NOTE(schsam): Should we auto-cast based on box_size? I can't imagine a case + # in which the box_size would not be accurately represented by an f32. + if isinstance(box_size, onp.ndarray) and ( + box_size.dtype == i32 or box_size.dtype == i64 + ): + box_size = float(box_size) + + cells_per_side = onp.floor(box_size / minimum_cell_size) + cell_size = box_size / cells_per_side + cells_per_side = onp.array(cells_per_side, dtype=i32) + + if isinstance(box_size, (onp.ndarray, jnp.ndarray)): + if box_size.ndim == 1 or box_size.ndim == 2: + assert box_size.size == spatial_dimension + flat_cells_per_side = onp.reshape(cells_per_side, (-1,)) + for cells in flat_cells_per_side: + if cells < 3: + msg = ( + "Box must be at least 3x the size of the grid spacing in each " + "dimension." + ) + raise ValueError(msg) + cell_count = reduce(mul, flat_cells_per_side, 1) + elif box_size.ndim == 0: + cell_count = cells_per_side**spatial_dimension + else: + raise ValueError( + ( + "Box must be either: a scalar, a vector, or a matrix. " + f"Found {box_size}." + ) + ) else: - raise ValueError(('Box must be either: a scalar, a vector, or a matrix. ' - f'Found {box_size}.')) - else: - cell_count = cells_per_side ** spatial_dimension + cell_count = cells_per_side**spatial_dimension - return box_size, cell_size, cells_per_side, int(cell_count) + return box_size, cell_size, cells_per_side, int(cell_count) -def count_cell_filling(position: Array, - box_size: Box, - minimum_cell_size: float) -> Array: - """Counts the number of particles per-cell in a spatial partition.""" - dim = int(position.shape[1]) - box_size, cell_size, cells_per_side, cell_count = \ - _cell_dimensions(dim, box_size, minimum_cell_size) +def count_cell_filling( + position: Array, box_size: Box, minimum_cell_size: float +) -> Array: + """Counts the number of particles per-cell in a spatial partition.""" + dim = int(position.shape[1]) + box_size, cell_size, cells_per_side, cell_count = _cell_dimensions( + dim, box_size, minimum_cell_size + ) - hash_multipliers = _compute_hash_constants(dim, cells_per_side) + hash_multipliers = _compute_hash_constants(dim, cells_per_side) - particle_index = jnp.array(position / cell_size, dtype=i32) - particle_hash = jnp.sum(particle_index * hash_multipliers, axis=1) + particle_index = jnp.array(position / cell_size, dtype=i32) + particle_hash = jnp.sum(particle_index * hash_multipliers, axis=1) - filling = ops.segment_sum(jnp.ones_like(particle_hash), - particle_hash, - cell_count) - return filling + filling = ops.segment_sum(jnp.ones_like(particle_hash), particle_hash, cell_count) + return filling -def _compute_hash_constants(spatial_dimension: int, - cells_per_side: Array) -> Array: - if cells_per_side.size == 1: - return jnp.array([[cells_per_side ** d for d in range(spatial_dimension)]], - dtype=i32) - elif cells_per_side.size == spatial_dimension: - one = jnp.array([[1]], dtype=i32) - cells_per_side = jnp.concatenate((one, cells_per_side[:, :-1]), axis=1) - return jnp.array(jnp.cumprod(cells_per_side), dtype=i32) - else: - raise ValueError() +def _compute_hash_constants(spatial_dimension: int, cells_per_side: Array) -> Array: + if cells_per_side.size == 1: + return jnp.array( + [[cells_per_side**d for d in range(spatial_dimension)]], dtype=i32 + ) + elif cells_per_side.size == spatial_dimension: + one = jnp.array([[1]], dtype=i32) + cells_per_side = jnp.concatenate((one, cells_per_side[:, :-1]), axis=1) + return jnp.array(jnp.cumprod(cells_per_side), dtype=i32) + else: + raise ValueError() def _neighboring_cells(dimension: int) -> Generator[onp.ndarray, None, None]: - for dindex in onp.ndindex(*([3] * dimension)): - yield onp.array(dindex, dtype=i32) - 1 + for dindex in onp.ndindex(*([3] * dimension)): + yield onp.array(dindex, dtype=i32) - 1 -def _estimate_cell_capacity(position: Array, - box_size: Box, - cell_size: float, - buffer_size_multiplier: float) -> int: - cell_capacity = onp.max(count_cell_filling(position, box_size, cell_size)) - return int(cell_capacity * buffer_size_multiplier) +def _estimate_cell_capacity( + position: Array, box_size: Box, cell_size: float, buffer_size_multiplier: float +) -> int: + cell_capacity = onp.max(count_cell_filling(position, box_size, cell_size)) + return int(cell_capacity * buffer_size_multiplier) def shift_array(arr: Array, dindex: Array) -> Array: - if len(dindex) == 2: - dx, dy = dindex - dz = 0 - elif len(dindex) == 3: - dx, dy, dz = dindex - - if dx < 0: - arr = jnp.concatenate((arr[1:], arr[:1])) - elif dx > 0: - arr = jnp.concatenate((arr[-1:], arr[:-1])) - - if dy < 0: - arr = jnp.concatenate((arr[:, 1:], arr[:, :1]), axis=1) - elif dy > 0: - arr = jnp.concatenate((arr[:, -1:], arr[:, :-1]), axis=1) - - if dz < 0: - arr = jnp.concatenate((arr[:, :, 1:], arr[:, :, :1]), axis=2) - elif dz > 0: - arr = jnp.concatenate((arr[:, :, -1:], arr[:, :, :-1]), axis=2) - - return arr - - -def unflatten_cell_buffer(arr: Array, - cells_per_side: Array, - dim: int) -> Array: - if (isinstance(cells_per_side, int) or - isinstance(cells_per_side, float) or - (util.is_array(cells_per_side) and not cells_per_side.shape)): - cells_per_side = (int(cells_per_side),) * dim - elif util.is_array(cells_per_side) and len(cells_per_side.shape) == 1: - cells_per_side = tuple([int(x) for x in cells_per_side[::-1]]) - elif util.is_array(cells_per_side) and len(cells_per_side.shape) == 2: - cells_per_side = tuple([int(x) for x in cells_per_side[0][::-1]]) - else: - raise ValueError() - return jnp.reshape(arr, cells_per_side + (-1,) + arr.shape[1:]) - - -def cell_list(box_size: Box, - minimum_cell_size: float, - buffer_size_multiplier: float = 1.25 - ) -> CellListFns: - r"""Returns a function that partitions point data spatially. - - Given a set of points :math:`\{x_i \in R^d\}` with associated data - :math:`\{k_i \in R^m\}` it is often useful to partition the points / data - spatially. A simple partitioning that can be implemented efficiently within - XLA is a dense partition into a uniform grid called a cell list. - - Since XLA requires that shapes be statically specified inside of a JIT block, - the cell list code can operate in two modes: allocation and update. - - Allocation creates a new cell list that uses a set of input positions to - estimate the capacity of the cell list. This capacity can be adjusted by - setting the `buffer_size_multiplier` or setting the `extra_capacity`. - Allocation cannot be JIT. - - Updating takes a previously allocated cell list and places a new set of - particles in the cells. Updating cannot resize the cell list and is therefore - compatible with JIT. However, if the configuration has changed substantially - it is possible that the existing cell list won't be large enough to - accommodate all of the particles. In this case the `did_buffer_overflow` bit - will be set to True. - - Args: - box_size: A float or an ndarray of shape `[spatial_dimension]` specifying - the size of the system. Note, this code is written for the case where the - boundaries are periodic. If this is not the case, then the current code - will be slightly less efficient. - minimum_cell_size: A float specifying the minimum side length of each cell. - Cells are enlarged so that they exactly fill the box. - buffer_size_multiplier: A floating point multiplier that multiplies the - estimated cell capacity to allow for fluctuations in the maximum cell - occupancy. - Returns: - A `CellListFns` object that contains two methods, one to allocate the cell - list and one to update the cell list. The update function can be called - with either a cell list from which the capacity can be inferred or with - an explicit integer denoting the capacity. Note that an existing cell list - can also be updated by calling `cell_list.update(position)`. - """ - - if util.is_array(box_size): - box_size = onp.array(box_size) - if len(box_size.shape) == 1: - box_size = onp.reshape(box_size, (1, -1)) - - if util.is_array(minimum_cell_size): - minimum_cell_size = onp.array(minimum_cell_size) - - def cell_list_fn(position: Array, - capacity_overflow_update: Optional[ - Tuple[int, bool, Callable[..., CellList]]] = None, - extra_capacity: int = 0, **kwargs) -> CellList: - N = position.shape[0] - dim = position.shape[1] - - if dim != 2 and dim != 3: - # NOTE(schsam): Do we want to check this in compute_fn as well? - raise ValueError( - f'Cell list spatial dimension must be 2 or 3. Found {dim}.') - - _, cell_size, cells_per_side, cell_count = \ - _cell_dimensions(dim, box_size, minimum_cell_size) - - if capacity_overflow_update is None: - cell_capacity = _estimate_cell_capacity(position, box_size, cell_size, - buffer_size_multiplier) - cell_capacity += extra_capacity - overflow = False - update_fn = cell_list_fn + if len(dindex) == 2: + dx, dy = dindex + dz = 0 + elif len(dindex) == 3: + dx, dy, dz = dindex + + if dx < 0: + arr = jnp.concatenate((arr[1:], arr[:1])) + elif dx > 0: + arr = jnp.concatenate((arr[-1:], arr[:-1])) + + if dy < 0: + arr = jnp.concatenate((arr[:, 1:], arr[:, :1]), axis=1) + elif dy > 0: + arr = jnp.concatenate((arr[:, -1:], arr[:, :-1]), axis=1) + + if dz < 0: + arr = jnp.concatenate((arr[:, :, 1:], arr[:, :, :1]), axis=2) + elif dz > 0: + arr = jnp.concatenate((arr[:, :, -1:], arr[:, :, :-1]), axis=2) + + return arr + + +def unflatten_cell_buffer(arr: Array, cells_per_side: Array, dim: int) -> Array: + if ( + isinstance(cells_per_side, (int, float)) + or util.is_array(cells_per_side) + and not cells_per_side.shape + ): + cells_per_side = (int(cells_per_side),) * dim + elif util.is_array(cells_per_side) and len(cells_per_side.shape) == 1: + cells_per_side = tuple([int(x) for x in cells_per_side[::-1]]) + elif util.is_array(cells_per_side) and len(cells_per_side.shape) == 2: + cells_per_side = tuple([int(x) for x in cells_per_side[0][::-1]]) else: - cell_capacity, overflow, update_fn = capacity_overflow_update + raise ValueError() + return jnp.reshape(arr, cells_per_side + (-1,) + arr.shape[1:]) - hash_multipliers = _compute_hash_constants(dim, cells_per_side) - # Create cell list data. - particle_id = lax.iota(i32, N) - # NOTE(schsam): We use the convention that particles that are successfully, - # copied have their true id whereas particles empty slots have id = N. - # Then when we copy data back from the grid, copy it to an array of shape - # [N + 1, output_dimension] and then truncate it to an array of shape - # [N, output_dimension] which ignores the empty slots. - cell_position = jnp.zeros((cell_count * cell_capacity, dim), - dtype=position.dtype) - cell_id = N * jnp.ones((cell_count * cell_capacity, 1), dtype=i32) - - # It might be worth adding an occupied mask. However, that will involve - # more compute since often we will do a mask for species that will include - # an occupancy test. It seems easier to design around this empty_data_value - # for now and revisit the issue if it comes up later. - empty_kwarg_value = 10 ** 5 - cell_kwargs = {} - # pytype: disable=attribute-error - for k, v in kwargs.items(): - if not util.is_array(v): - raise ValueError((f'Data must be specified as an ndarray. Found "{k}" ' - f'with type {type(v)}.')) - if v.shape[0] != position.shape[0]: - raise ValueError(('Data must be specified per-particle (an ndarray ' - f'with shape ({N}, ...)). Found "{k}" with ' - f'shape {v.shape}.')) - kwarg_shape = v.shape[1:] if v.ndim > 1 else (1,) - cell_kwargs[k] = empty_kwarg_value * jnp.ones( - (cell_count * cell_capacity,) + kwarg_shape, v.dtype) - # pytype: enable=attribute-error - indices = jnp.array(position / cell_size, dtype=i32) - hashes = jnp.sum(indices * hash_multipliers, axis=1) - - # Copy the particle data into the grid. Here we use a trick to allow us to - # copy into all cells simultaneously using a single lax.scatter call. To do - # this we first sort particles by their cell hash. We then assign each - # particle to have a cell id = hash * cell_capacity + grid_id where - # grid_id is a flat list that repeats 0, .., cell_capacity. So long as - # there are fewer than cell_capacity particles per cell, each particle is - # guaranteed to get a cell id that is unique. - sort_map = jnp.argsort(hashes) - sorted_position = position[sort_map] - sorted_hash = hashes[sort_map] - sorted_id = particle_id[sort_map] - - sorted_kwargs = {} - for k, v in kwargs.items(): - sorted_kwargs[k] = v[sort_map] - - sorted_cell_id = jnp.mod(lax.iota(i32, N), cell_capacity) - sorted_cell_id = sorted_hash * cell_capacity + sorted_cell_id - - cell_position = cell_position.at[sorted_cell_id].set(sorted_position) - sorted_id = jnp.reshape(sorted_id, (N, 1)) - cell_id = cell_id.at[sorted_cell_id].set(sorted_id) - cell_position = unflatten_cell_buffer(cell_position, cells_per_side, dim) - cell_id = unflatten_cell_buffer(cell_id, cells_per_side, dim) - - for k, v in sorted_kwargs.items(): - if v.ndim == 1: - v = jnp.reshape(v, v.shape + (1,)) - cell_kwargs[k] = cell_kwargs[k].at[sorted_cell_id].set(v) - cell_kwargs[k] = unflatten_cell_buffer( - cell_kwargs[k], cells_per_side, dim) - - occupancy = ops.segment_sum(jnp.ones_like(hashes), hashes, cell_count) - max_occupancy = jnp.max(occupancy) - overflow = overflow | (max_occupancy > cell_capacity) - - return CellList(cell_position, cell_id, cell_kwargs, - overflow, cell_capacity, cell_size, update_fn) # pytype: disable=wrong-arg-count - - def allocate_fn(position: Array, extra_capacity: int = 0, **kwargs - ) -> CellList: - return cell_list_fn(position, extra_capacity=extra_capacity, **kwargs) - - def update_fn(position: Array, cl_or_capacity: Union[CellList, int], **kwargs - ) -> CellList: - if isinstance(cl_or_capacity, int): - capacity = int(cl_or_capacity) - return cell_list_fn(position, (capacity, False, cell_list_fn), **kwargs) - cl = cl_or_capacity - cl_data = (cl.cell_capacity, cl.did_buffer_overflow, cl.update_fn) - return cell_list_fn(position, cl_data, **kwargs) - - return CellListFns(allocate_fn, update_fn) # pytype: disable=wrong-arg-count +def cell_list( + box_size: Box, minimum_cell_size: float, buffer_size_multiplier: float = 1.25 +) -> CellListFns: + r"""Returns a function that partitions point data spatially. + + Given a set of points :math:`\{x_i \in R^d\}` with associated data + :math:`\{k_i \in R^m\}` it is often useful to partition the points / data + spatially. A simple partitioning that can be implemented efficiently within + XLA is a dense partition into a uniform grid called a cell list. + + Since XLA requires that shapes be statically specified inside of a JIT block, + the cell list code can operate in two modes: allocation and update. + + Allocation creates a new cell list that uses a set of input positions to + estimate the capacity of the cell list. This capacity can be adjusted by + setting the `buffer_size_multiplier` or setting the `extra_capacity`. + Allocation cannot be JIT. + + Updating takes a previously allocated cell list and places a new set of + particles in the cells. Updating cannot resize the cell list and is therefore + compatible with JIT. However, if the configuration has changed substantially + it is possible that the existing cell list won't be large enough to + accommodate all of the particles. In this case the `did_buffer_overflow` bit + will be set to True. + + Args: + box_size: A float or an ndarray of shape `[spatial_dimension]` specifying + the size of the system. Note, this code is written for the case where the + boundaries are periodic. If this is not the case, then the current code + will be slightly less efficient. + minimum_cell_size: A float specifying the minimum side length of each cell. + Cells are enlarged so that they exactly fill the box. + buffer_size_multiplier: A floating point multiplier that multiplies the + estimated cell capacity to allow for fluctuations in the maximum cell + occupancy. + Returns: + A `CellListFns` object that contains two methods, one to allocate the cell + list and one to update the cell list. The update function can be called + with either a cell list from which the capacity can be inferred or with + an explicit integer denoting the capacity. Note that an existing cell list + can also be updated by calling `cell_list.update(position)`. + """ + + if util.is_array(box_size): + box_size = onp.array(box_size) + if len(box_size.shape) == 1: + box_size = onp.reshape(box_size, (1, -1)) + + if util.is_array(minimum_cell_size): + minimum_cell_size = onp.array(minimum_cell_size) + + def cell_list_fn( + position: Array, + capacity_overflow_update: Optional[ + Tuple[int, bool, Callable[..., CellList]] + ] = None, + extra_capacity: int = 0, + **kwargs, + ) -> CellList: + N = position.shape[0] + dim = position.shape[1] + + if dim != 2 and dim != 3: + # NOTE(schsam): Do we want to check this in compute_fn as well? + raise ValueError( + f"Cell list spatial dimension must be 2 or 3. Found {dim}." + ) + + _, cell_size, cells_per_side, cell_count = _cell_dimensions( + dim, box_size, minimum_cell_size + ) + + if capacity_overflow_update is None: + cell_capacity = _estimate_cell_capacity( + position, box_size, cell_size, buffer_size_multiplier + ) + cell_capacity += extra_capacity + overflow = False + update_fn = cell_list_fn + else: + cell_capacity, overflow, update_fn = capacity_overflow_update + + hash_multipliers = _compute_hash_constants(dim, cells_per_side) + + # Create cell list data. + particle_id = lax.iota(i32, N) + # NOTE(schsam): We use the convention that particles that are successfully, + # copied have their true id whereas particles empty slots have id = N. + # Then when we copy data back from the grid, copy it to an array of shape + # [N + 1, output_dimension] and then truncate it to an array of shape + # [N, output_dimension] which ignores the empty slots. + cell_position = jnp.zeros( + (cell_count * cell_capacity, dim), dtype=position.dtype + ) + cell_id = N * jnp.ones((cell_count * cell_capacity, 1), dtype=i32) + + # It might be worth adding an occupied mask. However, that will involve + # more compute since often we will do a mask for species that will include + # an occupancy test. It seems easier to design around this empty_data_value + # for now and revisit the issue if it comes up later. + empty_kwarg_value = 10**5 + cell_kwargs = {} + # pytype: disable=attribute-error + for k, v in kwargs.items(): + if not util.is_array(v): + raise ValueError( + ( + f'Data must be specified as an ndarray. Found "{k}" ' + f"with type {type(v)}." + ) + ) + if v.shape[0] != position.shape[0]: + raise ValueError( + ( + "Data must be specified per-particle (an ndarray " + f'with shape ({N}, ...)). Found "{k}" with ' + f"shape {v.shape}." + ) + ) + kwarg_shape = v.shape[1:] if v.ndim > 1 else (1,) + cell_kwargs[k] = empty_kwarg_value * jnp.ones( + (cell_count * cell_capacity,) + kwarg_shape, v.dtype + ) + # pytype: enable=attribute-error + indices = jnp.array(position / cell_size, dtype=i32) + hashes = jnp.sum(indices * hash_multipliers, axis=1) + + # Copy the particle data into the grid. Here we use a trick to allow us to + # copy into all cells simultaneously using a single lax.scatter call. To do + # this we first sort particles by their cell hash. We then assign each + # particle to have a cell id = hash * cell_capacity + grid_id where + # grid_id is a flat list that repeats 0, .., cell_capacity. So long as + # there are fewer than cell_capacity particles per cell, each particle is + # guaranteed to get a cell id that is unique. + sort_map = jnp.argsort(hashes) + sorted_position = position[sort_map] + sorted_hash = hashes[sort_map] + sorted_id = particle_id[sort_map] + + sorted_kwargs = {} + for k, v in kwargs.items(): + sorted_kwargs[k] = v[sort_map] + + sorted_cell_id = jnp.mod(lax.iota(i32, N), cell_capacity) + sorted_cell_id = sorted_hash * cell_capacity + sorted_cell_id + + cell_position = cell_position.at[sorted_cell_id].set(sorted_position) + sorted_id = jnp.reshape(sorted_id, (N, 1)) + cell_id = cell_id.at[sorted_cell_id].set(sorted_id) + cell_position = unflatten_cell_buffer(cell_position, cells_per_side, dim) + cell_id = unflatten_cell_buffer(cell_id, cells_per_side, dim) + + for k, v in sorted_kwargs.items(): + if v.ndim == 1: + v = jnp.reshape(v, v.shape + (1,)) + cell_kwargs[k] = cell_kwargs[k].at[sorted_cell_id].set(v) + cell_kwargs[k] = unflatten_cell_buffer(cell_kwargs[k], cells_per_side, dim) + + occupancy = ops.segment_sum(jnp.ones_like(hashes), hashes, cell_count) + max_occupancy = jnp.max(occupancy) + overflow = overflow | (max_occupancy > cell_capacity) + + return CellList( + cell_position, + cell_id, + cell_kwargs, + overflow, + cell_capacity, + cell_size, + update_fn, + ) # pytype: disable=wrong-arg-count + + def allocate_fn(position: Array, extra_capacity: int = 0, **kwargs) -> CellList: + return cell_list_fn(position, extra_capacity=extra_capacity, **kwargs) + + def update_fn( + position: Array, cl_or_capacity: Union[CellList, int], **kwargs + ) -> CellList: + if isinstance(cl_or_capacity, int): + capacity = int(cl_or_capacity) + return cell_list_fn(position, (capacity, False, cell_list_fn), **kwargs) + cl = cl_or_capacity + cl_data = (cl.cell_capacity, cl.did_buffer_overflow, cl.update_fn) + return cell_list_fn(position, cl_data, **kwargs) + + return CellListFns(allocate_fn, update_fn) # pytype: disable=wrong-arg-count # Neighbor Lists class PartitionErrorCode(IntEnum): - """An enum specifying different error codes. - - Attributes: - NONE: Means that no error was encountered during simulation. - NEIGHBOR_LIST_OVERFLOW: Indicates that the neighbor list was not large - enough to contain all of the particles. This should indicate that it is - necessary to allocate a new neighbor list. - CELL_LIST_OVERFLOW: Indicates that the cell list was not large enough to - contain all of the particles. This should indicate that it is necessary - to allocate a new cell list. - CELL_SIZE_TOO_SMALL: Indicates that the size of cells in a cell list was - not large enough to properly capture particle interactions. This - indicates that it is necessary to allcoate a new cell list with larger - cells. - MALFORMED_BOX: Indicates that a box matrix was not properly upper - triangular. - """ - NONE = 0 - NEIGHBOR_LIST_OVERFLOW = 1 << 0 - CELL_LIST_OVERFLOW = 1 << 1 - CELL_SIZE_TOO_SMALL = 1 << 2 - MALFORMED_BOX = 1 << 3 + """An enum specifying different error codes. + + Attributes: + NONE: Means that no error was encountered during simulation. + NEIGHBOR_LIST_OVERFLOW: Indicates that the neighbor list was not large + enough to contain all of the particles. This should indicate that it is + necessary to allocate a new neighbor list. + CELL_LIST_OVERFLOW: Indicates that the cell list was not large enough to + contain all of the particles. This should indicate that it is necessary + to allocate a new cell list. + CELL_SIZE_TOO_SMALL: Indicates that the size of cells in a cell list was + not large enough to properly capture particle interactions. This + indicates that it is necessary to allcoate a new cell list with larger + cells. + MALFORMED_BOX: Indicates that a box matrix was not properly upper + triangular. + """ + + NONE = 0 + NEIGHBOR_LIST_OVERFLOW = 1 << 0 + CELL_LIST_OVERFLOW = 1 << 1 + CELL_SIZE_TOO_SMALL = 1 << 2 + MALFORMED_BOX = 1 << 3 + + PEC = PartitionErrorCode @dataclasses.dataclass class PartitionError: - """A struct containing error codes while building / updating neighbor lists. + """A struct containing error codes while building / updating neighbor lists. - Attributes: - code: An array storing the error code. See `PartitionErrorCode` for - details. - """ - code: Array + Attributes: + code: An array storing the error code. See `PartitionErrorCode` for + details. + """ - def update(self, bit: bytes, pred: Array) -> Array: - """Possibly adds an error based on a predicate.""" - zero = jnp.zeros((), jnp.uint8) - bit = jnp.array(bit, dtype=jnp.uint8) - return PartitionError(self.code | jnp.where(pred, bit, zero)) + code: Array - def __str__(self) -> str: - """Produces a string representation of the error code.""" - if not jnp.any(self.code): - return '' + def update(self, bit: bytes, pred: Array) -> Array: + """Possibly adds an error based on a predicate.""" + zero = jnp.zeros((), jnp.uint8) + bit = jnp.array(bit, dtype=jnp.uint8) + return PartitionError(self.code | jnp.where(pred, bit, zero)) - if jnp.any(self.code & PEC.NEIGHBOR_LIST_OVERFLOW): - return 'Partition Error: Neighbor list buffer overflow.' + def __str__(self) -> str: + """Produces a string representation of the error code.""" + if not jnp.any(self.code): + return "" - if jnp.any(self.code & PEC.CELL_LIST_OVERFLOW): - return 'Partition Error: Cell list buffer overflow' + if jnp.any(self.code & PEC.NEIGHBOR_LIST_OVERFLOW): + return "Partition Error: Neighbor list buffer overflow." - if jnp.any(self.code & PEC.CELL_SIZE_TOO_SMALL): - return 'Partition Error: Cell size too small' + if jnp.any(self.code & PEC.CELL_LIST_OVERFLOW): + return "Partition Error: Cell list buffer overflow" - if jnp.any(self.code & PEC.MALFORMED_BOX): - return ('Partition Error: Incorrect box format. Expecting upper ' - 'triangular.') + if jnp.any(self.code & PEC.CELL_SIZE_TOO_SMALL): + return "Partition Error: Cell size too small" - raise ValueError(f'Unexpected Error Code {self.code}.') + if jnp.any(self.code & PEC.MALFORMED_BOX): + return ( + "Partition Error: Incorrect box format. Expecting upper " "triangular." + ) - __repr__ = __str__ + raise ValueError(f"Unexpected Error Code {self.code}.") + __repr__ = __str__ def _displacement_or_metric_to_metric_sq( - displacement_or_metric: DisplacementOrMetricFn) -> MetricFn: - """Checks whether or not a displacement or metric was provided.""" - for dim in range(1, 4): - try: - R = ShapedArray((dim,), f32) - dR_or_dr = eval_shape(displacement_or_metric, R, R, t=0) - if len(dR_or_dr.shape) == 0: - return lambda Ra, Rb, **kwargs: \ - displacement_or_metric(Ra, Rb, **kwargs) ** 2 - else: - return lambda Ra, Rb, **kwargs: space.square_distance( - displacement_or_metric(Ra, Rb, **kwargs)) - except TypeError: - continue - except ValueError: - continue - raise ValueError( - 'Canonicalize displacement not implemented for spatial dimension larger' - 'than 4.') + displacement_or_metric: DisplacementOrMetricFn, +) -> MetricFn: + """Checks whether or not a displacement or metric was provided.""" + for dim in range(1, 4): + try: + R = ShapedArray((dim,), f32) + dR_or_dr = eval_shape(displacement_or_metric, R, R, t=0) + if len(dR_or_dr.shape) == 0: + return ( + lambda Ra, Rb, **kwargs: displacement_or_metric(Ra, Rb, **kwargs) + ** 2 + ) + else: + return lambda Ra, Rb, **kwargs: space.square_distance( + displacement_or_metric(Ra, Rb, **kwargs) + ) + except TypeError: + continue + except ValueError: + continue + raise ValueError( + "Canonicalize displacement not implemented for spatial dimension larger" + "than 4." + ) def _cell_size(box, minimum_cell_size) -> Array: - cells_per_side = jnp.floor(box / minimum_cell_size) - return box / cells_per_side + cells_per_side = jnp.floor(box / minimum_cell_size) + return box / cells_per_side def _fractional_cell_size(box, cutoff): - if jnp.isscalar(box) or box.ndim == 0: - return cutoff / box - elif box.ndim == 1: - return cutoff / jnp.min(box) - elif box.ndim == 2: - if box.shape[0] == 1: - return 1 / jnp.floor(box[0, 0] / cutoff) - elif box.shape[0] == 2: - xx = box[0, 0] - yy = box[1, 1] - xy = box[0, 1] / yy - - nx = xx / jnp.sqrt(1 + xy**2) - ny = yy - - nmin = jnp.floor(jnp.min(jnp.array([nx, ny])) / cutoff) - nmin = jnp.where(nmin == 0, 1, nmin) - return 1 / nmin - elif box.shape[0] == 3: - xx = box[0, 0] - yy = box[1, 1] - zz = box[2, 2] - xy = box[0, 1] / yy - xz = box[0, 2] / zz - yz = box[1, 2] / zz - - nx = xx / jnp.sqrt(1 + xy**2 + (xy * yz - xz)**2) - ny = yy / jnp.sqrt(1 + yz**2) - nz = zz - - nmin = jnp.floor(jnp.min(jnp.array([nx, ny, nz])) / cutoff) - nmin = jnp.where(nmin == 0, 1, nmin) - return 1 / nmin + if jnp.isscalar(box) or box.ndim == 0: + return cutoff / box + elif box.ndim == 1: + return cutoff / jnp.min(box) + elif box.ndim == 2: + if box.shape[0] == 1: + return 1 / jnp.floor(box[0, 0] / cutoff) + elif box.shape[0] == 2: + xx = box[0, 0] + yy = box[1, 1] + xy = box[0, 1] / yy + + nx = xx / jnp.sqrt(1 + xy**2) + ny = yy + + nmin = jnp.floor(jnp.min(jnp.array([nx, ny])) / cutoff) + nmin = jnp.where(nmin == 0, 1, nmin) + return 1 / nmin + elif box.shape[0] == 3: + xx = box[0, 0] + yy = box[1, 1] + zz = box[2, 2] + xy = box[0, 1] / yy + xz = box[0, 2] / zz + yz = box[1, 2] / zz + + nx = xx / jnp.sqrt(1 + xy**2 + (xy * yz - xz) ** 2) + ny = yy / jnp.sqrt(1 + yz**2) + nz = zz + + nmin = jnp.floor(jnp.min(jnp.array([nx, ny, nz])) / cutoff) + nmin = jnp.where(nmin == 0, 1, nmin) + return 1 / nmin + else: + raise ValueError( + "Expected box to be either 1-, 2-, or 3-dimensional " + f"found {box.shape[0]}" + ) else: - raise ValueError('Expected box to be either 1-, 2-, or 3-dimensional ' - f'found {box.shape[0]}') - else: - raise ValueError('Expected box to be either a scalar, a vector, or a ' - f'matrix. Found {type(box)}.') + raise ValueError( + "Expected box to be either a scalar, a vector, or a " + f"matrix. Found {type(box)}." + ) class NeighborListFormat(Enum): - """An enum listing the different neighbor list formats. - - Attributes: - Dense: A dense neighbor list where the ids are a square matrix - of shape `(N, max_neighbors_per_atom)`. Here the capacity of the neighbor - list must scale with the highest connectivity neighbor. - Sparse: A sparse neighbor list where the ids are a rectangular - matrix of shape `(2, max_neighbors)` specifying the start / end particle - of each neighbor pair. - OrderedSparse: A sparse neighbor list whose format is the same as `Sparse` - where only bonds with i < j are included. - """ - Dense = 0 - Sparse = 1 - OrderedSparse = 2 + """An enum listing the different neighbor list formats. + + Attributes: + Dense: A dense neighbor list where the ids are a square matrix + of shape `(N, max_neighbors_per_atom)`. Here the capacity of the neighbor + list must scale with the highest connectivity neighbor. + Sparse: A sparse neighbor list where the ids are a rectangular + matrix of shape `(2, max_neighbors)` specifying the start / end particle + of each neighbor pair. + OrderedSparse: A sparse neighbor list whose format is the same as `Sparse` + where only bonds with i < j are included. + """ + + Dense = 0 + Sparse = 1 + OrderedSparse = 2 def is_sparse(fmt: NeighborListFormat) -> bool: - return (fmt is NeighborListFormat.Sparse or - fmt is NeighborListFormat.OrderedSparse) + return fmt is NeighborListFormat.Sparse or fmt is NeighborListFormat.OrderedSparse def is_format_valid(fmt: NeighborListFormat): - if fmt not in list(NeighborListFormat): - raise ValueError(( - 'Neighbor list format must be a member of NeighborListFormat' - f' found {fmt}.')) + if fmt not in list(NeighborListFormat): + raise ValueError( + ( + "Neighbor list format must be a member of NeighborListFormat" + f" found {fmt}." + ) + ) def is_box_valid(box: Array) -> bool: - if jnp.isscalar(box) or box.ndim == 0 or box.ndim == 1: - return True - if box.ndim == 2: - return jnp.triu(box) == box - return False + if jnp.isscalar(box) or box.ndim == 0 or box.ndim == 1: + return True + if box.ndim == 2: + return jnp.triu(box) == box + return False @dataclasses.dataclass class NeighborList: - """A struct containing the state of a Neighbor List. - - Attributes: - idx: For an N particle system this is an `[N, max_occupancy]` array of - integers such that `idx[i, j]` is the j-th neighbor of particle i. - reference_position: The positions of particles when the neighbor list was - constructed. This is used to decide whether the neighbor list ought to be - updated. - error: An error code that is used to identify errors that occured during - neighbor list construction. See `PartitionError` and `PartitionErrorCode` - for details. - cell_list_capacity: An optional integer specifying the capacity of the cell - list used as an intermediate step in the creation of the neighbor list. - max_occupancy: A static integer specifying the maximum size of the - neighbor list. Changing this will invoke a recompilation. - format: A NeighborListFormat enum specifying the format of the neighbor - list. - cell_size: A float specifying the current minimum size of the cells used - in cell list construction. - cell_list_fn: The function used to construct the cell list. - update_fn: A static python function used to update the neighbor list. - """ - idx: Array - reference_position: Array - error: PartitionError - cell_list_capacity: Optional[int] = dataclasses.static_field() - max_occupancy: int = dataclasses.static_field() - - format: NeighborListFormat = dataclasses.static_field() - cell_size: Optional[float] = dataclasses.static_field() - cell_list_fn: Callable[[Array, CellList], - CellList] = dataclasses.static_field() - update_fn: Callable[[Array, 'NeighborList'], - 'NeighborList'] = dataclasses.static_field() - - def update(self, position: Array, **kwargs) -> 'NeighborList': - return self.update_fn(position, self, **kwargs) - - @property - def did_buffer_overflow(self) -> bool: - return self.error.code & (PEC.NEIGHBOR_LIST_OVERFLOW | - PEC.CELL_LIST_OVERFLOW) - - @property - def cell_size_too_small(self) -> bool: - return self.error.code & PEC.CELL_SIZE_TOO_SMALL - - @property - def malformed_box(self) -> bool: - return self.error.code & PEC.MALFORMED_BOX + """A struct containing the state of a Neighbor List. + + Attributes: + idx: For an N particle system this is an `[N, max_occupancy]` array of + integers such that `idx[i, j]` is the j-th neighbor of particle i. + reference_position: The positions of particles when the neighbor list was + constructed. This is used to decide whether the neighbor list ought to be + updated. + error: An error code that is used to identify errors that occured during + neighbor list construction. See `PartitionError` and `PartitionErrorCode` + for details. + cell_list_capacity: An optional integer specifying the capacity of the cell + list used as an intermediate step in the creation of the neighbor list. + max_occupancy: A static integer specifying the maximum size of the + neighbor list. Changing this will invoke a recompilation. + format: A NeighborListFormat enum specifying the format of the neighbor + list. + cell_size: A float specifying the current minimum size of the cells used + in cell list construction. + cell_list_fn: The function used to construct the cell list. + update_fn: A static python function used to update the neighbor list. + """ + + idx: Array + reference_position: Array + error: PartitionError + cell_list_capacity: Optional[int] = dataclasses.static_field() + max_occupancy: int = dataclasses.static_field() + + format: NeighborListFormat = dataclasses.static_field() + cell_size: Optional[float] = dataclasses.static_field() + cell_list_fn: Callable[[Array, CellList], CellList] = dataclasses.static_field() + update_fn: Callable[ + [Array, "NeighborList"], "NeighborList" + ] = dataclasses.static_field() + + def update(self, position: Array, **kwargs) -> "NeighborList": + return self.update_fn(position, self, **kwargs) + + @property + def did_buffer_overflow(self) -> bool: + return self.error.code & (PEC.NEIGHBOR_LIST_OVERFLOW | PEC.CELL_LIST_OVERFLOW) + + @property + def cell_size_too_small(self) -> bool: + return self.error.code & PEC.CELL_SIZE_TOO_SMALL + + @property + def malformed_box(self) -> bool: + return self.error.code & PEC.MALFORMED_BOX @dataclasses.dataclass class NeighborListFns: - """A struct containing functions to allocate and update neighbor lists. - - Attributes: - allocate: A function to allocate a new neighbor list. This function cannot - be compiled, since it uses the values of positions to infer the shapes. - update: A function to update a neighbor list given a new set of positions - and a previously allocated neighbor list. - """ - allocate: Callable[..., NeighborList] = dataclasses.static_field() - update: Callable[[Array, NeighborList], - NeighborList] = dataclasses.static_field() - - def __call__(self, - position: Array, - neighbors: Optional[NeighborList] = None, - extra_capacity: int = 0, - **kwargs) -> NeighborList: - """A function for backward compatibility with previous neighbor lists. + """A struct containing functions to allocate and update neighbor lists. + + Attributes: + allocate: A function to allocate a new neighbor list. This function cannot + be compiled, since it uses the values of positions to infer the shapes. + update: A function to update a neighbor list given a new set of positions + and a previously allocated neighbor list. + """ + + allocate: Callable[..., NeighborList] = dataclasses.static_field() + update: Callable[[Array, NeighborList], NeighborList] = dataclasses.static_field() + + def __call__( + self, + position: Array, + neighbors: Optional[NeighborList] = None, + extra_capacity: int = 0, + **kwargs, + ) -> NeighborList: + """A function for backward compatibility with previous neighbor lists. + + Args: + position: An `(N, dim)` array of particle positions. + neighbors: An optional neighbor list object. If it is provided then + the function updates the neighbor list, otherwise it allocates a new + neighbor list. + extra_capacity: Extra capacity to add if allocating the neighbor list. + Returns: + A neighbor list object. + """ + logging.warning( + "Using a deprecated code path to create / update neighbor " + "lists. It will be removed in a later version of JAX MD. " + "Using `neighbor_fn.allocate` and `neighbor_fn.update` " + "is preferred." + ) + if neighbors is None: + return self.allocate(position, extra_capacity, **kwargs) + return self.update(position, neighbors, **kwargs) + + def __iter__(self): + return iter((self.allocate, self.update)) + + +NeighborFn = Callable[[Array, Optional[NeighborList], Optional[int]], NeighborList] + + +def neighbor_list( + displacement_or_metric: DisplacementOrMetricFn, + box: Box, + r_cutoff: float, + dr_threshold: float = 0.0, + capacity_multiplier: float = 1.25, + disable_cell_list: bool = False, + mask_self: bool = True, + custom_mask_function: Optional[MaskFn] = None, + fractional_coordinates: bool = False, + format: NeighborListFormat = NeighborListFormat.Dense, + **static_kwargs, +) -> NeighborFn: + """Returns a function that builds a list neighbors for collections of points. + + Neighbor lists must balance the need to be jit compatible with the fact that + under a jit the maximum number of neighbors cannot change (owing to static + shape requirements). To deal with this, our `neighbor_list` returns a + `NeighborListFns` object that contains two functions: 1) + `neighbor_fn.allocate` create a new neighbor list and 2) `neighbor_fn.update` + updates an existing neighbor list. Neighbor lists themselves additionally + have a convenience `update` member function. + + Note that allocation of a new neighbor list cannot be jit compiled since it + uses the positions to infer the maximum number of neighbors (along with + additional space specified by the `capacity_multiplier`). Updating the + neighbor list can be jit compiled; if the neighbor list capacity is not + sufficient to store all the neighbors, the `did_buffer_overflow` bit + will be set to `True` and a new neighbor list will need to be reallocated. + + Here is a typical example of a simulation loop with neighbor lists: + + .. code-block:: python + + init_fn, apply_fn = simulate.nve(energy_fn, shift, 1e-3) + exact_init_fn, exact_apply_fn = simulate.nve(exact_energy_fn, shift, 1e-3) + + nbrs = neighbor_fn.allocate(R) + state = init_fn(random.PRNGKey(0), R, neighbor_idx=nbrs.idx) + + def body_fn(i, state): + state, nbrs = state + nbrs = nbrs.update(state.position) + state = apply_fn(state, neighbor_idx=nbrs.idx) + return state, nbrs + + step = 0 + for _ in range(20): + new_state, nbrs = lax.fori_loop(0, 100, body_fn, (state, nbrs)) + if nbrs.did_buffer_overflow: + nbrs = neighbor_fn.allocate(state.position) + else: + state = new_state + step += 1 Args: - position: An `(N, dim)` array of particle positions. - neighbors: An optional neighbor list object. If it is provided then - the function updates the neighbor list, otherwise it allocates a new - neighbor list. - extra_capacity: Extra capacity to add if allocating the neighbor list. + displacement: A function `d(R_a, R_b)` that computes the displacement + between pairs of points. + box: Either a float specifying the size of the box, an array of + shape `[spatial_dim]` specifying the box size for a cubic box in each + spatial dimension, or a matrix of shape `[spatial_dim, spatial_dim]` that + is _upper triangular_ and specifies the lattice vectors of the box. + r_cutoff: A scalar specifying the neighborhood radius. + dr_threshold: A scalar specifying the maximum distance particles can move + before rebuilding the neighbor list. + capacity_multiplier: A floating point scalar specifying the fractional + increase in maximum neighborhood occupancy we allocate compared with the + maximum in the example positions. + disable_cell_list: An optional boolean. If set to `True` then the neighbor + list is constructed using only distances. This can be useful for + debugging but should generally be left as `False`. + mask_self: An optional boolean. Determines whether points can consider + themselves to be their own neighbors. + custom_mask_function: An optional function. Takes the neighbor array + and masks selected elements. Note: The input array to the function is + `(n_particles, m)` where the index of particle 1 is in index in the first + dimension of the array, the index of particle 2 is given by the value in + the array + fractional_coordinates: An optional boolean. Specifies whether positions + will be supplied in fractional coordinates in the unit cube, :math:`[0, 1]^d`. + If this is set to True then the `box_size` will be set to `1.0` and the + cell size used in the cell list will be set to `cutoff / box_size`. + format: The format of the neighbor list; see the :meth:`NeighborListFormat` enum + for details about the different choices for formats. Defaults to `Dense`. + **static_kwargs: kwargs that get threaded through the calculation of + example positions. Returns: - A neighbor list object. + A NeighborListFns object that contains a method to allocate a new neighbor + list and a method to update an existing neighbor list. """ - logging.warning('Using a deprecated code path to create / update neighbor ' - 'lists. It will be removed in a later version of JAX MD. ' - 'Using `neighbor_fn.allocate` and `neighbor_fn.update` ' - 'is preferred.') - if neighbors is None: - return self.allocate(position, extra_capacity, **kwargs) - return self.update(position, neighbors, **kwargs) - - def __iter__(self): - return iter((self.allocate, self.update)) - - -NeighborFn = Callable[[Array, Optional[NeighborList], Optional[int]], - NeighborList] - - -def neighbor_list(displacement_or_metric: DisplacementOrMetricFn, - box: Box, - r_cutoff: float, - dr_threshold: float = 0.0, - capacity_multiplier: float = 1.25, - disable_cell_list: bool = False, - mask_self: bool = True, - custom_mask_function: Optional[MaskFn] = None, - fractional_coordinates: bool = False, - format: NeighborListFormat = NeighborListFormat.Dense, - **static_kwargs) -> NeighborFn: - """Returns a function that builds a list neighbors for collections of points. - - Neighbor lists must balance the need to be jit compatible with the fact that - under a jit the maximum number of neighbors cannot change (owing to static - shape requirements). To deal with this, our `neighbor_list` returns a - `NeighborListFns` object that contains two functions: 1) - `neighbor_fn.allocate` create a new neighbor list and 2) `neighbor_fn.update` - updates an existing neighbor list. Neighbor lists themselves additionally - have a convenience `update` member function. - - Note that allocation of a new neighbor list cannot be jit compiled since it - uses the positions to infer the maximum number of neighbors (along with - additional space specified by the `capacity_multiplier`). Updating the - neighbor list can be jit compiled; if the neighbor list capacity is not - sufficient to store all the neighbors, the `did_buffer_overflow` bit - will be set to `True` and a new neighbor list will need to be reallocated. - - Here is a typical example of a simulation loop with neighbor lists: - - .. code-block:: python - - init_fn, apply_fn = simulate.nve(energy_fn, shift, 1e-3) - exact_init_fn, exact_apply_fn = simulate.nve(exact_energy_fn, shift, 1e-3) - - nbrs = neighbor_fn.allocate(R) - state = init_fn(random.PRNGKey(0), R, neighbor_idx=nbrs.idx) - - def body_fn(i, state): - state, nbrs = state - nbrs = nbrs.update(state.position) - state = apply_fn(state, neighbor_idx=nbrs.idx) - return state, nbrs - - step = 0 - for _ in range(20): - new_state, nbrs = lax.fori_loop(0, 100, body_fn, (state, nbrs)) - if nbrs.did_buffer_overflow: - nbrs = neighbor_fn.allocate(state.position) - else: - state = new_state - step += 1 - - Args: - displacement: A function `d(R_a, R_b)` that computes the displacement - between pairs of points. - box: Either a float specifying the size of the box, an array of - shape `[spatial_dim]` specifying the box size for a cubic box in each - spatial dimension, or a matrix of shape `[spatial_dim, spatial_dim]` that - is _upper triangular_ and specifies the lattice vectors of the box. - r_cutoff: A scalar specifying the neighborhood radius. - dr_threshold: A scalar specifying the maximum distance particles can move - before rebuilding the neighbor list. - capacity_multiplier: A floating point scalar specifying the fractional - increase in maximum neighborhood occupancy we allocate compared with the - maximum in the example positions. - disable_cell_list: An optional boolean. If set to `True` then the neighbor - list is constructed using only distances. This can be useful for - debugging but should generally be left as `False`. - mask_self: An optional boolean. Determines whether points can consider - themselves to be their own neighbors. - custom_mask_function: An optional function. Takes the neighbor array - and masks selected elements. Note: The input array to the function is - `(n_particles, m)` where the index of particle 1 is in index in the first - dimension of the array, the index of particle 2 is given by the value in - the array - fractional_coordinates: An optional boolean. Specifies whether positions - will be supplied in fractional coordinates in the unit cube, :math:`[0, 1]^d`. - If this is set to True then the `box_size` will be set to `1.0` and the - cell size used in the cell list will be set to `cutoff / box_size`. - format: The format of the neighbor list; see the :meth:`NeighborListFormat` enum - for details about the different choices for formats. Defaults to `Dense`. - **static_kwargs: kwargs that get threaded through the calculation of - example positions. - Returns: - A NeighborListFns object that contains a method to allocate a new neighbor - list and a method to update an existing neighbor list. - """ - is_format_valid(format) - box = lax.stop_gradient(box) - r_cutoff = lax.stop_gradient(r_cutoff) - dr_threshold = lax.stop_gradient(dr_threshold) - - box = f32(box) - - cutoff = r_cutoff + dr_threshold - cutoff_sq = cutoff ** 2 - threshold_sq = (dr_threshold / f32(2)) ** 2 - metric_sq = _displacement_or_metric_to_metric_sq(displacement_or_metric) - - @partial(jit, static_argnums=0) - def candidate_fn(positionShape) -> Array: - candidates = jnp.arange(positionShape[0]) - return jnp.broadcast_to(candidates[None, :], - (positionShape[0], positionShape[0])) - - @partial(jit, static_argnums=1) - def cell_list_candidate_fn(cl_id_buffer, positionShape) -> Array: - N, dim = positionShape - - idx = cl_id_buffer - - cell_idx = [idx] - - for dindex in _neighboring_cells(dim): - if onp.all(dindex == 0): - continue - cell_idx += [shift_array(idx, dindex)] - - cell_idx = jnp.concatenate(cell_idx, axis=-2) - cell_idx = cell_idx[..., jnp.newaxis, :, :] - cell_idx = jnp.broadcast_to(cell_idx, idx.shape[:-1] + cell_idx.shape[-2:]) - - def copy_values_from_cell(value, cell_value, cell_id): - scatter_indices = jnp.reshape(cell_id, (-1,)) - cell_value = jnp.reshape(cell_value, (-1,) + cell_value.shape[-2:]) - return value.at[scatter_indices].set(cell_value) - - neighbor_idx = jnp.zeros((N + 1,) + cell_idx.shape[-2:], i32) - neighbor_idx = copy_values_from_cell(neighbor_idx, cell_idx, idx) - return neighbor_idx[:-1, :, 0] - - @jit - def mask_self_fn(idx: Array) -> Array: - self_mask = idx == jnp.reshape(jnp.arange(idx.shape[0], dtype=i32), - (idx.shape[0], 1)) - return jnp.where(self_mask, idx.shape[0], idx) - - @jit - def prune_neighbor_list_dense(position: Array, idx: Array, **kwargs - ) -> Array: - d = partial(metric_sq, **kwargs) - d = space.map_neighbor(d) - - N = position.shape[0] - neigh_position = position[idx] - dR = d(position, neigh_position) - - mask = (dR < cutoff_sq) & (idx < N) - out_idx = N * jnp.ones(idx.shape, i32) - - cumsum = jnp.cumsum(mask, axis=1) - index = jnp.where(mask, cumsum - 1, idx.shape[1] - 1) - p_index = jnp.arange(idx.shape[0])[:, None] - out_idx = out_idx.at[p_index, index].set(idx) - max_occupancy = jnp.max(cumsum[:, -1]) - - return out_idx, max_occupancy - - @jit - def prune_neighbor_list_sparse(position: Array, idx: Array, **kwargs - ) -> Array: - d = partial(metric_sq, **kwargs) - d = space.map_bond(d) - - N = position.shape[0] - sender_idx = jnp.broadcast_to(jnp.arange(N)[:, None], idx.shape) - - sender_idx = jnp.reshape(sender_idx, (-1,)) - receiver_idx = jnp.reshape(idx, (-1,)) - dR = d(position[sender_idx], position[receiver_idx]) - - mask = (dR < cutoff_sq) & (receiver_idx < N) - if format is NeighborListFormat.OrderedSparse: - mask = mask & (receiver_idx < sender_idx) - - out_idx = N * jnp.ones(receiver_idx.shape, i32) - - cumsum = jnp.cumsum(mask) - index = jnp.where(mask, cumsum - 1, len(receiver_idx) - 1) - receiver_idx = out_idx.at[index].set(receiver_idx) - sender_idx = out_idx.at[index].set(sender_idx) - max_occupancy = cumsum[-1] - - return jnp.stack((receiver_idx, sender_idx)), max_occupancy - - def neighbor_list_fn(position: Array, - neighbors = None, - extra_capacity: int = 0, - **kwargs) -> NeighborList: - def neighbor_fn(position_and_error, max_occupancy=None): - position, err = position_and_error - N = position.shape[0] - - cl_fn = None - cl = None - cell_size = None - if not disable_cell_list: - if neighbors is None: - _box = kwargs.get('box', box) - cell_size = cutoff - if fractional_coordinates: - err = err.update(PEC.MALFORMED_BOX, is_box_valid(_box)) - cell_size = _fractional_cell_size(_box, cutoff) - _box = 1.0 - if jnp.all(cell_size < _box / 3.): - cl_fn = cell_list(_box, cell_size, capacity_multiplier) - cl = cl_fn.allocate(position, extra_capacity=extra_capacity) - else: - cell_size = neighbors.cell_size - cl_fn = neighbors.cell_list_fn - if cl_fn is not None: - cl = cl_fn.update(position, neighbors.cell_list_capacity) - - if cl is None: - cl_capacity = None - idx = candidate_fn(position.shape) - else: - err = err.update(PEC.CELL_LIST_OVERFLOW, cl.did_buffer_overflow) - idx = cell_list_candidate_fn(cl.id_buffer, position.shape) - cl_capacity = cl.cell_capacity - - if mask_self: - idx = mask_self_fn(idx) - if custom_mask_function is not None: - idx = custom_mask_function(idx) - - if is_sparse(format): - idx, occupancy = prune_neighbor_list_sparse(position, idx, **kwargs) - else: - idx, occupancy = prune_neighbor_list_dense(position, idx, **kwargs) - - if max_occupancy is None: - _extra_capacity = (extra_capacity if not is_sparse(format) - else N * extra_capacity) - max_occupancy = int(occupancy * capacity_multiplier + _extra_capacity) - if max_occupancy > idx.shape[-1]: - max_occupancy = idx.shape[-1] - if not is_sparse(format): - capacity_limit = N - 1 if mask_self else N - elif format is NeighborListFormat.Sparse: - capacity_limit = N * (N - 1) if mask_self else N**2 - else: - capacity_limit = N * (N - 1) // 2 - if max_occupancy > capacity_limit: - max_occupancy = capacity_limit - idx = idx[:, :max_occupancy] - update_fn = (neighbor_list_fn if neighbors is None else - neighbors.update_fn) - return NeighborList( - idx, - position, - err.update(PEC.NEIGHBOR_LIST_OVERFLOW, occupancy > max_occupancy), - cl_capacity, - max_occupancy, - format, - cell_size, - cl_fn, - update_fn) # pytype: disable=wrong-arg-count - - nbrs = neighbors - if nbrs is None: - return neighbor_fn((position, PartitionError(jnp.zeros((), jnp.uint8)))) - - neighbor_fn = partial(neighbor_fn, max_occupancy=nbrs.max_occupancy) - - # If the box has been updated, then check that fractional coordinates are - # enabled and that the cell list has big enough cells. - if 'box' in kwargs and not disable_cell_list: - if not fractional_coordinates: - raise ValueError('Neighbor list cannot accept a box keyword argument ' - 'if fractional_coordinates is not enabled.') - # `cell_size` is really the minimum cell size. - cur_cell_size = _cell_size(1.0, nbrs.cell_size) - new_cell_size = _cell_size(1.0, - _fractional_cell_size(kwargs['box'], cutoff)) - err = nbrs.error.update(PEC.CELL_SIZE_TOO_SMALL, - new_cell_size > cur_cell_size) - err = err.update(PEC.MALFORMED_BOX, is_box_valid(kwargs['box'])) - nbrs = dataclasses.replace(nbrs, error=err) - - d = partial(metric_sq, **kwargs) - d = vmap(d) - return lax.cond( - jnp.any(d(position, nbrs.reference_position) > threshold_sq), - (position, nbrs.error), neighbor_fn, - nbrs, lambda x: x) - - def allocate_fn(position: Array, extra_capacity: int = 0, **kwargs - ): - return neighbor_list_fn(position, extra_capacity=extra_capacity, **kwargs) - - def update_fn(position: Array, neighbors, **kwargs - ): - return neighbor_list_fn(position, neighbors, **kwargs) - - return NeighborListFns(allocate_fn, update_fn) # pytype: disable=wrong-arg-count - - -def neighbor_list_mask(neighbor: NeighborList, mask_self: bool = False - ) -> Array: - """Compute a mask for neighbor list.""" - if is_sparse(neighbor.format): - mask = neighbor.idx[0] < len(neighbor.reference_position) + is_format_valid(format) + box = lax.stop_gradient(box) + r_cutoff = lax.stop_gradient(r_cutoff) + dr_threshold = lax.stop_gradient(dr_threshold) + + box = f32(box) + + cutoff = r_cutoff + dr_threshold + cutoff_sq = cutoff**2 + threshold_sq = (dr_threshold / f32(2)) ** 2 + metric_sq = _displacement_or_metric_to_metric_sq(displacement_or_metric) + + @partial(jit, static_argnums=0) + def candidate_fn(positionShape) -> Array: + candidates = jnp.arange(positionShape[0]) + return jnp.broadcast_to( + candidates[None, :], (positionShape[0], positionShape[0]) + ) + + @partial(jit, static_argnums=1) + def cell_list_candidate_fn(cl_id_buffer, positionShape) -> Array: + N, dim = positionShape + + idx = cl_id_buffer + + cell_idx = [idx] + + for dindex in _neighboring_cells(dim): + if onp.all(dindex == 0): + continue + cell_idx += [shift_array(idx, dindex)] + + cell_idx = jnp.concatenate(cell_idx, axis=-2) + cell_idx = cell_idx[..., jnp.newaxis, :, :] + cell_idx = jnp.broadcast_to(cell_idx, idx.shape[:-1] + cell_idx.shape[-2:]) + + def copy_values_from_cell(value, cell_value, cell_id): + scatter_indices = jnp.reshape(cell_id, (-1,)) + cell_value = jnp.reshape(cell_value, (-1,) + cell_value.shape[-2:]) + return value.at[scatter_indices].set(cell_value) + + neighbor_idx = jnp.zeros((N + 1,) + cell_idx.shape[-2:], i32) + neighbor_idx = copy_values_from_cell(neighbor_idx, cell_idx, idx) + return neighbor_idx[:-1, :, 0] + + @jit + def mask_self_fn(idx: Array) -> Array: + self_mask = idx == jnp.reshape( + jnp.arange(idx.shape[0], dtype=i32), (idx.shape[0], 1) + ) + return jnp.where(self_mask, idx.shape[0], idx) + + @jit + def prune_neighbor_list_dense(position: Array, idx: Array, **kwargs) -> Array: + d = partial(metric_sq, **kwargs) + d = space.map_neighbor(d) + + N = position.shape[0] + neigh_position = position[idx] + dR = d(position, neigh_position) + + mask = (dR < cutoff_sq) & (idx < N) + out_idx = N * jnp.ones(idx.shape, i32) + + cumsum = jnp.cumsum(mask, axis=1) + index = jnp.where(mask, cumsum - 1, idx.shape[1] - 1) + p_index = jnp.arange(idx.shape[0])[:, None] + out_idx = out_idx.at[p_index, index].set(idx) + max_occupancy = jnp.max(cumsum[:, -1]) + + return out_idx, max_occupancy + + @jit + def prune_neighbor_list_sparse(position: Array, idx: Array, **kwargs) -> Array: + d = partial(metric_sq, **kwargs) + d = space.map_bond(d) + + N = position.shape[0] + sender_idx = jnp.broadcast_to(jnp.arange(N)[:, None], idx.shape) + + sender_idx = jnp.reshape(sender_idx, (-1,)) + receiver_idx = jnp.reshape(idx, (-1,)) + dR = d(position[sender_idx], position[receiver_idx]) + + mask = (dR < cutoff_sq) & (receiver_idx < N) + if format is NeighborListFormat.OrderedSparse: + mask = mask & (receiver_idx < sender_idx) + + out_idx = N * jnp.ones(receiver_idx.shape, i32) + + cumsum = jnp.cumsum(mask) + index = jnp.where(mask, cumsum - 1, len(receiver_idx) - 1) + receiver_idx = out_idx.at[index].set(receiver_idx) + sender_idx = out_idx.at[index].set(sender_idx) + max_occupancy = cumsum[-1] + + return jnp.stack((receiver_idx, sender_idx)), max_occupancy + + def neighbor_list_fn( + position: Array, neighbors=None, extra_capacity: int = 0, **kwargs + ) -> NeighborList: + def neighbor_fn(position_and_error, max_occupancy=None): + position, err = position_and_error + N = position.shape[0] + + cl_fn = None + cl = None + cell_size = None + if not disable_cell_list: + if neighbors is None: + _box = kwargs.get("box", box) + cell_size = cutoff + if fractional_coordinates: + err = err.update(PEC.MALFORMED_BOX, is_box_valid(_box)) + cell_size = _fractional_cell_size(_box, cutoff) + _box = 1.0 + if jnp.all(cell_size < _box / 3.0): + cl_fn = cell_list(_box, cell_size, capacity_multiplier) + cl = cl_fn.allocate(position, extra_capacity=extra_capacity) + else: + cell_size = neighbors.cell_size + cl_fn = neighbors.cell_list_fn + if cl_fn is not None: + cl = cl_fn.update(position, neighbors.cell_list_capacity) + + if cl is None: + cl_capacity = None + idx = candidate_fn(position.shape) + else: + err = err.update(PEC.CELL_LIST_OVERFLOW, cl.did_buffer_overflow) + idx = cell_list_candidate_fn(cl.id_buffer, position.shape) + cl_capacity = cl.cell_capacity + + if mask_self: + idx = mask_self_fn(idx) + if custom_mask_function is not None: + idx = custom_mask_function(idx) + + if is_sparse(format): + idx, occupancy = prune_neighbor_list_sparse(position, idx, **kwargs) + else: + idx, occupancy = prune_neighbor_list_dense(position, idx, **kwargs) + + if max_occupancy is None: + _extra_capacity = ( + extra_capacity if not is_sparse(format) else N * extra_capacity + ) + max_occupancy = int(occupancy * capacity_multiplier + _extra_capacity) + if max_occupancy > idx.shape[-1]: + max_occupancy = idx.shape[-1] + if not is_sparse(format): + capacity_limit = N - 1 if mask_self else N + elif format is NeighborListFormat.Sparse: + capacity_limit = N * (N - 1) if mask_self else N**2 + else: + capacity_limit = N * (N - 1) // 2 + if max_occupancy > capacity_limit: + max_occupancy = capacity_limit + idx = idx[:, :max_occupancy] + update_fn = neighbor_list_fn if neighbors is None else neighbors.update_fn + return NeighborList( + idx, + position, + err.update(PEC.NEIGHBOR_LIST_OVERFLOW, occupancy > max_occupancy), + cl_capacity, + max_occupancy, + format, + cell_size, + cl_fn, + update_fn, + ) # pytype: disable=wrong-arg-count + + nbrs = neighbors + if nbrs is None: + return neighbor_fn((position, PartitionError(jnp.zeros((), jnp.uint8)))) + + neighbor_fn = partial(neighbor_fn, max_occupancy=nbrs.max_occupancy) + + # If the box has been updated, then check that fractional coordinates are + # enabled and that the cell list has big enough cells. + if "box" in kwargs and not disable_cell_list: + if not fractional_coordinates: + raise ValueError( + "Neighbor list cannot accept a box keyword argument " + "if fractional_coordinates is not enabled." + ) + # `cell_size` is really the minimum cell size. + cur_cell_size = _cell_size(1.0, nbrs.cell_size) + new_cell_size = _cell_size( + 1.0, _fractional_cell_size(kwargs["box"], cutoff) + ) + err = nbrs.error.update( + PEC.CELL_SIZE_TOO_SMALL, new_cell_size > cur_cell_size + ) + err = err.update(PEC.MALFORMED_BOX, is_box_valid(kwargs["box"])) + nbrs = dataclasses.replace(nbrs, error=err) + + d = partial(metric_sq, **kwargs) + d = vmap(d) + return lax.cond( + jnp.any(d(position, nbrs.reference_position) > threshold_sq), + (position, nbrs.error), + neighbor_fn, + nbrs, + lambda x: x, + ) + + def allocate_fn(position: Array, extra_capacity: int = 0, **kwargs): + return neighbor_list_fn(position, extra_capacity=extra_capacity, **kwargs) + + def update_fn(position: Array, neighbors, **kwargs): + return neighbor_list_fn(position, neighbors, **kwargs) + + return NeighborListFns(allocate_fn, update_fn) # pytype: disable=wrong-arg-count + + +def neighbor_list_mask(neighbor: NeighborList, mask_self: bool = False) -> Array: + """Compute a mask for neighbor list.""" + if is_sparse(neighbor.format): + mask = neighbor.idx[0] < len(neighbor.reference_position) + if mask_self: + mask = mask & (neighbor.idx[0] != neighbor.idx[1]) + return mask + + mask = neighbor.idx < len(neighbor.idx) if mask_self: - mask = mask & (neighbor.idx[0] != neighbor.idx[1]) + N = len(neighbor.reference_position) + self_mask = neighbor.idx != jnp.reshape(jnp.arange(N, dtype=i32), (N, 1)) + mask = mask & self_mask return mask - mask = neighbor.idx < len(neighbor.idx) - if mask_self: + +def to_jraph( + neighbor: NeighborList, + mask: Optional[Array] = None, + nodes: Optional[PyTree] = None, + edges: Optional[PyTree] = None, + globals: Optional[PyTree] = None, +) -> jraph.GraphsTuple: + """Convert a sparse neighbor list to a `jraph.GraphsTuple`. + + As in jraph, padding here is accomplished by adding a ficticious graph with a + single node. + + Args: + neighbor: A neighbor list that we will convert to the jraph format. Must be + sparse. + mask: An optional mask on the edges. + + Returns: + A `jraph.GraphsTuple` that contains the topology of the neighbor list. + """ + if not is_sparse(neighbor.format): + raise ValueError( + "Cannot convert a dense neighbor list to jraph format. " + "Please use either NeighborListFormat.Sparse or " + "NeighborListFormat.OrderedSparse." + ) + + receivers, senders = neighbor.idx N = len(neighbor.reference_position) - self_mask = neighbor.idx != jnp.reshape(jnp.arange(N, dtype=i32), (N, 1)) - mask = mask & self_mask - return mask - - -def to_jraph(neighbor: NeighborList, - mask: Optional[Array] = None, - nodes: Optional[PyTree] = None, - edges: Optional[PyTree] = None, - globals: Optional[PyTree] = None - ) -> jraph.GraphsTuple: - """Convert a sparse neighbor list to a `jraph.GraphsTuple`. - - As in jraph, padding here is accomplished by adding a ficticious graph with a - single node. - - Args: - neighbor: A neighbor list that we will convert to the jraph format. Must be - sparse. - mask: An optional mask on the edges. - - Returns: - A `jraph.GraphsTuple` that contains the topology of the neighbor list. - """ - if not is_sparse(neighbor.format): - raise ValueError('Cannot convert a dense neighbor list to jraph format. ' - 'Please use either NeighborListFormat.Sparse or ' - 'NeighborListFormat.OrderedSparse.') - - receivers, senders = neighbor.idx - N = len(neighbor.reference_position) - - _mask = neighbor_list_mask(neighbor) - - # Pad the nodes to add one fictitious node. - def pad(x): - padding = jnp.zeros((1,) + x.shape[1:], dtype=x.dtype) - return jnp.concatenate((x, padding), axis=0) - nodes = tree_map(pad, nodes) - - # Pad the globals to add one fictitious global. - globals = tree_map(pad, globals) - - # If there is an additional mask, reorder the edges. - if mask is not None: - _mask = _mask & mask - cumsum = jnp.cumsum(_mask) - index = jnp.where(_mask, cumsum - 1, len(receivers)) - ordered = N * jnp.ones((len(receivers) + 1,), i32) - receivers = ordered.at[index].set(receivers)[:-1] - senders = ordered.at[index].set(senders)[:-1] - def reorder_edges(x): - return jnp.zeros_like(x).at[index].set(x) - edges = tree_map(reorder_edges, edges) - mask = receivers < N - - return jraph.GraphsTuple( - nodes=nodes, - edges=edges, - receivers=receivers, - senders=senders, - globals=globals, - n_node=jnp.array([N, 1]), - n_edge=jnp.array([jnp.sum(_mask), jnp.sum(~_mask)]), - ) + + _mask = neighbor_list_mask(neighbor) + + # Pad the nodes to add one fictitious node. + def pad(x): + padding = jnp.zeros((1,) + x.shape[1:], dtype=x.dtype) + return jnp.concatenate((x, padding), axis=0) + + nodes = tree_map(pad, nodes) + + # Pad the globals to add one fictitious global. + globals = tree_map(pad, globals) + + # If there is an additional mask, reorder the edges. + if mask is not None: + _mask = _mask & mask + cumsum = jnp.cumsum(_mask) + index = jnp.where(_mask, cumsum - 1, len(receivers)) + ordered = N * jnp.ones((len(receivers) + 1,), i32) + receivers = ordered.at[index].set(receivers)[:-1] + senders = ordered.at[index].set(senders)[:-1] + + def reorder_edges(x): + return jnp.zeros_like(x).at[index].set(x) + + edges = tree_map(reorder_edges, edges) + mask = receivers < N + + return jraph.GraphsTuple( + nodes=nodes, + edges=edges, + receivers=receivers, + senders=senders, + globals=globals, + n_node=jnp.array([N, 1]), + n_edge=jnp.array([jnp.sum(_mask), jnp.sum(~_mask)]), + ) def to_dense(neighbor: NeighborList) -> Array: - """Converts a sparse neighbor list to dense ids. Cannot be JIT.""" - if neighbor.format is not Sparse: - raise ValueError('Can only convert sparse neighbor lists to dense ones.') + """Converts a sparse neighbor list to dense ids. Cannot be JIT.""" + if neighbor.format is not Sparse: + raise ValueError("Can only convert sparse neighbor lists to dense ones.") - receivers, senders = neighbor.idx - mask = neighbor_list_mask(neighbor) + receivers, senders = neighbor.idx + mask = neighbor_list_mask(neighbor) - receivers = receivers[mask] - senders = senders[mask] + receivers = receivers[mask] + senders = senders[mask] - N = len(neighbor.reference_position) - count = ops.segment_sum(jnp.ones(len(receivers), i32), receivers, N) - max_count = jnp.max(count) - offset = jnp.tile(jnp.arange(max_count), N)[:len(senders)] - hashes = senders * max_count + offset - dense_idx = N * jnp.ones((N * max_count,), i32) - dense_idx = dense_idx.at[hashes].set(receivers).reshape((N, max_count)) - return dense_idx + N = len(neighbor.reference_position) + count = ops.segment_sum(jnp.ones(len(receivers), i32), receivers, N) + max_count = jnp.max(count) + offset = jnp.tile(jnp.arange(max_count), N)[: len(senders)] + hashes = senders * max_count + offset + dense_idx = N * jnp.ones((N * max_count,), i32) + dense_idx = dense_idx.at[hashes].set(receivers).reshape((N, max_count)) + return dense_idx Dense = NeighborListFormat.Dense Sparse = NeighborListFormat.Sparse -OrderedSparse = NeighborListFormat.OrderedSparse \ No newline at end of file +OrderedSparse = NeighborListFormat.OrderedSparse diff --git a/jax_sph/jax_md/space.py b/jax_sph/jax_md/space.py index 22bba01..b088630 100644 --- a/jax_sph/jax_md/space.py +++ b/jax_sph/jax_md/space.py @@ -1,5 +1,5 @@ # Source: https://github.com/jax-md/jax-md -# +# # Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -72,381 +72,407 @@ class UnexpectedBoxException(Exception): - pass + pass # Primitive Spatial Transforms def inverse(box: Box) -> Box: - """Compute the inverse of an affine transformation.""" - if jnp.isscalar(box) or box.size == 1: - return 1 / box - elif box.ndim == 1: - return 1 / box - elif box.ndim == 2: - return jnp.linalg.inv(box) - raise ValueError(('Box must be either: a scalar, a vector, or a matrix. ' - f'Found {box}.')) + """Compute the inverse of an affine transformation.""" + if jnp.isscalar(box) or box.size == 1 or box.ndim == 1: + return 1 / box + elif box.ndim == 2: + return jnp.linalg.inv(box) + raise ValueError( + ("Box must be either: a scalar, a vector, or a matrix. " f"Found {box}.") + ) def _get_free_indices(n: int) -> str: - return ''.join([chr(ord('a') + i) for i in range(n)]) + return "".join([chr(ord("a") + i) for i in range(n)]) def raw_transform(box: Box, R: Array) -> Array: - """Apply an affine transformation to positions. - - See `periodic_general` for a description of the semantics of `box`. - - Args: - box: An affine transformation described in `periodic_general`. - R: Array of positions. Should have shape `(..., spatial_dimension)`. - - Returns: - A transformed array positions of shape `(..., spatial_dimension)`. - """ - if jnp.isscalar(box) or box.size == 1: - return R * box - elif box.ndim == 1: - indices = _get_free_indices(R.ndim - 1) + 'i' - return jnp.einsum(f'i,{indices}->{indices}', box, R) - elif box.ndim == 2: - free_indices = _get_free_indices(R.ndim - 1) - left_indices = free_indices + 'j' - right_indices = free_indices + 'i' - return jnp.einsum(f'ij,{left_indices}->{right_indices}', box, R) - raise ValueError(('Box must be either: a scalar, a vector, or a matrix. ' - f'Found {box}.')) + """Apply an affine transformation to positions. + + See `periodic_general` for a description of the semantics of `box`. + + Args: + box: An affine transformation described in `periodic_general`. + R: Array of positions. Should have shape `(..., spatial_dimension)`. + + Returns: + A transformed array positions of shape `(..., spatial_dimension)`. + """ + if jnp.isscalar(box) or box.size == 1: + return R * box + elif box.ndim == 1: + indices = _get_free_indices(R.ndim - 1) + "i" + return jnp.einsum(f"i,{indices}->{indices}", box, R) + elif box.ndim == 2: + free_indices = _get_free_indices(R.ndim - 1) + left_indices = free_indices + "j" + right_indices = free_indices + "i" + return jnp.einsum(f"ij,{left_indices}->{right_indices}", box, R) + raise ValueError( + ("Box must be either: a scalar, a vector, or a matrix. " f"Found {box}.") + ) @custom_jvp def transform(box: Box, R: Array) -> Array: - """Apply an affine transformation to positions. + """Apply an affine transformation to positions. - See `periodic_general` for a description of the semantics of `box`. + See `periodic_general` for a description of the semantics of `box`. - Args: - box: An affine transformation described in `periodic_general`. - R: Array of positions. Should have shape `(..., spatial_dimension)`. + Args: + box: An affine transformation described in `periodic_general`. + R: Array of positions. Should have shape `(..., spatial_dimension)`. - Returns: - A transformed array positions of shape `(..., spatial_dimension)`. - """ - return raw_transform(box, R) + Returns: + A transformed array positions of shape `(..., spatial_dimension)`. + """ + return raw_transform(box, R) @transform.defjvp def transform_jvp(primals, tangents): - box, R = primals - dbox, dR = tangents - return (transform(box, R), dR + transform(dbox, R)) + box, R = primals + dbox, dR = tangents + return (transform(box, R), dR + transform(dbox, R)) def pairwise_displacement(Ra: Array, Rb: Array) -> Array: - """Compute a matrix of pairwise displacements given two sets of positions. - - Args: - Ra: Vector of positions; `ndarray(shape=[spatial_dim])`. - Rb: Vector of positions; `ndarray(shape=[spatial_dim])`. - - Returns: - Matrix of displacements; `ndarray(shape=[spatial_dim])`. - """ - if len(Ra.shape) != 1: - msg = ( - 'Can only compute displacements between vectors. To compute ' - 'displacements between sets of vectors use vmap or TODO.' - ) - raise ValueError(msg) + """Compute a matrix of pairwise displacements given two sets of positions. + + Args: + Ra: Vector of positions; `ndarray(shape=[spatial_dim])`. + Rb: Vector of positions; `ndarray(shape=[spatial_dim])`. + + Returns: + Matrix of displacements; `ndarray(shape=[spatial_dim])`. + """ + if len(Ra.shape) != 1: + msg = ( + "Can only compute displacements between vectors. To compute " + "displacements between sets of vectors use vmap or TODO." + ) + raise ValueError(msg) - if Ra.shape != Rb.shape: - msg = 'Can only compute displacement between vectors of equal dimension.' - raise ValueError(msg) + if Ra.shape != Rb.shape: + msg = "Can only compute displacement between vectors of equal dimension." + raise ValueError(msg) - return Ra - Rb + return Ra - Rb def periodic_displacement(side: Box, dR: Array) -> Array: - """Wraps displacement vectors into a hypercube. + """Wraps displacement vectors into a hypercube. - Args: - side: Specification of hypercube size. Either, - (a) float if all sides have equal length. - (b) ndarray(spatial_dim) if sides have different lengths. - dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. - Returns: - Matrix of wrapped displacements; `ndarray(shape=[..., spatial_dim])`. - """ - return jnp.mod(dR + side * f32(0.5), side) - f32(0.5) * side + Args: + side: Specification of hypercube size. Either, + (a) float if all sides have equal length. + (b) ndarray(spatial_dim) if sides have different lengths. + dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. + Returns: + Matrix of wrapped displacements; `ndarray(shape=[..., spatial_dim])`. + """ + return jnp.mod(dR + side * f32(0.5), side) - f32(0.5) * side def square_distance(dR: Array) -> Array: - """Computes square distances. + """Computes square distances. - Args: - dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. - Returns: - Matrix of squared distances; `ndarray(shape=[...])`. - """ - return jnp.sum(dR ** 2, axis=-1) + Args: + dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. + Returns: + Matrix of squared distances; `ndarray(shape=[...])`. + """ + return jnp.sum(dR**2, axis=-1) def distance(dR: Array) -> Array: - """Computes distances. + """Computes distances. - Args: - dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. - Returns: - Matrix of distances; `ndarray(shape=[...])`. - """ - dr = square_distance(dR) - return safe_mask(dr > 0, jnp.sqrt, dr) + Args: + dR: Matrix of displacements; `ndarray(shape=[..., spatial_dim])`. + Returns: + Matrix of distances; `ndarray(shape=[...])`. + """ + dr = square_distance(dR) + return safe_mask(dr > 0, jnp.sqrt, dr) def periodic_shift(side: Box, R: Array, dR: Array) -> Array: - """Shifts positions, wrapping them back within a periodic hypercube.""" - return jnp.mod(R + dR, side) + """Shifts positions, wrapping them back within a periodic hypercube.""" + return jnp.mod(R + dR, side) -""" Spaces """ +### Spaces def free() -> Space: - """Free boundary conditions.""" - def displacement_fn(Ra: Array, Rb: Array, perturbation: Optional[Array]=None, - **unused_kwargs) -> Array: - dR = pairwise_displacement(Ra, Rb) - if perturbation is not None: - dR = raw_transform(perturbation, dR) - return dR - def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: - return R + dR - return displacement_fn, shift_fn - - -def periodic(side: Box, wrapped: bool=True) -> Space: - """Periodic boundary conditions on a hypercube of sidelength side. - - Args: - side: Either a float or an ndarray of shape [spatial_dimension] specifying - the size of each side of the periodic box. - wrapped: A boolean specifying whether or not particle positions are - remapped back into the box after each step - Returns: - `(displacement_fn, shift_fn)` tuple. - """ - def displacement_fn(Ra: Array, Rb: Array, - perturbation: Optional[Array] = None, - **unused_kwargs) -> Array: - if 'box' in unused_kwargs: - raise UnexpectedBoxException(('`space.periodic` does not accept a box ' - 'argument. Perhaps you meant to use ' - '`space.periodic_general`?')) - dR = periodic_displacement(side, pairwise_displacement(Ra, Rb)) - if perturbation is not None: - dR = raw_transform(perturbation, dR) - return dR - if wrapped: - def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: - if 'box' in unused_kwargs: - raise UnexpectedBoxException(('`space.periodic` does not accept a box ' - 'argument. Perhaps you meant to use ' - '`space.periodic_general`?')) + """Free boundary conditions.""" + + def displacement_fn( + Ra: Array, Rb: Array, perturbation: Optional[Array] = None, **unused_kwargs + ) -> Array: + dR = pairwise_displacement(Ra, Rb) + if perturbation is not None: + dR = raw_transform(perturbation, dR) + return dR - return periodic_shift(side, R, dR) - else: def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: - if 'box' in unused_kwargs: - raise UnexpectedBoxException(('`space.periodic` does not accept a box ' - 'argument. Perhaps you meant to use ' - '`space.periodic_general`?')) - return R + dR - return displacement_fn, shift_fn - - -def periodic_general(box: Box, - fractional_coordinates: bool=True, - wrapped: bool=True) -> Space: - """Periodic boundary conditions on a parallelepiped. - - This function defines a simulation on a parallelepiped, :math:`X`, formed by - applying an affine transformation, :math:`T`, to the unit hypercube - :math:`U = [0, 1]^d` along with periodic boundary conditions across all - of the faces. - - Formally, the space is defined such that :math:`X = {Tu : u \in [0, 1]^d}`. - - The affine transformation, :math:`T`, can be specified in a number of different - ways. For a parallelepiped that is: 1) a cube of side length :math:`L`, the affine - transformation can simply be a scalar; 2) an orthorhombic unit cell can be - specified by a vector `[Lx, Ly, Lz]` of lengths for each axis; 3) a general - triclinic cell can be specified by an upper triangular matrix. - - There are a number of ways to parameterize a simulation on :math:`X`. - `periodic_general` supports two parametrizations of :math:`X` that can be selected - using the `fractional_coordinates` keyword argument. - - 1) When `fractional_coordinates=True`, particle positions are stored in the - unit cube, :math:`u\in U`. Here, the displacement function computes the - displacement between :math:`x, y \in X` as :math:`d_X(x, y) = Td_U(u, v)` where - :math:`d_U` is the displacement function on the unit cube, :math:`U`, :math:`x = Tu`, and - :math:`v = Tv` with :math:`u, v \in U`. The derivative of the displacement function - is defined so that derivatives live in :math:`X` (as opposed to being - backpropagated to :math:`U`). The shift function, `shift_fn(R, dR)` is defined - so that :math:`R` is expected to lie in :math:`U` while :math:`dR` should lie in :math:`X`. This - combination enables code such as `shift_fn(R, force_fn(R))` to work as - intended. - - 2) When `fractional_coordinates=False`, particle positions are stored in - the parallelepiped :math:`X`. Here, for :math:`x, y \in X`, the displacement function - is defined as :math:`d_X(x, y) = Td_U(T^{-1}x, T^{-1}y)`. Since there is an - extra multiplication by :math:`T^{-1}`, this parameterization is typically - slower than `fractional_coordinates=False`. As in 1), the displacement - function is defined to compute derivatives in :math:`X`. The shift function - is defined so that :math:`R` and :math:`dR` should both lie in :math:`X`. - - Example: - - .. code-block:: python - - from jax import random - side_length = 10.0 - disp_frac, shift_frac = periodic_general(side_length, - fractional_coordinates=True) - disp_real, shift_real = periodic_general(side_length, - fractional_coordinates=False) - - # Instantiate random positions in both parameterizations. - R_frac = random.uniform(random.PRNGKey(0), (4, 3)) - R_real = side_length * R_frac - - # Make some shift vectors. - dR = random.normal(random.PRNGKey(0), (4, 3)) - - disp_real(R_real[0], R_real[1]) == disp_frac(R_frac[0], R_frac[1]) - transform(side_length, shift_frac(R_frac, 1.0)) == shift_real(R_real, 1.0) - - It is often desirable to deform a simulation cell either: using a finite - deformation during a simulation, or using an infinitesimal deformation while - computing elastic constants. To do this using fractional coordinates, we can - supply a new affine transformation as `displacement_fn(Ra, Rb, box=new_box)`. - When using real coordinates, we can specify positions in a space :math:`X` defined - by an affine transformation :math:`T` and compute displacements in a deformed space - :math:`X'` defined by an affine transformation :math:`T'`. This is done by writing - `displacement_fn(Ra, Rb, new_box=new_box)`. - - There are a few caveats when using `periodic_general`. `periodic_general` - uses the minimum image convention, and so it will fail for potentials whose - cutoff is longer than the half of the side-length of the box. It will also - fail to find the correct image when the box is too deformed. We hope to add a - more robust box for small simulations soon (TODO) along with better error - checking. In the meantime caution is recommended. - - Args: - box: A `(spatial_dim, spatial_dim)` affine transformation. - fractional_coordinates: A boolean specifying whether positions are stored - in the parallelepiped or the unit cube. - wrapped: A boolean specifying whether or not particle positions are - remapped back into the box after each step - Returns: - `(displacement_fn, shift_fn)` tuple. - """ - inv_box = inverse(box) - - def displacement_fn(Ra, Rb, perturbation=None, **kwargs): - _box, _inv_box = box, inv_box - - if 'box' in kwargs: - _box = kwargs['box'] - - if not fractional_coordinates: - _inv_box = inverse(_box) - - if 'new_box' in kwargs: - _box = kwargs['new_box'] - - if not fractional_coordinates: - Ra = transform(_inv_box, Ra) - Rb = transform(_inv_box, Rb) - - dR = periodic_displacement(f32(1.0), pairwise_displacement(Ra, Rb)) - dR = transform(_box, dR) - - if perturbation is not None: - dR = raw_transform(perturbation, dR) - - return dR - - def u(R, dR): + return R + dR + + return displacement_fn, shift_fn + + +def periodic(side: Box, wrapped: bool = True) -> Space: + """Periodic boundary conditions on a hypercube of sidelength side. + + Args: + side: Either a float or an ndarray of shape [spatial_dimension] specifying + the size of each side of the periodic box. + wrapped: A boolean specifying whether or not particle positions are + remapped back into the box after each step + Returns: + `(displacement_fn, shift_fn)` tuple. + """ + + def displacement_fn( + Ra: Array, Rb: Array, perturbation: Optional[Array] = None, **unused_kwargs + ) -> Array: + if "box" in unused_kwargs: + raise UnexpectedBoxException( + ( + "`space.periodic` does not accept a box " + "argument. Perhaps you meant to use " + "`space.periodic_general`?" + ) + ) + dR = periodic_displacement(side, pairwise_displacement(Ra, Rb)) + if perturbation is not None: + dR = raw_transform(perturbation, dR) + return dR + if wrapped: - return periodic_shift(f32(1.0), R, dR) - return R + dR - def shift_fn(R, dR, **kwargs): - if not fractional_coordinates and not wrapped: - return R + dR + def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: + if "box" in unused_kwargs: + raise UnexpectedBoxException( + ( + "`space.periodic` does not accept a box " + "argument. Perhaps you meant to use " + "`space.periodic_general`?" + ) + ) + + return periodic_shift(side, R, dR) + else: + + def shift_fn(R: Array, dR: Array, **unused_kwargs) -> Array: + if "box" in unused_kwargs: + raise UnexpectedBoxException( + ( + "`space.periodic` does not accept a box " + "argument. Perhaps you meant to use " + "`space.periodic_general`?" + ) + ) + return R + dR + + return displacement_fn, shift_fn + + +def periodic_general( + box: Box, fractional_coordinates: bool = True, wrapped: bool = True +) -> Space: + """Periodic boundary conditions on a parallelepiped. + + This function defines a simulation on a parallelepiped, :math:`X`, formed by + applying an affine transformation, :math:`T`, to the unit hypercube + :math:`U = [0, 1]^d` along with periodic boundary conditions across all + of the faces. + + Formally, the space is defined such that :math:`X = {Tu : u \in [0, 1]^d}`. + + The affine transformation, :math:`T`, can be specified in a number of different + ways. For a parallelepiped that is: 1) a cube of side length :math:`L`, the affine + transformation can simply be a scalar; 2) an orthorhombic unit cell can be + specified by a vector `[Lx, Ly, Lz]` of lengths for each axis; 3) a general + triclinic cell can be specified by an upper triangular matrix. + + There are a number of ways to parameterize a simulation on :math:`X`. + `periodic_general` supports two parametrizations of :math:`X` that can be selected + using the `fractional_coordinates` keyword argument. + + 1) When `fractional_coordinates=True`, particle positions are stored in the + unit cube, :math:`u\in U`. Here, the displacement function computes the + displacement between :math:`x, y \in X` as :math:`d_X(x, y) = Td_U(u, v)` where + :math:`d_U` is the displacement function on the unit cube, :math:`U`, + :math:`x = Tu`, and :math:`v = Tv` with :math:`u, v \in U`. The derivative of + the displacement function is defined so that derivatives live in :math:`X` (as + opposed to being backpropagated to :math:`U`). The shift function, + `shift_fn(R, dR)` is defined so that :math:`R` is expected to lie in :math:`U` + while :math:`dR` should lie in :math:`X`. This combination enables code such as + `shift_fn(R, force_fn(R))` to work as intended. + + 2) When `fractional_coordinates=False`, particle positions are stored in + the parallelepiped :math:`X`. Here, for :math:`x, y \in X`, the displacement + function is defined as :math:`d_X(x, y) = Td_U(T^{-1}x, T^{-1}y)`. Since there + is an extra multiplication by :math:`T^{-1}`, this parameterization is + typically slower than `fractional_coordinates=False`. As in 1), the + displacement function is defined to compute derivatives in :math:`X`. The shift + function is defined so that :math:`R` and :math:`dR` should both lie in + :math:`X`. + + Example: + + .. code-block:: python + + from jax import random + side_length = 10.0 + disp_frac, shift_frac = periodic_general(side_length, + fractional_coordinates=True) + disp_real, shift_real = periodic_general(side_length, + fractional_coordinates=False) + + # Instantiate random positions in both parameterizations. + R_frac = random.uniform(random.PRNGKey(0), (4, 3)) + R_real = side_length * R_frac + + # Make some shift vectors. + dR = random.normal(random.PRNGKey(0), (4, 3)) + + disp_real(R_real[0], R_real[1]) == disp_frac(R_frac[0], R_frac[1]) + transform(side_length, shift_frac(R_frac, 1.0)) == shift_real(R_real, 1.0) + + It is often desirable to deform a simulation cell either: using a finite + deformation during a simulation, or using an infinitesimal deformation while + computing elastic constants. To do this using fractional coordinates, we can + supply a new affine transformation as `displacement_fn(Ra, Rb, box=new_box)`. + When using real coordinates, we can specify positions in a space :math:`X` defined + by an affine transformation :math:`T` and compute displacements in a deformed space + :math:`X'` defined by an affine transformation :math:`T'`. This is done by writing + `displacement_fn(Ra, Rb, new_box=new_box)`. + + There are a few caveats when using `periodic_general`. `periodic_general` + uses the minimum image convention, and so it will fail for potentials whose + cutoff is longer than the half of the side-length of the box. It will also + fail to find the correct image when the box is too deformed. We hope to add a + more robust box for small simulations soon (TODO) along with better error + checking. In the meantime caution is recommended. + + Args: + box: A `(spatial_dim, spatial_dim)` affine transformation. + fractional_coordinates: A boolean specifying whether positions are stored + in the parallelepiped or the unit cube. + wrapped: A boolean specifying whether or not particle positions are + remapped back into the box after each step + Returns: + `(displacement_fn, shift_fn)` tuple. + """ + inv_box = inverse(box) + + def displacement_fn(Ra, Rb, perturbation=None, **kwargs): + _box, _inv_box = box, inv_box + + if "box" in kwargs: + _box = kwargs["box"] + + if not fractional_coordinates: + _inv_box = inverse(_box) + + if "new_box" in kwargs: + _box = kwargs["new_box"] + + if not fractional_coordinates: + Ra = transform(_inv_box, Ra) + Rb = transform(_inv_box, Rb) + + dR = periodic_displacement(f32(1.0), pairwise_displacement(Ra, Rb)) + dR = transform(_box, dR) + + if perturbation is not None: + dR = raw_transform(perturbation, dR) + + return dR + + def u(R, dR): + if wrapped: + return periodic_shift(f32(1.0), R, dR) + return R + dR + + def shift_fn(R, dR, **kwargs): + if not fractional_coordinates and not wrapped: + return R + dR + + _box, _inv_box = box, inv_box + if "box" in kwargs: + _box = kwargs["box"] + _inv_box = inverse(_box) + + if "new_box" in kwargs: + _box = kwargs["new_box"] - _box, _inv_box = box, inv_box - if 'box' in kwargs: - _box = kwargs['box'] - _inv_box = inverse(_box) + dR = transform(_inv_box, dR) + if not fractional_coordinates: + R = transform(_inv_box, R) - if 'new_box' in kwargs: - _box = kwargs['new_box'] + R = u(R, dR) - dR = transform(_inv_box, dR) - if not fractional_coordinates: - R = transform(_inv_box, R) + if not fractional_coordinates: + R = transform(_box, R) + return R - R = u(R, dR) + return displacement_fn, shift_fn - if not fractional_coordinates: - R = transform(_box, R) - return R - return displacement_fn, shift_fn +def metric(displacement: DisplacementFn) -> MetricFn: + """Takes a displacement function and creates a metric.""" + return lambda Ra, Rb, **kwargs: distance(displacement(Ra, Rb, **kwargs)) -def metric(displacement: DisplacementFn) -> MetricFn: - """Takes a displacement function and creates a metric.""" - return lambda Ra, Rb, **kwargs: distance(displacement(Ra, Rb, **kwargs)) +def map_product( + metric_or_displacement: DisplacementOrMetricFn, +) -> DisplacementOrMetricFn: + """Vectorizes a metric or displacement function over all pairs.""" + return vmap(vmap(metric_or_displacement, (0, None), 0), (None, 0), 0) -def map_product(metric_or_displacement: DisplacementOrMetricFn - ) -> DisplacementOrMetricFn: - """Vectorizes a metric or displacement function over all pairs.""" - return vmap(vmap(metric_or_displacement, (0, None), 0), (None, 0), 0) +def map_bond(metric_or_displacement: DisplacementOrMetricFn) -> DisplacementOrMetricFn: + """Vectorizes a metric or displacement function over bonds.""" + return vmap(metric_or_displacement, (0, 0), 0) -def map_bond(metric_or_displacement: DisplacementOrMetricFn - ) -> DisplacementOrMetricFn: - """Vectorizes a metric or displacement function over bonds.""" - return vmap(metric_or_displacement, (0, 0), 0) +def map_neighbor( + metric_or_displacement: DisplacementOrMetricFn, +) -> DisplacementOrMetricFn: + """Vectorizes a metric or displacement function over neighborhoods.""" + def wrapped_fn(Ra, Rb, **kwargs): + return vmap(vmap(metric_or_displacement, (0, None)))(Rb, Ra, **kwargs) -def map_neighbor(metric_or_displacement: DisplacementOrMetricFn - ) -> DisplacementOrMetricFn: - """Vectorizes a metric or displacement function over neighborhoods.""" - def wrapped_fn(Ra, Rb, **kwargs): - return vmap(vmap(metric_or_displacement, (0, None)))(Rb, Ra, **kwargs) - return wrapped_fn + return wrapped_fn def canonicalize_displacement_or_metric(displacement_or_metric): - """Checks whether or not a displacement or metric was provided.""" - for dim in range(1, 4): - try: - R = ShapedArray((dim,), f32) - dR_or_dr = eval_shape(displacement_or_metric, R, R, t=0) - if len(dR_or_dr.shape) == 0: - return displacement_or_metric - else: - return metric(displacement_or_metric) - except TypeError: - continue - except ValueError: - continue - raise ValueError( - 'Canonicalize displacement not implemented for spatial dimension larger' - 'than 4.') + """Checks whether or not a displacement or metric was provided.""" + for dim in range(1, 4): + try: + R = ShapedArray((dim,), f32) + dR_or_dr = eval_shape(displacement_or_metric, R, R, t=0) + if len(dR_or_dr.shape) == 0: + return displacement_or_metric + else: + return metric(displacement_or_metric) + except TypeError: + continue + except ValueError: + continue + raise ValueError( + "Canonicalize displacement not implemented for spatial dimension larger" + "than 4." + ) diff --git a/jax_sph/jax_md/util.py b/jax_sph/jax_md/util.py index f74bb07..27ffe30 100644 --- a/jax_sph/jax_md/util.py +++ b/jax_sph/jax_md/util.py @@ -1,5 +1,5 @@ # Source: https://github.com/jax-md/jax-md -# +# # Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,8 +36,9 @@ @partial(jit, static_argnums=(1,)) def safe_mask(mask, fn, operand, placeholder=0): - masked = jnp.where(mask, operand, 0) - return jnp.where(mask, fn(masked), placeholder) + masked = jnp.where(mask, operand, 0) + return jnp.where(mask, fn(masked), placeholder) + def is_array(x: Any) -> bool: - return isinstance(x, (jnp.ndarray, onp.ndarray)) \ No newline at end of file + return isinstance(x, (jnp.ndarray, onp.ndarray)) From bee1d1b0d54191bb865832d07cfdf457e69037fa Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 03:20:18 +0200 Subject: [PATCH 04/21] improve docs --- README.md | 18 ++++++----- docs/index.rst | 21 +++++++++++-- docs/pages/defaults.rst | 44 +++++++++++++++++++++++++++ docs/pages/neighbors.rst | 0 jax_sph/defaults.py | 64 ++++++++++++++++++++-------------------- 5 files changed, 106 insertions(+), 41 deletions(-) create mode 100644 docs/pages/defaults.rst create mode 100644 docs/pages/neighbors.rst diff --git a/README.md b/README.md index ebdac7c..f4b2dbd 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,17 @@ -JAX-SPH [(Toshev et al., 2024)](https://arxiv.org/abs/2403.04750) is a modular JAX-based weakly compressible SPH framework, which implements the following SPH routines: -- Standard SPH [(Adami et al., 2012)](https://www.sciencedirect.com/science/article/pii/S002199911200229X) -- Transport velocity SPH [(Adami et al., 2013)](https://www.sciencedirect.com/science/article/pii/S002199911300096X) -- Riemann SPH [(Zhang et al., 2017)](https://www.sciencedirect.com/science/article/abs/pii/S0021999117300438) - ![HT_T.gif](https://s9.gifyu.com/images/SUwUD.gif) +## Table of Contents + +1. [**Installation**](#installation) +1. [**Getting Started**](#getting-started) +1. [**Setting up a case**](#setting-up-a-case) +1. [**Contributing**](#contributing) +1. [**Citation**](#citation) +1. [**Acknowledgements**](#acknowledgements) + ## Installation ### Standalone library @@ -90,10 +94,10 @@ We provide four notebooks demonstrating how to use JAX-SPH: - [`iclr24_inverse.ipynb`](notebooks/tutorial.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_inverse.ipynb), solving the inverse problem of finding the initial state of a 100-step-long SPH simulation. - [`iclr24_sitl.ipynb`](notebooks/tutorial.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_sitl.ipynb), including training and testing a Solver-in-the-Loop model using the [LagrangeBench](https://github.com/tumaer/lagrangebench) library. -## Setting up a case +## Setting up a Case To set up a case, just add a `my_case.py` and a `my_case.yaml` file to the `cases/` directory. Every *.py case should inherit from `SimulationSetup` in `jax_sph/case_setup.py` or another case, and every *.yaml config file should either contain a complete set of parameters (see `jax_sph/defaults.py`) or extend `JAX_SPH_DEFAULTS`. Running a case in relaxation mode `case.mode=rlx` overwrites certain parts of the selected case. Passed CLI arguments overwrite any argument. -## Development and Contribution +## Contributing If you wish to contribute, please run ```bash pre-commit install diff --git a/docs/index.rst b/docs/index.rst index 0f9fb24..98efd8a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,14 +3,31 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to JAX-SPH's documentation! -=================================== +JAX-SPH +======== + +.. image:: https://s9.gifyu.com/images/SUwUD.gif + :alt: GIF + + +What is ``JAX-SPH``? +-------------------- + +JAX-SPH `(Toshev et al., 2024) `_ is a Smoothed Particle Hydrodynamics (SPH) code written in `JAX `_. JAX-SPH is designed to be simple, fast, and compatible with deep learning workflows. We currently support the following SPH routines: + +* Standard SPH `(Adami et al., 2012) `_ +* Transport velocity SPH `(Adami et al., 2013) `_ +* Riemann SPH `(Zhang et al., 2017) `_ + +Check out our `GitHub repository `_ for more information including installation instructions and tutorial notebooks. .. toctree:: :maxdepth: 2 :caption: Contents: + pages/defaults pages/case_setup pages/solver pages/simulate pages/utils + pages/neighbors \ No newline at end of file diff --git a/docs/pages/defaults.rst b/docs/pages/defaults.rst new file mode 100644 index 0000000..cc14c09 --- /dev/null +++ b/docs/pages/defaults.rst @@ -0,0 +1,44 @@ +Defaults +=================================== + + +.. exec_code:: + :hide_code: + :linenos_output: + :language_output: python + :caption: JAX-SPH default values + + + with open("jax_sph/defaults.py", "r") as file: + defaults_full = file.read() + + # parse defaults: remove imports, only keep the set_defaults function + + defaults_full = defaults_full.split("\n") + + # remove imports + defaults_full = [line for line in defaults_full if not line.startswith("import")] + defaults_full = [line for line in defaults_full if len(line.replace(" ", "")) > 0] + + # remove other functions + keep = False + defaults = [] + for i, line in enumerate(defaults_full): + if line.startswith("def"): + if "set_defaults" in line: + keep = True + else: + keep = False + + if keep: + defaults.append(line) + + # remove function declaration and return + defaults = defaults[2:-2] + + # remove indent + defaults = [line[4:] for line in defaults] + + + print("\n".join(defaults)) + \ No newline at end of file diff --git a/docs/pages/neighbors.rst b/docs/pages/neighbors.rst new file mode 100644 index 0000000..e69de29 diff --git a/jax_sph/defaults.py b/jax_sph/defaults.py index 857d0eb..4574c2b 100644 --- a/jax_sph/defaults.py +++ b/jax_sph/defaults.py @@ -9,7 +9,7 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: ### global and hardware-related configs # .yaml case configuration file - cfg.config = None # previously: case + cfg.config = None # Seed for random number generator cfg.seed = 123 # Whether to disable jitting compilation @@ -17,7 +17,7 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: # Which GPU to use. -1 for CPU cfg.gpu = 0 # Data type. One of "float32" or "float64" - cfg.dtype = "float64" # previously: no_f64 + cfg.dtype = "float64" # XLA memory fraction to be preallocated. The JAX default is 0.75. # Should be specified before importing the library. cfg.xla_mem_fraction = 0.75 @@ -30,30 +30,30 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: # Simulation mode. One of "sim" (run simulation) or "rlx" (run relaxation) cfg.case.mode = "sim" # Dimension of the simulation. One of 2 or 3 - cfg.case.dim = 3 # previously: dim + cfg.case.dim = 3 # Average distance between particles [0.001, 0.1] - cfg.case.dx = 0.05 # previously: dx + cfg.case.dx = 0.05 # Initial state h5 path. Overrides `r0_type`. Can be useful to restart a simulation. - cfg.case.state0_path = None # previously: state0-path + cfg.case.state0_path = None # Which properties to adopt from state0_path. Include all to restart a simulation. cfg.case.state0_keys = ["r"] # Position initialization type. One of "cartesian" or "relaxed". Cartesian can have # `r0_noise_factor` and relaxed requires a state to be present in `data_relaxed`. - cfg.case.r0_type = "cartesian" # previously: r0-type + cfg.case.r0_type = "cartesian" # How much Gaussian noise to add to r0. ( _ * dx) - cfg.case.r0_noise_factor = 0.0 # previously: r0-noise-factor + cfg.case.r0_noise_factor = 0.0 # Magnitude of external force field - cfg.case.g_ext_magnitude = 0.0 # previously: g-ext-magnitude + cfg.case.g_ext_magnitude = 0.0 # Reference dynamic viscosity. Inversely proportional to Re. - cfg.case.viscosity = 0.01 # previously: viscosity + cfg.case.viscosity = 0.01 # Estimate max flow velocity to calculate artificial speed of sound. - cfg.case.u_ref = 1.0 # previously: u_ref + cfg.case.u_ref = 1.0 # Reference speed of sound factor w.r.t. u_ref. - cfg.case.c_ref_factor = 10.0 # previously: p-bg-factor + cfg.case.c_ref_factor = 10.0 # Reference density cfg.case.rho_ref = 1.0 # Reference temperature - cfg.case.T_ref = 1.0 # previously: T-ref + cfg.case.T_ref = 1.0 # Reference thermal conductivity cfg.case.kappa_ref = 0.0 # Reference heat capacity at constant pressure @@ -65,31 +65,31 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: cfg.solver = OmegaConf.create({}) # Solver name. One of "SPH" (standard SPH) or "RIE" (Riemann SPH) - cfg.solver.name = "SPH" # previously: solver + cfg.solver.name = "SPH" # Transport velocity inclusion factor [0,...,1] - cfg.solver.tvf = 0.0 # previously: tvf + cfg.solver.tvf = 0.0 # CFL condition factor - cfg.solver.cfl = 0.25 # previously: cfl + cfg.solver.cfl = 0.25 # Density evolution vs density summation - cfg.solver.density_evolution = False # previously: density-evolution + cfg.solver.density_evolution = False # Density renormalization when density evolution - cfg.solver.density_renormalize = False # previously: density-renormalize + cfg.solver.density_renormalize = False # Integration time step. If None, it is calculated from the CFL condition. - cfg.solver.dt = None # previously: dt + cfg.solver.dt = None # Physical time length of simulation - cfg.solver.t_end = 0.2 # previously: t-end + cfg.solver.t_end = 0.2 # Parameter alpha of artificial viscosity term - cfg.solver.artificial_alpha = 0.0 # previously: artificial-alpha + cfg.solver.artificial_alpha = 0.0 # Whether to turn on free-slip boundary condition - cfg.solver.free_slip = False # previously: free-slip + cfg.solver.free_slip = False # Riemann dissipation limiter parameter, -1 = off - cfg.solver.eta_limiter = 3 # previously: eta-limiter + cfg.solver.eta_limiter = 3 # Thermal conductivity (non-dimensional) - cfg.solver.kappa = 0 # previously: kappa + cfg.solver.kappa = 0 # Number of wall boundary particle layers cfg.solver.n_walls = 3 # Whether to apply the heat conduction term - cfg.solver.heat_conduction = False # previously: heat-conduction + cfg.solver.heat_conduction = False # Whether to apply boundaty conditions cfg.solver.is_bc_trick = False # new @@ -104,7 +104,7 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: # "WC6K" (Wendland C4 kernel) # "GK" (gaussian kernel) # "SGK" (super gaussian kernel) - cfg.kernel.name = "QSK" # previously: kernel + cfg.kernel.name = "QSK" # Smoothing length factor cfg.kernel.h_factor = 1.0 # new. Should default to 1.3 WC2K and 1.0 QSK @@ -112,29 +112,29 @@ def set_defaults(cfg: DictConfig = OmegaConf.create({})) -> DictConfig: cfg.eos = OmegaConf.create({}) # EoS name. One of "Tait" or "RIEMANN" - cfg.eos.name = "Tait" # previously: eos + cfg.eos.name = "Tait" # power in the Tait equation of state cfg.eos.gamma = 1.0 # background pressure factor w.r.t. p_ref - cfg.eos.p_bg_factor = 0.0 # previously: p-bg-factor + cfg.eos.p_bg_factor = 0.0 ### neighbor list cfg.nl = OmegaConf.create({}) # Neighbor list backend. One of "jaxmd_vmap", "jaxmd_scan", "matscipy" - cfg.nl.backend = "jaxmd_vmap" # previously: nl-backend + cfg.nl.backend = "jaxmd_vmap" # Number of partitions for neighbor list. Applies to jaxmd_scan only. - cfg.nl.num_partitions = 1 # previously: num-partitions + cfg.nl.num_partitions = 1 ### output writing cfg.io = OmegaConf.create({}) # In which format to write states. A subset of ["h5", "vtk"] - cfg.io.write_type = [] # previously: write-h5, write-vtk + cfg.io.write_type = [] # Every `write_every` step will be saved - cfg.io.write_every = 1 # previously: write-every + cfg.io.write_every = 1 # Where to write and read data - cfg.io.data_path = "./" # previously: data-path + cfg.io.data_path = "./" # What to print to stdout. As list of possible properties. cfg.io.print_props = ["Ekin", "u_max"] From ad1d1edcec048437bff210a22393e578ffb6161e Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 03:25:57 +0200 Subject: [PATCH 05/21] add empty neighbors.rst --- docs/pages/neighbors.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/pages/neighbors.rst b/docs/pages/neighbors.rst index e69de29..047e239 100644 --- a/docs/pages/neighbors.rst +++ b/docs/pages/neighbors.rst @@ -0,0 +1,4 @@ +Neighbor Search +================ + +.. TODO: Put the neighbor search documentation here, incl. from LagrangeBench. From b0a05c80feb687043123d7e371987780a3350c2b Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 03:40:41 +0200 Subject: [PATCH 06/21] omit jax_md from codecov --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fd2b88f..7b2c93b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,12 @@ filterwarnings = [ "ignore::DeprecationWarning:^(?!.*jax_sph).*" ] +[tool.coverage.run] +omit = ["jax_sph/jax_md/*"] + +[tool.coverage.report] +omit = ["jax_sph/jax_md/*"] + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From 57a217a3b23376ef11577d9173ffc5ccaf626135 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 15:25:50 +0200 Subject: [PATCH 07/21] README on JAX-MD deprecation --- jax_sph/jax_md/README.md | 1 + pyproject.toml | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 jax_sph/jax_md/README.md diff --git a/jax_sph/jax_md/README.md b/jax_sph/jax_md/README.md new file mode 100644 index 0000000..868ca27 --- /dev/null +++ b/jax_sph/jax_md/README.md @@ -0,0 +1 @@ +At the time of writing this (08.06.2024), the latest JAX-MD on PyPI 0.2.8 is 10 months old and not compatible with the latest JAX. Although the main branch on GitHub is somewhat up to date, it seems that one cannot have GitHub repositories as PyPI dependencies, see https://stackoverflow.com/a/54894359/21577142. And as we only rely on `space` and `partition`, we copy all relevant files here. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7b2c93b..da886e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,10 @@ pyvista = ">=0.42.2" # for visualization jax = {version = "0.4.28", extras = ["cpu"]} jaxlib = "0.4.28" omegaconf = "^2.3.0" +matscipy = ">=0.8.0" +dataclasses = "0.6" # for jax-md +jraph = "^0.0.6.dev0" # for jax-md +absl-py = "^2.1.0" # for jax-md [tool.poetry.group.dev.dependencies] pre-commit = ">=3.3.1" @@ -28,7 +32,6 @@ ruff = ">=0.1.8" [tool.poetry.group.temp.dependencies] ott-jax = ">=0.4.2" ipykernel = ">=6.25.1" -matscipy = ">=0.8.0" [tool.poetry.group.docs.dependencies] sphinx = "7.2.6" @@ -36,11 +39,6 @@ sphinx-exec-code = "0.12" sphinx-rtd-theme = "1.3.0" toml = "^0.10.2" -[tool.poetry.group.jaxmd.dependencies] -dataclasses = "0.6" -jraph = "^0.0.6.dev0" -absl-py = "^2.1.0" - [tool.ruff] ignore = ["F821", "E402"] exclude = [ From 0dde5aa0ec1505a63e1c7dfee15a8d9185c85f80 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 15:27:12 +0200 Subject: [PATCH 08/21] move neighbor_search from lagrangebench to a notebook here --- README.md | 10 ++-- docs/index.rst | 5 +- docs/pages/neighbors.rst | 4 -- notebooks/kernel_plots.ipynb | 46 ++++++++-------- notebooks/neighbors.ipynb | 102 +++++++++++++++++++++++++++++++++++ notebooks/neighbors.py | 78 +++++++++++++++++++++++++++ notebooks/neighbors.sh | 42 +++++++++++++++ 7 files changed, 252 insertions(+), 35 deletions(-) delete mode 100644 docs/pages/neighbors.rst create mode 100644 notebooks/neighbors.ipynb create mode 100644 notebooks/neighbors.py create mode 100644 notebooks/neighbors.sh diff --git a/README.md b/README.md index f4b2dbd..1e42385 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,13 @@ python main.py config=cases/ht.yaml ``` ### Notebooks -We provide four notebooks demonstrating how to use JAX-SPH: +We provide various notebooks demonstrating how to use JAX-SPH: - [`tutorial.ipynb`](notebooks/tutorial.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/tutorial.ipynb), with a general overview of JAX-SPH and an example how to run the channel flow with hot bottom wall. -- [`iclr24_grads.ipynb`](notebooks/tutorial.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_grads.ipynb), with a validation of the gradients through the solver. -- [`iclr24_inverse.ipynb`](notebooks/tutorial.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_inverse.ipynb), solving the inverse problem of finding the initial state of a 100-step-long SPH simulation. -- [`iclr24_sitl.ipynb`](notebooks/tutorial.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_sitl.ipynb), including training and testing a Solver-in-the-Loop model using the [LagrangeBench](https://github.com/tumaer/lagrangebench) library. +- [`iclr24_grads.ipynb`](notebooks/iclr24_grads.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_grads.ipynb), with a validation of the gradients through the solver. +- [`iclr24_inverse.ipynb`](notebooks/iclr24_inverse.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_inverse.ipynb), solving the inverse problem of finding the initial state of a 100-step-long SPH simulation. +- [`iclr24_sitl.ipynb`](notebooks/iclr24_sitl.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/iclr24_sitl.ipynb), including training and testing a Solver-in-the-Loop model using the [LagrangeBench](https://github.com/tumaer/lagrangebench) library. +- [`neighbors.ipynb`](notebooks/neighbors.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/neighbors.ipynb), explaining the difference between the three neighbor search implementations and comparing their performance. +- [`kernel_plots.ipynb`](notebooks/kernel_plots.ipynb) [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/kernel_plots.ipynb), visualizing the SPH kernels. ## Setting up a Case To set up a case, just add a `my_case.py` and a `my_case.yaml` file to the `cases/` directory. Every *.py case should inherit from `SimulationSetup` in `jax_sph/case_setup.py` or another case, and every *.yaml config file should either contain a complete set of parameters (see `jax_sph/defaults.py`) or extend `JAX_SPH_DEFAULTS`. Running a case in relaxation mode `case.mode=rlx` overwrites certain parts of the selected case. Passed CLI arguments overwrite any argument. diff --git a/docs/index.rst b/docs/index.rst index 98efd8a..66f902d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,11 +23,10 @@ Check out our `GitHub repository `_ for more .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: API pages/defaults pages/case_setup pages/solver pages/simulate - pages/utils - pages/neighbors \ No newline at end of file + pages/utils \ No newline at end of file diff --git a/docs/pages/neighbors.rst b/docs/pages/neighbors.rst deleted file mode 100644 index 047e239..0000000 --- a/docs/pages/neighbors.rst +++ /dev/null @@ -1,4 +0,0 @@ -Neighbor Search -================ - -.. TODO: Put the neighbor search documentation here, incl. from LagrangeBench. diff --git a/notebooks/kernel_plots.ipynb b/notebooks/kernel_plots.ipynb index d18992b..21865e1 100644 --- a/notebooks/kernel_plots.ipynb +++ b/notebooks/kernel_plots.ipynb @@ -1,5 +1,14 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plots of kernels and their gradients evaluated in 1D [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/kernel_plots.ipynb)\n", + "\n", + "Evaluate the kernels and their derivatives." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -8,8 +17,8 @@ "source": [ "import jax.numpy as jnp\n", "import matplotlib.pyplot as plt\n", - "\n", "from jax import vmap\n", + "\n", "from jax_sph.kernel import (\n", " CubicKernel,\n", " GaussianKernel,\n", @@ -21,14 +30,6 @@ ")" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Plots of kernels and their gradients evaluated in 1D\n", - "calculate the kernel values itself and the values of the gradients" - ] - }, { "cell_type": "code", "execution_count": 2, @@ -57,7 +58,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "plot values" + "Visualize kernels." ] }, { @@ -67,17 +68,7 @@ "outputs": [ { "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -92,8 +83,15 @@ " axs[0].plot(t, w[i], label=str(kernels[i][0].__name__))\n", " axs[1].plot(t, w_grad[i], label=str(kernels[i][0].__name__))\n", "\n", - "axs[0].legend()\n", - "axs[1].legend()" + "for ax in axs:\n", + " ax.set_xlabel(\"x\")\n", + " ax.legend()\n", + " ax.grid()\n", + "\n", + "axs[0].set_ylabel(\"W(x)\")\n", + "axs[1].set_ylabel(\"dW(x)/dx\")\n", + "plt.tight_layout()\n", + "plt.show()" ] } ], @@ -113,7 +111,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/notebooks/neighbors.ipynb b/notebooks/neighbors.ipynb new file mode 100644 index 0000000..3467452 --- /dev/null +++ b/notebooks/neighbors.ipynb @@ -0,0 +1,102 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Neighbor Search Implementations [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tumaer/jax-sph/blob/main/notebooks/neighbors.ipynb)\n", + "\n", + "## Algorithms\n", + "\n", + "We integrate three neighbor list routines in our codebase:\n", + "\n", + "- `jaxmd_vmap`: refers to using the original cell list-based implementation from the [JAX-MD](https://github.com/jax-md/jax-md) library.\n", + "- `jaxmd_scan`: refers to using a more memory-efficient implementation of the JAX-MD function. We achieve this by partitioning the search over potential neighbors from the cell list-based candidate neighbors into `num_partitions` chunks. We need to define three variables to explain how our implementation works:\n", + " - $X \\in \\mathbb{R}^{N\\times d}$ - the particle coordinates of $N$ particles in $d$ dimensions.\n", + " - $h \\in \\mathbb{N}^{N}$ - the list specifying to which cell a particle belongs.\n", + " - $L \\in \\mathbb{N}^{C \\times cand}$ - list specifying which particles are potential candidates to a particle in cell $c \\in [1, ..., C]$. The number of potential candidates $cand$ is the product of the fixed cell capacity (needed for jit-ability) and the number of reachable cells, e.g. 27 in 3D.\n", + "\n", + " The `jaxmd_vmap` implementation essentially instantiates all possible connections by creating an object of size $N \\cdot cand$, and only after all distances between potential neighbors have been computed the edge list is pruned to its actual size being ~6x smaller in 3D. This factor comes from the fact that the cell size is approximately equal to the cutoff radius and if we split a unit cube into $3^3$ cells, then the volume of a sphere with $r=1/3$ will be around $1/6$ the volume of the cube. By splitting $X$ and $h$ into `num_partitions` parts and iterating over $L$ with a `jax.lax.scan` loop, we can remove $~5/6$ of the edges before putting them together into one list.\n", + "\n", + "- `matscipy`: to enable computations over systems with variable number of particles, none of the above implementation can be used and that is why we wrote a wrapper around the [matscipy](https://github.com/libAtoms/matscipy) neighbos search routine `matscipy.neighbours.neighbour_list`. This is again a cell list-based algorithms, however only available on CPU. Our wrapper essentially mimics the behavior of the JAX-MD function, but pads all non-existing particles to the maximal number of particles in the dataset.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Performance\n", + "\n", + "> Note: We observe reasonable performance from each of these implementations with up to ~10k particles, but more investigation need to be conducted towards comparing these algorithms on larger systems. Remember that we limit the system size of our benchmark datasets to 10k for memory reasons on the GNN side, and scaling eventually requires domain decomposition and parallelization.\n", + "\n", + "### `vmap` vs `scan`\n", + "\n", + "We compare the largest number of particles whose neighbor list computation fits into memory. We ran the script [`neighbors.sh`](./neighbors.sh) on an A6000 GPU with 48GB memory and observed that the default vectorized implementation (`vmap`) can handle up to 1M particles before running out of memory, while our `scan` implementation reaches 3.3M. This happens at almost no additional time cost and holds for both allocating a system and updating it after jit compilation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! neighbors.sh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output of the above script looks like follows:\n", + "\n", + "```tty\n", + "###################################################\n", + "###################################################\n", + "Start with Nx=100, mode=allocate, backend=jaxmd_vmap\n", + "Finish with 1000000 particles and 141283880 edges!\n", + "Start with Nx=102, mode=allocate, backend=jaxmd_vmap\n", + "Start with Nx=104, mode=allocate, backend=jaxmd_vmap\n", + "Start with Nx=106, mode=allocate, backend=jaxmd_vmap\n", + "Start with Nx=108, mode=allocate, backend=jaxmd_vmap\n", + "Start with Nx=110, mode=allocate, backend=jaxmd_vmap\n", + "###################################################\n", + "Start with Nx=150, mode=allocate, backend=jaxmd_scan\n", + "Finish with 3375000 particles and 476838165 edges!\n", + "Start with Nx=152, mode=allocate, backend=jaxmd_scan\n", + "Start with Nx=154, mode=allocate, backend=jaxmd_scan\n", + "Start with Nx=156, mode=allocate, backend=jaxmd_scan\n", + "Start with Nx=158, mode=allocate, backend=jaxmd_scan\n", + "Start with Nx=160, mode=allocate, backend=jaxmd_scan\n", + "###################################################\n", + "###################################################\n", + "Start with Nx=100, mode=update, backend=jaxmd_vmap\n", + "Finish with 1000000 particles and 141283880 edges!\n", + "Start with Nx=102, mode=update, backend=jaxmd_vmap\n", + "Start with Nx=104, mode=update, backend=jaxmd_vmap\n", + "Start with Nx=106, mode=update, backend=jaxmd_vmap\n", + "Start with Nx=108, mode=update, backend=jaxmd_vmap\n", + "Start with Nx=110, mode=update, backend=jaxmd_vmap\n", + "###################################################\n", + "Start with Nx=150, mode=update, backend=jaxmd_scan\n", + "Finish with 3375000 particles and 476838165 edges!\n", + "Start with Nx=152, mode=update, backend=jaxmd_scan\n", + "Start with Nx=154, mode=update, backend=jaxmd_scan\n", + "Start with Nx=156, mode=update, backend=jaxmd_scan\n", + "Start with Nx=158, mode=update, backend=jaxmd_scan\n", + "Start with Nx=160, mode=update, backend=jaxmd_scan\n", + "```\n", + "\n", + "### `matscipy`\n", + "\n", + "The matscipy implementation is extremely fast for small systems (10k particles) and doesn't take any GPU memory for the construction of the edge list, however, as the systems size increases, copying memory between CPU and GPU becomes a bottleneck. Also, it seems like matscipy uses a single CPU computation which is rather limiting.\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/neighbors.py b/notebooks/neighbors.py new file mode 100644 index 0000000..ef0b750 --- /dev/null +++ b/notebooks/neighbors.py @@ -0,0 +1,78 @@ +import argparse + +from jax.config import config + +config.update("jax_enable_x64", True) + +import jax.numpy as jnp +import numpy as np +from jax import jit + +from jax_sph import partition +from jax_sph.jax_md import space + + +def pos_init_cartesian_3d(box_size, dx, noise_std_factor=0.3333): + n = np.array((box_size / dx).round(), dtype=int) + grid = np.meshgrid(range(n[0]), range(n[1]), range(n[2]), indexing="xy") + r = (jnp.vstack(list(map(jnp.ravel, grid))).T + 0.5) * dx + np.random.seed(0) + r += np.random.randn(*r.shape) * dx * noise_std_factor + r = r % box_size # project back into unit box + return r + + +def update_wrapper(neighbors_old, r_new): + neighbors_new = neighbors_old.update(r_new) + return neighbors_new + + +def compute_neighbors(args): + Nx = args.Nx + mode = args.mode + nl_backend = args.nl_backend + num_partitions = args.num_partitions + print(f"Start with Nx={Nx}, mode={mode}, backend={nl_backend}") + + dx = 1 / Nx + box_size = np.array([1.0, 1.0, 1.0]) + r = pos_init_cartesian_3d(box_size, dx) + + displacement_fn, _ = space.periodic(side=box_size) + neighbor_fn = partition.neighbor_list( + displacement_fn, + box_size, + r_cutoff=3 * dx, + backend=nl_backend, + dr_threshold=0.0, + capacity_multiplier=1.25, + mask_self=False, + format=partition.NeighborListFormat.Sparse, + num_particles_max=r.shape[0], + num_partitions=num_partitions, + pbc=np.array([True, True, True]), + ) + current_num_particles = r.shape[0] + neighbors = neighbor_fn.allocate(r, num_particles=current_num_particles) + + if mode == "update": + updater = jit(update_wrapper) + neighbors = updater(neighbors, r) + + print(f"Finish with {r.shape[0]} particles and {neighbors.idx.shape[1]} edges!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--mode", default="update", choices=["allocate", "update"]) + parser.add_argument("--num-partitions", type=int, default=8) + parser.add_argument("--Nx", type=int, default=30, help="alternative to --dx") + parser.add_argument( + "--nl-backend", + default="jaxmd_scan", + choices=["jaxmd_vmap", "jaxmd_scan", "matscipy"], + help="Which backend to use for neighbor list", + ) + args = parser.parse_args() + + compute_neighbors(args) diff --git a/notebooks/neighbors.sh b/notebooks/neighbors.sh new file mode 100644 index 0000000..72c9fa0 --- /dev/null +++ b/notebooks/neighbors.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "###################################################" >> std.out +echo "###################################################" >> std.out + +######### Allocate -> vmap to 100^3, numcells to 150^3 +for (( Nx=100; Nx<=110; Nx++ )); do + if (( Nx % 2 == 0 )); then + echo "Run with Nx = $Nx" + .venv/bin/python neighbors_search/scaling.py --Nx=$Nx --mode=allocate --nl-backend=jaxmd_vmap >> std.out 2> std.err + fi +done + +echo "###################################################" >> std.out + +for (( Nx=150; Nx<=160; Nx++ )); do + if (( Nx % 2 == 0 )); then + echo "Run with Nx = $Nx" + .venv/bin/python neighbors_search/scaling.py --Nx=$Nx --mode=allocate --nl-backend=jaxmd_scan --num-partitions=4 >> std.out 2> std.err + fi +done + +echo "###################################################" >> std.out +echo "###################################################" >> std.out + +######### Update -> vmap to 100^3, numcells to 150^3 +for (( Nx=100; Nx<=110; Nx++ )); do + if (( Nx % 2 == 0 )); then + echo "Run with Nx = $Nx" + .venv/bin/python neighbors_search/scaling.py --Nx=$Nx --mode=update --nl-backend=jaxmd_vmap >> std.out 2> std.err + fi +done + +echo "###################################################" >> std.out + +# Run a for loop over different Nx values +for (( Nx=150; Nx<=160; Nx++ )); do + if (( Nx % 2 == 0 )); then + echo "Run with Nx = $Nx" + .venv/bin/python neighbors_search/scaling.py --Nx=$Nx --mode=update --nl-backend=jaxmd_scan --num-partitions=4 >> std.out 2> std.err + fi +done From 8d3161e308d4abc6a7a4366e5f6c49e0d8eaa2c8 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 15:34:16 +0200 Subject: [PATCH 09/21] regenerate poetry.lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 9156892..9a8956a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2682,4 +2682,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.11" -content-hash = "98afca7167130fab482743ab1792bc6393190a7a4967eadc4449ed001b33e58d" +content-hash = "9dd3f880130bab1f475f71d18e7215d861517140fb64a69ac4cd3fa76d63a129" From 79e2a6acfdd19ba9d1ea3860fa4aedac51adcb64 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Sat, 8 Jun 2024 16:04:13 +0200 Subject: [PATCH 10/21] add 'Getting Started' to docs --- docs/index.rst | 8 +++++++- docs/pages/defaults.rst | 4 ++++ docs/pages/tutorials.rst | 8 ++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docs/pages/tutorials.rst diff --git a/docs/index.rst b/docs/index.rst index 66f902d..85d7f5e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,11 +21,17 @@ JAX-SPH `(Toshev et al., 2024) `_ is a Smoothe Check out our `GitHub repository `_ for more information including installation instructions and tutorial notebooks. +.. toctree:: + :maxdepth: 1 + :caption: Getting Started + + pages/tutorials + pages/defaults + .. toctree:: :maxdepth: 2 :caption: API - pages/defaults pages/case_setup pages/solver pages/simulate diff --git a/docs/pages/defaults.rst b/docs/pages/defaults.rst index cc14c09..56b7647 100644 --- a/docs/pages/defaults.rst +++ b/docs/pages/defaults.rst @@ -1,6 +1,10 @@ Defaults =================================== +The defaults are defined through a function ``jax_sph.defaults.set_defaults()``, which +takes a potentially empty ``omegaconf.DictConfig`` object and creates or overwrites the +default values. One can also directly call ``from jax_sph.defaults import defaults``, +with ``defaults=set_defaults()``, to get the default DictConfig, which we unpack below. .. exec_code:: :hide_code: diff --git a/docs/pages/tutorials.rst b/docs/pages/tutorials.rst new file mode 100644 index 0000000..b23ce58 --- /dev/null +++ b/docs/pages/tutorials.rst @@ -0,0 +1,8 @@ +Tutorials +========= + +Currently, there are two places to look for tutorials: + +* The README of our `GitHub repository `_. +* The `notebooks `_ in the same + repository. \ No newline at end of file From 6235712a8b5ce62e1cd3e94f00c7c2a513f45a3c Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Sat, 8 Jun 2024 04:22:34 +0200 Subject: [PATCH 11/21] Riemann viscous BC fix v1 --- jax_sph/solver.py | 37 +++++++++++++++++++++++++++++++------ tests/test_pf2d.py | 2 +- validation/pf2d.sh | 2 ++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/jax_sph/solver.py b/jax_sph/solver.py index b96f086..5976c09 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -197,6 +197,7 @@ def acceleration_fn_riemann( mask, n_w_j, g_ext_i, + u_tilde_j, ): # Compute unit vector, above eq. (6), Zhang (2017) e_ij = e_s @@ -211,9 +212,12 @@ def acceleration_fn_riemann( rho_L = rho_i # u_w from eq. (15), Yang (2020) + # u_d = 2 * u_i - u_j + u_d = 2 * u_i - u_tilde_j u_R = jnp.where( wall_mask_j == 1, - -u_L + 2 * jnp.dot(u_j, n_w_j), + # -u_L + 2 * jnp.dot(u_j, n_w_j), + jnp.dot(u_d, -n_w_j), jnp.dot(u_j, -e_ij), ) p_R = jnp.where(wall_mask_j == 1, p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j) @@ -236,7 +240,13 @@ def acceleration_fn_riemann( eq_9 = -2 * m_j * (P_star / (rho_i * rho_j)) * kernel_grad # viscosity term eq. (6), Zhang (2019) - v_ij = u_i - u_j + # TODO: u_j is supposed to be u_i, but why is it not working? + u_d = 2 * u_j - u_tilde_j + v_ij = jnp.where( + wall_mask_j == 1, + u_i - u_d, + u_i - u_j, + ) eq_6 = 2 * m_j * eta_ij / (rho_i * rho_j) * v_ij / (d_ij + EPS) eq_6 *= kernel_part_diff * mask @@ -388,11 +398,22 @@ def gwbc_fn_riemann_wrapper(is_free_slip, is_heat_conduction): def free_weight(fluid_mask_i, tag_i): return fluid_mask_i + + def Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): + u_tilde = jnp.empty_like(u) + return u_tilde else: def free_weight(fluid_mask_i, tag_i): return jnp.ones_like(tag_i) + def Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): + w_dist_fluid = w_dist * fluid_mask[j_s] + u_wall_nom = ops.segment_sum(w_dist_fluid[:, None] * u[j_s], i_s, N) + u_wall_denom = ops.segment_sum(w_dist_fluid, i_s, N) + u_tilde = u_wall_nom / (u_wall_denom[:, None] + EPS) + return u_tilde + if is_heat_conduction: def heat_bc(mask_j_s_fluid, w_dist, temperature, i_s, j_s, tag, N): @@ -410,7 +431,7 @@ def heat_bc(mask_j_s_fluid, w_dist, temperature, i_s, j_s, tag, N): def heat_bc(mask_j_s_fluid, w_dist, temperature, i_s, j_s, tag, N): return temperature - return free_weight, heat_bc + return free_weight, Riemann_velocities, heat_bc def limiter_fn_wrapper(eta_limiter, c_ref): @@ -503,9 +524,11 @@ def __init__( self._kernel_fn = SuperGaussianKernel(h=dx, dim=dim) self._gwbc_fn = gwbc_fn_wrapper(is_free_slip, is_heat_conduction, eos) - self._free_weight, self._heat_bc = gwbc_fn_riemann_wrapper( - is_free_slip, is_heat_conduction - ) + ( + self._free_weight, + self._Riemann_velocities, + self._heat_bc, + ) = gwbc_fn_riemann_wrapper(is_free_slip, is_heat_conduction) self._acceleration_tvf_fn = acceleration_tvf_fn_wrapper(self._kernel_fn) self._acceleration_riemann_fn = acceleration_riemann_fn_wrapper( self._kernel_fn, eos, _beta_fn, eta_limiter @@ -621,6 +644,7 @@ def forward(state, neighbors): ) elif self.is_bc_trick and (self.solver == "RIE"): mask = self._free_weight(fluid_mask[i_s], tag[i_s]) + u_tilde = self._Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) temperature = self._heat_bc( fluid_mask[j_s], w_dist, temperature, i_s, j_s, tag, N ) @@ -687,6 +711,7 @@ def forward(state, neighbors): mask, n_w[j_s], g_ext[i_s], + u_tilde[j_s], ) dudt = ops.segment_sum(out, i_s, N) diff --git a/tests/test_pf2d.py b/tests/test_pf2d.py index 36f4105..1a8a58d 100644 --- a/tests/test_pf2d.py +++ b/tests/test_pf2d.py @@ -103,7 +103,7 @@ def get_solution(data_path, t_dimless, y_axis): return solutions -@pytest.mark.parametrize("tvf, solver", [(0.0, "SPH"), (1.0, "SPH")]) # (0.0, "RIE") +@pytest.mark.parametrize("tvf, solver", [(0.0, "SPH"), (1.0, "SPH"), (0.0, "RIE")]) def test_pf2d(tvf, solver, tmp_path, setup_simulation): """Test whether the poiseuille flow simulation matches the analytical solution""" y_axis, t_dimless, ref_solutions = setup_simulation diff --git a/validation/pf2d.sh b/validation/pf2d.sh index 09973ef..7ffa69f 100755 --- a/validation/pf2d.sh +++ b/validation/pf2d.sh @@ -6,7 +6,9 @@ # Generate data python main.py config=cases/pf.yaml solver.tvf=1.0 io.data_path=data_valid/pf2d_tvf/ python main.py config=cases/pf.yaml solver.tvf=0.0 io.data_path=data_valid/pf2d_notvf/ +python main.py config=cases/pf.yaml solver.tvf=0.0 solver.name=RIE solver.density_evolution=True io.data_path=data_valid/pf2d_Rie/ # Run validation script python validation/validate.py --case=2D_PF --src_dir=data_valid/pf2d_tvf/ python validation/validate.py --case=2D_PF --src_dir=data_valid/pf2d_notvf/ +python validation/validate.py --case=2D_PF --src_dir=data_valid/pf2d_Rie/ From d52b95335091cbe71eb907a36c333e04cd749fe4 Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Sat, 8 Jun 2024 04:34:21 +0200 Subject: [PATCH 12/21] Riemann velocity term BC fix v1 --- jax_sph/solver.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/jax_sph/solver.py b/jax_sph/solver.py index 5976c09..dfc0197 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -47,6 +47,7 @@ def rho_evol_riemann_fn( wall_mask_j, n_w_j, g_ext_i, + u_tilde_j, **kwargs, ): # Compute unit vector, above eq. (6), Zhang (2017) @@ -61,9 +62,12 @@ def rho_evol_riemann_fn( rho_L = rho_i # u_w from eq. (15), Yang (2020) + # u_d = 2 * u_i - u_j + u_d = 2 * u_i - u_tilde_j u_R = jnp.where( wall_mask_j == 1, - -u_L + 2 * jnp.dot(u_j, n_w_j), + # -u_L + 2 * jnp.dot(u_j, n_w_j), + jnp.dot(u_d, -n_w_j), jnp.dot(u_j, -e_ij), ) p_R = jnp.where(wall_mask_j == 1, p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j) @@ -595,6 +599,10 @@ def forward(state, neighbors): ) n_w = jnp.where(jnp.absolute(n_w) < EPS, 0.0, n_w) + ##### Riemann velocity BCs + if self.is_bc_trick and (self.solver == "RIE"): + u_tilde = self._Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) + ##### Density summation or evolution # update evolution @@ -621,6 +629,7 @@ def forward(state, neighbors): wall_mask[j_s], n_w[j_s], g_ext[i_s], + u_tilde[j_s], ) drhodt = ops.segment_sum(temp, i_s, N) * fluid_mask rho = rho + self.dt * drhodt @@ -644,7 +653,7 @@ def forward(state, neighbors): ) elif self.is_bc_trick and (self.solver == "RIE"): mask = self._free_weight(fluid_mask[i_s], tag[i_s]) - u_tilde = self._Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) + # u_tilde = self._Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) temperature = self._heat_bc( fluid_mask[j_s], w_dist, temperature, i_s, j_s, tag, N ) From e7aafee8c89a62450a770e0a60bc8212dd431191 Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Sun, 9 Jun 2024 04:12:18 +0200 Subject: [PATCH 13/21] solver clean up --- cases/db.yaml | 2 +- jax_sph/solver.py | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/cases/db.yaml b/cases/db.yaml index d0748c4..4ad6e13 100644 --- a/cases/db.yaml +++ b/cases/db.yaml @@ -9,7 +9,7 @@ case: viscosity: 0.00005 special: L_wall: 5.366 - H_wall: 2.0 + H_wall: 5.366 #2.0 L: 2.0 # water column length H: 1.0 # water column height W: 0.2 # width in 3D case diff --git a/jax_sph/solver.py b/jax_sph/solver.py index dfc0197..41c6f05 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -62,12 +62,9 @@ def rho_evol_riemann_fn( rho_L = rho_i # u_w from eq. (15), Yang (2020) - # u_d = 2 * u_i - u_j - u_d = 2 * u_i - u_tilde_j u_R = jnp.where( wall_mask_j == 1, - # -u_L + 2 * jnp.dot(u_j, n_w_j), - jnp.dot(u_d, -n_w_j), + -u_L + 2 * jnp.dot(u_j, -n_w_j), jnp.dot(u_j, -e_ij), ) p_R = jnp.where(wall_mask_j == 1, p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j) @@ -215,13 +212,10 @@ def acceleration_fn_riemann( p_L = p_i rho_L = rho_i - # u_w from eq. (15), Yang (2020) - # u_d = 2 * u_i - u_j - u_d = 2 * u_i - u_tilde_j + # u_w from eq. (15), Yang (2020) u_R = jnp.where( wall_mask_j == 1, - # -u_L + 2 * jnp.dot(u_j, n_w_j), - jnp.dot(u_d, -n_w_j), + -u_L + 2 * jnp.dot(u_j, -n_w_j), jnp.dot(u_j, -e_ij), ) p_R = jnp.where(wall_mask_j == 1, p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j) @@ -244,7 +238,6 @@ def acceleration_fn_riemann( eq_9 = -2 * m_j * (P_star / (rho_i * rho_j)) * kernel_grad # viscosity term eq. (6), Zhang (2019) - # TODO: u_j is supposed to be u_i, but why is it not working? u_d = 2 * u_j - u_tilde_j v_ij = jnp.where( wall_mask_j == 1, @@ -404,8 +397,7 @@ def free_weight(fluid_mask_i, tag_i): return fluid_mask_i def Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): - u_tilde = jnp.empty_like(u) - return u_tilde + return u else: def free_weight(fluid_mask_i, tag_i): From d12f52d3f6dc7b85fd100be5ca7a868bafc4a519 Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Mon, 10 Jun 2024 01:08:03 +0200 Subject: [PATCH 14/21] clean up and fixes --- cases/db.yaml | 2 +- jax_sph/solver.py | 44 ++++++++++++++++++++++++-------------------- tests/test_pf2d.py | 2 -- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/cases/db.yaml b/cases/db.yaml index 4ad6e13..d0748c4 100644 --- a/cases/db.yaml +++ b/cases/db.yaml @@ -9,7 +9,7 @@ case: viscosity: 0.00005 special: L_wall: 5.366 - H_wall: 5.366 #2.0 + H_wall: 2.0 L: 2.0 # water column length H: 1.0 # water column height W: 0.2 # width in 3D case diff --git a/jax_sph/solver.py b/jax_sph/solver.py index 41c6f05..02c84bd 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -57,18 +57,22 @@ def rho_evol_riemann_fn( kernel_grad = kernel_fn.grad_w(d_ij) * (e_ij) # Compute average states eq. (6)/(12)/(13), Zhang (2017) - u_L = jnp.where(wall_mask_j == 1, jnp.dot(u_i, -n_w_j), jnp.dot(u_i, -e_ij)) + u_L = jnp.where( + jnp.isin(wall_mask_j, wall_tags), jnp.dot(u_i, -n_w_j), jnp.dot(u_i, -e_ij) + ) p_L = p_i rho_L = rho_i # u_w from eq. (15), Yang (2020) u_R = jnp.where( - wall_mask_j == 1, - -u_L + 2 * jnp.dot(u_j, -n_w_j), + jnp.isin(wall_mask_j, wall_tags), + -u_L + 2 * jnp.dot(u_j, n_w_j), jnp.dot(u_j, -e_ij), ) - p_R = jnp.where(wall_mask_j == 1, p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j) - rho_R = jnp.where(wall_mask_j == 1, eos.rho_fn(p_R), rho_j) + p_R = jnp.where( + jnp.isin(wall_mask_j, wall_tags), p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j + ) + rho_R = jnp.where(jnp.isin(wall_mask_j, wall_tags), eos.rho_fn(p_R), rho_j) U_avg = (u_L + u_R) / 2 v_avg = (u_i + u_j) / 2 @@ -208,18 +212,22 @@ def acceleration_fn_riemann( kernel_grad = kernel_part_diff * (e_ij) # Compute average states eq. (6)/(12)/(13), Zhang (2017) - u_L = jnp.where(wall_mask_j == 1, jnp.dot(u_i, -n_w_j), jnp.dot(u_i, -e_ij)) + u_L = jnp.where( + jnp.isin(wall_mask_j, wall_tags), jnp.dot(u_i, -n_w_j), jnp.dot(u_i, -e_ij) + ) p_L = p_i rho_L = rho_i # u_w from eq. (15), Yang (2020) u_R = jnp.where( - wall_mask_j == 1, - -u_L + 2 * jnp.dot(u_j, -n_w_j), + jnp.isin(wall_mask_j, wall_tags), + -u_L + 2 * jnp.dot(u_j, n_w_j), jnp.dot(u_j, -e_ij), ) - p_R = jnp.where(wall_mask_j == 1, p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j) - rho_R = jnp.where(wall_mask_j == 1, eos.rho_fn(p_R), rho_j) + p_R = jnp.where( + jnp.isin(wall_mask_j, wall_tags), p_L + rho_L * jnp.dot(g_ext_i, -r_ij), p_j + ) + rho_R = jnp.where(jnp.isin(wall_mask_j, wall_tags), eos.rho_fn(p_R), rho_j) P_avg = (p_L + p_R) / 2 rho_avg = (rho_L + rho_R) / 2 @@ -229,9 +237,6 @@ def acceleration_fn_riemann( eta_ij = 2 * eta_i * eta_j / (eta_i + eta_j + EPS) # Compute Riemann states eq. (7) and (10), Zhang (2017) - # u_R = jnp.where( - # wall_mask_j == 1, -u_L - 2 * jnp.dot(v_j, -n_w_j), jnp.dot(v_j, -e_ij) - # ) P_star = P_avg + 0.5 * rho_avg * (u_L - u_R) * beta_fn(u_L, u_R, eta_limiter) # pressure term with linear Riemann solver eq. (9), Zhang (2017) @@ -240,7 +245,7 @@ def acceleration_fn_riemann( # viscosity term eq. (6), Zhang (2019) u_d = 2 * u_j - u_tilde_j v_ij = jnp.where( - wall_mask_j == 1, + jnp.isin(wall_mask_j, wall_tags), u_i - u_d, u_i - u_j, ) @@ -396,14 +401,14 @@ def gwbc_fn_riemann_wrapper(is_free_slip, is_heat_conduction): def free_weight(fluid_mask_i, tag_i): return fluid_mask_i - def Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): + def riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): return u else: def free_weight(fluid_mask_i, tag_i): return jnp.ones_like(tag_i) - def Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): + def riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N): w_dist_fluid = w_dist * fluid_mask[j_s] u_wall_nom = ops.segment_sum(w_dist_fluid[:, None] * u[j_s], i_s, N) u_wall_denom = ops.segment_sum(w_dist_fluid, i_s, N) @@ -427,7 +432,7 @@ def heat_bc(mask_j_s_fluid, w_dist, temperature, i_s, j_s, tag, N): def heat_bc(mask_j_s_fluid, w_dist, temperature, i_s, j_s, tag, N): return temperature - return free_weight, Riemann_velocities, heat_bc + return free_weight, riemann_velocities, heat_bc def limiter_fn_wrapper(eta_limiter, c_ref): @@ -522,7 +527,7 @@ def __init__( self._gwbc_fn = gwbc_fn_wrapper(is_free_slip, is_heat_conduction, eos) ( self._free_weight, - self._Riemann_velocities, + self._riemann_velocities, self._heat_bc, ) = gwbc_fn_riemann_wrapper(is_free_slip, is_heat_conduction) self._acceleration_tvf_fn = acceleration_tvf_fn_wrapper(self._kernel_fn) @@ -593,7 +598,7 @@ def forward(state, neighbors): ##### Riemann velocity BCs if self.is_bc_trick and (self.solver == "RIE"): - u_tilde = self._Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) + u_tilde = self._riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) ##### Density summation or evolution @@ -645,7 +650,6 @@ def forward(state, neighbors): ) elif self.is_bc_trick and (self.solver == "RIE"): mask = self._free_weight(fluid_mask[i_s], tag[i_s]) - # u_tilde = self._Riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) temperature = self._heat_bc( fluid_mask[j_s], w_dist, temperature, i_s, j_s, tag, N ) diff --git a/tests/test_pf2d.py b/tests/test_pf2d.py index 1a8a58d..6c8b7cb 100644 --- a/tests/test_pf2d.py +++ b/tests/test_pf2d.py @@ -108,8 +108,6 @@ def test_pf2d(tvf, solver, tmp_path, setup_simulation): """Test whether the poiseuille flow simulation matches the analytical solution""" y_axis, t_dimless, ref_solutions = setup_simulation data_path = run_simulation(tmp_path, tvf, solver) - # print(f"tmp_path = {tmp_path}, subdirs = {subdirs}") solutions = get_solution(data_path, t_dimless, y_axis) - # print(f"solution: {solutions[-1]} \nref_solution: {ref_solutions[-1]}") for sol, ref_sol in zip(solutions, ref_solutions): assert np.allclose(sol, ref_sol, atol=1e-2), "Velocity profile does not match." From d7869427039aa1defe2494681f0c42831c198aaa Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Mon, 10 Jun 2024 04:39:06 +0200 Subject: [PATCH 15/21] box normal vectors are now pre computed only DB works, WIP --- jax_sph/case_setup.py | 27 +++++++++++++ jax_sph/solver.py | 65 ++++++++++++++++++------------- jax_sph/utils.py | 91 ++++++++++++++++++++++++++++++++----------- 3 files changed, 134 insertions(+), 49 deletions(-) diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index e17ef26..99c9e9c 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -9,12 +9,14 @@ import jax.numpy as jnp import numpy as np from jax import vmap +from scipy.spatial import KDTree from jax_sph.eos import RIEMANNEoS, TaitEoS from jax_sph.io_state import read_h5 from jax_sph.jax_md import space from jax_sph.utils import ( Tag, + get_box_nws, get_noise_masked, pos_init_cartesian_2d, pos_init_cartesian_3d, @@ -153,6 +155,22 @@ def initialize(self): num_particles, mass_ref, cfg.case ) + # calculate wall normal vectors and match indices + # TODO: this works only for the box walls -> e.g. clinder flow not working. + # Should we use another approach and scrap this one? + # TODO: adapt other cases besides DB + # TODO: PF won't work because KDTree does not know periodic BCs, right? + # TODO: check whether free slip works for standard SPH + nws, r_nws = get_box_nws( + box_size - cfg.case.special.box_offset, # TODO: How to treat offset? + cfg.case.dx, + cfg.solver.n_walls, + cfg.case.dim, + rho, + mass, + ) + nw = self._match_nws(r, r_nws, nws) + # initialize the state dictionary state = { "r": r, @@ -170,6 +188,7 @@ def initialize(self): "T": temperature, "kappa": kappa, "Cp": Cp, + "nw": nw, } # overwrite the state dictionary with the provided one @@ -290,6 +309,14 @@ def _set_default_rlx(self): self._tag2D_rlx = self._tag2D self._tag3D_rlx = self._tag3D + def _match_nws(self, r, r_nws, nws): + tree = KDTree(r) + dist, match_idx = tree.query(r_nws, k=1) + nw = jnp.zeros_like(r) + nw = nw.at[match_idx].set(nws) + + return nw + def set_relaxation(Case, cfg): """Make a relaxation case from a SimulationSetup instance. diff --git a/jax_sph/solver.py b/jax_sph/solver.py index 02c84bd..bf74e20 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -311,7 +311,7 @@ def gwbc_fn_wrapper(is_free_slip, is_heat_conduction, eos): particle hydrodynamics", Adami, Hu, Adams, 2012 """ - def gwbc_fn(temperature, rho, tag, u, v, p, g_ext, i_s, j_s, w_dist, dr_i_j, N): + def gwbc_fn(temperature, rho, tag, u, v, p, g_ext, i_s, j_s, w_dist, dr_i_j, nw, N): mask_bc = jnp.isin(tag, wall_tags) def no_slip_bc_fn(x): @@ -325,16 +325,16 @@ def no_slip_bc_fn(x): x = jnp.where(mask_bc[:, None], 2 * x - x_wall, x) return x - def free_slip_bc_fn(x): + def free_slip_bc_fn(x, wall_inner_normals): # # normal vectors pointing from fluid to wall # (1) implement via summing over fluid particles - wall_inner = ops.segment_sum(dr_i_j * mask_j_s_fluid[:, None], i_s, N) - # (2) implement using color gradient. Requires 2*rc thick wall - # wall_inner = - ops.segment_sum(dr_i_j*mask_j_s_wall[:, None], i_s, N) + # wall_inner = ops.segment_sum(dr_i_j * mask_j_s_fluid[:, None], i_s, N) + # # (2) implement using color gradient. Requires 2*rc thick wall + # # wall_inner = - ops.segment_sum(dr_i_j*mask_j_s_wall[:, None], i_s, N) - normalization = jnp.sqrt((wall_inner**2).sum(axis=1, keepdims=True)) - wall_inner_normals = wall_inner / (normalization + EPS) - wall_inner_normals = jnp.where(mask_bc[:, None], wall_inner_normals, 0.0) + # normalization = jnp.sqrt((wall_inner**2).sum(axis=1, keepdims=True)) + # wall_inner_normals = wall_inner / (normalization + EPS) + # wall_inner_normals = jnp.where(mask_bc[:, None], wall_inner_normals, 0.0) # for boundary particles, sum over fluid velocities x_wall_unnorm = ops.segment_sum(w_j_s_fluid[:, None] * x[j_s], i_s, N) @@ -357,8 +357,8 @@ def free_slip_bc_fn(x): if is_free_slip: # free-slip boundary - ignore viscous interactions with wall - u = free_slip_bc_fn(u) - v = free_slip_bc_fn(v) + u = free_slip_bc_fn(u, -nw) + v = free_slip_bc_fn(v, -nw) else: # no-slip boundary condition u = no_slip_bc_fn(u) @@ -558,7 +558,7 @@ def forward(state, neighbors): r, tag, mass, eta = state["r"], state["tag"], state["mass"], state["eta"] u, v, dudt, dvdt = state["u"], state["v"], state["dudt"], state["dvdt"] rho, drhodt, p = state["rho"], state["drhodt"], state["p"] - kappa, Cp = state["kappa"], state["Cp"] + nw, kappa, Cp = state["nw"], state["kappa"], state["Cp"] temperature, dTdt = state["T"], state["dTdt"] N = len(r) @@ -583,18 +583,18 @@ def forward(state, neighbors): fluid_mask = jnp.where(tag == Tag.FLUID, 1.0, 0.0) # calculate normal vector of wall boundaries - temp = vmap(self._wall_phi_vec)( - rho[j_s], mass[j_s], dr_i_j, dist, wall_mask[j_s], wall_mask[i_s] - ) - phi = ops.segment_sum(temp, i_s, N) - - # compute normal vector for boundary particles eq. (15), Zhang (2017) - n_w = ( - phi - / (jnp.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] - * wall_mask[:, None] - ) - n_w = jnp.where(jnp.absolute(n_w) < EPS, 0.0, n_w) + # temp = vmap(self._wall_phi_vec)( + # rho[j_s], mass[j_s], dr_i_j, dist, wall_mask[j_s], wall_mask[i_s] + # ) + # phi = ops.segment_sum(temp, i_s, N) + + # # compute normal vector for boundary particles eq. (15), Zhang (2017) + # n_w = ( + # phi + # / (jnp.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] + # * wall_mask[:, None] + # ) + # n_w = jnp.where(jnp.absolute(n_w) < EPS, 0.0, n_w) ##### Riemann velocity BCs if self.is_bc_trick and (self.solver == "RIE"): @@ -624,7 +624,7 @@ def forward(state, neighbors): dr_i_j, dist, wall_mask[j_s], - n_w[j_s], + nw[j_s], g_ext[i_s], u_tilde[j_s], ) @@ -646,7 +646,19 @@ def forward(state, neighbors): if self.is_bc_trick and (self.solver == "SPH"): p, rho, u, v, temperature = self._gwbc_fn( - temperature, rho, tag, u, v, p, g_ext, i_s, j_s, w_dist, dr_i_j, N + temperature, + rho, + tag, + u, + v, + p, + g_ext, + i_s, + j_s, + w_dist, + dr_i_j, + nw, + N, ) elif self.is_bc_trick and (self.solver == "RIE"): mask = self._free_weight(fluid_mask[i_s], tag[i_s]) @@ -714,7 +726,7 @@ def forward(state, neighbors): eta[j_s], wall_mask[j_s], mask, - n_w[j_s], + nw[j_s], g_ext[i_s], u_tilde[j_s], ) @@ -754,6 +766,7 @@ def forward(state, neighbors): "T": temperature, "kappa": kappa, "Cp": Cp, + "nw": nw, } return state diff --git a/jax_sph/utils.py b/jax_sph/utils.py index fbff28b..da2613c 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -9,6 +9,7 @@ from jax import ops, vmap from numpy import array from omegaconf import DictConfig +from scipy.spatial import KDTree from jax_sph.io_state import read_h5 from jax_sph.jax_md import partition, space @@ -57,16 +58,16 @@ def pos_box_2d(L: float, H: float, dx: float, num_wall_layers: int = 3): The box is of size (L + num_wall_layers * dx) x (H + num_wall_layers * dx). The inner part of the box starts at (num_wall_layers * dx, num_wall_layers * dx). """ - dx3 = num_wall_layers * dx + dxn = num_wall_layers * dx # horizontal and vertical blocks - vertical = pos_init_cartesian_2d(np.array([dx3, H + 2 * dx3]), dx) - horiz = pos_init_cartesian_2d(np.array([L, dx3]), dx) + vertical = pos_init_cartesian_2d(np.array([dxn, H + 2 * dxn]), dx) + horiz = pos_init_cartesian_2d(np.array([L, dxn]), dx) # wall: left, bottom, right, top wall_l = vertical.copy() - wall_b = horiz.copy() + np.array([dx3, 0.0]) - wall_r = vertical.copy() + np.array([L + dx3, 0.0]) - wall_t = horiz.copy() + np.array([dx3, H + dx3]) + wall_b = horiz.copy() + np.array([dxn, 0.0]) + wall_r = vertical.copy() + np.array([L + dxn, 0.0]) + wall_t = horiz.copy() + np.array([dxn, H + dxn]) res = jnp.concatenate([wall_l, wall_b, wall_r, wall_t]) return res @@ -120,17 +121,27 @@ def get_stats(state: Dict, props: list, dx: float): return res -def get_nws(dx, dim, r, rho, m, tag, neighbors, displacement_fn): - """Computes the wall normal vectors at boundaries""" +def get_box_nws(box_size, dx, n_walls, dim, rho, m): + """Computes the normal vectors at box wall boundaries""" - N = len(r) - i_s, j_s = neighbors.idx - dr_ij = vmap(displacement_fn)(r[i_s], r[j_s]) - dist = space.distance(dr_ij) - wall_mask = jnp.where(jnp.isin(tag, wall_tags), 1.0, 0.0) + # TODO: having a pos_box_3d would be useful + # TODO: pos_box_* having array as input would also be useful + length = box_size[0] - 2 * n_walls * dx + height = box_size[1] - 2 * n_walls * dx + + # define 5 layers of wall BC partilces and position them accordingly + layers = {} + idx_len = {} + for i in range(5): + layer = pos_box_2d(length + 2 * i * dx, height + 2 * i * dx, dx, 1) + layers[f"layer_{i}"] = layer + np.ones(2) * ((n_walls - 1) - i) * dx + idx_len[f"len_{i}"] = len(layer) + + # define kernel function kernel_fn = QuinticKernel(h=dx, dim=dim) - def wall_phi_vec(rho_j, m_j, dr_ij, dist, tag_j, tag_i): + # define function to calculate phi, Zhang (2017) + def wall_phi_vec(rho_j, m_j, dr_ij, dist): # Compute unit vector, above eq. (6), Zhang (2017) e_ij_w = dr_ij / (dist + EPS) @@ -138,20 +149,54 @@ def wall_phi_vec(rho_j, m_j, dr_ij, dist, tag_j, tag_i): kernel_grad = kernel_fn.grad_w(dist) * (e_ij_w) # compute phi eq. (15), Zhang (2017) - phi = -1.0 * m_j / rho_j * kernel_grad * tag_j * tag_i + phi = -1.0 * m_j / rho_j * kernel_grad return phi - temp = vmap(wall_phi_vec)( - rho[j_s], m[j_s], dr_ij, dist, wall_mask[j_s], wall_mask[i_s] - ) - phi = ops.segment_sum(temp, i_s, N) - n_w = ( - phi / (jnp.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] * wall_mask[:, None] + nw = [] + for i in range(3): + # setup of the temporary box, consisting out of 3 particle layers + temp_box = np.concatenate( + ( + layers[f"layer_{i}"], + layers[f"layer_{i + 1}"], + layers[f"layer_{i + 2}"], + ), + axis=0, + ) + # define KD tree and get neighbors + tree = KDTree(temp_box) + neighbors = tree.query_ball_point( + temp_box[0 : idx_len[f"len_{i}"]], 3 * dx, p=2.0 + ) + # get neighbor and nw indices + neighbors_idx = np.concatenate(neighbors, axis=0) + nw_idx = np.repeat(range(idx_len[f"len_{i}"]), [len(x) for x in neighbors]) + + # calculate distances + dr_ij = vmap(space.pairwise_displacement)( + temp_box[nw_idx], temp_box[neighbors_idx] + ) + dist = space.distance(dr_ij) + + # calculate normal vectors + temp = vmap(wall_phi_vec)(rho[neighbors_idx], m[neighbors_idx], dr_ij, dist) + phi = ops.segment_sum(temp, nw_idx, idx_len[f"len_{i}"]) + nw_temp = phi / (np.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] + nw.append(nw_temp) + + nw = np.concatenate(nw, axis=0) + nw = np.where(np.absolute(nw) < EPS, 0.0, nw) + r_nw = np.concatenate( + ( + layers["layer_0"], + layers["layer_1"], + layers["layer_2"], + ), + axis=0, ) - n_w = jnp.where(jnp.absolute(n_w) < EPS, 0.0, n_w) - return n_w + return nw, r_nw class Logger: From e6d25142e50caee4c15b667b33269b68405b7774 Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Mon, 10 Jun 2024 19:09:10 +0200 Subject: [PATCH 16/21] change precomputation of nws to a more general approach only DB working, WIP --- cases/db.py | 12 +++++++++--- jax_sph/case_setup.py | 34 +++++++++------------------------- jax_sph/utils.py | 38 +++++++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/cases/db.py b/cases/db.py index 89f33f4..98377bc 100644 --- a/cases/db.py +++ b/cases/db.py @@ -24,6 +24,10 @@ def __init__(self, cfg: DictConfig): # | --------------------------| # < L_wall > + # define offset vector + self.offset_vec = np.ones(2) * cfg.solver.n_walls * cfg.case.dx + self.fluid_size = np.array([self.special.L_wall, self.special.H_wall]) + # relaxation configurations if self.case.mode == "rlx": self.special.L_wall = self.special.L @@ -56,17 +60,19 @@ def _init_pos2D(self, box_size, dx, n_walls): else: r_fluid = self._get_relaxed_r0(None, dx) - walls = pos_box_2d(sp.L_wall, sp.H_wall, dx) + walls = pos_box_2d( + np.array([sp.L_wall, sp.H_wall]), dx, self.cfg.solver.n_walls + ) res = np.concatenate([walls, r_fluid]) return res - def _init_pos3D(self, box_size, dx): + def _init_pos3D(self, box_size, dx, n_walls): # cartesian coordinates in z Lz = box_size[2] zs = np.arange(0, Lz, dx) + 0.5 * dx # extend 2D points to 3D - xy = self._init_pos2D(box_size, dx) + xy = self._init_pos2D(box_size, dx, n_walls) xy_ext = np.hstack([xy, np.ones((len(xy), 1))]) r_xyz = np.vstack([xy_ext * [1, 1, z] for z in zs]) diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index 99c9e9c..744e58b 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -9,15 +9,15 @@ import jax.numpy as jnp import numpy as np from jax import vmap -from scipy.spatial import KDTree from jax_sph.eos import RIEMANNEoS, TaitEoS from jax_sph.io_state import read_h5 from jax_sph.jax_md import space from jax_sph.utils import ( Tag, - get_box_nws, get_noise_masked, + get_nws, + pos_box_2d, pos_init_cartesian_2d, pos_init_cartesian_3d, wall_tags, @@ -155,21 +155,8 @@ def initialize(self): num_particles, mass_ref, cfg.case ) - # calculate wall normal vectors and match indices - # TODO: this works only for the box walls -> e.g. clinder flow not working. - # Should we use another approach and scrap this one? - # TODO: adapt other cases besides DB - # TODO: PF won't work because KDTree does not know periodic BCs, right? - # TODO: check whether free slip works for standard SPH - nws, r_nws = get_box_nws( - box_size - cfg.case.special.box_offset, # TODO: How to treat offset? - cfg.case.dx, - cfg.solver.n_walls, - cfg.case.dim, - rho, - mass, - ) - nw = self._match_nws(r, r_nws, nws) + wall_part_fn = self._get_boundary_particles_fn() + nw = get_nws(r, tag, self.fluid_size, dx, self.offset_vec, wall_part_fn) # initialize the state dictionary state = { @@ -265,6 +252,11 @@ def _external_acceleration_fn(self, r): def _boundary_conditions_fn(self, state): pass + def _get_boundary_particles_fn(self): + if self.case.dim == 2: + boundary_particles_fn = pos_box_2d + return boundary_particles_fn + def _get_relaxed_r0(self, box_size, dx): assert hasattr(self, "_load_only_fluid"), AttributeError @@ -309,14 +301,6 @@ def _set_default_rlx(self): self._tag2D_rlx = self._tag2D self._tag3D_rlx = self._tag3D - def _match_nws(self, r, r_nws, nws): - tree = KDTree(r) - dist, match_idx = tree.query(r_nws, k=1) - nw = jnp.zeros_like(r) - nw = nw.at[match_idx].set(nws) - - return nw - def set_relaxation(Case, cfg): """Make a relaxation case from a SimulationSetup instance. diff --git a/jax_sph/utils.py b/jax_sph/utils.py index da2613c..8ac44ba 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -52,22 +52,23 @@ def pos_init_cartesian_3d(box_size: array, dx: float): return r -def pos_box_2d(L: float, H: float, dx: float, num_wall_layers: int = 3): +def pos_box_2d(fluid_box: array, dx: float, num_wall_layers: int = 3): """Create an empty box of particles in 2D. + fluid_box is an array of the form: [L, H] The box is of size (L + num_wall_layers * dx) x (H + num_wall_layers * dx). The inner part of the box starts at (num_wall_layers * dx, num_wall_layers * dx). """ dxn = num_wall_layers * dx # horizontal and vertical blocks - vertical = pos_init_cartesian_2d(np.array([dxn, H + 2 * dxn]), dx) - horiz = pos_init_cartesian_2d(np.array([L, dxn]), dx) + vertical = pos_init_cartesian_2d(np.array([dxn, fluid_box[1] + 2 * dxn]), dx) + horiz = pos_init_cartesian_2d(np.array([fluid_box[0], dxn]), dx) # wall: left, bottom, right, top wall_l = vertical.copy() wall_b = horiz.copy() + np.array([dxn, 0.0]) - wall_r = vertical.copy() + np.array([L + dxn, 0.0]) - wall_t = horiz.copy() + np.array([dxn, H + dxn]) + wall_r = vertical.copy() + np.array([fluid_box[0] + dxn, 0.0]) + wall_t = horiz.copy() + np.array([dxn, fluid_box[1] + dxn]) res = jnp.concatenate([wall_l, wall_b, wall_r, wall_t]) return res @@ -125,15 +126,13 @@ def get_box_nws(box_size, dx, n_walls, dim, rho, m): """Computes the normal vectors at box wall boundaries""" # TODO: having a pos_box_3d would be useful - # TODO: pos_box_* having array as input would also be useful - length = box_size[0] - 2 * n_walls * dx - height = box_size[1] - 2 * n_walls * dx + box = box_size - 2 * n_walls * dx # define 5 layers of wall BC partilces and position them accordingly layers = {} idx_len = {} for i in range(5): - layer = pos_box_2d(length + 2 * i * dx, height + 2 * i * dx, dx, 1) + layer = pos_box_2d(box + 2 * i * dx, dx, 1) layers[f"layer_{i}"] = layer + np.ones(2) * ((n_walls - 1) - i) * dx idx_len[f"len_{i}"] = len(layer) @@ -199,6 +198,27 @@ def wall_phi_vec(rho_j, m_j, dr_ij, dist): return nw, r_nw +def get_nws(r, tag, fluid_size, dx, offset_vec, wall_part_fn): + """Computes the normal vectors all wall boundaries""" + + # align fluid to [0, 0] + r_aligned = r - offset_vec + + # define fine layer of wall BC partilces and position them accordingly + layer = wall_part_fn(fluid_size, dx / 5, 1) - np.ones(2) * dx / 5 + + # match thin layer to particles + tree = KDTree(layer) + dist, match_idx = tree.query(r_aligned, k=1) + dr = layer[match_idx] - r_aligned + + # compute normal vectors + nw = dr / (dist[:, None] + EPS) + nw = np.where(np.isin(tag, wall_tags)[:, None], nw, np.zeros(2)) + + return nw + + class Logger: """Logger for printing stats to stdout.""" From 25c3950fd50fcd28ff5e32e86e91804c169ab031 Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Mon, 10 Jun 2024 22:15:54 +0200 Subject: [PATCH 17/21] extend to TGV still WIP --- cases/db.py | 2 ++ cases/tgv.py | 8 ++++---- jax_sph/case_setup.py | 24 ++++++++++++++---------- jax_sph/utils.py | 26 +++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/cases/db.py b/cases/db.py index 98377bc..e1e2a36 100644 --- a/cases/db.py +++ b/cases/db.py @@ -26,6 +26,8 @@ def __init__(self, cfg: DictConfig): # define offset vector self.offset_vec = np.ones(2) * cfg.solver.n_walls * cfg.case.dx + + # set fluid domain size self.fluid_size = np.array([self.special.L_wall, self.special.H_wall]) # relaxation configurations diff --git a/cases/tgv.py b/cases/tgv.py index 6430191..b100ce1 100644 --- a/cases/tgv.py +++ b/cases/tgv.py @@ -24,17 +24,17 @@ def __init__(self, cfg: DictConfig): self._init_pos2D = self._get_relaxed_r0 self._init_pos3D = self._get_relaxed_r0 - def _box_size2D(self): + def _box_size2D(self, n_walls): return np.array([1.0, 1.0]) - def _box_size3D(self): + def _box_size3D(self, n_walls): return 2 * np.pi * np.array([1.0, 1.0, 1.0]) - def _tag2D(self, r): + def _tag2D(self, r, n_walls): tag = jnp.full(len(r), Tag.FLUID, dtype=int) return tag - def _tag3D(self, r): + def _tag3D(self, r, n_walls): return self._tag2D(r) def _init_velocity2D(self, r): diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index 744e58b..42836fe 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -18,6 +18,7 @@ get_noise_masked, get_nws, pos_box_2d, + pos_box_3d, pos_init_cartesian_2d, pos_init_cartesian_3d, wall_tags, @@ -155,9 +156,6 @@ def initialize(self): num_particles, mass_ref, cfg.case ) - wall_part_fn = self._get_boundary_particles_fn() - nw = get_nws(r, tag, self.fluid_size, dx, self.offset_vec, wall_part_fn) - # initialize the state dictionary state = { "r": r, @@ -175,8 +173,12 @@ def initialize(self): "T": temperature, "kappa": kappa, "Cp": Cp, - "nw": nw, + "nw": jnp.zeros_like(r), } + if cfg.solver.is_bc_trick: + wall_part_fn = self._get_boundary_particles_fn() + nw = get_nws(r, tag, self.fluid_size, dx, self.offset_vec, wall_part_fn) + state["nw"] = nw # overwrite the state dictionary with the provided one if cfg.case.state0_path is not None: @@ -215,25 +217,25 @@ def initialize(self): ) @abstractmethod - def _box_size2D(self, cfg): + def _box_size2D(self, n_walls): pass @abstractmethod - def _box_size3D(self, cfg): + def _box_size3D(self, n_walls): pass - def _init_pos2D(self, box_size, dx): + def _init_pos2D(self, box_size, dx, n_walls): return pos_init_cartesian_2d(box_size, dx) - def _init_pos3D(self, box_size, dx): + def _init_pos3D(self, box_size, dx, n_walls): return pos_init_cartesian_3d(box_size, dx) @abstractmethod - def _tag2D(self, r): + def _tag2D(self, r, n_walls): pass @abstractmethod - def _tag3D(self, r): + def _tag3D(self, r, n_walls): pass @abstractmethod @@ -255,6 +257,8 @@ def _boundary_conditions_fn(self, state): def _get_boundary_particles_fn(self): if self.case.dim == 2: boundary_particles_fn = pos_box_2d + elif self.case.dim == 3: + boundary_particles_fn = pos_box_3d return boundary_particles_fn def _get_relaxed_r0(self, box_size, dx): diff --git a/jax_sph/utils.py b/jax_sph/utils.py index 8ac44ba..393b327 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -74,6 +74,30 @@ def pos_box_2d(fluid_box: array, dx: float, num_wall_layers: int = 3): return res +def pos_box_3d(fluid_box: array, dx: float, num_wall_layers: int = 3): + """Create an z-periodic empty box of particles in 3D. + + fluid_box is an array of the form: [L, H, D] + The box is of size (L + num_wall_layers * dx) x (H + num_wall_layers * dx) x D. + The inner part of the box starts at (num_wall_layers * dx, num_wall_layers * dx). + """ + dxn = num_wall_layers * dx + # horizontal and vertical blocks + vertical = pos_init_cartesian_3d( + np.array([dxn, fluid_box[1] + 2 * dxn, fluid_box[2]]), dx + ) + horiz = pos_init_cartesian_3d(np.array([fluid_box[0], dxn, fluid_box[2]]), dx) + + # wall: left, bottom, right, top + wall_l = vertical.copy() + wall_b = horiz.copy() + np.array([dxn, 0.0, 0.0]) + wall_r = vertical.copy() + np.array([fluid_box[0] + dxn, 0.0, 0.0]) + wall_t = horiz.copy() + np.array([dxn, fluid_box[1] + dxn, 0.0]) + + res = jnp.concatenate([wall_l, wall_b, wall_r, wall_t]) + return res + + def get_noise_masked(shape: tuple, mask: array, key: jax.random.PRNGKey, std: float): """Generate Gaussian noise with `std` where `mask` is True.""" noise = std * jax.random.normal(key, shape) @@ -199,7 +223,7 @@ def wall_phi_vec(rho_j, m_j, dr_ij, dist): def get_nws(r, tag, fluid_size, dx, offset_vec, wall_part_fn): - """Computes the normal vectors all wall boundaries""" + """Computes the normal vectors of all wall boundaries""" # align fluid to [0, 0] r_aligned = r - offset_vec From 6d189743be6a9181d2cfcea7a3b3889675f751c7 Mon Sep 17 00:00:00 2001 From: Jonas Erbesdobler Date: Tue, 11 Jun 2024 05:16:48 +0200 Subject: [PATCH 18/21] extend precomputation of nws to all cases and restructure coordinate initialization --- cases/cw.py | 83 ++++++++++++++++++-------- cases/db.py | 88 +++++++++++++++++----------- cases/ht.py | 131 ++++++++++++++++++++++++++++++++++-------- cases/ht.yaml | 2 + cases/ldc.py | 80 ++++++++++++++++++++++---- cases/pf.py | 110 +++++++++++++++++++++++++++++------ cases/pf.yaml | 3 + cases/rpf.py | 14 ++--- cases/tgv.py | 10 ++-- cases/ut.py | 32 ++++++----- jax_sph/case_setup.py | 54 +++++++++-------- jax_sph/integrator.py | 10 +++- jax_sph/solver.py | 16 +----- jax_sph/utils.py | 44 ++++++++++---- pyproject.toml | 2 +- 15 files changed, 492 insertions(+), 187 deletions(-) diff --git a/cases/cw.py b/cases/cw.py index ae9ea86..5190470 100644 --- a/cases/cw.py +++ b/cases/cw.py @@ -5,7 +5,13 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag, pos_box_2d, pos_init_cartesian_2d +from jax_sph.utils import ( + Tag, + pos_box_2d, + pos_box_3d, + pos_init_cartesian_2d, + pos_init_cartesian_3d, +) class CW(SimulationSetup): @@ -14,40 +20,67 @@ class CW(SimulationSetup): def __init__(self, cfg: DictConfig): super().__init__(cfg) + # define offset vector + self.offset_vec = np.ones(cfg.case.dim) * cfg.solver.n_walls * cfg.case.dx + # relaxation configurations if cfg.case.mode == "rlx" or cfg.case.r0_type == "relaxed": raise NotImplementedError("Relaxation not implemented for CW.") - def _box_size2D(self): - return np.array([self.special.L_wall, self.special.H_wall]) + 6 * self.case.dx + def _box_size2D(self, n_walls): + sp = self.special + return np.array([sp.L_wall, sp.H_wall]) + 2 * n_walls * self.case.dx - def _box_size3D(self): - dx6 = 6 * self.case.dx - return np.array([self.special.L_wall + dx6, self.special.H_wall + dx6, 0.5]) + def _box_size3D(self, n_walls): + sp = self.special + dx2n = 2 * n_walls * self.case.dx + return np.array([sp.L_wall + dx2n, sp.H_wall + dx2n, 1.0 + dx2n]) - def _init_pos2D(self, box_size, dx): - dx3 = 3 * self.case.dx - walls = pos_box_2d(self.special.L_wall, self.special.H_wall, dx) + def _init_walls_2d(self, dx, n_walls): + sp = self.special + rw = pos_box_2d(np.array([sp.L_wall, sp.H_wall]), dx, n_walls) + return rw - r_fluid = pos_init_cartesian_2d(np.array([self.special.L, self.special.H]), dx) - r_fluid += dx3 + np.array(self.special.cube_offset) - res = np.concatenate([walls, r_fluid]) - return res + def _init_walls_3d(self, dx, n_walls): + sp = self.special + rw = pos_box_3d(np.array([sp.L_wall, sp.H_wall, 1.0]), dx, n_walls, False) + return rw + + def _init_pos2D(self, box_size, dx, n_walls): + dxn = n_walls * self.case.dx + + # initialize walls + r_w = self._init_walls_2d(dx, n_walls) + + # initialize fluid phase + r_f = pos_init_cartesian_2d(np.array([self.special.L, self.special.H]), dx) + r_f += dxn + np.array(self.special.cube_offset) + + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) + + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + return r, tag + + def _init_pos3D(self, box_size, dx, n_walls): + dxn = n_walls * self.case.dx + + # initialize walls + r_w = self._init_walls_3d(dx, n_walls) - def _tag2D(self, r): - dx3 = 3 * self.case.dx - mask_left = jnp.where(r[:, 0] < dx3, True, False) - mask_bottom = jnp.where(r[:, 1] < dx3, True, False) - mask_right = jnp.where(r[:, 0] > self.special.L_wall + dx3, True, False) - mask_top = jnp.where(r[:, 1] > self.special.H_wall + dx3, True, False) - mask_wall = mask_left + mask_bottom + mask_right + mask_top + # initialize fluid phase + r_f = pos_init_cartesian_3d(np.array([self.special.L, self.special.H, 0.3]), dx) + r_f += dxn + np.array(self.special.cube_offset) - tag = jnp.full(len(r), Tag.FLUID, dtype=int) - tag = jnp.where(mask_wall, Tag.SOLID_WALL, tag) - return tag + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) - def _tag3D(self, r): - return self._tag2D(r) + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + return r, tag def _init_velocity2D(self, r): res = jnp.array(self.special.u_init) diff --git a/cases/db.py b/cases/db.py index e1e2a36..b9e4b9b 100644 --- a/cases/db.py +++ b/cases/db.py @@ -5,7 +5,13 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag, pos_box_2d, pos_init_cartesian_2d +from jax_sph.utils import ( + Tag, + pos_box_2d, + pos_box_3d, + pos_init_cartesian_2d, + pos_init_cartesian_3d, +) class DB(SimulationSetup): @@ -25,10 +31,7 @@ def __init__(self, cfg: DictConfig): # < L_wall > # define offset vector - self.offset_vec = np.ones(2) * cfg.solver.n_walls * cfg.case.dx - - # set fluid domain size - self.fluid_size = np.array([self.special.L_wall, self.special.H_wall]) + self.offset_vec = self._offset_vec() # relaxation configurations if self.case.mode == "rlx": @@ -55,46 +58,67 @@ def _box_size3D(self, n_walls): [sp.L_wall + 2 * n_walls * dx + bo, sp.H_wall + 2 * n_walls * dx + bo, sp.W] ) + def _init_walls_2d(self, dx, n_walls): + sp = self.special + rw = pos_box_2d(np.array([sp.L_wall, sp.H_wall]), dx, n_walls) + return rw + + def _init_walls_3d(self, dx, n_walls): + sp = self.special + rw = pos_box_3d(np.array([sp.L_wall, sp.H_wall, 1.0]), dx, n_walls) + return rw + def _init_pos2D(self, box_size, dx, n_walls): sp = self.special + + # initialize fluid phase if self.case.r0_type == "cartesian": - r_fluid = n_walls * dx + pos_init_cartesian_2d(np.array([sp.L, sp.H]), dx) + r_f = n_walls * dx + pos_init_cartesian_2d(np.array([sp.L, sp.H]), dx) else: - r_fluid = self._get_relaxed_r0(None, dx) + r_f = self._get_relaxed_r0(None, dx) - walls = pos_box_2d( - np.array([sp.L_wall, sp.H_wall]), dx, self.cfg.solver.n_walls - ) - res = np.concatenate([walls, r_fluid]) - return res + # initialize walls + r_w = self._init_walls_2d(dx, n_walls) + + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) + + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + return r, tag def _init_pos3D(self, box_size, dx, n_walls): - # cartesian coordinates in z - Lz = box_size[2] - zs = np.arange(0, Lz, dx) + 0.5 * dx + # TODO: not validated yet + sp = self.special - # extend 2D points to 3D - xy = self._init_pos2D(box_size, dx, n_walls) - xy_ext = np.hstack([xy, np.ones((len(xy), 1))]) + # initialize fluid phase + if self.case.r0_type == "cartesian": + r_f = np.array([1.0, 1.0, 0.0]) * n_walls * dx + pos_init_cartesian_3d( + np.array([sp.L, sp.H, 1.0]), dx + ) + else: + r_f = self._get_relaxed_r0(None, dx) - r_xyz = np.vstack([xy_ext * [1, 1, z] for z in zs]) - return r_xyz + # initialize walls + r_w = self._init_walls_3d(dx, n_walls) - def _tag2D(self, r, n_walls): - dxn = n_walls * self.case.dx - mask_left = jnp.where(r[:, 0] < dxn, True, False) - mask_bottom = jnp.where(r[:, 1] < dxn, True, False) - mask_right = jnp.where(r[:, 0] > self.special.L_wall + dxn, True, False) - mask_top = jnp.where(r[:, 1] > self.special.H_wall + dxn, True, False) + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) - mask_wall = mask_left + mask_bottom + mask_right + mask_top + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) - tag = jnp.full(len(r), Tag.FLUID, dtype=int) - tag = jnp.where(mask_wall, Tag.SOLID_WALL, tag) - return tag + return r, tag - def _tag3D(self, r): - return self._tag2D(r) + def _offset_vec(self): + dim = self.cfg.case.dim + if dim == 2: + res = np.ones(dim) * self.cfg.solver.n_walls * self.cfg.case.dx + elif dim == 3: + res = np.array([1.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + return res def _init_velocity2D(self, r): return jnp.zeros_like(r) diff --git a/cases/ht.py b/cases/ht.py index 73262e6..ea17db7 100644 --- a/cases/ht.py +++ b/cases/ht.py @@ -6,7 +6,7 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag +from jax_sph.utils import Tag, pos_init_cartesian_2d, pos_init_cartesian_3d class HT(SimulationSetup): @@ -15,6 +15,9 @@ class HT(SimulationSetup): def __init__(self, cfg: DictConfig): super().__init__(cfg) + # define offset vector + self.offset_vec = self._offset_vec() + # relaxation configurations if self.case.mode == "rlx": self._set_default_rlx() @@ -24,33 +27,113 @@ def __init__(self, cfg: DictConfig): self._init_pos2D = self._get_relaxed_r0 self._init_pos3D = self._get_relaxed_r0 - def _box_size2D(self): - dx = self.case.dx - return np.array([1, 0.2 + 6 * dx]) + def _box_size2D(self, n_walls): + dx2n = self.case.dx * n_walls * 2 + sp = self.special + return np.array([sp.L, sp.H + dx2n]) + + def _box_size3D(self, n_walls): + dx2n = self.case.dx * n_walls * 2 + sp = self.special + return np.array([sp.L, sp.H + dx2n, 0.5]) + + def _init_walls_2d(self, dx, n_walls): + sp = self.special + + # thickness of wall particles + dxn = dx * n_walls + + # horizontal and vertical blocks + horiz = pos_init_cartesian_2d(np.array([sp.L, dxn]), dx) + + # wall: bottom, top + wall_b = horiz.copy() + wall_t = horiz.copy() + np.array([0.0, sp.H + dxn]) + + rw = np.concatenate([wall_b, wall_t]) + return rw + + def _init_walls_3d(self, dx, n_walls): + sp = self.special - def _box_size3D(self): - dx = self.case.dx - return np.array([1, 0.2 + 6 * dx, 0.5]) + # thickness of wall particles + dxn = dx * n_walls - def _tag2D(self, r): - dx3 = 3 * self.case.dx - _box_size = self._box_size2D() - tag = jnp.full(len(r), Tag.FLUID, dtype=int) + # horizontal and vertical blocks + horiz = pos_init_cartesian_3d(np.array([sp.L, dxn, 0.5]), dx) - mask_no_slip_wall = (r[:, 1] < dx3) + ( - r[:, 1] > (_box_size[1] - 6 * self.case.dx) + dx3 + # wall: bottom, top + wall_b = horiz.copy() + wall_t = horiz.copy() + np.array([0.0, sp.H + dxn, 0.0]) + + rw = np.concatenate([wall_b, wall_t]) + return rw + + def _init_pos2D(self, box_size, dx, n_walls): + sp = self.special + + # initialize fluid phase + r_f = np.array([0.0, 1.0]) * n_walls * dx + pos_init_cartesian_2d( + np.array([sp.L, sp.H]), dx ) + + # initialize walls + r_w = self._init_walls_2d(dx, n_walls) + + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) + + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + + # set thermal tags + _box_size = self._box_size2D(n_walls) mask_hot_wall = ( - (r[:, 1] < dx3) + (r[:, 1] < dx * n_walls) * (r[:, 0] < (_box_size[0] / 2) + self.special.hot_wall_half_width) * (r[:, 0] > (_box_size[0] / 2) - self.special.hot_wall_half_width) ) - tag = jnp.where(mask_no_slip_wall, Tag.SOLID_WALL, tag) tag = jnp.where(mask_hot_wall, Tag.DIRICHLET_WALL, tag) - return tag - def _tag3D(self, r): - return self._tag2D(r) + return r, tag + + def _init_pos3D(self, box_size, dx, n_walls): + sp = self.special + + # initialize fluid phase + r_f = np.array([0.0, 1.0, 0.0]) * n_walls * dx + pos_init_cartesian_3d( + np.array([sp.L, sp.H, 0.5]), dx + ) + + # initialize walls + r_w = self._init_walls_3d(dx, n_walls) + + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) + + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + + # set thermal tags + _box_size = self._box_size3D(n_walls) + mask_hot_wall = ( + (r[:, 1] < dx * n_walls) + * (r[:, 0] < (_box_size[0] / 2) + self.special.hot_wall_half_width) + * (r[:, 0] > (_box_size[0] / 2) - self.special.hot_wall_half_width) + ) + tag = jnp.where(mask_hot_wall, Tag.DIRICHLET_WALL, tag) + + return r, tag + + def _offset_vec(self): + dim = self.cfg.case.dim + if dim == 2: + res = np.array([0.0, 1.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + elif dim == 3: + res = np.array([0.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + return res def _init_velocity2D(self, r): return jnp.zeros_like(r) @@ -59,20 +142,22 @@ def _init_velocity3D(self, r): return jnp.zeros_like(r) def _external_acceleration_fn(self, r): - dx3 = 3 * self.case.dx + n_walls = self.cfg.solver.n_walls + dxn = n_walls * self.case.dx res = jnp.zeros_like(r) x_force = jnp.ones((len(r))) - box_size = self._box_size2D() - fluid_mask = (r[:, 1] < box_size[1] - dx3) * (r[:, 1] > dx3) + box_size = self._box_size2D(n_walls) + fluid_mask = (r[:, 1] < box_size[1] - dxn) * (r[:, 1] > dxn) x_force = jnp.where(fluid_mask, x_force, 0) res = res.at[:, 0].set(x_force) return res * self.case.g_ext_magnitude def _boundary_conditions_fn(self, state): + n_walls = self.cfg.solver.n_walls mask_fluid = state["tag"] == Tag.FLUID # set incoming fluid temperature to reference_temperature - mask_inflow = mask_fluid * (state["r"][:, 0] < 3 * self.case.dx) + mask_inflow = mask_fluid * (state["r"][:, 0] < n_walls * self.case.dx) state["T"] = jnp.where(mask_inflow, self.case.T_ref, state["T"]) state["dTdt"] = jnp.where(mask_inflow, 0.0, state["dTdt"]) @@ -96,7 +181,7 @@ def _boundary_conditions_fn(self, state): # set outlet temperature gradients to zero to avoid interaction with inflow # bounds[0][1] is the x-coordinate of the outlet mask_outflow = mask_fluid * ( - state["r"][:, 0] > self.case.bounds[0][1] - 3 * self.case.dx + state["r"][:, 0] > self.case.bounds[0][1] - n_walls * self.case.dx ) state["dTdt"] = jnp.where(mask_outflow, 0.0, state["dTdt"]) diff --git a/cases/ht.yaml b/cases/ht.yaml index 808b6bf..eba2503 100644 --- a/cases/ht.yaml +++ b/cases/ht.yaml @@ -11,6 +11,8 @@ case: special: hot_wall_temperature: 1.23 # nondimensionalized corresponding to 100 hot_wall_half_width: 0.25 + L: 1.0 # water column length + H: 0.2 # water column height solver: diff --git a/cases/ldc.py b/cases/ldc.py index 220d235..c52909b 100644 --- a/cases/ldc.py +++ b/cases/ldc.py @@ -6,7 +6,13 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag +from jax_sph.utils import ( + Tag, + pos_box_2d, + pos_box_3d, + pos_init_cartesian_2d, + pos_init_cartesian_3d, +) class LDC(SimulationSetup): @@ -21,6 +27,9 @@ def __init__(self, cfg: DictConfig): elif self.case.dim == 3: self.u_lid = jnp.array([self.special.u_x_lid, 0.0, 0.0]) + # define offset vector + self.offset_vec = self._offset_vec() + # relaxation configurations if self.case.mode == "rlx": self._set_default_rlx() @@ -37,19 +46,68 @@ def _box_size3D(self): dx6 = 6 * self.case.dx return np.array([1 + dx6, 1 + dx6, 0.5]) - def _tag2D(self, r): - box_size = self._box_size2D() if self.case.dim == 2 else self._box_size3D() + def _box_size2D(self, n_walls): + return np.ones((2,)) + 2 * n_walls * self.case.dx + + def _box_size3D(self, n_walls): + dx2n = 2 * n_walls * self.case.dx + return np.array([1 + dx2n, 1 + dx2n, 0.5]) + + def _init_walls_2d(self, dx, n_walls): + rw = pos_box_2d(np.ones(2), dx, n_walls) + return rw + + def _init_walls_3d(self, dx, n_walls): + rw = pos_box_3d(np.array([1.0, 1.0, 0.5]), dx, n_walls) + return rw + + def _init_pos2D(self, box_size, dx, n_walls): + # initialize fluid phase + r_f = n_walls * dx + pos_init_cartesian_2d(np.ones(2), dx) + + # initialize walls + r_w = self._init_walls_2d(dx, n_walls) + + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) - mask_lid = r[:, 1] > (box_size[1] - 3 * self.case.dx) - r_centered_abs = jnp.abs(r - r.mean(axis=0)) - mask_water = jnp.where(r_centered_abs.max(axis=1) < 0.5, True, False) - tag = jnp.full(len(r), Tag.SOLID_WALL, dtype=int) - tag = jnp.where(mask_water, Tag.FLUID, tag) + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + + # set velocity wall tag + box_size = self._box_size2D(n_walls) + mask_lid = r[:, 1] > (box_size[1] - n_walls * self.case.dx) tag = jnp.where(mask_lid, Tag.MOVING_WALL, tag) - return tag + return r, tag + + def _init_pos3D(self, box_size, dx, n_walls): + # initialize fluid phase + r_f = n_walls * dx + pos_init_cartesian_3d(np.array([1.0, 1.0, 0.5]), dx) + + # initialize walls + r_w = self._init_walls_3d(dx, n_walls) - def _tag3D(self, r): - return self._tag2D(r) + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) + + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + + # set velocity wall tag + box_size = self._box_size3D(n_walls) + mask_lid = r[:, 1] > (box_size[1] - n_walls * self.case.dx) + tag = jnp.where(mask_lid, Tag.MOVING_WALL, tag) + return r, tag + + def _offset_vec(self): + dim = self.cfg.case.dim + if dim == 2: + res = np.ones(dim) * self.cfg.solver.n_walls * self.cfg.case.dx + elif dim == 3: + res = np.array([1.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + return res def _init_velocity2D(self, r): u = jnp.zeros_like(r) diff --git a/cases/pf.py b/cases/pf.py index 07e347d..1f2a9d3 100644 --- a/cases/pf.py +++ b/cases/pf.py @@ -5,7 +5,7 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag +from jax_sph.utils import Tag, pos_init_cartesian_2d, pos_init_cartesian_3d class PF(SimulationSetup): @@ -17,6 +17,9 @@ class PF(SimulationSetup): def __init__(self, cfg: DictConfig): super().__init__(cfg) + # define offset vector + self.offset_vec = self._offset_vec() + # relaxation configurations if self.case.mode == "rlx": self._set_default_rlx() @@ -26,23 +29,95 @@ def __init__(self, cfg: DictConfig): self._init_pos2D = self._get_relaxed_r0 self._init_pos3D = self._get_relaxed_r0 - def _box_size2D(self): - return np.array([0.4, 1 + 6 * self.case.dx]) + def _box_size2D(self, n_walls): + dx2n = self.case.dx * n_walls * 2 + sp = self.special + return np.array([sp.L, sp.H + dx2n]) + + def _box_size3D(self, n_walls): + dx2n = self.case.dx * n_walls * 2 + sp = self.special + return np.array([sp.L, sp.H + dx2n, 0.4]) + + def _init_walls_2d(self, dx, n_walls): + sp = self.special + + # thickness of wall particles + dxn = dx * n_walls + + # horizontal and vertical blocks + horiz = pos_init_cartesian_2d(np.array([sp.L, dxn]), dx) + + # wall: bottom, top + wall_b = horiz.copy() + wall_t = horiz.copy() + np.array([0.0, sp.H + dxn]) + + rw = np.concatenate([wall_b, wall_t]) + return rw + + def _init_walls_3d(self, dx, n_walls): + sp = self.special + + # thickness of wall particles + dxn = dx * n_walls + + # horizontal and vertical blocks + horiz = pos_init_cartesian_3d(np.array([sp.L, dxn, 0.4]), dx) + + # wall: bottom, top + wall_b = horiz.copy() + wall_t = horiz.copy() + np.array([0.0, sp.H + dxn, 0.0]) + + rw = np.concatenate([wall_b, wall_t]) + return rw + + def _init_pos2D(self, box_size, dx, n_walls): + sp = self.special + + # initialize fluid phase + r_f = np.array([0.0, 1.0]) * n_walls * dx + pos_init_cartesian_2d( + np.array([sp.L, sp.H]), dx + ) + + # initialize walls + r_w = self._init_walls_2d(dx, n_walls) + + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) + + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) + + return r, tag + + def _init_pos3D(self, box_size, dx, n_walls): + sp = self.special + + # initialize fluid phase + r_f = np.array([0.0, 1.0, 0.0]) * n_walls * dx + pos_init_cartesian_3d( + np.array([sp.L, sp.H, 0.4]), dx + ) + + # initialize walls + r_w = self._init_walls_3d(dx, n_walls) - def _box_size3D(self): - return np.array([0.4, 1 + 6 * self.case.dx, 0.4]) + # set tags + tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int) + tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int) - def _tag2D(self, r): - dx3 = 3 * self.case.dx - box_size = self._box_size2D() - tag = jnp.full(len(r), Tag.FLUID, dtype=int) + r = np.concatenate([r_w, r_f]) + tag = np.concatenate([tag_w, tag_f]) - mask_wall = (r[:, 1] < dx3) + (r[:, 1] > box_size[1] - dx3) - tag = jnp.where(mask_wall, Tag.SOLID_WALL, tag) - return tag + return r, tag - def _tag3D(self, r): - return self._tag2D(r) + def _offset_vec(self): + dim = self.cfg.case.dim + if dim == 2: + res = np.array([0.0, 1.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + elif dim == 3: + res = np.array([0.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + return res def _init_velocity2D(self, r): return jnp.zeros_like(r) @@ -51,11 +126,12 @@ def _init_velocity3D(self, r): return jnp.zeros_like(r) def _external_acceleration_fn(self, r): - dx3 = 3 * self.case.dx + n_walls = self.cfg.solver.n_walls + dxn = n_walls * self.case.dx res = jnp.zeros_like(r) x_force = jnp.ones((len(r))) - box_size = self._box_size2D() - fluid_mask = (r[:, 1] < box_size[1] - dx3) * (r[:, 1] > dx3) + box_size = self._box_size2D(n_walls) + fluid_mask = (r[:, 1] < box_size[1] - dxn) * (r[:, 1] > dxn) x_force = jnp.where(fluid_mask, x_force, 0) res = res.at[:, 0].set(x_force) return res * self.case.g_ext_magnitude diff --git a/cases/pf.yaml b/cases/pf.yaml index cca1cb9..0bd5557 100644 --- a/cases/pf.yaml +++ b/cases/pf.yaml @@ -9,6 +9,9 @@ case: viscosity: 100.0 u_ref: 1.25 g_ext_magnitude: 1000.0 + special: + L: 0.4 # water column length + H: 1.0 # water column height solver: dt: 0.0000005 diff --git a/cases/rpf.py b/cases/rpf.py index 5218cf0..0e08b3e 100644 --- a/cases/rpf.py +++ b/cases/rpf.py @@ -5,7 +5,6 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag class RPF(SimulationSetup): @@ -23,18 +22,17 @@ def __init__(self, cfg: DictConfig): self._init_pos2D = self._get_relaxed_r0 self._init_pos3D = self._get_relaxed_r0 - def _box_size2D(self): + def _box_size2D(self, n_walls): return np.array([1.0, 2.0]) - def _box_size3D(self): + def _box_size3D(self, n_walls): return np.array([1.0, 2.0, 0.5]) - def _tag2D(self, r): - tag = jnp.full(len(r), Tag.FLUID, dtype=int) - return tag + def _init_walls_2d(self): + pass - def _tag3D(self, r): - return self._tag2D(r) + def _init_walls_3d(self): + pass def _init_velocity2D(self, r): u = jnp.zeros_like(r) diff --git a/cases/tgv.py b/cases/tgv.py index b100ce1..80f008c 100644 --- a/cases/tgv.py +++ b/cases/tgv.py @@ -6,7 +6,6 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import Tag class TGV(SimulationSetup): @@ -30,12 +29,11 @@ def _box_size2D(self, n_walls): def _box_size3D(self, n_walls): return 2 * np.pi * np.array([1.0, 1.0, 1.0]) - def _tag2D(self, r, n_walls): - tag = jnp.full(len(r), Tag.FLUID, dtype=int) - return tag + def _init_walls_2d(self): + pass - def _tag3D(self, r, n_walls): - return self._tag2D(r) + def _init_walls_3d(self): + pass def _init_velocity2D(self, r): x, y = r diff --git a/cases/ut.py b/cases/ut.py index 9a4d42f..c5f9c21 100644 --- a/cases/ut.py +++ b/cases/ut.py @@ -5,10 +5,10 @@ from omegaconf import DictConfig from jax_sph.case_setup import SimulationSetup -from jax_sph.utils import pos_init_cartesian_2d, pos_init_cartesian_3d +from jax_sph.utils import Tag, pos_init_cartesian_2d, pos_init_cartesian_3d -class UTSetup(SimulationSetup): +class UT(SimulationSetup): """Unit Test: cube of water in periodic boundary box""" def __init__(self, cfg: DictConfig): @@ -20,25 +20,29 @@ def __init__(self, cfg: DictConfig): if self.case.mode == "rlx" or self.case.r0_type == "relaxed": raise NotImplementedError("Relaxation not implemented for CW") - def _box_size2D(self): + def _box_size2D(self, n_walls): return np.array([self.special.L_wall, self.special.H_wall]) - def _box_size3D(self): + def _box_size3D(self, n_walls): return np.array([self.special.L_wall, self.special.L_wall, self.special.L_wall]) - def _init_pos2D(self, box_size, dx): - cube = np.array([self.special.L, self.special.H]) - return self.cube_offset + pos_init_cartesian_2d(cube, dx) + def _init_walls_2d(self): + pass - def _init_pos3D(self, box_size, dx): - cube = np.array([self.special.L, self.special.L, self.special.H]) - return self.cube_offset + pos_init_cartesian_3d(cube, dx) + def _init_walls_3d(self): + pass - def _tag2D(self, r): - return jnp.zeros(len(r), dtype=int) + def _init_pos2D(self, box_size, dx, n_walls): + cube = np.array([self.special.L, self.special.H]) + r = self.cube_offset + pos_init_cartesian_2d(cube, dx) + tag = jnp.full(len(r), Tag.FLUID, dtype=int) + return r, tag - def _tag3D(self, r): - return self._tag2D(r) + def _init_pos3D(self, box_size, dx, n_walls): + cube = np.array([self.special.L, self.special.L, self.special.H]) + r = self.cube_offset + pos_init_cartesian_3d(cube, dx) + tag = jnp.full(len(r), Tag.FLUID, dtype=int) + return r, tag def _init_velocity2D(self, r): return jnp.zeros_like(r) diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index 42836fe..8a6ace3 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -17,8 +17,6 @@ Tag, get_noise_masked, get_nws, - pos_box_2d, - pos_box_3d, pos_init_cartesian_2d, pos_init_cartesian_3d, wall_tags, @@ -126,12 +124,10 @@ def initialize(self): # initialize box and positions of particles if dim == 2: box_size = self._box_size2D(cfg.solver.n_walls) - r = self._init_pos2D(box_size, dx, cfg.solver.n_walls) - tag = self._tag2D(r, cfg.solver.n_walls) + r, tag = self._init_pos2D(box_size, dx, cfg.solver.n_walls) elif dim == 3: box_size = self._box_size3D(cfg.solver.n_walls) - r = self._init_pos3D(box_size, dx, cfg.solver.n_walls) - tag = self._tag3D(r, cfg.solver.n_walls) + r, tag = self._init_pos3D(box_size, dx, cfg.solver.n_walls) displacement_fn, shift_fn = space.periodic(side=box_size) num_particles = len(r) @@ -175,9 +171,24 @@ def initialize(self): "Cp": Cp, "nw": jnp.zeros_like(r), } + + # calculate wall normals if necessary if cfg.solver.is_bc_trick: - wall_part_fn = self._get_boundary_particles_fn() - nw = get_nws(r, tag, self.fluid_size, dx, self.offset_vec, wall_part_fn) + if dim == 2: + wall_part_fn = self._init_walls_2d + elif dim == 3: + wall_part_fn = self._init_walls_3d + else: + raise NotImplementedError("1D wall BCs not yet implemented") + nw = get_nws( + r, + tag, + dx, + cfg.solver.n_walls, + dim, + self.offset_vec, + wall_part_fn, + ) state["nw"] = nw # overwrite the state dictionary with the provided one @@ -225,17 +236,23 @@ def _box_size3D(self, n_walls): pass def _init_pos2D(self, box_size, dx, n_walls): - return pos_init_cartesian_2d(box_size, dx) + r = pos_init_cartesian_2d(box_size, dx) + tag = jnp.full(len(r), Tag.FLUID, dtype=int) + return r, tag def _init_pos3D(self, box_size, dx, n_walls): - return pos_init_cartesian_3d(box_size, dx) + r = pos_init_cartesian_3d(box_size, dx) + tag = jnp.full(len(r), Tag.FLUID, dtype=int) + return r, tag @abstractmethod - def _tag2D(self, r, n_walls): + def _init_walls_2d(self): + """Create all solid walls of a 2D case.""" pass @abstractmethod - def _tag3D(self, r, n_walls): + def _init_walls_3d(self): + """Create all solid walls of a 3D case.""" pass @abstractmethod @@ -254,13 +271,6 @@ def _external_acceleration_fn(self, r): def _boundary_conditions_fn(self, state): pass - def _get_boundary_particles_fn(self): - if self.case.dim == 2: - boundary_particles_fn = pos_box_2d - elif self.case.dim == 3: - boundary_particles_fn = pos_box_3d - return boundary_particles_fn - def _get_relaxed_r0(self, box_size, dx): assert hasattr(self, "_load_only_fluid"), AttributeError @@ -281,7 +291,7 @@ def _get_relaxed_r0(self, box_size, dx): if self._load_only_fluid: return state["r"][state["tag"] == Tag.FLUID] else: - return state["r"] + return state["r"], state["tag"] def _set_field_properties(self, num_particles, mass_ref, case): rho = jnp.ones(num_particles) * case.rho_ref @@ -302,8 +312,6 @@ def _set_default_rlx(self): self._box_size3D_rlx = self._box_size3D self._init_pos2D_rlx = self._init_pos2D self._init_pos3D_rlx = self._init_pos3D - self._tag2D_rlx = self._tag2D - self._tag3D_rlx = self._tag3D def set_relaxation(Case, cfg): @@ -333,8 +341,6 @@ def __init__(self, cfg): self._init_pos3D = self._init_pos3D_rlx self._box_size2D = self._box_size2D_rlx self._box_size3D = self._box_size3D_rlx - self._tag2D = self._tag2D_rlx - self._tag3D = self._tag3D_rlx def _init_velocity2D(self, r): return jnp.zeros_like(r) diff --git a/jax_sph/integrator.py b/jax_sph/integrator.py index b82159b..5ede231 100644 --- a/jax_sph/integrator.py +++ b/jax_sph/integrator.py @@ -1,8 +1,9 @@ """Integrator schemes.""" - from typing import Callable, Dict +import jax.numpy as jnp + from jax_sph.utils import Tag @@ -25,6 +26,13 @@ def advance(dt: float, state: Dict, neighbors): state["u"] += 1.0 * dt * state["dudt"] state["v"] = state["u"] + tvf * 0.5 * dt * state["dvdt"] + # TODO: put elsewhere + # Quick fix to stop advection of moving wall boundary particles + dim = jnp.shape(state["v"])[1] + state["v"] = jnp.where( + jnp.isin(state["tag"], Tag.MOVING_WALL)[:, None], jnp.zeros(dim), state["v"] + ) + # 2. Integrate position with velocity v state["r"] = shift_fn(state["r"], 1.0 * dt * state["v"]) diff --git a/jax_sph/solver.py b/jax_sph/solver.py index bf74e20..760d593 100644 --- a/jax_sph/solver.py +++ b/jax_sph/solver.py @@ -582,23 +582,11 @@ def forward(state, neighbors): wall_mask = jnp.where(jnp.isin(tag, wall_tags), 1.0, 0.0) fluid_mask = jnp.where(tag == Tag.FLUID, 1.0, 0.0) - # calculate normal vector of wall boundaries - # temp = vmap(self._wall_phi_vec)( - # rho[j_s], mass[j_s], dr_i_j, dist, wall_mask[j_s], wall_mask[i_s] - # ) - # phi = ops.segment_sum(temp, i_s, N) - - # # compute normal vector for boundary particles eq. (15), Zhang (2017) - # n_w = ( - # phi - # / (jnp.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] - # * wall_mask[:, None] - # ) - # n_w = jnp.where(jnp.absolute(n_w) < EPS, 0.0, n_w) - ##### Riemann velocity BCs if self.is_bc_trick and (self.solver == "RIE"): u_tilde = self._riemann_velocities(u, w_dist, fluid_mask, i_s, j_s, N) + else: + u_tilde = u ##### Density summation or evolution diff --git a/jax_sph/utils.py b/jax_sph/utils.py index 393b327..f14e794 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -52,14 +52,16 @@ def pos_init_cartesian_3d(box_size: array, dx: float): return r -def pos_box_2d(fluid_box: array, dx: float, num_wall_layers: int = 3): +def pos_box_2d(fluid_box: array, dx: float, n_walls: int = 3): """Create an empty box of particles in 2D. fluid_box is an array of the form: [L, H] - The box is of size (L + num_wall_layers * dx) x (H + num_wall_layers * dx). - The inner part of the box starts at (num_wall_layers * dx, num_wall_layers * dx). + The box is of size (L + n_walls * dx) x (H + n_walls * dx). + The inner part of the box starts at (n_walls * dx, n_walls * dx). """ - dxn = num_wall_layers * dx + # thickness of wall particles + dxn = n_walls * dx + # horizontal and vertical blocks vertical = pos_init_cartesian_2d(np.array([dxn, fluid_box[1] + 2 * dxn]), dx) horiz = pos_init_cartesian_2d(np.array([fluid_box[0], dxn]), dx) @@ -74,14 +76,17 @@ def pos_box_2d(fluid_box: array, dx: float, num_wall_layers: int = 3): return res -def pos_box_3d(fluid_box: array, dx: float, num_wall_layers: int = 3): +def pos_box_3d(fluid_box: array, dx: float, n_walls: int = 3, z_periodic: bool = True): """Create an z-periodic empty box of particles in 3D. fluid_box is an array of the form: [L, H, D] - The box is of size (L + num_wall_layers * dx) x (H + num_wall_layers * dx) x D. - The inner part of the box starts at (num_wall_layers * dx, num_wall_layers * dx). + The box is of size (L + n_walls * dx) x (H + n_walls * dx) x D. + The inner part of the box starts at (n_walls * dx, n_walls * dx). + z_periodic states whether the box is periodic in z-direction. """ - dxn = num_wall_layers * dx + # thickness of wall particles + dxn = n_walls * dx + # horizontal and vertical blocks vertical = pos_init_cartesian_3d( np.array([dxn, fluid_box[1] + 2 * dxn, fluid_box[2]]), dx @@ -95,6 +100,20 @@ def pos_box_3d(fluid_box: array, dx: float, num_wall_layers: int = 3): wall_t = horiz.copy() + np.array([dxn, fluid_box[1] + dxn, 0.0]) res = jnp.concatenate([wall_l, wall_b, wall_r, wall_t]) + + # add walls in z-direction + if not z_periodic: + res += np.array([0.0, 0.0, dxn]) + # front block + front = pos_init_cartesian_3d( + np.array([fluid_box[0] + 2 * dxn, fluid_box[1] + 2 * dxn, dxn]), dx + ) + + # wall: front, end + wall_f = front.copy() + wall_e = front.copy() + np.array([0.0, 0.0, fluid_box[2] + dxn]) + res = jnp.concatenate([res, wall_f, wall_e]) + return res @@ -222,14 +241,17 @@ def wall_phi_vec(rho_j, m_j, dr_ij, dist): return nw, r_nw -def get_nws(r, tag, fluid_size, dx, offset_vec, wall_part_fn): +def get_nws(r, tag, dx, n_walls, dim, offset_vec, wall_part_fn): """Computes the normal vectors of all wall boundaries""" + dx_fac = 5 + # align fluid to [0, 0] r_aligned = r - offset_vec # define fine layer of wall BC partilces and position them accordingly - layer = wall_part_fn(fluid_size, dx / 5, 1) - np.ones(2) * dx / 5 + # layer = wall_part_fn(fluid_size, dx / dx_fac, 1) - offset_vec / n_walls / dx_fac + layer = wall_part_fn(dx / dx_fac, 1) - offset_vec / n_walls / dx_fac # match thin layer to particles tree = KDTree(layer) @@ -238,7 +260,7 @@ def get_nws(r, tag, fluid_size, dx, offset_vec, wall_part_fn): # compute normal vectors nw = dr / (dist[:, None] + EPS) - nw = np.where(np.isin(tag, wall_tags)[:, None], nw, np.zeros(2)) + nw = np.where(np.isin(tag, wall_tags)[:, None], nw, np.zeros(dim)) return nw diff --git a/pyproject.toml b/pyproject.toml index da886e5..0c7a5e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ sphinx-rtd-theme = "1.3.0" toml = "^0.10.2" [tool.ruff] -ignore = ["F821", "E402"] exclude = [ ".git", ".venv*", @@ -52,6 +51,7 @@ show-fixes = true line-length = 88 [tool.ruff.lint] +ignore = ["F821", "E402"] select = [ "E", # pycodestyle "F", # Pyflakes From 1b3c6804e1dfe0e2d4c1ef00fb2cf6e30bd9fe22 Mon Sep 17 00:00:00 2001 From: Artur Toshev Date: Tue, 11 Jun 2024 20:53:39 +0200 Subject: [PATCH 19/21] JAX 0.4.28 -> 0.4.29 --- README.md | 2 +- docs/requirements.txt | 3 +- jax_sph/utils.py | 2 +- poetry.lock | 91 +++++++++++++++++++++---------------------- pyproject.toml | 12 +++--- requirements.txt | 3 +- 6 files changed, 55 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 1e42385..b47464e 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Later, you just need to `source venv/bin/activate` to activate the environment. If you want to use a CUDA GPU, you first need a running Nvidia driver. And then just follow the instructions [here](https://jax.readthedocs.io/en/latest/installation.html). The whole process could look like this: ```bash source .venv/bin/activate -pip install -U "jax[cuda12]" +pip install -U "jax[cuda12]==0.4.29" ``` ## Getting Started diff --git a/docs/requirements.txt b/docs/requirements.txt index 186f921..26f1e2c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,5 @@ h5py --e git+https://github.com/jax-md/jax-md.git@c451353f6ddcab031f660befda256d8a4f657855#egg=jax-md -jax[cpu]==0.4.28 +jax[cpu]==0.4.29 omegaconf pandas pyvista diff --git a/jax_sph/utils.py b/jax_sph/utils.py index f14e794..ad7c39e 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -148,7 +148,7 @@ def get_array_stats(state: Dict, var: str = "u", operation="max"): if jnp.size(state[var].shape) > 1: val_array = jnp.sqrt(jnp.square(state[var]).sum(axis=1)) else: - val_array = state[var] # TODO: check difference to jnp.absolute(state[var]) + val_array = state[var] return func(val_array) diff --git a/poetry.lock b/poetry.lock index 9a8956a..91e0bb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "absl-py" @@ -843,19 +843,19 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pa [[package]] name = "jax" -version = "0.4.28" +version = "0.4.29" description = "Differentiate, compile, and transform Numpy code." optional = false python-versions = ">=3.9" files = [ - {file = "jax-0.4.28-py3-none-any.whl", hash = "sha256:6a181e6b5a5b1140e19cdd2d5c4aa779e4cb4ec627757b918be322d8e81035ba"}, - {file = "jax-0.4.28.tar.gz", hash = "sha256:dcf0a44aff2e1713f0a2b369281cd5b79d8c18fc1018905c4125897cb06b37e9"}, + {file = "jax-0.4.29-py3-none-any.whl", hash = "sha256:cfdc594d133d7dfba2ec19bc10742ffaa0e8827ead29be00d3ec4215a3f7892e"}, + {file = "jax-0.4.29.tar.gz", hash = "sha256:12904571eaefddcdc8c3b8d4936482b783d5a216e99ef5adcd3522fdfb4fc186"}, ] [package.dependencies] importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} -jaxlib = {version = "0.4.28", optional = true, markers = "extra == \"cpu\""} -ml-dtypes = ">=0.2.0" +jaxlib = {version = "0.4.29", optional = true, markers = "extra == \"cpu\""} +ml-dtypes = ">=0.4.0" numpy = [ {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, {version = ">=1.22", markers = "python_version < \"3.11\""}, @@ -864,53 +864,52 @@ opt-einsum = "*" scipy = ">=1.9" [package.extras] -australis = ["protobuf (>=3.13,<4)"] -ci = ["jaxlib (==0.4.27)"] -cpu = ["jaxlib (==0.4.28)"] -cuda = ["jaxlib (==0.4.28+cuda12.cudnn89)"] -cuda12 = ["jax-cuda12-plugin (==0.4.28)", "jaxlib (==0.4.28)", "nvidia-cublas-cu12 (>=12.1.3.1)", "nvidia-cuda-cupti-cu12 (>=12.1.105)", "nvidia-cuda-nvcc-cu12 (>=12.1.105)", "nvidia-cuda-runtime-cu12 (>=12.1.105)", "nvidia-cudnn-cu12 (>=8.9.2.26,<9.0)", "nvidia-cufft-cu12 (>=11.0.2.54)", "nvidia-cusolver-cu12 (>=11.4.5.107)", "nvidia-cusparse-cu12 (>=12.1.0.106)", "nvidia-nccl-cu12 (>=2.18.1)", "nvidia-nvjitlink-cu12 (>=12.1.105)"] -cuda12-cudnn89 = ["jaxlib (==0.4.28+cuda12.cudnn89)"] -cuda12-local = ["jaxlib (==0.4.28+cuda12.cudnn89)"] -cuda12-pip = ["jaxlib (==0.4.28+cuda12.cudnn89)", "nvidia-cublas-cu12 (>=12.1.3.1)", "nvidia-cuda-cupti-cu12 (>=12.1.105)", "nvidia-cuda-nvcc-cu12 (>=12.1.105)", "nvidia-cuda-runtime-cu12 (>=12.1.105)", "nvidia-cudnn-cu12 (>=8.9.2.26,<9.0)", "nvidia-cufft-cu12 (>=11.0.2.54)", "nvidia-cusolver-cu12 (>=11.4.5.107)", "nvidia-cusparse-cu12 (>=12.1.0.106)", "nvidia-nccl-cu12 (>=2.18.1)", "nvidia-nvjitlink-cu12 (>=12.1.105)"] +ci = ["jaxlib (==0.4.28)"] +cpu = ["jaxlib (==0.4.29)"] +cuda = ["jaxlib (==0.4.29+cuda12.cudnn91)"] +cuda12 = ["jax-cuda12-plugin (==0.4.29)", "jaxlib (==0.4.29)", "nvidia-cublas-cu12 (>=12.1.3.1)", "nvidia-cuda-cupti-cu12 (>=12.1.105)", "nvidia-cuda-nvcc-cu12 (>=12.1.105)", "nvidia-cuda-runtime-cu12 (>=12.1.105)", "nvidia-cudnn-cu12 (>=9.0,<10.0)", "nvidia-cufft-cu12 (>=11.0.2.54)", "nvidia-cusolver-cu12 (>=11.4.5.107)", "nvidia-cusparse-cu12 (>=12.1.0.106)", "nvidia-nccl-cu12 (>=2.18.1)", "nvidia-nvjitlink-cu12 (>=12.1.105)"] +cuda12-cudnn91 = ["jaxlib (==0.4.29+cuda12.cudnn91)"] +cuda12-local = ["jaxlib (==0.4.29+cuda12.cudnn91)"] +cuda12-pip = ["jaxlib (==0.4.29+cuda12.cudnn91)", "nvidia-cublas-cu12 (>=12.1.3.1)", "nvidia-cuda-cupti-cu12 (>=12.1.105)", "nvidia-cuda-nvcc-cu12 (>=12.1.105)", "nvidia-cuda-runtime-cu12 (>=12.1.105)", "nvidia-cudnn-cu12 (>=9.0,<10.0)", "nvidia-cufft-cu12 (>=11.0.2.54)", "nvidia-cusolver-cu12 (>=11.4.5.107)", "nvidia-cusparse-cu12 (>=12.1.0.106)", "nvidia-nccl-cu12 (>=2.18.1)", "nvidia-nvjitlink-cu12 (>=12.1.105)"] minimum-jaxlib = ["jaxlib (==0.4.27)"] -tpu = ["jaxlib (==0.4.28)", "libtpu-nightly (==0.1.dev20240508)", "requests"] +tpu = ["jaxlib (==0.4.29)", "libtpu-nightly (==0.1.dev20240609)", "requests"] [[package]] name = "jaxlib" -version = "0.4.28" +version = "0.4.29" description = "XLA library for JAX" optional = false python-versions = ">=3.9" files = [ - {file = "jaxlib-0.4.28-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:a421d237f8c25d2850166d334603c673ddb9b6c26f52bc496704b8782297bd66"}, - {file = "jaxlib-0.4.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f038e68bd10d1a3554722b0bbe36e6a448384437a75aa9d283f696f0ed9f8c09"}, - {file = "jaxlib-0.4.28-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:fabe77c174e9e196e9373097cefbb67e00c7e5f9d864583a7cfcf9dabd2429b6"}, - {file = "jaxlib-0.4.28-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:e3bcdc6f8e60f8554f415c14d930134e602e3ca33c38e546274fd545f875769b"}, - {file = "jaxlib-0.4.28-cp310-cp310-win_amd64.whl", hash = "sha256:a8b31c0e5eea36b7915696b9be40ea8646edc395a3e5437bf7ef26b7239a567a"}, - {file = "jaxlib-0.4.28-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2ff8290edc7b92c7eae52517f65492633e267b2e9067bad3e4c323d213e77cf5"}, - {file = "jaxlib-0.4.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:793857faf37f371cafe752fea5fc811f435e43b8fb4b502058444a7f5eccf829"}, - {file = "jaxlib-0.4.28-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b41a6b0d506c09f86a18ecc05bd376f072b548af89c333107e49bb0c09c1a3f8"}, - {file = "jaxlib-0.4.28-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:45ce0f3c840cff8236cff26c37f26c9ff078695f93e0c162c320c281f5041275"}, - {file = "jaxlib-0.4.28-cp311-cp311-win_amd64.whl", hash = "sha256:d4d762c3971d74e610a0e85a7ee063cea81a004b365b2a7dc65133f08b04fac5"}, - {file = "jaxlib-0.4.28-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:d6c09a545329722461af056e735146d2c8c74c22ac7426a845eb69f326b4f7a0"}, - {file = "jaxlib-0.4.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8dd8bffe3853702f63cd924da0ee25734a4d19cd5c926be033d772ba7d1c175d"}, - {file = "jaxlib-0.4.28-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:de2e8521eb51e16e85093a42cb51a781773fa1040dcf9245d7ea160a14ee5a5b"}, - {file = "jaxlib-0.4.28-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:46a1aa857f4feee8a43fcba95c0e0ab62d40c26cc9730b6c69655908ba359f8d"}, - {file = "jaxlib-0.4.28-cp312-cp312-win_amd64.whl", hash = "sha256:eee428eac31697a070d655f1f24f6ab39ced76750d93b1de862377a52dcc2401"}, - {file = "jaxlib-0.4.28-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4f98cc837b2b6c6dcfe0ab7ff9eb109314920946119aa3af9faa139718ff2787"}, - {file = "jaxlib-0.4.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b01562ec8ad75719b7d0389752489e97eb6b4dcb4c8c113be491634d5282ad3c"}, - {file = "jaxlib-0.4.28-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:aa77a9360a395ba9faf6932df637686fb0c14ddcf4fdc1d2febe04bc88a580a6"}, - {file = "jaxlib-0.4.28-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:4a56ebf05b4a4c1791699d874e072f3f808f0986b4010b14fb549a69c90ca9dc"}, - {file = "jaxlib-0.4.28-cp39-cp39-win_amd64.whl", hash = "sha256:459a4ddcc3e120904b9f13a245430d7801d707bca48925981cbdc59628057dc8"}, + {file = "jaxlib-0.4.29-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:60ec8aa2ba133a0615b0fce8e084c90c179c019793551641dd3da6526d036953"}, + {file = "jaxlib-0.4.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adb37f9c01a0fbdf97ab4afc7b60939c1694a5c056d8224c3d292cc253a3dc55"}, + {file = "jaxlib-0.4.29-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:8b1062b804d95ddb8dbb039c48316cbaed1d6866f973ef39e03f39e452ac8a1c"}, + {file = "jaxlib-0.4.29-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:ae21b84dd08c015bf2bab9ba97fa6a1da30f9a51c35902e23c7ffe959ad2e86c"}, + {file = "jaxlib-0.4.29-cp310-cp310-win_amd64.whl", hash = "sha256:a4993ab2f91c8ee213cacc4ed341539a7980b1b231e9dac69d68b508118dc19d"}, + {file = "jaxlib-0.4.29-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1da0716c45c5b0e177d334938a09953915f8da2080fffee9366ad8a9a988f484"}, + {file = "jaxlib-0.4.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5da76e760be790896f7149eccafff64b800f129a282de7f7a1edc138d56ac997"}, + {file = "jaxlib-0.4.29-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:aec76c416657e25884ee1364e98e40fcedbd2235c79691026d9babf05d850ede"}, + {file = "jaxlib-0.4.29-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5a313e94c3ae87f147d561bc61e923d75e18e2448ac8fbf150d526ecd24404b0"}, + {file = "jaxlib-0.4.29-cp311-cp311-win_amd64.whl", hash = "sha256:7bfcc35a2991c2973489333f5c07dbb1f5d0ec78ef889097534c2c5f0d0149a7"}, + {file = "jaxlib-0.4.29-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7d7eabdb21814386cfa32e09ddaee76e374126c3709989b6de5f1e49a04f5f36"}, + {file = "jaxlib-0.4.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac11efc5eb7d0d25dc853efd68c18b204d071e78f762583dc9ebed84db272bf2"}, + {file = "jaxlib-0.4.29-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:08cb5b24f481f62ff432b0bbedc7e35c5d561dc42c1c8138bbf8514ea91ab17e"}, + {file = "jaxlib-0.4.29-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:a7c884c5651e5d1cc0fc57f2cf0abee4223b1f38c3e8cbd00fb142e6551cfe47"}, + {file = "jaxlib-0.4.29-cp312-cp312-win_amd64.whl", hash = "sha256:91058f1606312c42621b0a9979d0f14c0db9da6341ffd714ac5eb5e3be79c59a"}, + {file = "jaxlib-0.4.29-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:e59afd8026f43688fd0bf4f3dbcf913157c7f144d000850aaa7a88b1228a87ab"}, + {file = "jaxlib-0.4.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2801327384be3edab5f3adb38262206e488135d7c8e27d928ac3c6ffb71f7718"}, + {file = "jaxlib-0.4.29-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:dfce109290dbbd27176750b931bedbabb56a4f5e955f83eead2e3cdefe5108b8"}, + {file = "jaxlib-0.4.29-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:84be918201a7f06b73074ed154fc3344b02aa8736597b39397e7093807dcfc3c"}, + {file = "jaxlib-0.4.29-cp39-cp39-win_amd64.whl", hash = "sha256:9b0efd3ba45a7ee03fb91a4118099ea97aa02a96e50f4d91cc910554e226c5b9"}, ] [package.dependencies] -ml-dtypes = ">=0.2.0" +ml-dtypes = ">=0.4.0" numpy = ">=1.22" scipy = ">=1.9" [package.extras] -cuda12-pip = ["nvidia-cublas-cu12 (>=12.1.3.1)", "nvidia-cuda-cupti-cu12 (>=12.1.105)", "nvidia-cuda-nvcc-cu12 (>=12.1.105)", "nvidia-cuda-runtime-cu12 (>=12.1.105)", "nvidia-cudnn-cu12 (>=8.9.2.26,<9.0)", "nvidia-cufft-cu12 (>=11.0.2.54)", "nvidia-cusolver-cu12 (>=11.4.5.107)", "nvidia-cusparse-cu12 (>=12.1.0.106)", "nvidia-nccl-cu12 (>=2.18.1)", "nvidia-nvjitlink-cu12 (>=12.1.105)"] +cuda12-pip = ["nvidia-cublas-cu12 (>=12.1.3.1)", "nvidia-cuda-cupti-cu12 (>=12.1.105)", "nvidia-cuda-nvcc-cu12 (>=12.1.105)", "nvidia-cuda-runtime-cu12 (>=12.1.105)", "nvidia-cudnn-cu12 (>=9.0,<10.0)", "nvidia-cufft-cu12 (>=11.0.2.54)", "nvidia-cusolver-cu12 (>=11.4.5.107)", "nvidia-cusparse-cu12 (>=12.1.0.106)", "nvidia-nccl-cu12 (>=2.18.1)", "nvidia-nvjitlink-cu12 (>=12.1.105)"] [[package]] name = "jaxopt" @@ -1520,13 +1519,13 @@ test = ["chex", "coverage[toml]", "lineax", "matplotlib", "networkx (>=2.5)", "p [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1788,13 +1787,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.46" +version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.46-py3-none-any.whl", hash = "sha256:45abe60a8300f3c618b23c16c4bb98c6fc80af8ce8b17c7ae92db48db3ee63c1"}, - {file = "prompt_toolkit-3.0.46.tar.gz", hash = "sha256:869c50d682152336e23c4db7f74667639b5047494202ffe7670817053fd57795"}, + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] @@ -2682,4 +2681,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.11" -content-hash = "9dd3f880130bab1f475f71d18e7215d861517140fb64a69ac4cd3fa76d63a129" +content-hash = "3f89c0c18fea0692378f5b81a4d8c12fa9bdc0b964e4aca6c0c7c4e2dc2e45ab" diff --git a/pyproject.toml b/pyproject.toml index 0c7a5e2..b12d55a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,13 +15,13 @@ python = ">=3.9,<=3.11" h5py = ">=3.9.0" pandas = ">=2.1.4" # for validation pyvista = ">=0.42.2" # for visualization -jax = {version = "0.4.28", extras = ["cpu"]} -jaxlib = "0.4.28" -omegaconf = "^2.3.0" +jax = {version = "0.4.29", extras = ["cpu"]} +jaxlib = "0.4.29" +omegaconf = ">=2.3.0" matscipy = ">=0.8.0" dataclasses = "0.6" # for jax-md -jraph = "^0.0.6.dev0" # for jax-md -absl-py = "^2.1.0" # for jax-md +jraph = ">=0.0.6.dev0" # for jax-md +absl-py = ">=2.1.0" # for jax-md [tool.poetry.group.dev.dependencies] pre-commit = ">=3.3.1" @@ -37,7 +37,7 @@ ipykernel = ">=6.25.1" sphinx = "7.2.6" sphinx-exec-code = "0.12" sphinx-rtd-theme = "1.3.0" -toml = "^0.10.2" +toml = ">=0.10.2" [tool.ruff] exclude = [ diff --git a/requirements.txt b/requirements.txt index 2dd9bfd..ad56d35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ h5py --e git+https://github.com/jax-md/jax-md.git@c451353f6ddcab031f660befda256d8a4f657855#egg=jax-md -jax[cpu]==0.4.28 +jax[cpu]==0.4.29 omegaconf pandas pyvista From d8742c1457bebf938fc747d789d8c2245e6db3d9 Mon Sep 17 00:00:00 2001 From: Artur Toshev Date: Wed, 12 Jun 2024 02:32:32 +0200 Subject: [PATCH 20/21] fix normal vectors of moving walls --- cases/ldc.py | 4 +- jax_sph/case_setup.py | 78 ++++++++++++++----- jax_sph/integrator.py | 17 ++-- jax_sph/simulate.py | 3 +- jax_sph/utils.py | 176 ++++++++++++++++++++++-------------------- 5 files changed, 161 insertions(+), 117 deletions(-) diff --git a/cases/ldc.py b/cases/ldc.py index c52909b..478f44b 100644 --- a/cases/ldc.py +++ b/cases/ldc.py @@ -104,9 +104,9 @@ def _init_pos3D(self, box_size, dx, n_walls): def _offset_vec(self): dim = self.cfg.case.dim if dim == 2: - res = np.ones(dim) * self.cfg.solver.n_walls * self.cfg.case.dx + res = jnp.ones(dim) * self.cfg.solver.n_walls * self.case.dx elif dim == 3: - res = np.array([1.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.cfg.case.dx + res = jnp.array([1.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.case.dx return res def _init_velocity2D(self, r): diff --git a/jax_sph/case_setup.py b/jax_sph/case_setup.py index 8a6ace3..61383e6 100644 --- a/jax_sph/case_setup.py +++ b/jax_sph/case_setup.py @@ -15,8 +15,9 @@ from jax_sph.jax_md import space from jax_sph.utils import ( Tag, + compute_nws_jax_wrapper, + compute_nws_scipy, get_noise_masked, - get_nws, pos_init_cartesian_2d, pos_init_cartesian_3d, wall_tags, @@ -60,6 +61,7 @@ def initialize(self): - state (dict): dictionary containing all field values - g_ext_fn (Callable): external force function - bc_fn (Callable): boundary conditions function (e.g. velocity at walls) + - nw_fn (Callable): jit-able wall normal funct. when moving walls, else None - eos (Callable): equation of state function - key (PRNGKey): random key for sampling - displacement_fn (Callable): displacement function for edge features @@ -151,6 +153,10 @@ def initialize(self): rho, mass, eta, temperature, kappa, Cp = self._set_field_properties( num_particles, mass_ref, cfg.case ) + # whether to compute wall normals + is_nw = cfg.solver.free_slip or cfg.solver.name == "RIE" + # calculate wall normals if necessary + nw = self._compute_wall_normals("scipy")(r, tag) if is_nw else jnp.zeros_like(r) # initialize the state dictionary state = { @@ -169,28 +175,9 @@ def initialize(self): "T": temperature, "kappa": kappa, "Cp": Cp, - "nw": jnp.zeros_like(r), + "nw": nw, } - # calculate wall normals if necessary - if cfg.solver.is_bc_trick: - if dim == 2: - wall_part_fn = self._init_walls_2d - elif dim == 3: - wall_part_fn = self._init_walls_3d - else: - raise NotImplementedError("1D wall BCs not yet implemented") - nw = get_nws( - r, - tag, - dx, - cfg.solver.n_walls, - dim, - self.offset_vec, - wall_part_fn, - ) - state["nw"] = nw - # overwrite the state dictionary with the provided one if cfg.case.state0_path is not None: _state = read_h5(cfg.case.state0_path) @@ -215,12 +202,25 @@ def initialize(self): g_ext_fn = self._external_acceleration_fn bc_fn = self._boundary_conditions_fn + # whether to recompute the wall normals at every integration step + is_nw_recompute = (tag == Tag.MOVING_WALL).any() and is_nw + if is_nw_recompute: + assert cfg.nl.backend != "matscipy", NotImplementedError( + "Wall normals not yet implemented for matscipy neighbor list when " + "working with moving boundaries. \nIf you work with moving boundaries, " + "don't use one of: `nl.backend=matscipy` or `solver.free_slip=True` or " + "`solver.name=RIE`." + ) + kwargs = {"disp_fn": displacement_fn, "box_size": box_size, "state0": state} + nw_fn = self._compute_wall_normals("jax", **kwargs) if is_nw_recompute else None + return ( cfg, box_size, state, g_ext_fn, bc_fn, + nw_fn, eos, key, displacement_fn, @@ -313,6 +313,42 @@ def _set_default_rlx(self): self._init_pos2D_rlx = self._init_pos2D self._init_pos3D_rlx = self._init_pos3D + def _compute_wall_normals(self, backend="scipy", **kwargs): + if self.cfg.case.dim == 2: + wall_part_fn = self._init_walls_2d + elif self.cfg.case.dim == 3: + wall_part_fn = self._init_walls_3d + else: + raise NotImplementedError("1D wall BCs not yet implemented") + + if backend == "scipy": + # If one makes `tag` static (-> `self.tag`), this function can be jitted. + # But it is significatly slower than `backend="jax"` due to `pure_callback`. + def body(r, tag): + return compute_nws_scipy( + r, + tag, + self.cfg.case.dx, + self.cfg.solver.n_walls, + self.offset_vec, + wall_part_fn, + ) + elif backend == "jax": + # This implementation is used in the integrator when having moving walls. + body = compute_nws_jax_wrapper( + state0=kwargs["state0"], + dx=self.cfg.case.dx, + n_walls=self.cfg.solver.n_walls, + offset_vec=self.offset_vec, + box_size=kwargs["box_size"], + pbc=self.cfg.case.pbc, + cfg_nl=self.cfg.nl, + displacement_fn=kwargs["disp_fn"], + wall_part_fn=wall_part_fn, + ) + + return body + def set_relaxation(Case, cfg): """Make a relaxation case from a SimulationSetup instance. diff --git a/jax_sph/integrator.py b/jax_sph/integrator.py index 5ede231..98e6430 100644 --- a/jax_sph/integrator.py +++ b/jax_sph/integrator.py @@ -2,12 +2,12 @@ from typing import Callable, Dict -import jax.numpy as jnp - from jax_sph.utils import Tag -def si_euler(tvf: float, model: Callable, shift_fn: Callable, bc_fn: Callable): +def si_euler( + tvf: float, model: Callable, shift_fn: Callable, bc_fn: Callable, nw_fn: Callable +): """Semi-implicit Euler integrator including Transport Velocity. The integrator advances the state of the system following the steps: @@ -26,16 +26,13 @@ def advance(dt: float, state: Dict, neighbors): state["u"] += 1.0 * dt * state["dudt"] state["v"] = state["u"] + tvf * 0.5 * dt * state["dvdt"] - # TODO: put elsewhere - # Quick fix to stop advection of moving wall boundary particles - dim = jnp.shape(state["v"])[1] - state["v"] = jnp.where( - jnp.isin(state["tag"], Tag.MOVING_WALL)[:, None], jnp.zeros(dim), state["v"] - ) - # 2. Integrate position with velocity v state["r"] = shift_fn(state["r"], 1.0 * dt * state["v"]) + # recompute wall normals if needed + if nw_fn is not None: + state["nw"] = nw_fn(state["r"]) + # 3. Update neighbor list # The displacment and shift function from JAX MD are used for computing the diff --git a/jax_sph/simulate.py b/jax_sph/simulate.py index 0895d6c..5395666 100644 --- a/jax_sph/simulate.py +++ b/jax_sph/simulate.py @@ -38,6 +38,7 @@ def simulate(cfg: DictConfig): state, g_ext_fn, bc_fn, + nw_fn, eos_fn, key, displacement_fn, @@ -84,7 +85,7 @@ def simulate(cfg: DictConfig): neighbors = neighbor_fn.allocate(state["r"], num_particles=num_particles) # Instantiate advance function for our use case - advance = si_euler(cfg.solver.tvf, forward, shift_fn, bc_fn) + advance = si_euler(cfg.solver.tvf, forward, shift_fn, bc_fn, nw_fn) advance = advance if cfg.no_jit else jit(advance) diff --git a/jax_sph/utils.py b/jax_sph/utils.py index ad7c39e..d0023b5 100644 --- a/jax_sph/utils.py +++ b/jax_sph/utils.py @@ -1,7 +1,7 @@ """General jax-sph utils.""" import enum -from typing import Dict +from typing import Callable, Dict import jax import jax.numpy as jnp @@ -13,6 +13,7 @@ from jax_sph.io_state import read_h5 from jax_sph.jax_md import partition, space +from jax_sph.jax_md.partition import Dense from jax_sph.kernel import QuinticKernel EPS = jnp.finfo(float).eps @@ -165,106 +166,115 @@ def get_stats(state: Dict, props: list, dx: float): return res -def get_box_nws(box_size, dx, n_walls, dim, rho, m): - """Computes the normal vectors at box wall boundaries""" - - # TODO: having a pos_box_3d would be useful - box = box_size - 2 * n_walls * dx - - # define 5 layers of wall BC partilces and position them accordingly - layers = {} - idx_len = {} - for i in range(5): - layer = pos_box_2d(box + 2 * i * dx, dx, 1) - layers[f"layer_{i}"] = layer + np.ones(2) * ((n_walls - 1) - i) * dx - idx_len[f"len_{i}"] = len(layer) - - # define kernel function - kernel_fn = QuinticKernel(h=dx, dim=dim) - - # define function to calculate phi, Zhang (2017) - def wall_phi_vec(rho_j, m_j, dr_ij, dist): - # Compute unit vector, above eq. (6), Zhang (2017) - e_ij_w = dr_ij / (dist + EPS) - - # Compute kernel gradient - kernel_grad = kernel_fn.grad_w(dist) * (e_ij_w) - - # compute phi eq. (15), Zhang (2017) - phi = -1.0 * m_j / rho_j * kernel_grad - - return phi - - nw = [] - for i in range(3): - # setup of the temporary box, consisting out of 3 particle layers - temp_box = np.concatenate( - ( - layers[f"layer_{i}"], - layers[f"layer_{i + 1}"], - layers[f"layer_{i + 2}"], - ), - axis=0, - ) - # define KD tree and get neighbors - tree = KDTree(temp_box) - neighbors = tree.query_ball_point( - temp_box[0 : idx_len[f"len_{i}"]], 3 * dx, p=2.0 - ) - # get neighbor and nw indices - neighbors_idx = np.concatenate(neighbors, axis=0) - nw_idx = np.repeat(range(idx_len[f"len_{i}"]), [len(x) for x in neighbors]) - - # calculate distances - dr_ij = vmap(space.pairwise_displacement)( - temp_box[nw_idx], temp_box[neighbors_idx] - ) - dist = space.distance(dr_ij) - - # calculate normal vectors - temp = vmap(wall_phi_vec)(rho[neighbors_idx], m[neighbors_idx], dr_ij, dist) - phi = ops.segment_sum(temp, nw_idx, idx_len[f"len_{i}"]) - nw_temp = phi / (np.linalg.norm(phi, ord=2, axis=1) + EPS)[:, None] - nw.append(nw_temp) - - nw = np.concatenate(nw, axis=0) - nw = np.where(np.absolute(nw) < EPS, 0.0, nw) - r_nw = np.concatenate( - ( - layers["layer_0"], - layers["layer_1"], - layers["layer_2"], - ), - axis=0, - ) - - return nw, r_nw - - -def get_nws(r, tag, dx, n_walls, dim, offset_vec, wall_part_fn): - """Computes the normal vectors of all wall boundaries""" +def compute_nws_scipy(r, tag, dx, n_walls, offset_vec, wall_part_fn): + """Computes the normal vectors of all wall boundaries. Jit-able pure_callback.""" dx_fac = 5 + # operate only on wall particles, i.e. remove fluid + r_walls = r[np.isin(tag, wall_tags)] + # align fluid to [0, 0] - r_aligned = r - offset_vec + r_aligned = r_walls - offset_vec # define fine layer of wall BC partilces and position them accordingly - # layer = wall_part_fn(fluid_size, dx / dx_fac, 1) - offset_vec / n_walls / dx_fac layer = wall_part_fn(dx / dx_fac, 1) - offset_vec / n_walls / dx_fac # match thin layer to particles tree = KDTree(layer) dist, match_idx = tree.query(r_aligned, k=1) dr = layer[match_idx] - r_aligned + nw_walls = dr / (dist[:, None] + EPS) + nw_walls = jnp.asarray(nw_walls, dtype=r.dtype) # compute normal vectors - nw = dr / (dist[:, None] + EPS) - nw = np.where(np.isin(tag, wall_tags)[:, None], nw, np.zeros(dim)) + nw = jnp.zeros_like(r) + nw = nw.at[np.isin(tag, wall_tags)].set(nw_walls) return nw +def compute_nws_jax_wrapper( + state0: Dict, + dx: float, + n_walls: int, + offset_vec: jax.Array, + box_size: jax.Array, + pbc: jax.Array, + cfg_nl: DictConfig, + displacement_fn: Callable, + wall_part_fn: Callable, +): + """Compute wall normal vectors from wall to fluid. Jit-able JAX implementation. + + For the particles from `r_walls`, find the closest particle from `layer` + and compute the normal vector from each `r_walls` particle. + """ + r = state0["r"] + tag = state0["tag"] + + # operate only on wall particles, i.e. remove fluid + r_walls = r[np.isin(tag, wall_tags)] - offset_vec + + # discretize wall with one layer of 5x smaller particles + dx_fac = 5 + offset = offset_vec / n_walls / dx_fac + layer = wall_part_fn(dx / dx_fac, 1) - offset + + # construct a neighbor list over both point clouds + r_full = jnp.concatenate([r_walls, layer], axis=0) + + neighbor_fn = partition.neighbor_list( + displacement_fn, + box_size, + r_cutoff=dx * n_walls * 2.0**0.5 * 1.01, + backend=cfg_nl.backend, + capacity_multiplier=1.25, + mask_self=False, + format=Dense, + num_particles_max=r_full.shape[0], + num_partitions=cfg_nl.num_partitions, + pbc=np.array(pbc), + ) + num_particles = len(r_full) + neighbors = neighbor_fn.allocate(r_full, num_particles=num_particles) + + # jit-able function + def body(r: jax.Array): + r_walls = r[np.isin(tag, wall_tags)] - offset_vec + r_full = jnp.concatenate([r_walls, layer], axis=0) + + nbrs = neighbors.update(r_full, num_particles=num_particles) + + # get the relevant entries from the dense neighbor list + idx = nbrs.idx # dense list: [[0, 1, 5], [0, 1, 3], [2, 3, 6], ...] + idx = idx[: len(r_walls)] # only the wall particle neighbors + mask_to_layer = idx > len(r_walls) # mask toward `layer` particles + idx = jnp.where(mask_to_layer, idx, len(r_full)) # get rid of unwanted edges + + # compute distances `r_wall` and `layer` particles and set others to infinity + r_i_s = r_full[idx] + dr_i_j = vmap(vmap(displacement_fn, in_axes=(0, None)))(r_i_s, r_walls) + dist = space.distance(dr_i_j) + mask_real = idx != len(r_full) # identify padding entries + dist = jnp.where(mask_real, dist, jnp.inf) + + # find closest `layer` particle for each `r_wall` particle and normalize + # displacement vector between the two to use it as the normal vector + idx_closest = jnp.argmin(dist, axis=1) + nw_walls = dr_i_j[jnp.arange(len(r_walls)), idx_closest] + nw_walls /= (dist[jnp.arange(len(r_walls)), idx_closest] + EPS)[:, None] + nw_walls = jnp.asarray(nw_walls, dtype=r.dtype) + + # update normals only of wall particles + nw = jnp.zeros_like(r) + nw = nw.at[np.isin(tag, wall_tags)].set(nw_walls) + + return nw + + return body + + class Logger: """Logger for printing stats to stdout.""" From 40ad7af7c0db3488e0181fcc9850802830512280 Mon Sep 17 00:00:00 2001 From: arturtoshev Date: Wed, 12 Jun 2024 03:21:35 +0200 Subject: [PATCH 21/21] update tutorial --- notebooks/tutorial.ipynb | 198 ++++++++++++++++++++++++--------------- 1 file changed, 122 insertions(+), 76 deletions(-) diff --git a/notebooks/tutorial.ipynb b/notebooks/tutorial.ipynb index 87153dd..ab0b8a7 100644 --- a/notebooks/tutorial.ipynb +++ b/notebooks/tutorial.ipynb @@ -33,15 +33,7 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" - ] - } - ], + "outputs": [], "source": [ "import os\n", "import time\n", @@ -60,7 +52,7 @@ "from jax_sph.io_state import io_setup, read_h5, write_state\n", "from jax_sph.jax_md.partition import Sparse\n", "from jax_sph.solver import WCSPH\n", - "from jax_sph.utils import Logger, Tag\n", + "from jax_sph.utils import Logger, Tag, pos_init_cartesian_2d\n", "from jax_sph.visualize import plt_ekin\n" ] }, @@ -107,6 +99,9 @@ " def __init__(self, cfg: DictConfig):\n", " super().__init__(cfg)\n", "\n", + " # define offset vector\n", + " self.offset_vec = self._offset_vec()\n", + "\n", " # relaxation configurations\n", " if self.case.mode == \"rlx\":\n", " self._set_default_rlx()\n", @@ -116,55 +111,98 @@ " self._init_pos2D = self._get_relaxed_r0\n", " self._init_pos3D = self._get_relaxed_r0\n", "\n", - " def _box_size2D(self):\n", - " dx = self.case.dx\n", - " return np.array([1, 0.2 + 6 * dx])\n", + " def _box_size2D(self, n_walls):\n", + " dx2n = self.case.dx * n_walls * 2\n", + " sp = self.special\n", + " return np.array([sp.L, sp.H + dx2n])\n", + "\n", + " def _box_size3D(self, n_walls):\n", + " dx2n = self.case.dx * n_walls * 2\n", + " sp = self.special\n", + " return np.array([sp.L, sp.H + dx2n, 0.5])\n", + "\n", + " def _init_walls_2d(self, dx, n_walls):\n", + " sp = self.special\n", "\n", - " def _box_size3D(self):\n", - " dx = self.case.dx\n", - " return np.array([1, 0.2 + 6 * dx, 0.5])\n", + " # thickness of wall particles\n", + " dxn = dx * n_walls\n", "\n", - " def _tag2D(self, r):\n", - " dx3 = 3 * self.case.dx\n", - " _box_size = self._box_size2D()\n", - " tag = jnp.full(len(r), Tag.FLUID, dtype=int)\n", + " # horizontal and vertical blocks\n", + " horiz = pos_init_cartesian_2d(np.array([sp.L, dxn]), dx)\n", "\n", - " mask_no_slip_wall = (r[:, 1] < dx3) + (\n", - " r[:, 1] > (_box_size[1] - 6 * self.case.dx) + dx3\n", + " # wall: bottom, top\n", + " wall_b = horiz.copy()\n", + " wall_t = horiz.copy() + np.array([0.0, sp.H + dxn])\n", + "\n", + " rw = np.concatenate([wall_b, wall_t])\n", + " return rw\n", + "\n", + " def _init_walls_3d(self, dx, n_walls):\n", + " pass\n", + "\n", + " def _init_pos2D(self, box_size, dx, n_walls):\n", + " sp = self.special\n", + "\n", + " # initialize fluid phase\n", + " r_f = np.array([0.0, 1.0]) * n_walls * dx + pos_init_cartesian_2d(\n", + " np.array([sp.L, sp.H]), dx\n", " )\n", + "\n", + " # initialize walls\n", + " r_w = self._init_walls_2d(dx, n_walls)\n", + "\n", + " # set tags\n", + " tag_f = jnp.full(len(r_f), Tag.FLUID, dtype=int)\n", + " tag_w = jnp.full(len(r_w), Tag.SOLID_WALL, dtype=int)\n", + "\n", + " r = np.concatenate([r_w, r_f])\n", + " tag = np.concatenate([tag_w, tag_f])\n", + "\n", + " # set thermal tags\n", + " _box_size = self._box_size2D(n_walls)\n", " mask_hot_wall = (\n", - " (r[:, 1] < dx3)\n", + " (r[:, 1] < dx * n_walls)\n", " * (r[:, 0] < (_box_size[0] / 2) + self.special.hot_wall_half_width)\n", " * (r[:, 0] > (_box_size[0] / 2) - self.special.hot_wall_half_width)\n", " )\n", - " tag = jnp.where(mask_no_slip_wall, Tag.SOLID_WALL, tag)\n", " tag = jnp.where(mask_hot_wall, Tag.DIRICHLET_WALL, tag)\n", - " return tag\n", "\n", - " def _tag3D(self, r):\n", - " return self._tag2D(r)\n", + " return r, tag\n", + "\n", + " def _init_pos3D(self, box_size, dx, n_walls):\n", + " pass\n", + "\n", + " def _offset_vec(self):\n", + " dim = self.cfg.case.dim\n", + " if dim == 2:\n", + " res = np.array([0.0, 1.0]) * self.cfg.solver.n_walls * self.cfg.case.dx\n", + " elif dim == 3:\n", + " res = np.array([0.0, 1.0, 0.0]) * self.cfg.solver.n_walls * self.cfg.case.dx\n", + " return res\n", "\n", " def _init_velocity2D(self, r):\n", " return jnp.zeros_like(r)\n", "\n", " def _init_velocity3D(self, r):\n", - " return jnp.zeros_like(r)\n", + " pass\n", "\n", " def _external_acceleration_fn(self, r):\n", - " dx3 = 3 * self.case.dx\n", + " n_walls = self.cfg.solver.n_walls\n", + " dxn = n_walls * self.case.dx\n", " res = jnp.zeros_like(r)\n", " x_force = jnp.ones((len(r)))\n", - " box_size = self._box_size2D()\n", - " fluid_mask = (r[:, 1] < box_size[1] - dx3) * (r[:, 1] > dx3)\n", + " box_size = self._box_size2D(n_walls)\n", + " fluid_mask = (r[:, 1] < box_size[1] - dxn) * (r[:, 1] > dxn)\n", " x_force = jnp.where(fluid_mask, x_force, 0)\n", " res = res.at[:, 0].set(x_force)\n", " return res * self.case.g_ext_magnitude\n", "\n", " def _boundary_conditions_fn(self, state):\n", + " n_walls = self.cfg.solver.n_walls\n", " mask_fluid = state[\"tag\"] == Tag.FLUID\n", "\n", " # set incoming fluid temperature to reference_temperature\n", - " mask_inflow = mask_fluid * (state[\"r\"][:, 0] < 3 * self.case.dx)\n", + " mask_inflow = mask_fluid * (state[\"r\"][:, 0] < n_walls * self.case.dx)\n", " state[\"T\"] = jnp.where(mask_inflow, self.case.T_ref, state[\"T\"])\n", " state[\"dTdt\"] = jnp.where(mask_inflow, 0.0, state[\"dTdt\"])\n", "\n", @@ -188,7 +226,7 @@ " # set outlet temperature gradients to zero to avoid interaction with inflow\n", " # bounds[0][1] is the x-coordinate of the outlet\n", " mask_outflow = mask_fluid * (\n", - " state[\"r\"][:, 0] > self.case.bounds[0][1] - 3 * self.case.dx\n", + " state[\"r\"][:, 0] > self.case.bounds[0][1] - n_walls * self.case.dx\n", " )\n", " state[\"dTdt\"] = jnp.where(mask_outflow, 0.0, state[\"dTdt\"])\n", "\n", @@ -215,7 +253,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "And now add the other needed entries." + "And now add the other needed entries. For a full list of all parameters, see [`defaults.py`](../jax_sph/defaults.py) or the [list of defaults](https://jax-sph.readthedocs.io/en/latest/pages/defaults.html) in our documentation." ] }, { @@ -232,10 +270,13 @@ "cfg.case.g_ext_magnitude = 2.3 # external force scale\n", "cfg.case.kappa_ref = 7.313 # thermal conductivity at 50°C\n", "cfg.case.Cp_ref = 305.27 # heat capacity at 50°C\n", + "### case specific arguments in `special`\n", "# nondimensionalized temperature corresponding to 100°C -> 1.23\n", "cfg.case.special.hot_wall_temperature = 1.23\n", "# define the location of the hot section\n", "cfg.case.special.hot_wall_half_width = 0.25\n", + "cfg.case.special.L = 1.0 # channel length\n", + "cfg.case.special.H = 0.2 # channel width\n", "\n", "### numerical setup arguments\n", "cfg.solver.t_end = 1.5 # simulation end time\n", @@ -270,7 +311,7 @@ "config: null\n", "seed: 123\n", "no_jit: false\n", - "gpu: -1\n", + "gpu: 0\n", "dtype: float64\n", "xla_mem_fraction: 0.75\n", "case:\n", @@ -294,6 +335,8 @@ " special:\n", " hot_wall_temperature: 1.23\n", " hot_wall_half_width: 0.25\n", + " L: 1.0\n", + " H: 0.2\n", "solver:\n", " name: SPH\n", " tvf: 0.0\n", @@ -306,6 +349,7 @@ " free_slip: false\n", " eta_limiter: 3\n", " kappa: 0\n", + " n_walls: 3\n", " heat_conduction: true\n", " is_bc_trick: true\n", "kernel:\n", @@ -363,11 +407,12 @@ "\n", "Here, we unpack `jax_sph.simulate.simulate` and give some more details.\n", "\n", - "The `case.initialize` method not only defines the initial simulation state with positions, velocities, etc., but also initializes the following:\n", - "* external force function `g_ext_fn` from Python case file\n", - "* boundary conditions `bc_fn` from Python case file\n", - "* equation of state `eos_fn` which defines pressure and density as functions of each other\n", - "* displacement and shift function respecting potential periodic box boundaries" + "The `case.initialize` method not only defines the initial simulation state with positions, velocities, etc., but also initializes the following functions:\n", + "* External force function `g_ext_fn` from Python case file.\n", + "* Boundary conditions `bc_fn` from Python case file.\n", + "* Wall normal vector computation `nw_fn` in case of moving walls.\n", + "* Equation of state `eos_fn` which defines pressure and density as functions of each other.\n", + "* Displacement and shift function respecting potential periodic box boundaries." ] }, { @@ -400,6 +445,7 @@ " state,\n", " g_ext_fn,\n", " bc_fn,\n", + " nw_fn,\n", " eos_fn,\n", " key,\n", " displacement_fn,\n", @@ -500,7 +546,7 @@ "outputs": [], "source": [ "# Instantiate advance function for our use case\n", - "advance = si_euler(cfg.solver.tvf, forward, shift_fn, bc_fn)\n", + "advance = si_euler(cfg.solver.tvf, forward, shift_fn, bc_fn, nw_fn)\n", "advance = jit(advance)" ] }, @@ -547,40 +593,40 @@ "output_type": "stream", "text": [ "0000/3300, t=0.0005, Ekin=0.00000, u_max=0.00000\n", - "0100/3300, t=0.0459, Ekin=0.00162, u_max=0.31360\n", - "0200/3300, t=0.0914, Ekin=0.00312, u_max=0.25906\n", - "0300/3300, t=0.1368, Ekin=0.00605, u_max=0.32460\n", - "0400/3300, t=0.1823, Ekin=0.00959, u_max=0.40505\n", - "0500/3300, t=0.2277, Ekin=0.01343, u_max=0.48048\n", - "0600/3300, t=0.2732, Ekin=0.01748, u_max=0.55084\n", - "0700/3300, t=0.3186, Ekin=0.02150, u_max=0.61357\n", - "0800/3300, t=0.3641, Ekin=0.02547, u_max=0.67135\n", - "0900/3300, t=0.4095, Ekin=0.02930, u_max=0.72199\n", - "1000/3300, t=0.4550, Ekin=0.03297, u_max=0.76502\n", - "1100/3300, t=0.5005, Ekin=0.03641, u_max=0.80508\n", - "1200/3300, t=0.5459, Ekin=0.03965, u_max=0.83981\n", - "1300/3300, t=0.5914, Ekin=0.04263, u_max=0.87329\n", - "1400/3300, t=0.6368, Ekin=0.04546, u_max=0.90626\n", - "1500/3300, t=0.6823, Ekin=0.04807, u_max=0.93373\n", - "1600/3300, t=0.7277, Ekin=0.05041, u_max=0.95674\n", - "1700/3300, t=0.7732, Ekin=0.05262, u_max=0.97548\n", - "1800/3300, t=0.8186, Ekin=0.05458, u_max=0.99214\n", - "1900/3300, t=0.8641, Ekin=0.05639, u_max=1.00931\n", - "2000/3300, t=0.9095, Ekin=0.05800, u_max=1.02836\n", - "2100/3300, t=0.9550, Ekin=0.05955, u_max=1.04335\n", - "2200/3300, t=1.0005, Ekin=0.06088, u_max=1.05429\n", - "2300/3300, t=1.0459, Ekin=0.06214, u_max=1.06249\n", - "2400/3300, t=1.0914, Ekin=0.06317, u_max=1.06985\n", - "2500/3300, t=1.1368, Ekin=0.06418, u_max=1.08156\n", - "2600/3300, t=1.1823, Ekin=0.06511, u_max=1.09160\n", - "2700/3300, t=1.2277, Ekin=0.06593, u_max=1.09884\n", - "2800/3300, t=1.2732, Ekin=0.06667, u_max=1.10251\n", - "2900/3300, t=1.3186, Ekin=0.06727, u_max=1.10529\n", - "3000/3300, t=1.3641, Ekin=0.06786, u_max=1.11242\n", - "3100/3300, t=1.4095, Ekin=0.06844, u_max=1.11944\n", - "3200/3300, t=1.4550, Ekin=0.06891, u_max=1.12419\n", - "3300/3300, t=1.5005, Ekin=0.06936, u_max=1.12494\n", - "time: 43.21 s\n" + "0100/3300, t=0.0459, Ekin=0.00155, u_max=0.28668\n", + "0200/3300, t=0.0914, Ekin=0.00312, u_max=0.27229\n", + "0300/3300, t=0.1368, Ekin=0.00604, u_max=0.32408\n", + "0400/3300, t=0.1823, Ekin=0.00958, u_max=0.40263\n", + "0500/3300, t=0.2277, Ekin=0.01342, u_max=0.48013\n", + "0600/3300, t=0.2732, Ekin=0.01747, u_max=0.55052\n", + "0700/3300, t=0.3186, Ekin=0.02150, u_max=0.61441\n", + "0800/3300, t=0.3641, Ekin=0.02546, u_max=0.67120\n", + "0900/3300, t=0.4095, Ekin=0.02929, u_max=0.72111\n", + "1000/3300, t=0.4550, Ekin=0.03296, u_max=0.76472\n", + "1100/3300, t=0.5005, Ekin=0.03640, u_max=0.80485\n", + "1200/3300, t=0.5459, Ekin=0.03964, u_max=0.83979\n", + "1300/3300, t=0.5914, Ekin=0.04262, u_max=0.87452\n", + "1400/3300, t=0.6368, Ekin=0.04545, u_max=0.90765\n", + "1500/3300, t=0.6823, Ekin=0.04806, u_max=0.93497\n", + "1600/3300, t=0.7277, Ekin=0.05040, u_max=0.95750\n", + "1700/3300, t=0.7732, Ekin=0.05261, u_max=0.97556\n", + "1800/3300, t=0.8186, Ekin=0.05457, u_max=0.99333\n", + "1900/3300, t=0.8641, Ekin=0.05637, u_max=1.00955\n", + "2000/3300, t=0.9095, Ekin=0.05799, u_max=1.02807\n", + "2100/3300, t=0.9550, Ekin=0.05954, u_max=1.04281\n", + "2200/3300, t=1.0005, Ekin=0.06088, u_max=1.05362\n", + "2300/3300, t=1.0459, Ekin=0.06213, u_max=1.06218\n", + "2400/3300, t=1.0914, Ekin=0.06316, u_max=1.06955\n", + "2500/3300, t=1.1368, Ekin=0.06417, u_max=1.08134\n", + "2600/3300, t=1.1823, Ekin=0.06511, u_max=1.09179\n", + "2700/3300, t=1.2277, Ekin=0.06592, u_max=1.09803\n", + "2800/3300, t=1.2732, Ekin=0.06667, u_max=1.10173\n", + "2900/3300, t=1.3186, Ekin=0.06727, u_max=1.10500\n", + "3000/3300, t=1.3641, Ekin=0.06785, u_max=1.11255\n", + "3100/3300, t=1.4095, Ekin=0.06844, u_max=1.11976\n", + "3200/3300, t=1.4550, Ekin=0.06891, u_max=1.12356\n", + "3300/3300, t=1.5005, Ekin=0.06935, u_max=1.12462\n", + "time: 15.92 s\n" ] } ], @@ -739,7 +785,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [