zoomy-core 0.1.0__py3-none-any.whl → 0.1.2__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.

Potentially problematic release.


This version of zoomy-core might be problematic. Click here for more details.

Files changed (57) hide show
  1. zoomy_core/decorators/decorators.py +25 -0
  2. zoomy_core/fvm/flux.py +97 -0
  3. zoomy_core/fvm/nonconservative_flux.py +97 -0
  4. zoomy_core/fvm/ode.py +55 -0
  5. zoomy_core/fvm/solver_numpy.py +305 -0
  6. zoomy_core/fvm/timestepping.py +13 -0
  7. zoomy_core/mesh/gmsh_loader.py +301 -0
  8. zoomy_core/mesh/mesh.py +1192 -0
  9. zoomy_core/mesh/mesh_extrude.py +168 -0
  10. zoomy_core/mesh/mesh_util.py +487 -0
  11. zoomy_core/misc/custom_types.py +6 -0
  12. zoomy_core/misc/gui.py +61 -0
  13. zoomy_core/misc/interpolation.py +140 -0
  14. zoomy_core/misc/io.py +401 -0
  15. zoomy_core/misc/logger_config.py +18 -0
  16. zoomy_core/misc/misc.py +216 -0
  17. zoomy_core/misc/static_class.py +94 -0
  18. zoomy_core/model/analysis.py +147 -0
  19. zoomy_core/model/basefunction.py +113 -0
  20. zoomy_core/model/basemodel.py +512 -0
  21. zoomy_core/model/boundary_conditions.py +193 -0
  22. zoomy_core/model/initial_conditions.py +171 -0
  23. zoomy_core/model/model.py +63 -0
  24. zoomy_core/model/models/GN.py +70 -0
  25. zoomy_core/model/models/advection.py +53 -0
  26. zoomy_core/model/models/basisfunctions.py +181 -0
  27. zoomy_core/model/models/basismatrices.py +377 -0
  28. zoomy_core/model/models/core.py +564 -0
  29. zoomy_core/model/models/coupled_constrained.py +60 -0
  30. zoomy_core/model/models/old_smm copy.py +867 -0
  31. zoomy_core/model/models/poisson.py +41 -0
  32. zoomy_core/model/models/shallow_moments.py +757 -0
  33. zoomy_core/model/models/shallow_moments_sediment.py +378 -0
  34. zoomy_core/model/models/shallow_moments_topo.py +423 -0
  35. zoomy_core/model/models/shallow_moments_variants.py +1509 -0
  36. zoomy_core/model/models/shallow_water.py +266 -0
  37. zoomy_core/model/models/shallow_water_topo.py +111 -0
  38. zoomy_core/model/models/shear_shallow_flow.py +594 -0
  39. zoomy_core/model/models/sme_turbulent.py +613 -0
  40. zoomy_core/model/models/swe_old.py +1018 -0
  41. zoomy_core/model/models/vam.py +455 -0
  42. zoomy_core/postprocessing/postprocessing.py +72 -0
  43. zoomy_core/preprocessing/openfoam_moments.py +452 -0
  44. zoomy_core/transformation/helpers.py +25 -0
  45. zoomy_core/transformation/to_amrex.py +238 -0
  46. zoomy_core/transformation/to_c.py +181 -0
  47. zoomy_core/transformation/to_jax.py +14 -0
  48. zoomy_core/transformation/to_numpy.py +115 -0
  49. zoomy_core/transformation/to_openfoam.py +254 -0
  50. zoomy_core/transformation/to_ufl.py +67 -0
  51. {zoomy_core-0.1.0.dist-info → zoomy_core-0.1.2.dist-info}/METADATA +1 -1
  52. zoomy_core-0.1.2.dist-info/RECORD +55 -0
  53. zoomy_core-0.1.2.dist-info/top_level.txt +1 -0
  54. zoomy_core-0.1.0.dist-info/RECORD +0 -5
  55. zoomy_core-0.1.0.dist-info/top_level.txt +0 -1
  56. {zoomy_core-0.1.0.dist-info → zoomy_core-0.1.2.dist-info}/WHEEL +0 -0
  57. {zoomy_core-0.1.0.dist-info → zoomy_core-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,452 @@
1
+ try:
2
+ _HAVE_MATPLOTLIB = True
3
+ import matplotlib.pyplot as plt
4
+ except ImportError:
5
+ _HAVE_MATPLOTLIB = False
6
+
7
+ import numpy as np
8
+ from copy import deepcopy
9
+ import os
10
+ from scipy.spatial import KDTree
11
+
12
+ try:
13
+ import pyvista as pv
14
+
15
+ _HAVE_PYVISTA = True
16
+ except ImportError:
17
+ _HAVE_PYVISTA = False
18
+
19
+ from library.zoomy_core.mesh.fvm_mesh import Mesh
20
+ from library.zoomy_core.misc.io import _save_fields_to_hdf5 as save_fields_to_hdf5
21
+ import library.zoomy_core.misc.io as io
22
+
23
+
24
+ main_dir = os.getenv("ZOOMY_DIR")
25
+
26
+
27
+ def load_file(filename):
28
+ if not _HAVE_PYVISTA:
29
+ raise ImportError("pyvista is required for load_file function.")
30
+ reader = pv.get_reader(filename)
31
+ # get(0) gets us the internal (not boundary) data. The boundary data is non-existant anyways in our case
32
+ vtkfile = reader.read().get(0)
33
+ return vtkfile
34
+
35
+
36
+ def get_fields(vtkfile, fieldnames):
37
+ fields = []
38
+ for fieldname in fieldnames:
39
+ # point field)s
40
+ # field = vtkfile.point_data[fieldname]
41
+ field = vtkfile.cell_data[fieldname]
42
+ fields.append(np.array(field))
43
+ return fields
44
+ # return np.array(fields)
45
+
46
+
47
+ def get_coordinates(vtkfile):
48
+ # point coordinates
49
+ # return np.array(vtkfile.points)
50
+ # cell centers
51
+ return vtkfile.cell_centers().points
52
+
53
+
54
+ def get_time(vtkfile):
55
+ return vtkfile.field_data["TimeValue"][0]
56
+
57
+
58
+ def sort_data(coordinates, fields):
59
+ # Sort by z, y, x
60
+ order = np.lexsort((coordinates[:, 0], coordinates[:, 1], coordinates[:, 2]))
61
+ return order
62
+
63
+
64
+ def apply_order(coordinates, fields, order):
65
+ coordinates_copy = np.array(coordinates)
66
+ fields_copy = deepcopy(fields)
67
+ for d in range(coordinates.shape[1]):
68
+ coordinates[:, d] = coordinates_copy[order, d]
69
+ for field, field_copy in zip(fields, fields_copy):
70
+ field[:] = field_copy[order]
71
+ return coordinates, fields
72
+
73
+
74
+ def get_number_of_layers_and_elements_in_plane(coordinates):
75
+ layers = len(np.unique(coordinates[:, 2]))
76
+ n_elements_plane = int(coordinates[:, 0].size / layers)
77
+ return layers, n_elements_plane
78
+
79
+
80
+ def get_layer(data, layer, n_elements_plane):
81
+ return data[layer * n_elements_plane : (layer + 1) * n_elements_plane]
82
+
83
+
84
+ def compute_height(
85
+ alpha, n_layers, n_elements_per_layer, total_height=1.0, threshold=0.5
86
+ ):
87
+ height = np.zeros(n_elements_per_layer)
88
+ active = np.ones(n_elements_per_layer, dtype=bool)
89
+ dh = total_height / n_layers
90
+ for i in range(n_layers):
91
+ alpha_layer = get_layer(alpha, i, n_elements_per_layer)
92
+ height += np.where(np.logical_and((alpha_layer >= threshold), active), dh, 0)
93
+ return height
94
+
95
+
96
+ def extract_faces(mesh):
97
+ faces = []
98
+ i, offset = 0, 0
99
+ cc = mesh.cells # fetch up front
100
+ while i < mesh.n_cells:
101
+ nn = cc[offset]
102
+ faces.append(cc[offset + 1 : offset + 1 + nn])
103
+ offset += nn + 1
104
+ i += 1
105
+ return np.array(faces)
106
+
107
+
108
+ def sort_faces(faces, order):
109
+ for face in faces:
110
+ for i, point in enumerate(face):
111
+ face[i] = order[point]
112
+ faces = faces[order]
113
+ return faces
114
+
115
+
116
+ def add_noslip_layer(U, n_elements_per_layer):
117
+ Unew = np.zeros((U.shape[0] + n_elements_per_layer, U.shape[1]))
118
+ for d in range(U.shape[1]):
119
+ Unew[n_elements_per_layer:, d] = U[:, d]
120
+ return Unew
121
+
122
+
123
+ def extract_velocity_column_at_coordinate(U, n_elements_per_layer, n_layer, coordinate):
124
+ u = np.array(
125
+ [
126
+ get_layer(U[:, 0], i, n_elements_per_layer)[coordinate]
127
+ for i in range(n_layer + 1)
128
+ ]
129
+ )
130
+ v = np.array(
131
+ [
132
+ get_layer(U[:, 1], i, n_elements_per_layer)[coordinate]
133
+ for i in range(n_layer + 1)
134
+ ]
135
+ )
136
+ w = np.array(
137
+ [
138
+ get_layer(U[:, 2], i, n_elements_per_layer)[coordinate]
139
+ for i in range(n_layer + 1)
140
+ ]
141
+ )
142
+ return u, v, w
143
+
144
+
145
+ def shift_integration_interval(xi):
146
+ return (xi + 1) / 2
147
+
148
+
149
+ def plot_basis(basis_generator):
150
+ fig, ax = plt.subplots()
151
+ basis = [basis_generator(n) for n in range(0, 8)]
152
+ X = np.linspace(0, 1, 100)
153
+ for i in range(8):
154
+ ax.plot(basis[i](X), X)
155
+ return fig, ax
156
+
157
+
158
+ def moment_projection(field, n_layers, basis, integration_order=None):
159
+ if integration_order is None:
160
+ integration_order = max(len(basis), n_layers)
161
+ xi, wi = np.polynomial.legendre.leggauss(integration_order)
162
+ xi = shift_integration_interval(xi)
163
+ dz = 1 / (n_layers)
164
+ xp = np.arange(dz / 2, 1 - dz / 2, dz)
165
+ xp = np.insert(xp, 0, 0)
166
+ fp = field
167
+ field_xi = np.interp(xi, xp, fp)
168
+ basis_xi = [basis[i](xi) for i in range(len(basis))]
169
+ projections = np.zeros(len(basis))
170
+ for i in range(len(basis)):
171
+ projections[i] = np.sum(field_xi * basis_xi[i] * wi)
172
+ return projections
173
+
174
+
175
+ def convert_openfoam_to_moments_single(filename, n_levels):
176
+ vtkfile = load_file(filename)
177
+ coordinates = get_coordinates(vtkfile)
178
+ n_layer, n_elements_per_layer = get_number_of_layers_and_elements_in_plane(
179
+ coordinates
180
+ )
181
+ fields = get_fields(vtkfile, ["alpha.water", "U"])
182
+ sort_order = sort_data(coordinates, fields)
183
+ coordinates, fields = apply_order(coordinates, fields, sort_order)
184
+ fields[1] = add_noslip_layer(fields[1], n_elements_per_layer)
185
+ time = get_time(vtkfile)
186
+ Q, basis = compute_shallow_moment_projection(fields, coordinates, n_levels)
187
+ return coordinates, Q, time, basis
188
+
189
+
190
+ def convert_openfoam_to_moments(
191
+ filepath, n_levels, filepath_mesh_for_order, meshtype_order="triganle"
192
+ ):
193
+ filepath_vtk = os.path.join(filepath, "VTK")
194
+ filename = "fields_openfoam.hdf5"
195
+ sort_order = None
196
+
197
+ # file order
198
+ file_number_list = []
199
+ file_start = ""
200
+ for i_file, file in enumerate(os.listdir(filepath_vtk)):
201
+ if file.endswith(".vtm"):
202
+ file_start = file.split("_")[0]
203
+ file_start = file.rsplit("_", 1)[0]
204
+ index = int(file.rsplit("_", 1)[1].split(".")[0])
205
+ # index = int(re.findall(r'\d+', file)[-1])
206
+ file_number_list.append(index)
207
+
208
+ file_number_list.sort()
209
+ print(file_number_list)
210
+
211
+ # first iteration
212
+ file0 = (
213
+ os.path.join(filepath_vtk, file_start) + "_" + str(file_number_list[0]) + ".vtm"
214
+ )
215
+ coordinates, Q, time, basis = convert_openfoam_to_moments_single(file0, n_levels)
216
+ mesh_order = Mesh.load_gmsh(filepath_mesh_for_order, meshtype_order)
217
+ Q, sort_order = sort_fields_by_mesh(mesh_order, coordinates, Q)
218
+ if os.path.exists(os.path.join(filepath, filename)):
219
+ os.remove(os.path.join(filepath, filename))
220
+ save_fields_to_hdf5(filepath, 0, time, Q.T, Qaux=None, filename=filename)
221
+
222
+ # loop iterations
223
+ iter = 1
224
+ total = len(file_number_list)
225
+ print(f"Conversion {iter}/{total} completed.")
226
+ iter += 1
227
+ # loop iterations
228
+ for i, i_file in enumerate(file_number_list[1:]):
229
+ file = os.path.join(filepath_vtk, file_start) + "_" + str(i_file) + ".vtm"
230
+ coordinates, Q, time, basis = convert_openfoam_to_moments_single(file, n_levels)
231
+ Q = apply_order_to_fields(sort_order, Q)
232
+ save_fields_to_hdf5(filepath, i + 1, time, Q.T, Qaux=None, filename=filename)
233
+ print(f"Conversion {iter}/{total} completed.")
234
+ iter += 1
235
+
236
+
237
+ def sort_fields_by_mesh(mesh, coordinates, Q):
238
+ # coordinates are still 3d, while Q is 2d and the mesh is 2d
239
+ n_layers, n_elements_per_layer = get_number_of_layers_and_elements_in_plane(
240
+ coordinates
241
+ )
242
+ coords_2d = coordinates[:n_elements_per_layer, :2]
243
+ coords_ordered = mesh.element_center
244
+ # Construct a KDTree from coords_2d
245
+ tree = KDTree(coords_2d)
246
+
247
+ # Find the indices of the nearest points in coords_2d to the points in coords_ordered
248
+ _, indices = tree.query(coords_ordered)
249
+
250
+ # Use these indices to sort Q and coords_2d
251
+ Q_sorted = Q[:, indices]
252
+ return Q_sorted, indices
253
+
254
+
255
+ def apply_order_to_fields(order, Q):
256
+ return Q[:, order]
257
+
258
+
259
+ def plot_contour(coordinates, field):
260
+ if not _HAVE_MATPLOTLIB:
261
+ raise ImportError("matplotlib is required for plot_contour function.")
262
+ fig, ax = plt.subplots()
263
+ n_layer, n_elements_per_layer = get_number_of_layers_and_elements_in_plane(
264
+ coordinates
265
+ )
266
+ X = get_layer(coordinates[:, 0], 0, n_elements_per_layer)
267
+ Y = get_layer(coordinates[:, 1], 0, n_elements_per_layer)
268
+ # Z = get_layer(fields[1][:, 0], 1, n_elements_per_layer)
269
+ # Z = get_layer(fields[0], 12, n_elements_per_layer)
270
+ # Z = compute_height(fields[0], n_layer, n_elements_per_layer)
271
+ Z = field
272
+ colorbar = ax.tricontourf(X, Y, Z)
273
+ ax.set_aspect("equal")
274
+
275
+ circle = plt.Circle((0.5, 1), radius=0.2, fc="silver", zorder=10, edgecolor="k")
276
+ plt.gca().add_patch(circle)
277
+ fig.colorbar(colorbar)
278
+ return fig, ax
279
+
280
+
281
+ def basis_legendre(i):
282
+ def f(x):
283
+ basis = np.polynomial.legendre.Legendre.basis(i, domain=[0, 1], window=[-1, 1])
284
+ return basis(x) * basis(0)
285
+
286
+ return f
287
+
288
+
289
+ def compute_shallow_moment_projection(
290
+ fields, coordinates, n_levels, basis_generator=basis_legendre
291
+ ):
292
+ n_layers, n_elements_per_layer = get_number_of_layers_and_elements_in_plane(
293
+ coordinates
294
+ )
295
+ height = compute_height(fields[0], n_layers, n_elements_per_layer)
296
+ basis = [basis_generator(n) for n in range(0, n_levels + 1)]
297
+ alphas = np.zeros((n_elements_per_layer, n_levels + 1))
298
+ betas = np.zeros((n_elements_per_layer, n_levels + 1))
299
+ for i in range(n_elements_per_layer):
300
+ u, v, w = extract_velocity_column_at_coordinate(
301
+ fields[1], n_elements_per_layer, n_layers, i
302
+ )
303
+ alphas[i] = moment_projection(u, n_layers + 1, basis)
304
+ betas[i] = moment_projection(v, n_layers + 1, basis)
305
+ return np.concatenate(
306
+ (height.reshape((n_elements_per_layer, 1)), alphas, betas), axis=1
307
+ ).T, basis
308
+
309
+
310
+ def plot_data_vs_moments(fields, coordinates, Q, basis, coordinate_index):
311
+ if _HAVE_MATPLOTLIB is False:
312
+ raise ImportError("matplotlib is required for plot_data_vs_moments function.")
313
+ n_layers, n_elements_per_layer = get_number_of_layers_and_elements_in_plane(
314
+ coordinates
315
+ )
316
+ n_levels = int((Q.shape[0] - 1) / 2) - 1
317
+ X = np.linspace(0, 1, 100)
318
+ U = np.array(
319
+ [basis[i](X) * Q[1 + i, coordinate_index] for i in range(len(basis))]
320
+ ).sum(axis=0)
321
+ V = np.array(
322
+ [
323
+ basis[i](X) * Q[1 + n_levels + 1 + i, coordinate_index]
324
+ for i in range(len(basis))
325
+ ]
326
+ ).sum(axis=0)
327
+ dz = 1 / (n_layers)
328
+ x = np.linspace(dz / 2, 1 - dz / 2, n_layers)
329
+ x = np.insert(x, 0, 0)
330
+ u, v, w = extract_velocity_column_at_coordinate(
331
+ fields[1], n_elements_per_layer, n_layers, coordinate_index
332
+ )
333
+ fig, ax = plt.subplots(2)
334
+ ax[0].plot(U, X)
335
+ ax[0].plot(u, x)
336
+ ax[1].plot(V, X)
337
+ ax[1].plot(v, x)
338
+ return fig, ax
339
+
340
+
341
+ def load_openfoam_file(filepath):
342
+ vtkfile = load_file(filepath)
343
+ coordinates = get_coordinates(vtkfile)
344
+ n_layer, n_elements_per_layer = get_number_of_layers_and_elements_in_plane(
345
+ coordinates
346
+ )
347
+ fields = get_fields(vtkfile, ["alpha.water", "U"])
348
+ sort_order = sort_data(coordinates, fields)
349
+ coordinates, fields = apply_order(coordinates, fields, sort_order)
350
+ fields[1] = add_noslip_layer(fields[1], n_elements_per_layer)
351
+ time = get_time(vtkfile)
352
+ return coordinates, fields, time
353
+
354
+
355
+ def test_load():
356
+ filepath = os.path.join(
357
+ os.path.join(main_dir, "openfoam_data/channelflow_coarse"),
358
+ "channelflow_coarse_0.vtm",
359
+ )
360
+ coordinates, fields, time = load_openfoam_file(filepath)
361
+
362
+
363
+ def test_moment_projection():
364
+ filepath = os.path.join(
365
+ os.path.join(main_dir, "openfoam_data/channelflow_coarse"),
366
+ "channelflow_coarse_0.vtm",
367
+ )
368
+ coordinates, fields, time = load_openfoam_file(filepath)
369
+ Q, basis = compute_shallow_moment_projection(fields, coordinates, 3)
370
+
371
+
372
+ def test_convert_openfoam_single():
373
+ filepath = os.path.join(
374
+ os.path.join(main_dir, "openfoam_data/channelflow_coarse"),
375
+ "channelflow_coarse_0.vtm",
376
+ )
377
+ X, Q, t, basis = convert_openfoam_to_moments_single(filepath, 3)
378
+
379
+
380
+ def test_plots():
381
+ if _HAVE_MATPLOTLIB is False:
382
+ raise ImportError("matplotlib is required for test_plots function.")
383
+ filepath = os.path.join(
384
+ os.path.join(main_dir, "openfoam_data/channelflow_coarse"),
385
+ "channelflow_coarse_0.vtm",
386
+ )
387
+ coordinates, fields, time = load_openfoam_file(filepath)
388
+ X, Q, t, basis = convert_openfoam_to_moments_single(filepath, 3)
389
+ fig, ax = plot_data_vs_moments(fields, coordinates, Q, basis, 0)
390
+ plt.show()
391
+ fig, ax = plot_basis(basis_legendre)
392
+ plt.show()
393
+ fig, ax = plot_contour(coordinates, Q[0])
394
+ plt.show()
395
+
396
+
397
+ def test_sort():
398
+ filepath = os.path.join(
399
+ os.path.join(main_dir, "openfoam_data/channelflow_coarse"),
400
+ "channelflow_coarse_0.vtm",
401
+ )
402
+ filepath_gmsh = os.path.join(
403
+ os.path.join(main_dir, "meshes/channel_openfoam/mesh_coarse_2d.msh")
404
+ )
405
+
406
+ mesh_gmsh = Mesh.load_gmsh(filepath_gmsh, "triangle")
407
+ mesh_gmsh.write_to_hdf5(
408
+ os.path.join(os.path.join(main_dir, "openfoam_data/channelflow_coarse"))
409
+ )
410
+
411
+ filepath_hdf_mesh = os.path.join(
412
+ os.path.join(main_dir, "openfoam_data/channelflow_coarse/mesh.hdf5")
413
+ )
414
+ mesh = Mesh.from_hdf5(filepath_hdf_mesh)
415
+
416
+ coordinates, fields, time = load_openfoam_file(filepath)
417
+ Q, basis = compute_shallow_moment_projection(fields, coordinates, 3)
418
+
419
+ # mesh_comparison = Mesh.load_gmsh(os.path.join(main_dir, 'meshes/channel_openfoam/mesh_coarse_3d.msh'), 'tetra')
420
+
421
+ sort_fields_by_mesh(mesh, coordinates, Q)
422
+
423
+
424
+ def test_convert_openfoam_to_moments(level=0):
425
+ # filepath = os.path.join(main_dir, 'openfoam_data/channelflow_coarse')
426
+ foam_sim = os.getenv("FOAM_SIM")
427
+ filepath = os.path.join(foam_sim, "multiphase/interFoam/RAS/channelflow_mid")
428
+ filepath_target_mesh = os.path.join(
429
+ os.path.join(main_dir, "meshes/simple_openfoam/mesh_2d_mid.msh")
430
+ )
431
+ convert_openfoam_to_moments(
432
+ filepath, level, filepath_target_mesh, meshtype_order="triangle"
433
+ )
434
+
435
+
436
+ if __name__ == "__main__":
437
+ # test_load()
438
+ # test_moment_projection()
439
+ # test_convert_openfoam_single()
440
+ # test_plots()
441
+ # test_sort()
442
+ test_convert_openfoam_to_moments(level=1)
443
+ filepath = os.path.join(main_dir, "openfoam_data/channelflow_mid")
444
+ filepath_mesh = os.path.join(main_dir, "meshes/simple_openfoam/mesh_2d_mid.msh")
445
+ io.generate_vtk(
446
+ filepath,
447
+ filepath_gmsh=filepath_mesh,
448
+ gmsh_mesh_type="triangle",
449
+ filename_fields="fields_openfoam.hdf5",
450
+ filename_out="fields_openfoam_vtk",
451
+ skip_aux=True,
452
+ )
@@ -0,0 +1,25 @@
1
+ from sympy import MatrixSymbol, fraction, cancel, Matrix
2
+
3
+ from library.zoomy_core.misc.misc import Zstruct
4
+
5
+ def regularize_denominator(expr, regularization_constant = 10**(-4), regularize = False):
6
+ if not regularize:
7
+ return expr
8
+ def regularize(expr):
9
+ (nom, den) = fraction(cancel(expr))
10
+ return nom * den / (den*2 + regularization_constant)
11
+ for i in range(expr.shape[0]):
12
+ for j in range(expr.shape[1]):
13
+ expr[i,j] = regularize(expr[i,j])
14
+ return expr
15
+
16
+ def substitute_sympy_attributes_with_symbol_matrix(expr: Matrix, attr: Zstruct, attr_matrix: MatrixSymbol):
17
+ if expr is None:
18
+ return None
19
+ if type(attr) is Zstruct:
20
+ assert attr.length() <= attr_matrix.shape[0]
21
+ for i, k in enumerate(attr.get_list()):
22
+ expr = Matrix(expr).subs(k, attr_matrix[i])
23
+ else:
24
+ expr = Matrix(expr).subs(attr, attr_matrix)
25
+ return expr