evoxels 0.1.2__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {evoxels-0.1.2 → evoxels-1.0.0}/LICENSE +0 -0
  2. {evoxels-0.1.2 → evoxels-1.0.0}/PKG-INFO +5 -4
  3. {evoxels-0.1.2 → evoxels-1.0.0}/README.md +3 -3
  4. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/__init__.py +0 -0
  5. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/boundary_conditions.py +0 -0
  6. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/fd_stencils.py +0 -0
  7. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/function_approximators.py +0 -0
  8. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/inversion.py +0 -0
  9. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/precompiled_solvers/__init__.py +0 -0
  10. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/precompiled_solvers/allen_cahn.py +0 -0
  11. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/precompiled_solvers/cahn_hilliard.py +0 -0
  12. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/problem_definition.py +2 -0
  13. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/profiler.py +0 -0
  14. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/solvers.py +112 -46
  15. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/timesteppers.py +0 -0
  16. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/utils.py +8 -2
  17. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/voxelfields.py +23 -13
  18. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels/voxelgrid.py +0 -0
  19. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels.egg-info/PKG-INFO +5 -4
  20. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels.egg-info/SOURCES.txt +0 -0
  21. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels.egg-info/dependency_links.txt +0 -0
  22. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels.egg-info/requires.txt +1 -0
  23. {evoxels-0.1.2 → evoxels-1.0.0}/evoxels.egg-info/top_level.txt +0 -0
  24. {evoxels-0.1.2 → evoxels-1.0.0}/pyproject.toml +3 -2
  25. {evoxels-0.1.2 → evoxels-1.0.0}/setup.cfg +0 -0
  26. {evoxels-0.1.2 → evoxels-1.0.0}/tests/test_fields.py +0 -0
  27. {evoxels-0.1.2 → evoxels-1.0.0}/tests/test_inversion.py +0 -0
  28. {evoxels-0.1.2 → evoxels-1.0.0}/tests/test_laplace.py +0 -0
  29. {evoxels-0.1.2 → evoxels-1.0.0}/tests/test_rhs.py +0 -0
  30. {evoxels-0.1.2 → evoxels-1.0.0}/tests/test_solvers.py +0 -0
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evoxels
3
- Version: 0.1.2
3
+ Version: 1.0.0
4
4
  Summary: Differentiable physics framework for voxel-based microstructure simulations
5
5
  Author-email: Simon Daubner <s.daubner@imperial.ac.uk>
6
6
  License: MIT
@@ -40,6 +40,7 @@ Provides-Extra: notebooks
40
40
  Requires-Dist: ipywidgets; extra == "notebooks"
41
41
  Requires-Dist: ipympl; extra == "notebooks"
42
42
  Requires-Dist: notebook; extra == "notebooks"
43
+ Requires-Dist: taufactor; extra == "notebooks"
43
44
  Dynamic: license-file
44
45
 
45
46
  [![Python package](https://github.com/daubners/evoxels/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/daubners/evoxels/actions/workflows/python-package.yml)
@@ -81,7 +82,7 @@ TL;DR
81
82
  ```bash
82
83
  conda create --name voxenv python=3.12
83
84
  conda activate voxenv
84
- pip install evoxels[torch,jax,dev,notebooks]
85
+ pip install "evoxels[torch,jax,dev,notebooks]"
85
86
  pip install --upgrade "jax[cuda12]"
86
87
  ```
87
88
 
@@ -103,7 +104,7 @@ Navigate to the evoxels folder, then
103
104
  ```
104
105
  pip install -e .[torch] # install with torch backend
105
106
  pip install -e .[jax] # install with jax backend
106
- pip install -e .[dev, notebooks] # install testing and notebooks
107
+ pip install -e .[dev,notebooks] # install testing and notebooks
107
108
  ```
108
109
  Note that the default `[jax]` installation is only CPU compatible. To install the corresponding CUDA libraries check your CUDA version with
109
110
  ```bash
@@ -115,7 +116,7 @@ pip install -U "jax[cuda12]"
115
116
  ```
116
117
  To install both backends within one environment it is important to install torch first and then upgrade the `jax` installation e.g.
117
118
  ```bash
118
- pip install evoxels[torch, jax, dev, notebooks]
119
+ pip install "evoxels[torch,jax,dev,notebooks]"
119
120
  pip install --upgrade "jax[cuda12]"
120
121
  ```
121
122
  To work with the example notebooks install Jupyter and all notebook related dependencies via
@@ -37,7 +37,7 @@ TL;DR
37
37
  ```bash
38
38
  conda create --name voxenv python=3.12
39
39
  conda activate voxenv
40
- pip install evoxels[torch,jax,dev,notebooks]
40
+ pip install "evoxels[torch,jax,dev,notebooks]"
41
41
  pip install --upgrade "jax[cuda12]"
42
42
  ```
43
43
 
@@ -59,7 +59,7 @@ Navigate to the evoxels folder, then
59
59
  ```
60
60
  pip install -e .[torch] # install with torch backend
61
61
  pip install -e .[jax] # install with jax backend
62
- pip install -e .[dev, notebooks] # install testing and notebooks
62
+ pip install -e .[dev,notebooks] # install testing and notebooks
63
63
  ```
64
64
  Note that the default `[jax]` installation is only CPU compatible. To install the corresponding CUDA libraries check your CUDA version with
65
65
  ```bash
@@ -71,7 +71,7 @@ pip install -U "jax[cuda12]"
71
71
  ```
72
72
  To install both backends within one environment it is important to install torch first and then upgrade the `jax` installation e.g.
73
73
  ```bash
74
- pip install evoxels[torch, jax, dev, notebooks]
74
+ pip install "evoxels[torch,jax,dev,notebooks]"
75
75
  pip install --upgrade "jax[cuda12]"
76
76
  ```
77
77
  To work with the example notebooks install Jupyter and all notebook related dependencies via
File without changes
File without changes
File without changes
@@ -120,6 +120,8 @@ class ReactionDiffusion(SemiLinearODE):
120
120
  bc_fun = self.vg.bc.pad_zero_flux_periodic
121
121
  self.pad_boundary = lambda field, bc0, bc1: bc_fun(field)
122
122
  k_squared = self.vg.fft_k_squared_nonperiodic()
123
+ else:
124
+ raise ValueError(f"Unsupported BC type: {self.BC_type}")
123
125
 
124
126
  self._fourier_symbol = -self.D * self.A * k_squared
125
127
 
File without changes
@@ -1,13 +1,14 @@
1
1
  from IPython.display import clear_output
2
2
  from dataclasses import dataclass
3
3
  from typing import Callable, Any, Type
4
+ from abc import ABC, abstractmethod
4
5
  from timeit import default_timer as timer
5
6
  import sys
6
7
  from .problem_definition import ODE
7
8
  from .timesteppers import TimeStepper
8
9
 
9
10
  @dataclass
10
- class TimeDependentSolver:
11
+ class BaseSolver(ABC):
11
12
  """Generic wrapper for solving one or more fields with a time stepper."""
12
13
  vf: Any # VoxelFields object
13
14
  fieldnames: str | list[str]
@@ -33,46 +34,24 @@ class TimeDependentSolver:
33
34
  self.profiler = JAXMemoryProfiler()
34
35
  else:
35
36
  raise ValueError(f"Unsupported backend: {self.backend}")
36
-
37
- def solve(
38
- self,
39
- time_increment=0.1,
40
- frames=10,
41
- max_iters=100,
42
- problem_kwargs=None,
43
- jit=True,
44
- verbose=True,
45
- vtk_out=False,
46
- plot_bounds=None,
47
- colormap='viridis'
48
- ):
49
- """Run the time integration loop.
50
-
51
- Args:
52
- time_increment (float): Size of a single time step.
53
- frames (int): Number of output frames (for plotting, vtk, checks).
54
- max_iters (int): Number of time steps to compute.
55
- problem_kwargs (dict | None): Problem-specific input arguments.
56
- jit (bool): Create just-in-time compiled kernel if ``True``
57
- verbose (bool | str): If ``True`` prints memory stats, ``'plot'``
58
- updates an interactive plot.
59
- vtk_out (bool): Write VTK files for each frame if ``True``.
60
- plot_bounds (tuple | None): Optional value range for plots.
61
- """
62
-
63
- problem_kwargs = problem_kwargs or {}
37
+
64
38
  if isinstance(self.fieldnames, str):
65
39
  self.fieldnames = [self.fieldnames]
66
40
  else:
67
41
  self.fieldnames = list(self.fieldnames)
68
42
 
43
+ def _init_fields(self):
44
+ """Initialize fields in the voxel grid."""
69
45
  u_list = [self.vg.init_scalar_field(self.vf.fields[name]) for name in self.fieldnames]
70
46
  u = self.vg.concatenate(u_list, 0)
71
47
  u = self.vg.bc.trim_boundary_nodes(u)
72
-
48
+ return u
49
+
50
+ def _init_stepper(self, time_increment, problem_kwargs, jit):
51
+ problem_kwargs = problem_kwargs or {}
73
52
  if self.step_fn is not None:
74
- step = self.step_fn
75
53
  self.problem = None
54
+ step = self.step_fn
76
55
  else:
77
56
  if self.problem_cls is None or self.timestepper_cls is None:
78
57
  raise ValueError("Either provide step_fn or both problem_cls and timestepper_cls")
@@ -88,23 +67,47 @@ class TimeDependentSolver:
88
67
  import torch
89
68
  step = torch.compile(step)
90
69
 
91
- n_out = max_iters // frames
92
- frame = 0
93
- slice_idx = self.vf.Nz // 2
70
+ return step
71
+
72
+ @abstractmethod
73
+ def _run_loop(self, u, step, time_increment, frames, max_iters,
74
+ vtk_out, verbose, plot_bounds, colormap):
75
+ """Abstract method for running the time integration loop."""
76
+ raise NotImplementedError("Subclasses must implement _run_loop method.")
94
77
 
95
- start = timer()
96
- for i in range(max_iters):
97
- time = i * time_increment
98
- if i % n_out == 0:
99
- self._handle_outputs(u, frame, time, slice_idx, vtk_out, verbose, plot_bounds, colormap)
100
- frame += 1
78
+ def solve(
79
+ self,
80
+ time_increment=0.1,
81
+ frames=10,
82
+ max_iters=100,
83
+ problem_kwargs=None,
84
+ jit=True,
85
+ verbose=True,
86
+ vtk_out=False,
87
+ plot_bounds=None,
88
+ colormap='viridis'
89
+ ):
90
+ """Run the time integration loop.
101
91
 
102
- u = step(time, u)
92
+ Args:
93
+ time_increment (float): Size of a single time step.
94
+ frames (int): Number of output frames (for plotting, vtk, checks).
95
+ max_iters (int): Number of time steps to compute.
96
+ problem_kwargs (dict | None): Problem-specific input arguments.
97
+ jit (bool): Create just-in-time compiled kernel if ``True``
98
+ verbose (bool | str): If ``True`` prints memory stats, ``'plot'``
99
+ updates an interactive plot.
100
+ vtk_out (bool): Write VTK files for each frame if ``True``.
101
+ plot_bounds (tuple | None): Optional value range for plots.
102
+ """
103
+ u = self._init_fields()
104
+ step = self._init_stepper(time_increment, problem_kwargs, jit)
103
105
 
106
+ start = timer()
107
+ u = self._run_loop(u, step, time_increment, frames, max_iters,
108
+ vtk_out, verbose, plot_bounds, colormap)
104
109
  end = timer()
105
- time = max_iters * time_increment
106
- self._handle_outputs(u, frame, time, slice_idx, vtk_out, verbose, plot_bounds, colormap)
107
-
110
+ self.computation_time = end - start
108
111
  if verbose:
109
112
  self.profiler.print_memory_stats(start, end, max_iters)
110
113
 
@@ -113,10 +116,10 @@ class TimeDependentSolver:
113
116
  if getattr(self, 'problem', None) is not None:
114
117
  u_out = self.vg.bc.trim_ghost_nodes(self.problem.pad_bc(u))
115
118
  else:
116
- u_out = u
119
+ u_out = self.vg.bc.trim_ghost_nodes(self.vg.pad_zeros(u))
117
120
 
118
121
  for i, name in enumerate(self.fieldnames):
119
- self.vf.fields[name] = self.vg.export_scalar_field_to_numpy(u_out[i:i+1])
122
+ self.vf.set_field(name, self.vg.export_scalar_field_to_numpy(u_out[i:i+1]))
120
123
 
121
124
  if verbose:
122
125
  self.profiler.update_memory_stats()
@@ -133,3 +136,66 @@ class TimeDependentSolver:
133
136
  if verbose == 'plot':
134
137
  clear_output(wait=True)
135
138
  self.vf.plot_slice(self.fieldnames[0], slice_idx, time=time, colormap=colormap, value_bounds=plot_bounds)
139
+
140
+ @dataclass
141
+ class TimeDependentSolver(BaseSolver):
142
+ """Solver for time-dependent problems."""
143
+ def _run_loop(self, u, step, time_increment, frames, max_iters,
144
+ vtk_out, verbose, plot_bounds, colormap):
145
+ n_out = max_iters // frames
146
+ frame = 0
147
+ slice_idx = self.vf.Nz // 2
148
+
149
+ for i in range(max_iters):
150
+ time = i * time_increment
151
+ if i % n_out == 0:
152
+ self._handle_outputs(u, frame, time, slice_idx, vtk_out,
153
+ verbose, plot_bounds, colormap)
154
+ frame += 1
155
+
156
+ u = step(time, u)
157
+ time = max_iters * time_increment
158
+ self._handle_outputs(u, frame, time, slice_idx, vtk_out,
159
+ verbose, plot_bounds, colormap)
160
+ return u
161
+
162
+ @dataclass
163
+ class SteadyStatePseudoTimeSolver(BaseSolver):
164
+ """Solver for steady-state problems."""
165
+ conv_crit: float = 1e-6
166
+ check_freq: int = 10
167
+
168
+ def _run_loop(self, u, step, time_increment, frames, max_iters,
169
+ vtk_out, verbose, plot_bounds, colormap):
170
+ slice_idx = self.vf.Nz // 2
171
+ self.converged = False
172
+ self.iter = 0
173
+
174
+ while not self.converged and self.iter < max_iters:
175
+ time = self.iter * time_increment
176
+ diff = u - step(time, u)
177
+ u = step(time, u)
178
+
179
+ if self.iter % self.check_freq == 0:
180
+ self.converged = self.check_convergence(diff, verbose)
181
+
182
+ self._handle_outputs(u, 0, time, slice_idx, vtk_out,
183
+ verbose, plot_bounds, colormap)
184
+ return u
185
+
186
+ def check_convergence(self, diff, verbose):
187
+ """Check for convergence based on relative change in fields."""
188
+ converged = True
189
+ for i, name in enumerate(self.fieldnames):
190
+ # Check if Frobenius norm of change is below threshold
191
+ rel_change = self.vg.lib.linalg.norm(diff[i]) / \
192
+ self.vg.lib.sqrt(self.vf.Nx * self.vf.Ny * self.vf.Nz)
193
+ if rel_change > self.conv_crit:
194
+ converged = False
195
+ if verbose:
196
+ print(f"Iter {self.iter}: Field '{name}' relative change: {rel_change:.2e}")
197
+
198
+ if converged and verbose:
199
+ print(f"Converged after {self.iter} iterations.")
200
+
201
+ return converged
File without changes
@@ -215,6 +215,8 @@ def mms_convergence_test(
215
215
  vf = evo.VoxelFields((2**p, 2**p, 2**p), (1, 1, 1), convention=convention)
216
216
  elif convention == 'staggered_x':
217
217
  vf = evo.VoxelFields((2**p + 1, 2**p, 2**p), (1, 1, 1), convention=convention)
218
+ else:
219
+ raise ValueError("Chosen convention must be cell_center or staggered_x.")
218
220
  vf.precision = dtype
219
221
  dx[i] = vf.spacing[0]
220
222
 
@@ -231,6 +233,8 @@ def mms_convergence_test(
231
233
  ODE = ODE_class(vg, **problem_kwargs)
232
234
  rhs_orig = ODE.rhs
233
235
  grid = vg.meshgrid()
236
+ if convention == 'staggered_x':
237
+ grid = (grid[0][1:-1,:,:], grid[1][1:-1,:,:], grid[2][1:-1,:,:])
234
238
 
235
239
  # Construct new rhs including forcing term from MMS
236
240
  if mode == 'temporal':
@@ -246,7 +250,6 @@ def mms_convergence_test(
246
250
  u_ex_list.append(vg.expand_dim(u_list[j](t_, *grid), 0))
247
251
  rhs = vg.set(rhs, j, rhs[j] + u_t_list[j](t_, *grid))
248
252
  u_ex = vg.concatenate(u_ex_list, 0)
249
- u_ex = vg.bc.trim_boundary_nodes(u_ex)
250
253
  rhs -= rhs_orig(t, u_ex)
251
254
  return rhs
252
255
 
@@ -322,7 +325,10 @@ def mms_convergence_test(
322
325
  for j, func in enumerate(test_functions):
323
326
  exact = vf.fields[f'u{j}_final']
324
327
  diff = vf.fields[f'u{j}'] - exact
325
- errors[j, k, i] = np.linalg.norm(diff) / np.linalg.norm(exact)
328
+ if convention == 'staggered_x':
329
+ errors[j, k, i] = np.linalg.norm(diff[1:-1,:,:]) / np.linalg.norm(exact[1:-1,:,:])
330
+ else:
331
+ errors[j, k, i] = np.linalg.norm(diff) / np.linalg.norm(exact)
326
332
 
327
333
  # Fit slope after loop
328
334
  def calc_slope(x, y):
@@ -120,30 +120,40 @@ class VoxelFields:
120
120
  grid = Grid(self.shape, self.origin, self.spacing, self.convention)
121
121
  return grid
122
122
 
123
- def add_field(self, name: str, array=None):
123
+ def set_field(self, name: str, array: np.ndarray):
124
124
  """
125
- Adds a field to the voxel grid.
125
+ Set field values for an existing field in the voxel grid.
126
126
 
127
127
  Args:
128
128
  name (str): Name of the field.
129
- array (numpy.ndarray, optional): 3D array to initialize the field. If None, initializes with zeros.
129
+ array (numpy.ndarray, optional): 3D array.
130
130
 
131
131
  Raises:
132
132
  ValueError: If the provided array does not match the voxel grid dimensions.
133
133
  TypeError: If the provided array is not a numpy array.
134
134
  """
135
- if array is not None:
136
- if isinstance(array, np.ndarray):
137
- if array.shape == self.shape:
138
- self.fields[name] = array
139
- else:
140
- raise ValueError(
141
- f"The provided array must have the shape {self.shape}."
142
- )
135
+ if isinstance(array, np.ndarray):
136
+ if array.shape == self.shape:
137
+ self.fields[name] = array
143
138
  else:
144
- raise TypeError("The provided array must be a numpy array.")
139
+ raise ValueError(
140
+ f"The provided array must have the shape {self.shape}."
141
+ )
142
+ else:
143
+ raise TypeError("The provided array must be a numpy array.")
144
+
145
+ def add_field(self, name: str, array=None):
146
+ """
147
+ Adds a field to the voxel grid.
148
+
149
+ Args:
150
+ name (str): Name of the field.
151
+ array (numpy.ndarray, optional): 3D array to initialize the field. If None, initializes with zeros.
152
+ """
153
+ if array is not None:
154
+ self.set_field(name, array)
145
155
  else:
146
- self.fields[name] = np.zeros(self.shape)
156
+ self.set_field(name, np.zeros(self.shape))
147
157
 
148
158
  def set_voxel_sphere(self, name: str, center, radius, label: int | float = 1):
149
159
  """Create a voxelized representation of a sphere in 3D
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evoxels
3
- Version: 0.1.2
3
+ Version: 1.0.0
4
4
  Summary: Differentiable physics framework for voxel-based microstructure simulations
5
5
  Author-email: Simon Daubner <s.daubner@imperial.ac.uk>
6
6
  License: MIT
@@ -40,6 +40,7 @@ Provides-Extra: notebooks
40
40
  Requires-Dist: ipywidgets; extra == "notebooks"
41
41
  Requires-Dist: ipympl; extra == "notebooks"
42
42
  Requires-Dist: notebook; extra == "notebooks"
43
+ Requires-Dist: taufactor; extra == "notebooks"
43
44
  Dynamic: license-file
44
45
 
45
46
  [![Python package](https://github.com/daubners/evoxels/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/daubners/evoxels/actions/workflows/python-package.yml)
@@ -81,7 +82,7 @@ TL;DR
81
82
  ```bash
82
83
  conda create --name voxenv python=3.12
83
84
  conda activate voxenv
84
- pip install evoxels[torch,jax,dev,notebooks]
85
+ pip install "evoxels[torch,jax,dev,notebooks]"
85
86
  pip install --upgrade "jax[cuda12]"
86
87
  ```
87
88
 
@@ -103,7 +104,7 @@ Navigate to the evoxels folder, then
103
104
  ```
104
105
  pip install -e .[torch] # install with torch backend
105
106
  pip install -e .[jax] # install with jax backend
106
- pip install -e .[dev, notebooks] # install testing and notebooks
107
+ pip install -e .[dev,notebooks] # install testing and notebooks
107
108
  ```
108
109
  Note that the default `[jax]` installation is only CPU compatible. To install the corresponding CUDA libraries check your CUDA version with
109
110
  ```bash
@@ -115,7 +116,7 @@ pip install -U "jax[cuda12]"
115
116
  ```
116
117
  To install both backends within one environment it is important to install torch first and then upgrade the `jax` installation e.g.
117
118
  ```bash
118
- pip install evoxels[torch, jax, dev, notebooks]
119
+ pip install "evoxels[torch,jax,dev,notebooks]"
119
120
  pip install --upgrade "jax[cuda12]"
120
121
  ```
121
122
  To work with the example notebooks install Jupyter and all notebook related dependencies via
@@ -18,6 +18,7 @@ diffrax>=0.6.2
18
18
  ipywidgets
19
19
  ipympl
20
20
  notebook
21
+ taufactor
21
22
 
22
23
  [torch]
23
24
  torch>=2.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evoxels"
7
- version = "0.1.2"
7
+ version = "1.0.0"
8
8
  description = "Differentiable physics framework for voxel-based microstructure simulations"
9
9
  authors = [
10
10
  { name = "Simon Daubner", email = "s.daubner@imperial.ac.uk" }
@@ -53,7 +53,8 @@ dev = [
53
53
  notebooks = [
54
54
  "ipywidgets",
55
55
  "ipympl",
56
- "notebook"
56
+ "notebook",
57
+ "taufactor"
57
58
  ]
58
59
 
59
60
  [tool.setuptools.packages.find]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes