-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathelastic_wave_equation_abc.py
1242 lines (942 loc) · 44.5 KB
/
elastic_wave_equation_abc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import logging
import sys
from typing import Union
import numpy as np
import porepy as pp
from porepy.models.momentum_balance import MomentumBalance
sys.path.append("../")
import time_derivatives
from solvers.solver_mixins import CustomSolverMixin
from utils import acceleration_velocity_displacement, body_force_function, u_v_a_wrap
logger = logging.getLogger(__name__)
class NamesAndConstants:
def _is_nonlinear_problem(self) -> bool:
"""Determining if the problem is nonlinear or not.
The default behavior from momentum_balance.py is to assign True if there are
fractures present. Here it is hardcoded to be False, as the methodology has not
been used for domains with fractures yet.
"""
return False
@property
def beta(self) -> float:
"""Newmark time discretization parameter, beta.
Discretization parameter which somehow represents how the acceleration is
averaged over the coarse of one time step. The value of beta = 0.25 corresponds
to the "Average acceleration method".
"""
return 1 / 4
@property
def gamma(self) -> float:
"""Newmark time discretization parameter, gamma.
Discretization parameter which somehow represents the amount of numerical
damping (positive, ngeative or zero). The value of gamma = 0.5 corresponds to
no numerical damping.
"""
return 1 / 2
@property
def velocity_key(self) -> str:
"""Key/Name for the velocity variable/operator.
Velocity is represented by a time dependent dense array with the name provided
by this property, namely "velocity".
"""
return "velocity"
@property
def acceleration_key(self) -> str:
"""Key/Name for acceleration variable/operator.
Acceleration is represented by a time dependent dense array with the name
provided by this property, namely "acceleration".
"""
return "acceleration"
@property
def bc_values_mechanics_key(self) -> str:
"""Key for mechanical boundary conditions in the data dictionary."""
return "bc_values_mechanics"
def primary_wave_speed(self, is_scalar: bool = False) -> Union[float, np.ndarray]:
"""Primary wave speed (c_p).
Speed of the compressive elastic waves.
Parameters:
is_scalar: Whether the primary wavespeed should be scalar or not. Relevant
for use with manufactured solutions for constructing the source term. In
that case, the present method is called before the vector valued lambda
and vector valued mu are available. Thus, scalar valued wave speed is
accomodated.
Returns:
The value of the compressive elastic waves. Either scalar valued or the
cell-center values, depending on the input parameter.
"""
rho = self.solid.density
if not is_scalar:
return np.sqrt((self.lambda_vector + 2 * self.mu_vector) / rho)
else:
return np.sqrt(
(self.solid.lame_lambda + 2 * self.solid.shear_modulus / rho)
)
def secondary_wave_speed(self, is_scalar: bool = False) -> Union[float, np.ndarray]:
"""Secondary wave speed (c_s).
Speed of the shear elastic waves.
Parameters:
is_scalar: Whether the primary wavespeed should be scalar or not. Relevant
for use with manufactured solutions for constructing the source term. In
that case, the present method is called before the vector valued mu is
available. Thus, scalar valued wave speed is accomodated.
Returns:
The value of the shear elastic waves. Either scalar valued or the
cell-center values, depending on the input parameter.
"""
rho = self.solid.density
if not is_scalar:
return np.sqrt(self.mu_vector / rho)
else:
return np.sqrt(self.solid.shear_modulus / rho)
class BoundaryAndInitialConditions:
def bc_type_mechanics(self, sd: pp.Grid) -> pp.BoundaryConditionVectorial:
"""Boundary condition type for the absorbing boundary condition model class.
Assigns Robin boundaries to all subdomain boundaries by default. This includes
setting the Robin weight.
Parameters:
sd: The subdomain whose boundaries are to be set.
Returns:
The vectorial boundary condition operator for subdomain sd.
"""
# Fetch boundary sides and assign type of boundary condition for the different
# sides
bounds = self.domain_boundary_sides(sd)
bc = pp.BoundaryConditionVectorial(
sd,
bounds.north
+ bounds.south
+ bounds.east
+ bounds.west
+ bounds.bottom
+ bounds.top,
"rob",
)
# Calling helper function for assigning the Robin weight
self.assign_robin_weight(sd=sd, bc=bc)
return bc
def assign_robin_weight(
self, sd: pp.Grid, bc: pp.BoundaryConditionVectorial
) -> None:
"""Assigns the Robin weight for Robin boundary conditions.
This model class takes use of first order absorbing boundary conditions, and
those conditions can be seen as Robin boundary conditions. That is, they are on
the form:
sigma * n + alpha * u = G
The present method assigns the Robin weight (alpha). In the case of first order
absorbing boundary conditions, alpha depends on material properties such as lame
lambda and the shear modulus. In its discrete form it also depends on the
time-step size (see the method total_coefficient_matrix for the actual
construction of the weight).
Parameters:
sd: The subdomain whose boundary conditions are to be defined.
bc: The vectorial boundary condition object.
"""
# Initiating the arrays for the Robin weight
r_w = np.tile(np.eye(sd.dim), (1, sd.num_faces))
value = np.reshape(r_w, (sd.dim, sd.dim, sd.num_faces), "F")
# The Robin weight should only be assigned to subdomains of max dimension.
if sd.dim != self.nd:
bc.robin_weight = value
else:
# The coefficient matrix is constructed elsewhere, so we fetch it by making
# this call.
total_coefficient_matrix = self.total_coefficient_matrix(
sd=sd,
)
# Depending on which temporal discretization we use for the time derivative
# term in the absorbing boundary conditions, there are different extra
# coefficients arising in the expression (see the method
# total_coefficient_matrix).
total_coefficient_matrix *= self.discrete_robin_weight_coefficient
# Fethcing all boundary faces for the domain and assigning the
# total_coefficient_matrix to the boundary faces.
boundary_faces = sd.get_boundary_faces()
value[:, :, boundary_faces] *= total_coefficient_matrix.T
# Finally setting the actual Robin weight.
bc.robin_weight = value
def total_coefficient_matrix(self, sd: pp.Grid) -> np.ndarray:
"""Coefficient matrix for the absorbing boundary conditions.
This method is used together with Robin boundary condition (absorbing boundary
condition) assignment.
The absorbing boundary conditions, in the continuous form, look like the
following:
sigma * n + D * u_t = 0,
where u_t is the velocity and D is a matrix containing material parameters
and wave velocities.
Approximate the time derivative by a first or second order backward difference:
1: sigma * n + D_h * u_n = D_h * u_(n-1),
2: sigma * n + 3 / 2 * D_h * u_n = D_h * (2 * u_(n-1) - 0.5 * u_(n-2)),
where D_h = 1/dt * D. Note the coefficient 3 / 2 (termed
discrete_robin_weight_coefficient in the method assign_robin_weight)
This method thus creates the D_h matrix.
Parameters:
sd: The subdomain where the boundary conditions are assigned.
Returns:
A block array which is the sum of the normal and tangential component of the
coefficient matrices.
"""
# Fetching necessary grid related quantities
boundary_cells = self.boundary_cells_of_grid
boundary_faces = sd.get_boundary_faces()
face_normals = sd.face_normals[:, boundary_faces][: self.nd]
unitary_face_normals = face_normals / np.linalg.norm(
face_normals, axis=0, keepdims=True
)
# This cursed line of code does the same as the lines that are commented out
# below. Thank you Yura for helping with this!!
tensile_matrices = np.einsum(
"ik,jk->kij", unitary_face_normals, unitary_face_normals
)
# tensile_matrices = np.array(
# [
# np.outer(normal_vector, normal_vector)
# for normal_vector in unitary_face_normals.T
# ]
# )
# Creating a block array of identity matrices. Subtracting the tensile matrix
# block array from this provides us with the shear matrix block array.
eye_block_array = np.tile(np.eye(self.nd), (len(tensile_matrices), 1, 1))
shear_mat = eye_block_array - tensile_matrices
# Scaling with the Robin weight value
tensile_coeff = self.robin_weight_value(direction="tensile")[boundary_cells]
shear_coeff = self.robin_weight_value(direction="shear")[boundary_cells]
# Constructing the total coefficient matrix.
tensile_matrix_with_coeff = tensile_matrices * tensile_coeff[:, None, None]
shear_matrix_with_coeff = shear_mat * shear_coeff[:, None, None]
total_coefficient_matrix = tensile_matrix_with_coeff + shear_matrix_with_coeff
return total_coefficient_matrix
def robin_weight_value(self, direction: str) -> float:
"""Robin weight value for Robin boundary conditions.
Either shear or tensile Robin weight will be returned by this method. This
depends on whether shear or tensile "direction" is chosen.
Parameters:
direction: Whether the boundary condition that uses the weight is the shear
or tensile component of the displacement.
Returns:
The weight/coefficient for use in the Robin boundary conditions.
"""
dt = self.time_manager.dt
rho = self.solid.density
mu = self.mu_vector
if direction == "shear":
value = np.sqrt(rho * mu) * 1 / dt
elif direction == "tensile":
lmbda = self.lambda_vector
value = np.sqrt(rho * (lmbda + 2 * mu)) * 1 / dt
return value
def boundary_displacement(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
"""Method for reconstructing the boundary displacement.
Usage within the "realm" of absorbing boundary conditions: we need the
displacement values on the boundary at the previous time step.
Note: This is for the pure mechanical problem. Modifications are needed when a
coupling to fluid flow is introduced at some later point.
Parameters:
subdomains: List of subdomains. Should be of co-dimension 0.
Returns:
Ad operator representing the displacement on grid faces of the subdomains.
"""
# Discretization
discr = self.stress_discretization(subdomains)
# Boundary conditions on external boundaries
bc = self._combine_boundary_operators( # type: ignore [call-arg]
subdomains=subdomains,
dirichlet_operator=self.displacement,
neumann_operator=self.mechanical_stress,
robin_operator=self.mechanical_stress,
bc_type=self.bc_type_mechanics,
dim=self.nd,
name=self.bc_values_mechanics_key,
)
# Displacement
displacement = self.displacement(subdomains)
boundary_displacement = (
discr.bound_displacement_cell() @ displacement
+ discr.bound_displacement_face() @ bc
)
boundary_displacement.set_name("boundary_displacement")
return boundary_displacement
def initial_condition(self) -> None:
"""Assigning initial displacement, velocity and acceleration values."""
super().initial_condition()
for sd, data in self.mdg.subdomains(return_data=True, dim=self.nd):
dofs = sd.num_cells
initial_displacement = self.initial_displacement(dofs=dofs)
initial_velocity = self.initial_velocity(dofs=dofs)
initial_acceleration = self.initial_acceleration(dofs=dofs)
pp.set_solution_values(
name=self.displacement_variable,
values=initial_displacement,
data=data,
time_step_index=0,
iterate_index=0,
)
pp.set_solution_values(
name=self.velocity_key,
values=initial_velocity,
data=data,
time_step_index=0,
iterate_index=0,
)
pp.set_solution_values(
name=self.acceleration_key,
values=initial_acceleration,
data=data,
time_step_index=0,
iterate_index=0,
)
def initial_displacement(self, dofs: int) -> np.ndarray:
"""Cell-centered initial displacement values.
Parameters:
dofs: Number of degrees of freedom (typically cell number in the grid where
the initial values are defined).
Returns:
An array with the initial displacement values.
"""
sd = self.mdg.subdomains()[0]
t = self.time_manager.time
x = sd.cell_centers[0, :]
y = sd.cell_centers[1, :]
vals = np.zeros((self.nd, sd.num_cells))
if self.nd == 2:
displacement_function = u_v_a_wrap(self)
vals[0] = displacement_function[0](x, y, t)
vals[1] = displacement_function[1](x, y, t)
elif self.nd == 3:
z = sd.cell_centers[2, :]
displacement_function = u_v_a_wrap(self, is_2D=False)
vals[0] = displacement_function[0](x, y, z, t)
vals[1] = displacement_function[1](x, y, z, t)
vals[2] = displacement_function[2](x, y, z, t)
return vals.ravel("F")
def initial_velocity(self, dofs: int) -> np.ndarray:
"""Cell-centered initial velocity values.
Parameters:
dofs: Number of degrees of freedom (typically cell number in the grid where
the initial values are defined).
Returns:
An array with the initial velocity values.
"""
sd = self.mdg.subdomains()[0]
t = self.time_manager.time
x = sd.cell_centers[0, :]
y = sd.cell_centers[1, :]
vals = np.zeros((self.nd, sd.num_cells))
if self.nd == 2:
velocity_function = u_v_a_wrap(self, return_dt=True)
vals[0] = velocity_function[0](x, y, t)
vals[1] = velocity_function[1](x, y, t)
elif self.nd == 3:
z = sd.cell_centers[2, :]
velocity_function = u_v_a_wrap(self, is_2D=False, return_dt=True)
vals[0] = velocity_function[0](x, y, z, t)
vals[1] = velocity_function[1](x, y, z, t)
vals[2] = velocity_function[2](x, y, z, t)
return vals.ravel("F")
def initial_acceleration(self, dofs: int) -> np.ndarray:
"""Cell-centered initial acceleration values.
Parameters:
dofs: Number of degrees of freedom (typically cell number in the grid where
the initial values are defined).
Returns:
An array with the initial acceleration values.
"""
sd = self.mdg.subdomains()[0]
t = self.time_manager.time
x = sd.cell_centers[0, :]
y = sd.cell_centers[1, :]
vals = np.zeros((self.nd, sd.num_cells))
if self.nd == 2:
acceleration_function = u_v_a_wrap(self, return_ddt=True)
vals[0] = acceleration_function[0](x, y, t)
vals[1] = acceleration_function[1](x, y, t)
elif self.nd == 3:
z = sd.cell_centers[2, :]
acceleration_function = u_v_a_wrap(self, is_2D=False, return_ddt=True)
vals[0] = acceleration_function[0](x, y, z, t)
vals[1] = acceleration_function[1](x, y, z, t)
vals[2] = acceleration_function[2](x, y, z, t)
return vals.ravel("F")
class DynamicMomentumBalanceEquations:
def momentum_balance_equation(self, subdomains: list[pp.Grid]):
"""Momentum balance equation in the rock matrix.
Parameters:
subdomains: List of subdomains where the force balance is defined.
Returns:
Operator for the force balance equation in the matrix.
"""
inertia_mass = self.inertia(subdomains)
stress = pp.ad.Scalar(-1) * self.stress(subdomains)
body_force = self.body_force(subdomains)
equation = self.balance_equation(
subdomains, inertia_mass, stress, body_force, dim=self.nd
)
equation.set_name("momentum_balance_equation")
return equation
def inertia(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
"""Inertia mass in the elastic wave equation.
The elastic wave equation contains a term on the form M * u_tt (that is, M *
acceleration/inertia term). This method represents an operator for M.
Parameters:
subdomains: List of subdomains where the inertia mass is defined.
Returns:
Operator for the inertia mass.
"""
mass_density = self.solid_density(subdomains)
mass = self.volume_integral(mass_density, subdomains, dim=self.nd)
mass.set_name("inertia_mass")
return mass
def balance_equation(
self,
subdomains: list[pp.Grid],
inertia_mass: pp.ad.Operator,
surface_term: pp.ad.Operator,
source: pp.ad.Operator,
dim: int,
) -> pp.ad.Operator:
"""Balance equation that combines an acceleration and surface term.
The balance equation, namely the elastic wave equation, is given by
d_tt(accumulation) + div(surface_term) - source = 0.
Parameters:
subdomains: List of subdomains where the balance equation is defined.
inertia_mass: Operator for the cell-wise mass of the acceleration term,
integrated over the cells of the subdomains.
surface_term: Operator for the surface term (e.g. flux, stress), integrated
over the faces of the subdomains.
source: Operator for the source term, integrated over the cells of the
subdomains.
dim: Spatial dimension of the balance equation.
Returns:
Operator for the balance equation.
"""
div = pp.ad.Divergence(subdomains, dim=dim)
# Fetch the necessary operators for creating the acceleration operator
op = self.displacement(subdomains)
dt_op = self.velocity_time_dep_array(subdomains)
ddt_op = self.acceleration_time_dep_array(subdomains)
# Create the acceleration operator:
inertia_term = time_derivatives.inertia_term(
model=self,
op=op,
dt_op=dt_op,
ddt_op=ddt_op,
time_step=pp.ad.Scalar(self.time_manager.dt),
)
return inertia_mass * inertia_term + div @ surface_term - source
class SolutionStrategyDynamicMomentumBalance:
def prepare_simulation(self) -> None:
"""Run at the start of simulation. Used for initialization etc.
This method overrides the original prepare_simulation. The reason for this is
that we need to create the attributes boundary_cells_of_grid, lambda_vector and
mu_vector. All of which are needed in the call to initial_condition as well as
set_equations.
"""
# Set the material and geometry of the problem. The geometry method must be
# implemented in a ModelGeometry class.
self.set_materials()
self.set_geometry()
self.set_vector_valued_mu_lambda()
# Exporter initialization must be done after grid creation,
# but prior to data initialization.
self.initialize_data_saving()
# Set variables, constitutive relations, discretizations and equations.
# Order of operations is important here.
self.set_equation_system_manager()
self.create_variables()
self.initial_condition()
self.reset_state_from_file()
self.set_equations()
self.set_discretization_parameters()
self.discretize()
self._initialize_linear_solver()
self.set_nonlinear_discretizations()
# Export initial condition
self.save_data_time_step()
def set_vector_valued_mu_lambda(self):
sd = self.mdg.subdomains(dim=self.nd)[0]
boundary_faces = sd.get_boundary_faces()
self.boundary_cells_of_grid = sd.signs_and_cells_of_boundary_faces(
faces=boundary_faces
)[1]
self.vector_valued_mu_lambda()
def set_discretization_parameters(self) -> None:
"""Set discretization parameters for the simulation.
Sets eta = 1/3 on all faces if it is a simplex grid. Default is to have 0 on the
boundaries, but this causes divergence of the solution. 1/3 for all subfaces in
the grid fixes this issue.
"""
super().set_discretization_parameters()
if self.params["grid_type"] == "simplex":
num_subfaces = 0
for sd, data in self.mdg.subdomains(return_data=True, dim=self.nd):
subcell_topology = pp.fvutils.SubcellTopology(sd)
num_subfaces += subcell_topology.num_subfno
eta_values = np.ones(num_subfaces) * 1 / 3
if sd.dim == self.nd:
pp.initialize_data(
sd,
data,
self.stress_keyword,
{
"mpsa_eta": eta_values,
},
)
def velocity_time_dep_array(
self, subdomains: list[pp.Grid]
) -> pp.ad.TimeDependentDenseArray:
"""Time dependent dense array for the velocity.
Creates a time dependent dense array to represent the velocity, which is
needed for the Newmark time discretization.
Parameters:
subdomains: List of subdomains on which to define the velocity.
Returns:
Operator representation of the acceleration.
"""
return pp.ad.TimeDependentDenseArray(self.velocity_key, subdomains)
def acceleration_time_dep_array(
self, subdomains: list[pp.Grid]
) -> pp.ad.TimeDependentDenseArray:
"""Time dependent dense array for the acceleration.
Creates a time dependent dense array to represent the acceleration, which is
needed for the Newmark time discretization.
Parameters:
subdomains: List of subdomains on which to define the acceleration.
Returns:
Operator representation of the acceleration.
"""
return pp.ad.TimeDependentDenseArray(self.acceleration_key, subdomains)
def velocity_values(self, subdomain: pp.Grid) -> np.ndarray:
"""Update the velocity values at the end of each time step.
The velocity values are updated once the system is solved for the cell-centered
displacements (at the end of a time step). The values are updated by using the
Newmark formula for velocity (see e.g. Dynamics of Structures by A. K. Chopra
(pp. 175-176, 2014)).
Parameters:
subdomain: The subdomain the velocity is defined on.
Returns:
An array with the new velocity values.
"""
data = self.mdg.subdomain_data(subdomain)
dt = self.time_manager.dt
beta = self.beta
gamma = self.gamma
(
a_previous,
v_previous,
u_previous,
u_current,
) = acceleration_velocity_displacement(model=self, data=data)
v = (
v_previous * (1 - gamma / beta)
+ a_previous * (1 - gamma - (gamma * (1 - 2 * beta)) / (2 * beta)) * dt
+ (u_current - u_previous) * gamma / (beta * dt)
)
return v
def acceleration_values(self, subdomain: pp.Grid) -> np.ndarray:
"""Update the acceleration values at the end of each time step.
See the method velocity_values for more extensive documentation.
Parameters:
subdomain: The subdomain the acceleration is defined on.
Returns:
An array with the new acceleration values.
"""
data = self.mdg.subdomain_data(subdomain)
dt = self.time_manager.dt
beta = self.beta
(
a_previous,
v_previous,
u_previous,
u_current,
) = acceleration_velocity_displacement(model=self, data=data)
a = (
(u_current - u_previous) / (dt**2 * beta)
- v_previous / (dt * beta)
- a_previous * (1 - 2 * beta) / (2 * beta)
)
return a
def update_velocity_acceleration_time_dependent_ad_arrays(
self, initial: bool
) -> None:
"""Update the time dependent ad arrays for the velocity and acceleration.
The new velocity and acceleration values (the value at the end of each time
step) are set into the data dictionary.
Parameters:
initial: If True, the array generating method is called for both the stored
time steps and the stored iterates. If False, the array generating
method is called only for the iterate, and the time step solution is
updated by copying the iterate.
"""
for sd, data in self.mdg.subdomains(return_data=True, dim=self.nd):
vals_acceleration = self.acceleration_values(sd)
vals_velocity = self.velocity_values(sd)
pp.set_solution_values(
name=self.velocity_key,
values=vals_velocity,
data=data,
time_step_index=0,
)
pp.set_solution_values(
name=self.acceleration_key,
values=vals_acceleration,
data=data,
time_step_index=0,
)
pp.set_solution_values(
name=self.velocity_key,
values=vals_velocity,
data=data,
iterate_index=0,
)
pp.set_solution_values(
name=self.acceleration_key,
values=vals_acceleration,
data=data,
iterate_index=0,
)
def construct_and_save_boundary_displacement(self, boundary_grid: pp.BoundaryGrid):
data = self.mdg.boundary_grid_data(boundary_grid)
name = "boundary_displacement_values"
# The displacement value for the previous time step is constructed and the
# one two time steps back in time is fetched from the dictionary.
location = pp.TIME_STEP_SOLUTIONS
pp.shift_solution_values(
name=name,
data=data,
location=location,
max_index=2,
)
sd = boundary_grid.parent
displacement_boundary_operator = self.boundary_displacement([sd])
displacement_values = displacement_boundary_operator.value(self.equation_system)
displacement_values_0 = boundary_grid.projection(self.nd) @ displacement_values
pp.set_solution_values(
name=name,
values=displacement_values_0,
data=data,
time_step_index=0,
)
def after_nonlinear_convergence(self, iteration_counter: int = 1) -> None:
"""Method to be called after every non-linear iteration.
The method update_velocity_acceleration_time_dependent_ad_arrays needs to be
called at the end of each time step. This is not in PorePy itself, and therefore
this method is overriding the default after_nonlinear_convergence method.
Parameters:
solution: The new solution, as computed by the non-linear solver.
errors: The error in the solution, as computed by the non-linear solver.
iteration_counter: The number of iterations performed by the non-linear
solver.
"""
solution = self.equation_system.get_variable_values(iterate_index=0)
self.update_velocity_acceleration_time_dependent_ad_arrays(initial=True)
self.equation_system.shift_time_step_values()
self.equation_system.set_variable_values(
values=solution, time_step_index=0, additive=False
)
if self.time_manager.time_index >= 1:
bg = self.mdg.boundaries(dim=self.nd - 1)[0]
self.construct_and_save_boundary_displacement(boundary_grid=bg)
self.convergence_status = True
self.save_data_time_step()
class ConstitutiveLawsDynamicMomentumBalance:
def vector_valued_mu_lambda(self) -> None:
"""Vector representation of mu and lambda.
Cell-wise representation of the mu and lambda quantities in the rock matrix.
"""
subdomain = self.mdg.subdomains(dim=self.nd)[0]
self.lambda_vector = self.solid.lame_lambda * np.ones(subdomain.num_cells)
self.mu_vector = self.solid.shear_modulus * np.ones(subdomain.num_cells)
def stiffness_tensor(self, subdomain: pp.Grid) -> pp.FourthOrderTensor:
"""Stiffness tensor [Pa].
Overriding the stiffness_tensor method to accomodate vector valued mu and
lambda, which is treated as default in the model class for MPSA-Newmark with
ABCs.
Parameters:
subdomain: Subdomain where the stiffness tensor is defined.
Returns:
Cell-wise stiffness tensor in SI units.
"""
return pp.FourthOrderTensor(self.mu_vector, self.lambda_vector)
class TimeDependentSourceTerm:
def body_force(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
"""Time dependent dense array for the body force term.
Creates a time dependent dense array to represent the body force/source.
Parameters:
subdomains: List of subdomains on which to define the body force.
Returns:
Operator representation of the body force.
"""
external_sources = pp.ad.TimeDependentDenseArray(
name="source_mechanics",
domains=subdomains,
# previous_timestep=False,
)
return external_sources
def before_nonlinear_loop(self) -> None:
"""Update the time dependent mechanics source."""
super().before_nonlinear_loop()
sd = self.mdg.subdomains(dim=self.nd)[0]
data = self.mdg.subdomain_data(sd)
t = self.time_manager.time
# Mechanics source
if self.nd == 2:
mechanics_source_function = body_force_function(self)
elif self.nd == 3:
mechanics_source_function = body_force_function(self, is_2D=False)
mechanics_source_values = self.evaluate_mechanics_source(
f=mechanics_source_function, sd=sd, t=t
)
pp.set_solution_values(
name="source_mechanics",
values=mechanics_source_values,
data=data,
iterate_index=0,
)
def evaluate_mechanics_source(self, f: list, sd: pp.Grid, t: float) -> np.ndarray:
"""Computes the values for the body force.
The method computes the source values returned by the source value function (f)
integrated over the cell. The function is evaluated at the cell centers.
Parameters:
f: Function expression for the source term. It depends on time and space for
the source term. It is represented as a list, where the first list
component corresponds to the first vector component of the source,
second list component corresponds to the second vector component, and so
on.
sd: Subdomain where the source term is defined.
t: Current time in the time-stepping.
Returns:
An array of source values.
"""
cell_volume = sd.cell_volumes
vals = np.zeros((self.nd, sd.num_cells))
x = sd.cell_centers[0, :]
y = sd.cell_centers[1, :]
if self.nd == 2:
x_val = f[0](x, y, t)
y_val = f[1](x, y, t)
elif self.nd == 3:
z = sd.cell_centers[2, :]
x_val = f[0](x, y, z, t)
y_val = f[1](x, y, z, t)
z_val = f[2](x, y, z, t)
vals[2] = z_val * cell_volume
vals[0] = x_val * cell_volume
vals[1] = y_val * cell_volume
return vals.ravel("F")
class DynamicMomentumBalanceCommonParts(
NamesAndConstants,
CustomSolverMixin,
BoundaryAndInitialConditions,
DynamicMomentumBalanceEquations,
ConstitutiveLawsDynamicMomentumBalance,
TimeDependentSourceTerm,
SolutionStrategyDynamicMomentumBalance,
MomentumBalance,
):
"""Class of subclasses/methods that are common for ABC_1 and ABC_2.
ABC_1 is the absorbing boundary conditions approximated with a first order time
discretization for the u_t term. ABC_2 has a second order approximation to the u_t
term.
"""
...
# From here and downwards: The classes BoundaryAndInitialConditionValuesX that are
# unique for ABC_X.
class BoundaryAndInitialConditionValues1:
"""Class with methods that are unique to ABC_1
Note: Not tested anymore.
"""
@property
def discrete_robin_weight_coefficient(self) -> float:
"""Additional coefficient for discrete Robin boundary conditions.
After discretizing the time derivative in the absorbing boundary condition
expressions, there might appear coefficients additional to those within the
coefficient matrix. This model property assigns that coefficient.
Returns:
The Robin weight coefficient.
"""
return 1.0
def bc_values_stress(self, boundary_grid: pp.BoundaryGrid) -> np.ndarray:
"""Method for assigning Robin boundary condition values.
Specifically, this method assigns the values corresponding to ABC_1, namely
first order approximation to u_t in: