flapjax-full 1.2.0.dev0__py3-none-any.whl

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 (129) hide show
  1. flapjax/__init__.py +0 -0
  2. flapjax/aero/__init__.py +19 -0
  3. flapjax/aero/aic.py +310 -0
  4. flapjax/aero/data_structures.py +689 -0
  5. flapjax/aero/flowfields.py +200 -0
  6. flapjax/aero/gradients/__init__.py +0 -0
  7. flapjax/aero/gradients/data_structures.py +397 -0
  8. flapjax/aero/linear/__init__.py +2 -0
  9. flapjax/aero/linear/data_structures.py +166 -0
  10. flapjax/aero/linear/linear_uvlm.py +597 -0
  11. flapjax/aero/utils.py +722 -0
  12. flapjax/aero/uvlm.py +2462 -0
  13. flapjax/algebra/__init__.py +0 -0
  14. flapjax/algebra/array_utils.py +445 -0
  15. flapjax/algebra/base.py +745 -0
  16. flapjax/algebra/integration.py +81 -0
  17. flapjax/algebra/se3.py +395 -0
  18. flapjax/algebra/so3.py +212 -0
  19. flapjax/algebra/test_routines.py +280 -0
  20. flapjax/coupled/__init__.py +17 -0
  21. flapjax/coupled/coupled.py +484 -0
  22. flapjax/coupled/data_structures.py +441 -0
  23. flapjax/coupled/gradients/__init__.py +0 -0
  24. flapjax/coupled/gradients/coupled.py +2027 -0
  25. flapjax/coupled/gradients/data_structures.py +122 -0
  26. flapjax/coupled/linear/__init__.py +0 -0
  27. flapjax/coupled/linear/data_structures.py +36 -0
  28. flapjax/coupled/linear/linear_coupled.py +1051 -0
  29. flapjax/models/__init__.py +0 -0
  30. flapjax/models/cantilever_wing.py +82 -0
  31. flapjax/models/flying_spaghetti/__init__.py +0 -0
  32. flapjax/models/flying_spaghetti/flying_spaghetti.py +103 -0
  33. flapjax/models/flying_spaghetti/free_dynamic.ipynb +1248 -0
  34. flapjax/models/flying_spaghetti/mass_optimisation.ipynb +437 -0
  35. flapjax/models/geradin_beam/__init__.py +0 -0
  36. flapjax/models/geradin_beam/adjoint_gradients.ipynb +296 -0
  37. flapjax/models/geradin_beam/geradin_beam.py +41 -0
  38. flapjax/models/geradin_beam/static_deformation.ipynb +361 -0
  39. flapjax/models/ifasd_aviation_2026/__init__.py +0 -0
  40. flapjax/models/ifasd_aviation_2026/cantilever_wing_adjoint.py +158 -0
  41. flapjax/models/ifasd_aviation_2026/optimal_roll.py +360 -0
  42. flapjax/models/panel_scaling_profile.py +398 -0
  43. flapjax/models/patil_wing/__init__.py +0 -0
  44. flapjax/models/patil_wing/open_loop_control.ipynb +722 -0
  45. flapjax/models/patil_wing/patil_wing.py +127 -0
  46. flapjax/models/pazy/__init__.py +0 -0
  47. flapjax/models/pazy/base.py +280 -0
  48. flapjax/models/pazy/straight/__init__.py +0 -0
  49. flapjax/models/pazy/straight/data/__init__.py +0 -0
  50. flapjax/models/pazy/straight/data/prepazy_properties.py +819 -0
  51. flapjax/models/pazy/straight/data/technion_pazy_properties.py +796 -0
  52. flapjax/models/pazy/straight/deformed_modes.ipynb +259 -0
  53. flapjax/models/pazy/straight/flutter.ipynb +431 -0
  54. flapjax/models/pazy/straight/lco.ipynb +18097 -0
  55. flapjax/models/pazy/straight/pazy_wing.py +108 -0
  56. flapjax/models/pazy/straight/static_deflection.ipynb +335 -0
  57. flapjax/models/pazy/swept/__init__.py +0 -0
  58. flapjax/models/pazy/swept/data/__init__.py +0 -0
  59. flapjax/models/pazy/swept/data/properties.py +408 -0
  60. flapjax/models/pazy/swept/deformed_modes.ipynb +299 -0
  61. flapjax/models/pazy/swept/flutter.ipynb +474 -0
  62. flapjax/models/pazy/swept/lco.ipynb +9880 -0
  63. flapjax/models/pazy/swept/static_deflection.ipynb +477 -0
  64. flapjax/models/pazy/swept/swept_pazy_wing.py +61 -0
  65. flapjax/models/scitech_abstract_2027/__init__.py +0 -0
  66. flapjax/models/scitech_abstract_2027/linear_uvlm_case.py +186 -0
  67. flapjax/models/simple_hale/__init__.py +0 -0
  68. flapjax/models/simple_hale/parallel_gust.ipynb +26503 -0
  69. flapjax/models/simple_hale/simple_hale.py +397 -0
  70. flapjax/plotting/__init__.py +1 -0
  71. flapjax/plotting/aerogrid.py +134 -0
  72. flapjax/plotting/beam.py +271 -0
  73. flapjax/plotting/modal.py +241 -0
  74. flapjax/plotting/pvd.py +64 -0
  75. flapjax/structure/__init__.py +18 -0
  76. flapjax/structure/beam.py +3049 -0
  77. flapjax/structure/data_structures.py +658 -0
  78. flapjax/structure/gradients/__init__.py +0 -0
  79. flapjax/structure/gradients/beam.py +1363 -0
  80. flapjax/structure/gradients/data_structures.py +294 -0
  81. flapjax/structure/linear/__init__.py +0 -0
  82. flapjax/structure/linear/data_structures.py +59 -0
  83. flapjax/structure/linear/linear_beam.py +455 -0
  84. flapjax/structure/time_integration.py +174 -0
  85. flapjax/structure/utils.py +279 -0
  86. flapjax/tests/aero/test_infinite_wing_polar.py +130 -0
  87. flapjax/tests/aero/test_linear_planar_wing.py +380 -0
  88. flapjax/tests/aero/test_mirrored_wing.py +78 -0
  89. flapjax/tests/aero/test_static_no_wake.py +65 -0
  90. flapjax/tests/aero/test_variable_wake_disc.py +89 -0
  91. flapjax/tests/algebra/test_beam_algebra.py +592 -0
  92. flapjax/tests/algebra/test_kernels.py +98 -0
  93. flapjax/tests/conftest.py +16 -0
  94. flapjax/tests/coupled/dynamic/test_dynamic_equilibrium.py +69 -0
  95. flapjax/tests/coupled/dynamic/test_dynamic_equilibrium_adjoint.py +132 -0
  96. flapjax/tests/coupled/dynamic/test_dynamic_gust_adjoint.py +160 -0
  97. flapjax/tests/coupled/dynamic/test_parallel_cantilever.py +39 -0
  98. flapjax/tests/coupled/static/test_pazy.py +206 -0
  99. flapjax/tests/coupled/static/test_static_adjoint.py +127 -0
  100. flapjax/tests/plotting/test_beam_plot.py +35 -0
  101. flapjax/tests/structure/dynamic_linear/test_oscillating_cantilever.py +170 -0
  102. flapjax/tests/structure/dynamic_primal/test_applied_force_point.py +131 -0
  103. flapjax/tests/structure/dynamic_primal/test_const_initial_linear_velocity.py +91 -0
  104. flapjax/tests/structure/dynamic_primal/test_const_initial_rotational_velocity.py +186 -0
  105. flapjax/tests/structure/dynamic_primal/test_const_initial_velocity_lumped_mass.py +94 -0
  106. flapjax/tests/structure/dynamic_primal/test_gravity_beam_drop.py +113 -0
  107. flapjax/tests/structure/dynamic_primal/test_gravity_point_drop.py +82 -0
  108. flapjax/tests/structure/dynamic_primal/test_parallel_spaghetti.py +42 -0
  109. flapjax/tests/structure/dynamic_primal/test_two_lumped_mass_const_rot_velocity.py +195 -0
  110. flapjax/tests/structure/dynamic_tangent/test_beam_dynamic_adjoint.py +131 -0
  111. flapjax/tests/structure/dynamic_tangent/test_lumped_mass_dynamic_adjoint.py +119 -0
  112. flapjax/tests/structure/static_primal/test_geradin_beam.py +50 -0
  113. flapjax/tests/structure/static_primal/test_mass_gravity.py +153 -0
  114. flapjax/tests/structure/static_primal/test_matrices.py +60 -0
  115. flapjax/tests/structure/static_primal/test_modal.py +91 -0
  116. flapjax/tests/structure/static_primal/test_multi_element_beam.py +642 -0
  117. flapjax/tests/structure/static_primal/test_pazy_modes.py +173 -0
  118. flapjax/tests/structure/static_primal/test_two_node_beam.py +533 -0
  119. flapjax/tests/structure/static_tangent/test_geradin_gradients.py +111 -0
  120. flapjax/utils/__init__.py +0 -0
  121. flapjax/utils/constants.py +23 -0
  122. flapjax/utils/data_structures.py +435 -0
  123. flapjax/utils/linear.py +857 -0
  124. flapjax/utils/print_utils.py +102 -0
  125. flapjax/utils/utils.py +175 -0
  126. flapjax_full-1.2.0.dev0.dist-info/METADATA +66 -0
  127. flapjax_full-1.2.0.dev0.dist-info/RECORD +129 -0
  128. flapjax_full-1.2.0.dev0.dist-info/WHEEL +4 -0
  129. flapjax_full-1.2.0.dev0.dist-info/licenses/LICENSE +22 -0
flapjax/__init__.py ADDED
File without changes
@@ -0,0 +1,19 @@
1
+ from flapjax.aero.data_structures import (
2
+ AeroCase,
3
+ GridDiscretisation,
4
+ )
5
+ from flapjax.aero.flowfields import Constant, OneMinusCosine
6
+ from flapjax.aero.gradients.data_structures import (
7
+ AeroGradsToCompute,
8
+ AeroJacobianApproximations,
9
+ AeroStates,
10
+ )
11
+ from flapjax.aero.linear.data_structures import (
12
+ AeroInputUnflattened,
13
+ AeroLinearResult,
14
+ AeroOutputUnflattened,
15
+ AeroStateUnflattened,
16
+ )
17
+ from flapjax.aero.linear.linear_uvlm import LinearUVLM
18
+ from flapjax.aero.utils import add_control_surface, make_rectangular_grid
19
+ from flapjax.aero.uvlm import UVLM
flapjax/aero/aic.py ADDED
@@ -0,0 +1,310 @@
1
+ from collections.abc import Sequence
2
+
3
+ import jax
4
+ from jax import Array, vmap
5
+ from jax import numpy as jnp
6
+
7
+ from flapjax.aero.utils import KernelFunction, mirror_grid
8
+ from flapjax.algebra.array_utils import ArrayList, block_axis
9
+
10
+
11
+ def compute_aic_grid(
12
+ c: Array,
13
+ n: Array | None,
14
+ zeta: Array,
15
+ kernel: KernelFunction,
16
+ batch_size: int | None,
17
+ ):
18
+ """
19
+ Compute the aerodynamic influence coefficient (AIC) across grids of points. When normal is provided, fuses the dot
20
+ product inside each map step so the trailing 3-component axis is never accumulated, saving memory.
21
+ :param c: Collocation points, ``(c_m, c_n, 3)``.
22
+ :param n: Normal vectors at collocation points, ``(c_m, c_n, 3)``, or None.
23
+ :param zeta: Grid vertices, ``(zeta_m, zeta_n, 3)``.
24
+ :param kernel: Kernel function to compute the influence.
25
+ :param batch_size: Batch size for vectorising AIC computations.
26
+ :return: ``(c_m, c_n, zeta_m, zeta_n, 3)`` if normal is None, else ``(c_m, c_n, zeta_m, zeta_n)``.
27
+ """
28
+ c_m, c_n = c.shape[:2]
29
+ m_panels, n_panels = zeta.shape[0] - 1, zeta.shape[1] - 1
30
+
31
+ m_vect_flat = jnp.stack((zeta[:-1, :, :], zeta[1:, :, :]), axis=-2).reshape(
32
+ -1, 2, 3
33
+ )
34
+ n_vect_flat = jnp.stack((zeta[:, :-1, :], zeta[:, 1:, :]), axis=-2).reshape(
35
+ -1, 2, 3
36
+ )
37
+
38
+ # account for the degenerate case where there are no source panels to prevent division by zero
39
+ if not c_m or not c_n or not m_panels or not n_panels:
40
+ return jnp.zeros((c_m, c_n, m_panels, n_panels))
41
+
42
+ @jax.checkpoint
43
+ def row(args: tuple) -> Array:
44
+ # compute the influence of all spanwise (m) and chordwise (n) filaments before combining. This prevents any
45
+ # duplicate computations.
46
+ ci, ni = args
47
+ m_influence = vmap(kernel, (None, 0), 0)(ci, m_vect_flat)
48
+ m_influence_ni = jnp.dot(m_influence, ni).reshape(
49
+ m_panels, n_panels + 1
50
+ ) # [m, n+1]
51
+ n_influence = vmap(kernel, (None, 0), 0)(ci, n_vect_flat)
52
+ n_influence_ni = jnp.dot(n_influence, ni).reshape(
53
+ m_panels + 1, n_panels
54
+ ) # [m+1, n]
55
+ return -jnp.diff(m_influence_ni, axis=1) + jnp.diff(
56
+ n_influence_ni, axis=0
57
+ ) # [m, n]
58
+
59
+ return jax.lax.map(
60
+ row,
61
+ (c.reshape(-1, 3), n.reshape(-1, 3) if n is not None else None),
62
+ batch_size=batch_size,
63
+ ).reshape(c_m, c_n, m_panels, n_panels)
64
+
65
+
66
+ def compute_aic_sys(
67
+ zetas: ArrayList,
68
+ cs: ArrayList,
69
+ ns: ArrayList,
70
+ kernels: Sequence[KernelFunction],
71
+ batch_size: int | None,
72
+ mirror_point: Array | None,
73
+ mirror_normal: Array | None,
74
+ ) -> list[list[Array]]:
75
+ """
76
+ Compute the AIC matrix for a system of elements. Returns a list of AIC matrices, one for each element.
77
+ :param zetas: List of source points to compute the AIC from, ``(n_source,)(zeta_m, zeta_n, 3)``.
78
+ :param cs: List of target points to compute the AIC at, ``(n_target,)(c_m, c_n, 3)``.
79
+ :param ns: Bound normal vectors, ``(c_m, c_n, 3)``. If None, no projection will be done.
80
+ :param kernels: List of kernel functions to use for each source surface, ``(n_source, )``.
81
+ :param batch_size: Batch size for vectorising AIC computations.
82
+ :param mirror_normal: Normal vector to mirror across, ``(3, )``. If None, no mirroring will be done.
83
+ :param mirror_point: Point on mirror plane, ``(3, )``. If None, no mirroring will be done.
84
+ :return: Nested sequences of AIC matrices, ``(n_target,)(n_source, c_m, c_n, zeta_m, zeta_n, 3)``, or
85
+ ``(n_target,)(n_source,)(c_m, c_n, zeta_m, zeta_n)`` if projected onto normals.
86
+ """
87
+
88
+ aic_mats = []
89
+ for c, n in zip(cs, ns):
90
+ aic_mats.append([])
91
+ for zeta, kernel in zip(zetas, kernels):
92
+ # compute the AIC matrix, [n_cx, n_cy, n_ex, n_ey, 3]
93
+ aic_ = compute_aic_grid(
94
+ c=c,
95
+ n=n,
96
+ zeta=zeta,
97
+ kernel=kernel,
98
+ batch_size=batch_size,
99
+ )
100
+
101
+ if mirror_point is not None and mirror_normal is not None:
102
+ # add influence from mirrored grid, if specified
103
+ zeta_mirror = mirror_grid(
104
+ zeta=zeta,
105
+ mirror_point=mirror_point,
106
+ mirror_normal=mirror_normal,
107
+ )
108
+ aic_ -= compute_aic_grid(
109
+ c=c, n=n, zeta=zeta_mirror, kernel=kernel, batch_size=batch_size
110
+ )
111
+ aic_mats[-1].append(aic_)
112
+ return aic_mats
113
+
114
+
115
+ def reshape_aic_sys(aic_mat: Array) -> Array:
116
+ r"""
117
+ Reshape an AIC matrix such that the source and target dimensions are flattened.
118
+ :param aic_mat: Input AIC matrix, ``(c_m, c_n, zeta_m, zeta_n)`` or ``(c_m, c_n, zeta_m, zeta_n, 3)``.
119
+ :return: Reshaped AIC matrix, ``(c_m*c_n, zeta_m*zeta_n)`` or ``(c_m*c_n, zeta_m*zeta_n, 3)``.
120
+ """
121
+ shape = aic_mat.shape
122
+ return aic_mat.reshape([shape[0] * shape[1], shape[2] * shape[3]])
123
+
124
+
125
+ def assemble_aic_sys(aic_mats: Sequence[Sequence[Array]]) -> Array:
126
+ r"""
127
+ Assemble a nested sequence of AIC matrices into a single AIC matrix.
128
+ :param aic_mats: Nested sequence of AIC matrices, ``(n_target,)(n_source,)(c_m, c_n, zeta_m, zeta_n)`` or ``(n_target,)(n_source,)(c_m, c_n, zeta_m, zeta_n, 3)``.
129
+ :return: Assembled AIC matrix, ``(c_tot, zeta_tot)`` or ``(c_tot, zeta_tot, 3)``.
130
+ """
131
+ aic_mats_reshaped = [
132
+ [reshape_aic_sys(aic) for aic in aic_row] for aic_row in aic_mats
133
+ ]
134
+ return block_axis(aic_mats_reshaped, axes=(0, 1))
135
+
136
+
137
+ def compute_aic_solve(
138
+ cs: ArrayList,
139
+ ns: ArrayList,
140
+ zetas_b: ArrayList,
141
+ zetas_w: ArrayList | None,
142
+ kernels_b: Sequence[KernelFunction],
143
+ kernels_w: Sequence[KernelFunction] | None,
144
+ batch_size: int | None,
145
+ mirror_point: Array | None,
146
+ mirror_normal: Array | None,
147
+ ) -> Array:
148
+ r"""
149
+ Compute the AIC matrix used for the UVLM solve step.
150
+ :param cs: List of target points to compute the AIC at, ``(n_target,)(c_m, c_n, 3)``.
151
+ :param ns: Bound normal vectors, ``(n_target,)(c_m, c_n, 3)``. If None, no projection will be done.
152
+ :param zetas_b: Bound aerodynamic grids, ``(n_source,)(zeta_m, zeta_n, 3)``.
153
+ :param zetas_w: Wake aerodynamic grids, ``(n_source,)(zeta_m_star, zeta_n, 3)``. This is only passed in the static case,
154
+ as in the dynamic case the wake influence is instead included in the boundary conditions.
155
+ :param kernels_b: Bound grid kernels.
156
+ :param kernels_w: Wake grid kernels.
157
+ :param batch_size: Batch size for vectorising AIC computations.
158
+ :param mirror_normal: Normal vector to mirror across, ``(3, )``. If None, no mirroring will be done.
159
+ :param mirror_point: Point on mirror plane, ``(3, )``. If None, no mirroring will be done.
160
+ :return: Square AIC matrix for the solve step, ``(c_tot, zeta_tot)``.
161
+ """
162
+ aic_b_mats = compute_aic_sys(
163
+ cs=cs,
164
+ ns=ns,
165
+ zetas=zetas_b,
166
+ kernels=kernels_b,
167
+ batch_size=batch_size,
168
+ mirror_point=mirror_point,
169
+ mirror_normal=mirror_normal,
170
+ )
171
+
172
+ if zetas_w is not None:
173
+ if kernels_w is None:
174
+ raise ValueError("kernels_w must not be None")
175
+ aic_w_mats = compute_aic_sys(
176
+ cs=cs,
177
+ ns=ns,
178
+ zetas=zetas_w,
179
+ kernels=kernels_w,
180
+ batch_size=batch_size,
181
+ mirror_point=mirror_point,
182
+ mirror_normal=mirror_normal,
183
+ )
184
+
185
+ aic_b_mats = add_wake_influence(aic_b_mats, aic_w_mats)
186
+
187
+ return assemble_aic_sys(aic_b_mats)
188
+
189
+
190
+ def add_wake_influence(
191
+ aic_bs: list[list[Array]], aic_ws: list[list[Array]]
192
+ ) -> list[list[Array]]:
193
+ r"""
194
+ Lump the wake influence onto the last column of the bound AIC matrices. This captures the steady Kutta condition
195
+ by ensuring that the trailing edge panels have the same strength as all wake panels along a streamline.
196
+ :param aic_bs: Bound influence matrices, ``(n_target,)(n_source,)(c_m, c_n, zeta_m, zeta_n, 3)``.
197
+ :param aic_ws: Wake influence matrices, ``(n_target,)(n_source,)(c_m, c_n, zeta_m_star, zeta_n, 3)``.
198
+ :return: Updated bound influence matrices, ``(n_target,)(n_source,)(c_m, c_n, zeta_m, zeta_n, 3)``.
199
+ """
200
+ for i in range(len(aic_bs)):
201
+ for j in range(len(aic_bs[i])):
202
+ aic_bs[i][j] = (
203
+ aic_bs[i][j].at[:, :, -1, :].add(jnp.sum(aic_ws[i][j], axis=2))
204
+ )
205
+ return aic_bs
206
+
207
+
208
+ def v_ind_vmap(
209
+ c: Array,
210
+ zeta: Array,
211
+ gamma: Array,
212
+ kernel: KernelFunction,
213
+ batch_size: int | None,
214
+ ) -> Array:
215
+ """
216
+ Compute the induced velocity by the aerodynamic elements at some points in space for a single source-target panel
217
+ system. This is done without materialising the full AIC matrix, instead directly computing its contraction with the
218
+ circulation strength.
219
+ :param c: Points at which to sample the velocity, ``(c_m, c_n, 3)``.
220
+ :param zeta: Filament grid, ``(zeta_m, zeta_n, 2, 3)``.
221
+ :param gamma: Circulation strengths, ``(zeta_m, zeta_n)``.
222
+ :param kernel: Kernel function.
223
+ :param batch_size: Batch size for vectorising AIC computations.
224
+ :return: Induced velocity, ``(c_m, c_n, 3)``.
225
+ """
226
+ c_m, c_n = c.shape[:2]
227
+ c_flat = c.reshape(-1, 3)
228
+ zeta_flat = zeta.reshape(-1, 2, 3)
229
+ gamma_flat = gamma.ravel() # [zeta_m * zeta_n]
230
+
231
+ # account for case where zeta is empty
232
+ if zeta.size == 0:
233
+ return jnp.zeros_like(c)
234
+
235
+ @jax.checkpoint
236
+ def row(ci: Array) -> Array:
237
+ influence = vmap(kernel, (None, 0), 0)(ci, zeta_flat) # [zeta_m * zeta_n, 3]
238
+
239
+ if influence.shape[0] != gamma_flat.shape[0]:
240
+ pass
241
+
242
+ return jnp.einsum("lm,l->m", influence, gamma_flat) # [3]
243
+
244
+ result = jax.lax.map(row, c_flat, batch_size=batch_size) # [c_m * c_n, 3]
245
+ return result.reshape(c_m, c_n, 3)
246
+
247
+
248
+ def compute_v_ind[T: Array | ArrayList](
249
+ cs: T,
250
+ zetas: ArrayList,
251
+ gammas: ArrayList,
252
+ kernels: Sequence[KernelFunction],
253
+ mirror_point: Array | None,
254
+ mirror_normal: Array | None,
255
+ batch_size: int | None,
256
+ ) -> T:
257
+ """
258
+ Compute the induced velocity by multiple surfaces of aerodynamic elements at one or multiple grids of points in
259
+ space. This is done without materialising the full AIC matrix, instead directly computing its contraction with the circulation strength.
260
+ :param cs: Points at which to sample the velocity, ``(c_m, c_n, 3)`` or ``(n_target,)(c_m, c_n, 3)``.
261
+ :param zetas: Filament grid, ``(n_source,)(zeta_m, zeta_n, 3)``.
262
+ :param gammas: Circulation strengths, ``(n_source,)(zeta_m, zeta_n)``.
263
+ :param kernels: Kernel function.
264
+ :param mirror_point: Mirror point, ``(3, )``. If None, no mirroring will be done.
265
+ :param mirror_normal: Normal mirror vector, ``(3, )``. If None, no mirroring will be done.
266
+ :param batch_size: Batch size for vectorising AIC computations.
267
+ :return: Array or ArrayList of induced velocity, ``(c_m, c_n, 3)`` or ``(n_target,)(c_m, c_n, 3)``.
268
+ """
269
+
270
+ # convert cs to an ArrayList. If it is an Array, we will convert back before returning.
271
+ cs_: ArrayList = ArrayList([cs]) if isinstance(cs, Array) else cs
272
+
273
+ v = ArrayList([])
274
+ for c in cs_:
275
+ v.append(jnp.zeros_like(c))
276
+ for zeta, gamma, kernel in zip(zetas, gammas, kernels):
277
+ m_vect = jnp.stack(
278
+ (zeta[:-1, :, :], zeta[1:, :, :]), axis=-2
279
+ ) # [m, n+1, 2, 3]
280
+ n_vect = jnp.stack(
281
+ (zeta[:, :-1, :], zeta[:, 1:, :]), axis=-2
282
+ ) # [m+1, n, 2, 3]
283
+
284
+ gamma_eff_m = jnp.diff(jnp.pad(gamma, ((0, 0), (1, 1))), axis=1) # [m, n+1]
285
+ gamma_eff_n = -jnp.diff(
286
+ jnp.pad(gamma, ((1, 1), (0, 0))), axis=0
287
+ ) # [m+1, n]
288
+
289
+ v[-1] += v_ind_vmap(
290
+ c, m_vect, gamma_eff_m, kernel, batch_size
291
+ ) + v_ind_vmap(c, n_vect, gamma_eff_n, kernel, batch_size)
292
+
293
+ if mirror_point is not None and mirror_normal is not None:
294
+ zeta_mirror = mirror_grid(
295
+ zeta=zeta,
296
+ mirror_point=mirror_point,
297
+ mirror_normal=mirror_normal,
298
+ )
299
+ m_vect_mirror = jnp.stack(
300
+ (zeta_mirror[:-1, :, :], zeta_mirror[1:, :, :]), axis=-2
301
+ ) # [m, n+1, 2, 3]
302
+ n_vect_mirror = jnp.stack(
303
+ (zeta_mirror[:, :-1, :], zeta_mirror[:, 1:, :]), axis=-2
304
+ ) # [m+1, n, 2, 3]
305
+
306
+ v[-1] -= v_ind_vmap(
307
+ c, m_vect_mirror, gamma_eff_m, kernel, batch_size
308
+ ) + v_ind_vmap(c, n_vect_mirror, gamma_eff_n, kernel, batch_size)
309
+
310
+ return v[0] if isinstance(cs, Array) else v