tsphere.py - sphere - GPU-based 3D discrete element method algorithm with optional fluid coupling
(HTM) git clone git://src.adamsgaard.dk/sphere
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) LICENSE
---
tsphere.py (304836B)
---
1 #!/usr/bin/env python
2 import math
3 import os
4 import subprocess
5 import pickle as pl
6 import numpy
7 try:
8 import matplotlib
9 matplotlib.use('Agg')
10 import matplotlib.pyplot as plt
11 import matplotlib.collections
12 matplotlib.rcParams.update({'font.size': 7, 'font.family': 'serif'})
13 matplotlib.rc('text', usetex=True)
14 matplotlib.rcParams['text.latex.preamble'] = [r"\usepackage{amsmath}"]
15 from matplotlib.font_manager import FontProperties
16 py_mpl = True
17 except ImportError:
18 print('Info: Could not find "matplotlib" python module. ' +
19 'Plotting functionality will be unavailable')
20 py_mpl = False
21 try:
22 import vtk
23 py_vtk = True
24 except ImportError:
25 print('Info: Could not find "vtk" python module. ' +
26 'Fluid VTK calls will be unavailable')
27 print('Consider installing with `pip install --user vtk`')
28 py_vtk = False
29
30 numpy.seterr(all='warn', over='raise')
31
32 # Sphere version number. This field should correspond to the value in
33 # `../src/version.h`.
34 VERSION = 2.15
35
36 # Transparency on plot legends
37 legend_alpha = 0.5
38
39
40 class sim:
41 '''
42 Class containing all ``sphere`` data.
43
44 Contains functions for reading and writing binaries, as well as simulation
45 setup and data analysis. Most arrays are initialized to default values.
46
47 :param np: The number of particles to allocate memory for (default=1)
48 :type np: int
49 :param nd: The number of spatial dimensions (default=3). Note that 2D and
50 1D simulations currently are not possible.
51 :type nd: int
52 :param nw: The number of dynamic walls (default=1)
53 :type nw: int
54 :param sid: The simulation id (default='unnamed'). The simulation files
55 will be written with this base name.
56 :type sid: str
57 :param fluid: Setup fluid simulation (default=False)
58 :type fluid: bool
59 :param cfd_solver: Fluid solver to use if fluid == True. 0: Navier-Stokes
60 (default), 1: Darcy.
61 :type cfd_solver: int
62 '''
63
64 def __init__(self, sid='unnamed', np=0, nd=3, nw=0, fluid=False):
65
66 # Sphere version number
67 self.version = numpy.ones(1, dtype=numpy.float64)*VERSION
68
69 # The number of spatial dimensions. Values other that 3 do not work
70 self.nd = int(nd)
71
72 # The number of particles
73 self.np = int(np)
74
75 # The simulation id (text string)
76 self.sid = sid
77
78 ## Time parameters
79 # Computational time step length [s]
80 self.time_dt = numpy.zeros(1, dtype=numpy.float64)
81
82 # Current time [s]
83 self.time_current = numpy.zeros(1, dtype=numpy.float64)
84
85 # Total time [s]
86 self.time_total = numpy.zeros(1, dtype=numpy.float64)
87
88 # File output interval [s]
89 self.time_file_dt = numpy.zeros(1, dtype=numpy.float64)
90
91 # The number of files written
92 self.time_step_count = numpy.zeros(1, dtype=numpy.uint32)
93
94 ## World dimensions and grid data
95 # The Euclidean coordinate to the origo of the sorting grid
96 self.origo = numpy.zeros(self.nd, dtype=numpy.float64)
97
98 # The sorting grid size (x, y, z)
99 self.L = numpy.zeros(self.nd, dtype=numpy.float64)
100
101 # The number of sorting cells in each dimension
102 self.num = numpy.zeros(self.nd, dtype=numpy.uint32)
103
104 # Whether to treat the lateral boundaries as periodic (1) or not (0)
105 self.periodic = numpy.zeros(1, dtype=numpy.uint32)
106
107 # Adaptively resize grid to assemblage height (0: no, 1: yes)
108 self.adaptive = numpy.zeros(1, dtype=numpy.uint32)
109
110 ## Particle data
111 # Particle position vectors [m]
112 self.x = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
113
114 # Particle radii [m]
115 self.radius = numpy.ones(self.np, dtype=numpy.float64)
116
117 # The sums of x and y movement [m]
118 self.xyzsum = numpy.zeros((self.np, 3), dtype=numpy.float64)
119
120 # The linear velocities [m/s]
121 self.vel = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
122
123 # Fix the particle kinematics?
124 # 0: No (DEFAULT, don't fix linear or angular acceleration)
125 # 1: Yes (fix horizontal movement, allow vertical movement, disable rotation)
126 # 10: Yes (fix horizontal movement, allow vertical movement, disable rotation)
127 # -1: Yes (fix all linear and rotational movement)
128 # -10: Yes (fix all rotational movement)
129 self.fixvel = numpy.zeros(self.np, dtype=numpy.float64)
130
131 # The linear force vectors [N]
132 self.force = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
133
134 # The angular position vectors [rad]
135 self.angpos = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
136
137 # The angular velocity vectors [rad/s]
138 self.angvel = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
139
140 # The torque vectors [N*m]
141 self.torque = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
142
143 # The shear friction energy dissipation rates [W]
144 self.es_dot = numpy.zeros(self.np, dtype=numpy.float64)
145
146 # The total shear energy dissipations [J]
147 self.es = numpy.zeros(self.np, dtype=numpy.float64)
148
149 # The viscous energy dissipation rates [W]
150 self.ev_dot = numpy.zeros(self.np, dtype=numpy.float64)
151
152 # The total viscois energy dissipation [J]
153 self.ev = numpy.zeros(self.np, dtype=numpy.float64)
154
155 # The total particle pressures [Pa]
156 self.p = numpy.zeros(self.np, dtype=numpy.float64)
157
158 # The gravitational acceleration vector [N*m/s]
159 self.g = numpy.array([0.0, 0.0, 0.0], dtype=numpy.float64)
160
161 # The Hookean coefficient for elastic stiffness normal to the contacts
162 # [N/m]
163 self.k_n = numpy.ones(1, dtype=numpy.float64) * 1.16e9
164
165 # The Hookean coefficient for elastic stiffness tangential to the
166 # contacts [N/m]
167 self.k_t = numpy.ones(1, dtype=numpy.float64) * 1.16e9
168
169 # The Hookean coefficient for elastic stiffness opposite of contact
170 # rotations. UNUSED
171 self.k_r = numpy.zeros(1, dtype=numpy.float64)
172
173 # Young's modulus for contact stiffness [Pa]. This value is used
174 # instead of the Hookean stiffnesses (k_n, k_t) when self.E is larger
175 # than 0.0.
176 self.E = numpy.zeros(1, dtype=numpy.float64)
177
178 # The viscosity normal to the contact [N/(m/s)]
179 self.gamma_n = numpy.zeros(1, dtype=numpy.float64)
180
181 # The viscosity tangential to the contact [N/(m/s)]
182 self.gamma_t = numpy.zeros(1, dtype=numpy.float64)
183
184 # The viscosity to contact rotation [N/(m/s)]
185 self.gamma_r = numpy.zeros(1, dtype=numpy.float64)
186
187 # The coefficient of static friction on the contact [-]
188 self.mu_s = numpy.ones(1, dtype=numpy.float64) * 0.5
189
190 # The coefficient of dynamic friction on the contact [-]
191 self.mu_d = numpy.ones(1, dtype=numpy.float64) * 0.5
192
193 # The coefficient of rotational friction on the contact [-]
194 self.mu_r = numpy.zeros(1, dtype=numpy.float64)
195
196 # The viscosity normal to the walls [N/(m/s)]
197 self.gamma_wn = numpy.zeros(1, dtype=numpy.float64)
198
199 # The viscosity tangential to the walls [N/(m/s)]
200 self.gamma_wt = numpy.zeros(1, dtype=numpy.float64)
201
202 # The coeffient of static friction of the walls [-]
203 self.mu_ws = numpy.ones(1, dtype=numpy.float64) * 0.5
204
205 # The coeffient of dynamic friction of the walls [-]
206 self.mu_wd = numpy.ones(1, dtype=numpy.float64) * 0.5
207
208 # The particle density [kg/(m^3)]
209 self.rho = numpy.ones(1, dtype=numpy.float64) * 2600.0
210
211 # The contact model to use
212 # 1: Normal: elasto-viscous, tangential: visco-frictional
213 # 2: Normal: elasto-viscous, tangential: elasto-visco-frictional
214 self.contactmodel = numpy.ones(1, dtype=numpy.uint32) * 2 # lin-visc-el
215
216 # Capillary bond prefactor
217 self.kappa = numpy.zeros(1, dtype=numpy.float64)
218
219 # Capillary bond debonding distance [m]
220 self.db = numpy.zeros(1, dtype=numpy.float64)
221
222 # Capillary bond liquid volume [m^3]
223 self.V_b = numpy.zeros(1, dtype=numpy.float64)
224
225 ## Wall data
226 # Number of dynamic walls
227 # nw=1: Uniaxial (also used for shear experiments)
228 # nw=2: Biaxial
229 # nw=5: Triaxial
230 self.nw = int(nw)
231
232 # Wall modes
233 # 0: Fixed
234 # 1: Normal stress condition
235 # 2: Normal velocity condition
236 # 3: Normal stress and shear stress condition
237 self.wmode = numpy.zeros(self.nw, dtype=numpy.int32)
238
239 # Wall normals
240 self.w_n = numpy.zeros((self.nw, self.nd), dtype=numpy.float64)
241 if self.nw >= 1:
242 self.w_n[0, 2] = -1.0
243 if self.nw >= 2:
244 self.w_n[1, 0] = -1.0
245 if self.nw >= 3:
246 self.w_n[2, 0] = 1.0
247 if self.nw >= 4:
248 self.w_n[3, 1] = -1.0
249 if self.nw >= 5:
250 self.w_n[4, 1] = 1.0
251
252 # Wall positions on the axes that are parallel to the wall normal [m]
253 self.w_x = numpy.ones(self.nw, dtype=numpy.float64)
254
255 # Wall masses [kg]
256 self.w_m = numpy.zeros(self.nw, dtype=numpy.float64)
257
258 # Wall velocities on the axes that are parallel to the wall normal [m/s]
259 self.w_vel = numpy.zeros(self.nw, dtype=numpy.float64)
260
261 # Wall forces on the axes that are parallel to the wall normal [m/s]
262 self.w_force = numpy.zeros(self.nw, dtype=numpy.float64)
263
264 # Wall stress on the axes that are parallel to the wall normal [Pa]
265 self.w_sigma0 = numpy.zeros(self.nw, dtype=numpy.float64)
266
267 # Wall stress modulation amplitude [Pa]
268 self.w_sigma0_A = numpy.zeros(1, dtype=numpy.float64)
269
270 # Wall stress modulation frequency [Hz]
271 self.w_sigma0_f = numpy.zeros(1, dtype=numpy.float64)
272
273 # Wall shear stress, enforced when wmode == 3
274 self.w_tau_x = numpy.zeros(1, dtype=numpy.float64)
275
276 ## Bond parameters
277 # Radius multiplier to the parallel-bond radii
278 self.lambda_bar = numpy.ones(1, dtype=numpy.float64)
279
280 # Number of bonds
281 self.nb0 = 0
282
283 # Bond tensile strength [Pa]
284 self.sigma_b = numpy.ones(1, dtype=numpy.float64) * numpy.infty
285
286 # Bond shear strength [Pa]
287 self.tau_b = numpy.ones(1, dtype=numpy.float64) * numpy.infty
288
289 # Bond pairs
290 self.bonds = numpy.zeros((self.nb0, 2), dtype=numpy.uint32)
291
292 # Parallel bond movement
293 self.bonds_delta_n = numpy.zeros(self.nb0, dtype=numpy.float64)
294
295 # Shear bond movement
296 self.bonds_delta_t = numpy.zeros((self.nb0, self.nd), dtype=numpy.float64)
297
298 # Twisting bond movement
299 self.bonds_omega_n = numpy.zeros(self.nb0, dtype=numpy.float64)
300
301 # Bending bond movement
302 self.bonds_omega_t = numpy.zeros((self.nb0, self.nd), dtype=numpy.float64)
303
304 ## Fluid parameters
305
306 # Simulate fluid? True: Yes, False: no
307 self.fluid = fluid
308
309 if self.fluid:
310
311 # Fluid solver type
312 # 0: Navier Stokes (fluid with inertia)
313 # 1: Stokes-Darcy (fluid without inertia)
314 self.cfd_solver = numpy.zeros(1, dtype=numpy.int32)
315
316 # Fluid dynamic viscosity [N/(m/s)]
317 self.mu = numpy.zeros(1, dtype=numpy.float64)
318
319 # Fluid velocities [m/s]
320 self.v_f = numpy.zeros((self.num[0], self.num[1], self.num[2], self.nd),
321 dtype=numpy.float64)
322
323 # Fluid pressures [Pa]
324 self.p_f = numpy.zeros((self.num[0], self.num[1], self.num[2]),
325 dtype=numpy.float64)
326
327 # Fluid cell porosities [-]
328 self.phi = numpy.zeros((self.num[0], self.num[1], self.num[2]),
329 dtype=numpy.float64)
330
331 # Fluid cell porosity change [1/s]
332 self.dphi = numpy.zeros((self.num[0], self.num[1], self.num[2]),
333 dtype=numpy.float64)
334
335 # Fluid density [kg/(m^3)]
336 self.rho_f = numpy.ones(1, dtype=numpy.float64) * 1.0e3
337
338 # Pressure modulation at the top boundary
339 self.p_mod_A = numpy.zeros(1, dtype=numpy.float64) # Amplitude [Pa]
340 self.p_mod_f = numpy.zeros(1, dtype=numpy.float64) # Frequency [Hz]
341 self.p_mod_phi = numpy.zeros(1, dtype=numpy.float64) # Shift [rad]
342
343 ## Fluid solver parameters
344
345 if self.cfd_solver[0] == 1: # Darcy solver
346 # Boundary conditions at the sides of the fluid grid
347 # 0: Dirichlet
348 # 1: Neumann
349 # 2: Periodic (default)
350 self.bc_xn = numpy.ones(1, dtype=numpy.int32)*2 # Neg. x bc
351 self.bc_xp = numpy.ones(1, dtype=numpy.int32)*2 # Pos. x bc
352 self.bc_yn = numpy.ones(1, dtype=numpy.int32)*2 # Neg. y bc
353 self.bc_yp = numpy.ones(1, dtype=numpy.int32)*2 # Pos. y bc
354
355 # Boundary conditions at the top and bottom of the fluid grid
356 # 0: Dirichlet (default)
357 # 1: Neumann free slip
358 # 2: Neumann no slip (Navier Stokes), Periodic (Darcy)
359 # 3: Periodic (Navier-Stokes solver only)
360 # 4: Constant flux (Darcy solver only)
361 self.bc_bot = numpy.zeros(1, dtype=numpy.int32)
362 self.bc_top = numpy.zeros(1, dtype=numpy.int32)
363 # Free slip boundaries? 1: yes
364 self.free_slip_bot = numpy.ones(1, dtype=numpy.int32)
365 self.free_slip_top = numpy.ones(1, dtype=numpy.int32)
366
367 # Boundary-normal flux (in case of bc_*=4)
368 self.bc_bot_flux = numpy.zeros(1, dtype=numpy.float64)
369 self.bc_top_flux = numpy.zeros(1, dtype=numpy.float64)
370
371 # Hold pressures constant in fluid cell (0: True, 1: False)
372 self.p_f_constant = numpy.zeros((self.num[0],
373 self.num[1],
374 self.num[2]), dtype=numpy.int32)
375
376 # Navier-Stokes
377 if self.cfd_solver[0] == 0:
378
379 # Smoothing parameter, should be in the range [0.0;1.0[.
380 # 0.0=no smoothing.
381 self.gamma = numpy.array(0.0)
382
383 # Under-relaxation parameter, should be in the range ]0.0;1.0].
384 # 1.0=no under-relaxation
385 self.theta = numpy.array(1.0)
386
387 # Velocity projection parameter, should be in the range
388 # [0.0;1.0]
389 self.beta = numpy.array(0.0)
390
391 # Tolerance criteria for the normalized max. residual
392 self.tolerance = numpy.array(1.0e-3)
393
394 # The maximum number of iterations to perform per time step
395 self.maxiter = numpy.array(1e4)
396
397 # The number of DEM time steps to perform between CFD updates
398 self.ndem = numpy.array(1)
399
400 # Porosity scaling factor
401 self.c_phi = numpy.ones(1, dtype=numpy.float64)
402
403 # Fluid velocity scaling factor
404 self.c_v = numpy.ones(1, dtype=numpy.float64)
405
406 # DEM-CFD time scaling factor
407 self.dt_dem_fac = numpy.ones(1, dtype=numpy.float64)
408
409 ## Interaction forces
410 self.f_d = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
411 self.f_p = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
412 self.f_v = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
413 self.f_sum = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
414
415 # Darcy
416 elif self.cfd_solver[0] == 1:
417
418 # Tolerance criteria for the normalized max. residual
419 self.tolerance = numpy.array(1.0e-3)
420
421 # The maximum number of iterations to perform per time step
422 self.maxiter = numpy.array(1e4)
423
424 # The number of DEM time steps to perform between CFD updates
425 self.ndem = numpy.array(1)
426
427 # Porosity scaling factor
428 self.c_phi = numpy.ones(1, dtype=numpy.float64)
429
430 # Interaction forces
431 self.f_p = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
432
433 # Adiabatic fluid compressibility [1/Pa].
434 # Fluid bulk modulus=1/self.beta_f
435 self.beta_f = numpy.ones(1, dtype=numpy.float64)*4.5e-10
436
437 # Hydraulic permeability prefactor [m*m]
438 self.k_c = numpy.ones(1, dtype=numpy.float64)*4.6e-10
439
440 else:
441 raise Exception('Value of cfd_solver not understood (' + \
442 str(self.cfd_solver[0]) + ')')
443
444 # Particle color marker
445 self.color = numpy.zeros(self.np, dtype=numpy.int32)
446
447 def __eq__(self, other):
448 '''
449 Called when to sim objects are compared. Returns 0 if the values
450 are identical.
451 '''
452 if self.version != other.version:
453 print('version')
454 return False
455 elif self.nd != other.nd:
456 print('nd')
457 return False
458 elif self.np != other.np:
459 print('np')
460 return False
461 elif self.time_dt != other.time_dt:
462 print('time_dt')
463 return False
464 elif self.time_current != other.time_current:
465 print('time_current')
466 return False
467 elif self.time_total != other.time_total:
468 print('time_total')
469 return False
470 elif self.time_file_dt != other.time_file_dt:
471 print('time_file_dt')
472 return False
473 elif self.time_step_count != other.time_step_count:
474 print('time_step_count')
475 return False
476 elif (self.origo != other.origo).any():
477 print('origo')
478 return False
479 elif (self.L != other.L).any():
480 print('L')
481 return 11
482 elif (self.num != other.num).any():
483 print('num')
484 return False
485 elif self.periodic != other.periodic:
486 print('periodic')
487 return False
488 elif self.adaptive != other.adaptive:
489 print('adaptive')
490 return False
491 elif (self.x != other.x).any():
492 print('x')
493 return False
494 elif (self.radius != other.radius).any():
495 print('radius')
496 return False
497 elif (self.xyzsum != other.xyzsum).any():
498 print('xyzsum')
499 return False
500 elif (self.vel != other.vel).any():
501 print('vel')
502 return False
503 elif (self.fixvel != other.fixvel).any():
504 print('fixvel')
505 return False
506 elif (self.force != other.force).any():
507 print('force')
508 return False
509 elif (self.angpos != other.angpos).any():
510 print('angpos')
511 return False
512 elif (self.angvel != other.angvel).any():
513 print('angvel')
514 return False
515 elif (self.torque != other.torque).any():
516 print('torque')
517 return False
518 elif (self.es_dot != other.es_dot).any():
519 print('es_dot')
520 return False
521 elif (self.es != other.es).any():
522 print('es')
523 return False
524 elif (self.ev_dot != other.ev_dot).any():
525 print('ev_dot')
526 return False
527 elif (self.ev != other.ev).any():
528 print('ev')
529 return False
530 elif (self.p != other.p).any():
531 print('p')
532 return False
533 elif (self.g != other.g).any():
534 print('g')
535 return False
536 elif self.k_n != other.k_n:
537 print('k_n')
538 return False
539 elif self.k_t != other.k_t:
540 print('k_t')
541 return False
542 elif self.k_r != other.k_r:
543 print('k_r')
544 return False
545 elif self.E != other.E:
546 print('E')
547 return False
548 elif self.gamma_n != other.gamma_n:
549 print('gamma_n')
550 return False
551 elif self.gamma_t != other.gamma_t:
552 print('gamma_t')
553 return False
554 elif self.gamma_r != other.gamma_r:
555 print('gamma_r')
556 return False
557 elif self.mu_s != other.mu_s:
558 print('mu_s')
559 return False
560 elif self.mu_d != other.mu_d:
561 print('mu_d')
562 return False
563 elif self.mu_r != other.mu_r:
564 print('mu_r')
565 return False
566 elif self.rho != other.rho:
567 print('rho')
568 return False
569 elif self.contactmodel != other.contactmodel:
570 print('contactmodel')
571 return False
572 elif self.kappa != other.kappa:
573 print('kappa')
574 return False
575 elif self.db != other.db:
576 print('db')
577 return False
578 elif self.V_b != other.V_b:
579 print('V_b')
580 return False
581 elif self.nw != other.nw:
582 print('nw')
583 return False
584 elif (self.wmode != other.wmode).any():
585 print('wmode')
586 return False
587 elif (self.w_n != other.w_n).any():
588 print('w_n')
589 return False
590 elif (self.w_x != other.w_x).any():
591 print('w_x')
592 return False
593 elif (self.w_m != other.w_m).any():
594 print('w_m')
595 return False
596 elif (self.w_vel != other.w_vel).any():
597 print('w_vel')
598 return False
599 elif (self.w_force != other.w_force).any():
600 print('w_force')
601 return False
602 elif (self.w_sigma0 != other.w_sigma0).any():
603 print('w_sigma0')
604 return False
605 elif self.w_sigma0_A != other.w_sigma0_A:
606 print('w_sigma0_A')
607 return False
608 elif self.w_sigma0_f != other.w_sigma0_f:
609 print('w_sigma0_f')
610 return False
611 elif self.w_tau_x != other.w_tau_x:
612 print('w_tau_x')
613 return False
614 elif self.gamma_wn != other.gamma_wn:
615 print('gamma_wn')
616 return False
617 elif self.gamma_wt != other.gamma_wt:
618 print('gamma_wt')
619 return False
620 elif self.lambda_bar != other.lambda_bar:
621 print('lambda_bar')
622 return False
623 elif self.nb0 != other.nb0:
624 print('nb0')
625 return False
626 elif self.sigma_b != other.sigma_b:
627 print('sigma_b')
628 return False
629 elif self.tau_b != other.tau_b:
630 print('tau_b')
631 return False
632 elif self.bonds != other.bonds:
633 print('bonds')
634 return False
635 elif self.bonds_delta_n != other.bonds_delta_n:
636 print('bonds_delta_n')
637 return False
638 elif self.bonds_delta_t != other.bonds_delta_t:
639 print('bonds_delta_t')
640 return False
641 elif self.bonds_omega_n != other.bonds_omega_n:
642 print('bonds_omega_n')
643 return False
644 elif self.bonds_omega_t != other.bonds_omega_t:
645 print('bonds_omega_t')
646 return False
647 elif self.fluid != other.fluid:
648 print('fluid')
649 return False
650
651 if self.fluid:
652 if self.cfd_solver != other.cfd_solver:
653 print('cfd_solver')
654 return False
655 elif self.mu != other.mu:
656 print('mu')
657 return False
658 elif (self.v_f != other.v_f).any():
659 print('v_f')
660 return False
661 elif (self.p_f != other.p_f).any():
662 print('p_f')
663 return False
664 #elif self.phi != other.phi).any():
665 #print('phi')
666 #return False # Porosities not initialized correctly
667 elif (self.dphi != other.dphi).any():
668 print('d_phi')
669 return False
670 elif self.rho_f != other.rho_f:
671 print('rho_f')
672 return False
673 elif self.p_mod_A != other.p_mod_A:
674 print('p_mod_A')
675 return False
676 elif self.p_mod_f != other.p_mod_f:
677 print('p_mod_f')
678 return False
679 elif self.p_mod_phi != other.p_mod_phi:
680 print('p_mod_phi')
681 return False
682 elif self.bc_bot != other.bc_bot:
683 print('bc_bot')
684 return False
685 elif self.bc_top != other.bc_top:
686 print('bc_top')
687 return False
688 elif self.free_slip_bot != other.free_slip_bot:
689 print('free_slip_bot')
690 return False
691 elif self.free_slip_top != other.free_slip_top:
692 print('free_slip_top')
693 return False
694 elif self.bc_bot_flux != other.bc_bot_flux:
695 print('bc_bot_flux')
696 return False
697 elif self.bc_top_flux != other.bc_top_flux:
698 print('bc_top_flux')
699 return False
700 elif (self.p_f_constant != other.p_f_constant).any():
701 print('p_f_constant')
702 return False
703
704 if self.cfd_solver == 0:
705 if self.gamma != other.gamma:
706 print('gamma')
707 return False
708 elif self.theta != other.theta:
709 print('theta')
710 return False
711 elif self.beta != other.beta:
712 print('beta')
713 return False
714 elif self.tolerance != other.tolerance:
715 print('tolerance')
716 return False
717 elif self.maxiter != other.maxiter:
718 print('maxiter')
719 return False
720 elif self.ndem != other.ndem:
721 print('ndem')
722 return False
723 elif self.c_phi != other.c_phi:
724 print('c_phi')
725 return 84
726 elif self.c_v != other.c_v:
727 print('c_v')
728 elif self.dt_dem_fac != other.dt_dem_fac:
729 print('dt_dem_fac')
730 return 85
731 elif (self.f_d != other.f_d).any():
732 print('f_d')
733 return 86
734 elif (self.f_p != other.f_p).any():
735 print('f_p')
736 return 87
737 elif (self.f_v != other.f_v).any():
738 print('f_v')
739 return 88
740 elif (self.f_sum != other.f_sum).any():
741 print('f_sum')
742 return 89
743
744 if self.cfd_solver == 1:
745 if self.tolerance != other.tolerance:
746 print('tolerance')
747 return False
748 elif self.maxiter != other.maxiter:
749 print('maxiter')
750 return False
751 elif self.ndem != other.ndem:
752 print('ndem')
753 return False
754 elif self.c_phi != other.c_phi:
755 print('c_phi')
756 return 84
757 elif (self.f_p != other.f_p).any():
758 print('f_p')
759 return 86
760 elif self.beta_f != other.beta_f:
761 print('beta_f')
762 return 87
763 elif self.k_c != other.k_c:
764 print('k_c')
765 return 88
766 elif self.bc_xn != other.bc_xn:
767 print('bc_xn')
768 return False
769 elif self.bc_xp != other.bc_xp:
770 print('bc_xp')
771 return False
772 elif self.bc_yn != other.bc_yn:
773 print('bc_yn')
774 return False
775 elif self.bc_yp != other.bc_yp:
776 print('bc_yp')
777 return False
778
779 if (self.color != other.color).any():
780 print('color')
781 return False
782
783 # All equal
784 return True
785
786 def id(self, sid=''):
787 '''
788 Returns or sets the simulation id/name, which is used to identify
789 simulation files in the output folders.
790
791 :param sid: The desired simulation id. If left blank the current
792 simulation id will be returned.
793 :type sid: str
794 :returns: The current simulation id if no new value is set.
795 :return type: str
796 '''
797 if sid == '':
798 return self.sid
799 else:
800 self.sid = sid
801
802 def idAppend(self, string):
803 '''
804 Append a string to the simulation id/name, which is used to identify
805 simulation files in the output folders.
806
807 :param string: The string to append to the simulation id (`self.sid`).
808 :type string: str
809 '''
810 self.sid += string
811
812 def addParticle(self, x, radius, xyzsum=numpy.zeros(3), vel=numpy.zeros(3),
813 fixvel=numpy.zeros(1), force=numpy.zeros(3),
814 angpos=numpy.zeros(3), angvel=numpy.zeros(3),
815 torque=numpy.zeros(3), es_dot=numpy.zeros(1),
816 es=numpy.zeros(1), ev_dot=numpy.zeros(1),
817 ev=numpy.zeros(1), p=numpy.zeros(1), color=0):
818 '''
819 Add a single particle to the simulation object. The only required
820 parameters are the position (x) and the radius (radius).
821
822 :param x: A vector pointing to the particle center coordinate.
823 :type x: numpy.array
824 :param radius: The particle radius
825 :type radius: float
826 :param vel: The particle linear velocity (default=[0, 0, 0])
827 :type vel: numpy.array
828 :param fixvel: 0: Do not fix particle velocity (default), 1: Fix
829 horizontal linear velocity, -1: Fix horizontal and vertical linear
830 velocity
831 :type fixvel: float
832 :param angpos: The particle angular position (default=[0, 0, 0])
833 :type angpos: numpy.array
834 :param angvel: The particle angular velocity (default=[0, 0, 0])
835 :type angvel: numpy.array
836 :param torque: The particle torque (default=[0, 0, 0])
837 :type torque: numpy.array
838 :param es_dot: The particle shear energy loss rate (default=0)
839 :type es_dot: float
840 :param es: The particle shear energy loss (default=0)
841 :type es: float
842 :param ev_dot: The particle viscous energy rate loss (default=0)
843 :type ev_dot: float
844 :param ev: The particle viscous energy loss (default=0)
845 :type ev: float
846 :param p: The particle pressure (default=0)
847 :type p: float
848 '''
849
850 self.np += 1
851
852 self.x = numpy.append(self.x, [x], axis=0)
853 self.radius = numpy.append(self.radius, radius)
854 self.vel = numpy.append(self.vel, [vel], axis=0)
855 self.xyzsum = numpy.append(self.xyzsum, [xyzsum], axis=0)
856 self.fixvel = numpy.append(self.fixvel, fixvel)
857 self.force = numpy.append(self.force, [force], axis=0)
858 self.angpos = numpy.append(self.angpos, [angpos], axis=0)
859 self.angvel = numpy.append(self.angvel, [angvel], axis=0)
860 self.torque = numpy.append(self.torque, [torque], axis=0)
861 self.es_dot = numpy.append(self.es_dot, es_dot)
862 self.es = numpy.append(self.es, es)
863 self.ev_dot = numpy.append(self.ev_dot, ev_dot)
864 self.ev = numpy.append(self.ev, ev)
865 self.p = numpy.append(self.p, p)
866 self.color = numpy.append(self.color, color)
867 if self.fluid:
868 self.f_d = numpy.append(self.f_d, [numpy.zeros(3)], axis=0)
869 self.f_p = numpy.append(self.f_p, [numpy.zeros(3)], axis=0)
870 self.f_v = numpy.append(self.f_v, [numpy.zeros(3)], axis=0)
871 self.f_sum = numpy.append(self.f_sum, [numpy.zeros(3)], axis=0)
872
873 def deleteParticle(self, i):
874 '''
875 Delete particle(s) with index ``i``.
876
877 :param i: One or more particle indexes to delete
878 :type i: int, list or numpy.array
879 '''
880
881 # The user wants to delete several particles, indexes in a numpy.array
882 if type(i) == numpy.ndarray:
883 self.np -= i.size
884
885 # The user wants to delete several particles, indexes in a Python list
886 elif type(i) == list:
887 self.np -= len(i)
888
889 # The user wants to delete a single particle with a integer index
890 else:
891 self.np -= 1
892
893 if type(i) == tuple:
894 raise Exception('Cannot parse tuples as index value. ' +
895 'Valid types are int, list and numpy.ndarray')
896
897
898 self.x = numpy.delete(self.x, i, axis=0)
899 self.radius = numpy.delete(self.radius, i)
900 self.vel = numpy.delete(self.vel, i, axis=0)
901 self.xyzsum = numpy.delete(self.xyzsum, i, axis=0)
902 self.fixvel = numpy.delete(self.fixvel, i)
903 self.force = numpy.delete(self.force, i, axis=0)
904 self.angpos = numpy.delete(self.angpos, i, axis=0)
905 self.angvel = numpy.delete(self.angvel, i, axis=0)
906 self.torque = numpy.delete(self.torque, i, axis=0)
907 self.es_dot = numpy.delete(self.es_dot, i)
908 self.es = numpy.delete(self.es, i)
909 self.ev_dot = numpy.delete(self.ev_dot, i)
910 self.ev = numpy.delete(self.ev, i)
911 self.p = numpy.delete(self.p, i)
912 self.color = numpy.delete(self.color, i)
913 if self.fluid:
914 # Darcy and Navier-Stokes
915 self.f_p = numpy.delete(self.f_p, i, axis=0)
916 if self.cfd_solver[0] == 0: # Navier-Stokes
917 self.f_d = numpy.delete(self.f_d, i, axis=0)
918 self.f_v = numpy.delete(self.f_v, i, axis=0)
919 self.f_sum = numpy.delete(self.f_sum, i, axis=0)
920
921 def deleteAllParticles(self):
922 '''
923 Deletes all particles in the simulation object.
924 '''
925 self.np = 0
926 self.x = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
927 self.radius = numpy.ones(self.np, dtype=numpy.float64)
928 self.xyzsum = numpy.zeros((self.np, 3), dtype=numpy.float64)
929 self.vel = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
930 self.fixvel = numpy.zeros(self.np, dtype=numpy.float64)
931 self.force = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
932 self.angpos = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
933 self.angvel = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
934 self.torque = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
935 self.es_dot = numpy.zeros(self.np, dtype=numpy.float64)
936 self.es = numpy.zeros(self.np, dtype=numpy.float64)
937 self.ev_dot = numpy.zeros(self.np, dtype=numpy.float64)
938 self.ev = numpy.zeros(self.np, dtype=numpy.float64)
939 self.p = numpy.zeros(self.np, dtype=numpy.float64)
940 self.color = numpy.zeros(self.np, dtype=numpy.int32)
941 self.f_d = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
942 self.f_p = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
943 self.f_v = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
944 self.f_sum = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
945
946 def readbin(self, targetbin, verbose=True, bonds=True, sigma0mod=True,
947 esysparticle=False):
948 '''
949 Reads a target ``sphere`` binary file.
950
951 See also :func:`writebin()`, :func:`readfirst()`, :func:`readlast()`,
952 :func:`readsecond`, and :func:`readstep`.
953
954 :param targetbin: The path to the binary ``sphere`` file
955 :type targetbin: str
956 :param verbose: Show diagnostic information (default=True)
957 :type verbose: bool
958 :param bonds: The input file contains bond information (default=True).
959 This parameter should be true for all recent ``sphere`` versions.
960 :type bonds: bool
961 :param sigma0mod: The input file contains information about modulating
962 stresses at the top wall (default=True). This parameter should be
963 true for all recent ``sphere`` versions.
964 :type sigma0mod: bool
965 :param esysparticle: Stop reading the file after reading the kinematics,
966 which is useful for reading output files from other DEM programs.
967 (default=False)
968 :type esysparticle: bool
969 '''
970
971 fh = None
972 try:
973 if verbose:
974 print("Input file: {0}".format(targetbin))
975 fh = open(targetbin, "rb")
976
977 # Read the file version
978 self.version = numpy.fromfile(fh, dtype=numpy.float64, count=1)
979
980 # Read the number of dimensions and particles
981 self.nd = int(numpy.fromfile(fh, dtype=numpy.int32, count=1))
982 self.np = int(numpy.fromfile(fh, dtype=numpy.uint32, count=1))
983
984 # Read the time variables
985 self.time_dt = numpy.fromfile(fh, dtype=numpy.float64, count=1)
986 self.time_current = numpy.fromfile(fh, dtype=numpy.float64, count=1)
987 self.time_total = numpy.fromfile(fh, dtype=numpy.float64, count=1)
988 self.time_file_dt = numpy.fromfile(fh, dtype=numpy.float64, count=1)
989 self.time_step_count = numpy.fromfile(fh, dtype=numpy.uint32, count=1)
990
991 # Allocate array memory for particles
992 self.x = numpy.empty((self.np, self.nd), dtype=numpy.float64)
993 self.radius = numpy.empty(self.np, dtype=numpy.float64)
994 self.xyzsum = numpy.empty((self.np, 3), dtype=numpy.float64)
995 self.vel = numpy.empty((self.np, self.nd), dtype=numpy.float64)
996 self.fixvel = numpy.empty(self.np, dtype=numpy.float64)
997 self.es_dot = numpy.empty(self.np, dtype=numpy.float64)
998 self.es = numpy.empty(self.np, dtype=numpy.float64)
999 self.ev_dot = numpy.empty(self.np, dtype=numpy.float64)
1000 self.ev = numpy.empty(self.np, dtype=numpy.float64)
1001 self.p = numpy.empty(self.np, dtype=numpy.float64)
1002
1003 # Read remaining data from binary
1004 self.origo = numpy.fromfile(fh, dtype=numpy.float64, count=self.nd)
1005 self.L = numpy.fromfile(fh, dtype=numpy.float64, count=self.nd)
1006 self.num = numpy.fromfile(fh, dtype=numpy.uint32, count=self.nd)
1007 self.periodic = numpy.fromfile(fh, dtype=numpy.int32, count=1)
1008
1009 if self.version >= 2.14:
1010 self.adaptive = numpy.fromfile(fh, dtype=numpy.int32, count=1)
1011 else:
1012 self.adaptive = numpy.zeros(1, dtype=numpy.float64)
1013
1014 # Per-particle vectors
1015 for i in numpy.arange(self.np):
1016 self.x[i, :] =\
1017 numpy.fromfile(fh, dtype=numpy.float64, count=self.nd)
1018 self.radius[i] =\
1019 numpy.fromfile(fh, dtype=numpy.float64, count=1)
1020
1021 if self.version >= 1.03:
1022 self.xyzsum = numpy.fromfile(fh, dtype=numpy.float64,\
1023 count=self.np*3).reshape(self.np, 3)
1024 else:
1025 self.xyzsum = numpy.fromfile(fh, dtype=numpy.float64,\
1026 count=self.np*2).reshape(self.np, 2)
1027
1028 for i in numpy.arange(self.np):
1029 self.vel[i, :] = numpy.fromfile(fh, dtype=numpy.float64, count=self.nd)
1030 self.fixvel[i] = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1031
1032 self.force = numpy.fromfile(fh, dtype=numpy.float64,\
1033 count=self.np*self.nd)\
1034 .reshape(self.np, self.nd)
1035
1036 self.angpos = numpy.fromfile(fh, dtype=numpy.float64,\
1037 count=self.np*self.nd)\
1038 .reshape(self.np, self.nd)
1039 self.angvel = numpy.fromfile(fh, dtype=numpy.float64,\
1040 count=self.np*self.nd)\
1041 .reshape(self.np, self.nd)
1042 self.torque = numpy.fromfile(fh, dtype=numpy.float64,\
1043 count=self.np*self.nd)\
1044 .reshape(self.np, self.nd)
1045
1046 if esysparticle:
1047 return
1048
1049 # Per-particle single-value parameters
1050 self.es_dot = numpy.fromfile(fh, dtype=numpy.float64, count=self.np)
1051 self.es = numpy.fromfile(fh, dtype=numpy.float64, count=self.np)
1052 self.ev_dot = numpy.fromfile(fh, dtype=numpy.float64, count=self.np)
1053 self.ev = numpy.fromfile(fh, dtype=numpy.float64, count=self.np)
1054 self.p = numpy.fromfile(fh, dtype=numpy.float64, count=self.np)
1055
1056 # Constant, global physical parameters
1057 self.g = numpy.fromfile(fh, dtype=numpy.float64, count=self.nd)
1058 self.k_n = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1059 self.k_t = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1060 self.k_r = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1061 if self.version >= 2.13:
1062 self.E = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1063 else:
1064 self.E = numpy.zeros(1, dtype=numpy.float64)
1065 self.gamma_n = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1066 self.gamma_t = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1067 self.gamma_r = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1068 self.mu_s = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1069 self.mu_d = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1070 self.mu_r = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1071 self.gamma_wn = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1072 self.gamma_wt = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1073 self.mu_ws = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1074 self.mu_wd = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1075 self.rho = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1076 self.contactmodel = numpy.fromfile(fh, dtype=numpy.uint32, count=1)
1077 self.kappa = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1078 self.db = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1079 self.V_b = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1080
1081 # Wall data
1082 self.nw = int(numpy.fromfile(fh, dtype=numpy.uint32, count=1))
1083 self.wmode = numpy.empty(self.nw, dtype=numpy.int32)
1084 self.w_n = numpy.empty(self.nw*self.nd, dtype=numpy.float64)\
1085 .reshape(self.nw, self.nd)
1086 self.w_x = numpy.empty(self.nw, dtype=numpy.float64)
1087 self.w_m = numpy.empty(self.nw, dtype=numpy.float64)
1088 self.w_vel = numpy.empty(self.nw, dtype=numpy.float64)
1089 self.w_force = numpy.empty(self.nw, dtype=numpy.float64)
1090 self.w_sigma0 = numpy.empty(self.nw, dtype=numpy.float64)
1091
1092 self.wmode = numpy.fromfile(fh, dtype=numpy.int32, count=self.nw)
1093 for i in numpy.arange(self.nw):
1094 self.w_n[i, :] =\
1095 numpy.fromfile(fh, dtype=numpy.float64, count=self.nd)
1096 self.w_x[i] = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1097 for i in numpy.arange(self.nw):
1098 self.w_m[i] = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1099 self.w_vel[i] = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1100 self.w_force[i] = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1101 self.w_sigma0[i] = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1102 if sigma0mod:
1103 self.w_sigma0_A = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1104 self.w_sigma0_f = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1105 if self.version >= 2.1:
1106 self.w_tau_x = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1107 else:
1108 self.w_tau_x = numpy.zeros(1, dtype=numpy.float64)
1109
1110 if bonds:
1111 # Inter-particle bonds
1112 self.lambda_bar = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1113 self.nb0 = int(numpy.fromfile(fh, dtype=numpy.uint32, count=1))
1114 self.sigma_b = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1115 self.tau_b = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1116 self.bonds = numpy.empty((self.nb0, 2), dtype=numpy.uint32)
1117 for i in numpy.arange(self.nb0):
1118 self.bonds[i, 0] = numpy.fromfile(fh, dtype=numpy.uint32, count=1)
1119 self.bonds[i, 1] = numpy.fromfile(fh, dtype=numpy.uint32, count=1)
1120 self.bonds_delta_n = numpy.fromfile(fh, dtype=numpy.float64,
1121 count=self.nb0)
1122 self.bonds_delta_t = numpy.fromfile(fh, dtype=numpy.float64,
1123 count=self.nb0*self.nd)\
1124 .reshape(self.nb0, self.nd)
1125 self.bonds_omega_n = numpy.fromfile(fh, dtype=numpy.float64,
1126 count=self.nb0)
1127 self.bonds_omega_t = numpy.fromfile(fh, dtype=numpy.float64,
1128 count=self.nb0*self.nd)\
1129 .reshape(self.nb0, self.nd)
1130 else:
1131 self.nb0 = 0
1132
1133 if self.fluid:
1134
1135 if self.version >= 2.0:
1136 self.cfd_solver = numpy.fromfile(fh, dtype=numpy.int32, count=1)
1137 else:
1138 self.cfd_solver = numpy.zeros(1, dtype=numpy.int32)
1139
1140 self.mu = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1141
1142 self.v_f = numpy.empty((self.num[0],
1143 self.num[1],
1144 self.num[2],
1145 self.nd), dtype=numpy.float64)
1146 self.p_f = numpy.empty((self.num[0],
1147 self.num[1],
1148 self.num[2]), dtype=numpy.float64)
1149 self.phi = numpy.empty((self.num[0],
1150 self.num[1],
1151 self.num[2]), dtype=numpy.float64)
1152 self.dphi = numpy.empty((self.num[0],
1153 self.num[1],
1154 self.num[2]), dtype=numpy.float64)
1155
1156 for z in numpy.arange(self.num[2]):
1157 for y in numpy.arange(self.num[1]):
1158 for x in numpy.arange(self.num[0]):
1159 self.v_f[x, y, z, 0] = numpy.fromfile(fh,
1160 dtype=numpy.float64,
1161 count=1)
1162 self.v_f[x, y, z, 1] = numpy.fromfile(fh,
1163 dtype=numpy.float64,
1164 count=1)
1165 self.v_f[x, y, z, 2] = numpy.fromfile(fh,
1166 dtype=numpy.float64,
1167 count=1)
1168 self.p_f[x, y, z] = numpy.fromfile(fh,
1169 dtype=numpy.float64,
1170 count=1)
1171 self.phi[x, y, z] = numpy.fromfile(fh,
1172 dtype=numpy.float64,
1173 count=1)
1174 self.dphi[x, y, z] = numpy.fromfile(fh,
1175 dtype=numpy.float64,
1176 count=1)\
1177 /(self.time_dt*self.ndem)
1178
1179 if self.version >= 0.36:
1180 self.rho_f = numpy.fromfile(fh, dtype=numpy.float64,
1181 count=1)
1182 self.p_mod_A = numpy.fromfile(fh, dtype=numpy.float64,
1183 count=1)
1184 self.p_mod_f = numpy.fromfile(fh, dtype=numpy.float64,
1185 count=1)
1186 self.p_mod_phi = numpy.fromfile(fh, dtype=numpy.float64,
1187 count=1)
1188
1189 if self.version >= 2.12 and self.cfd_solver[0] == 1:
1190 self.bc_xn = numpy.fromfile(fh, dtype=numpy.int32,
1191 count=1)
1192 self.bc_xp = numpy.fromfile(fh, dtype=numpy.int32,
1193 count=1)
1194 self.bc_yn = numpy.fromfile(fh, dtype=numpy.int32,
1195 count=1)
1196 self.bc_yp = numpy.fromfile(fh, dtype=numpy.int32,
1197 count=1)
1198
1199 self.bc_bot = numpy.fromfile(fh, dtype=numpy.int32, count=1)
1200 self.bc_top = numpy.fromfile(fh, dtype=numpy.int32, count=1)
1201 self.free_slip_bot = numpy.fromfile(fh, dtype=numpy.int32,
1202 count=1)
1203 self.free_slip_top = numpy.fromfile(fh, dtype=numpy.int32,
1204 count=1)
1205 if self.version >= 2.11:
1206 self.bc_bot_flux = numpy.fromfile(fh,
1207 dtype=numpy.float64,
1208 count=1)
1209 self.bc_top_flux = numpy.fromfile(fh,
1210 dtype=numpy.float64,
1211 count=1)
1212 else:
1213 self.bc_bot_flux = numpy.zeros(1, dtype=numpy.float64)
1214 self.bc_top_flux = numpy.zeros(1, dtype=numpy.float64)
1215
1216 if self.version >= 2.15:
1217 self.p_f_constant = numpy.empty((self.num[0],
1218 self.num[1],
1219 self.num[2]),
1220 dtype=numpy.int32)
1221
1222 for z in numpy.arange(self.num[2]):
1223 for y in numpy.arange(self.num[1]):
1224 for x in numpy.arange(self.num[0]):
1225 self.p_f_constant[x, y, z] = \
1226 numpy.fromfile(fh, dtype=numpy.int32,
1227 count=1)
1228 else:
1229 self.p_f_constant = numpy.zeros((self.num[0],
1230 self.num[1],
1231 self.num[2]),
1232 dtype=numpy.int32)
1233
1234 if self.version >= 2.0 and self.cfd_solver == 0:
1235 self.gamma = numpy.fromfile(fh, dtype=numpy.float64,
1236 count=1)
1237 self.theta = numpy.fromfile(fh, dtype=numpy.float64,
1238 count=1)
1239 self.beta = numpy.fromfile(fh, dtype=numpy.float64,
1240 count=1)
1241 self.tolerance = numpy.fromfile(fh, dtype=numpy.float64,
1242 count=1)
1243 self.maxiter = numpy.fromfile(fh, dtype=numpy.uint32,
1244 count=1)
1245 if self.version >= 1.01:
1246 self.ndem = numpy.fromfile(fh, dtype=numpy.uint32,
1247 count=1)
1248 else:
1249 self.ndem = 1
1250
1251 if self.version >= 1.04:
1252 self.c_phi = numpy.fromfile(fh, dtype=numpy.float64,
1253 count=1)
1254 self.c_v = numpy.fromfile(fh, dtype=numpy.float64,
1255 count=1)
1256 if self.version == 1.06:
1257 self.c_a = numpy.fromfile(fh, dtype=numpy.float64,
1258 count=1)
1259 elif self.version >= 1.07:
1260 self.dt_dem_fac = numpy.fromfile(fh,
1261 dtype=numpy.float64,
1262 count=1)
1263 else:
1264 self.c_a = numpy.ones(1, dtype=numpy.float64)
1265 else:
1266 self.c_phi = numpy.ones(1, dtype=numpy.float64)
1267 self.c_v = numpy.ones(1, dtype=numpy.float64)
1268
1269 if self.version >= 1.05:
1270 self.f_d = numpy.empty_like(self.x)
1271 self.f_p = numpy.empty_like(self.x)
1272 self.f_v = numpy.empty_like(self.x)
1273 self.f_sum = numpy.empty_like(self.x)
1274
1275 for i in numpy.arange(self.np):
1276 self.f_d[i, :] = numpy.fromfile(fh,
1277 dtype=numpy.float64,
1278 count=self.nd)
1279 for i in numpy.arange(self.np):
1280 self.f_p[i, :] = numpy.fromfile(fh,
1281 dtype=numpy.float64,
1282 count=self.nd)
1283 for i in numpy.arange(self.np):
1284 self.f_v[i, :] = numpy.fromfile(fh,
1285 dtype=numpy.float64,
1286 count=self.nd)
1287 for i in numpy.arange(self.np):
1288 self.f_sum[i, :] = numpy.fromfile(fh,
1289 dtype=numpy.float64,
1290 count=self.nd)
1291 else:
1292 self.f_d = numpy.zeros((self.np, self.nd),
1293 dtype=numpy.float64)
1294 self.f_p = numpy.zeros((self.np, self.nd),
1295 dtype=numpy.float64)
1296 self.f_v = numpy.zeros((self.np, self.nd),
1297 dtype=numpy.float64)
1298 self.f_sum = numpy.zeros((self.np, self.nd),
1299 dtype=numpy.float64)
1300
1301 elif self.version >= 2.0 and self.cfd_solver == 1:
1302
1303 self.tolerance = numpy.fromfile(fh, dtype=numpy.float64,
1304 count=1)
1305 self.maxiter = numpy.fromfile(fh, dtype=numpy.uint32,
1306 count=1)
1307 self.ndem = numpy.fromfile(fh, dtype=numpy.uint32, count=1)
1308 self.c_phi = numpy.fromfile(fh, dtype=numpy.float64,
1309 count=1)
1310 self.f_p = numpy.empty_like(self.x)
1311 for i in numpy.arange(self.np):
1312 self.f_p[i, :] = numpy.fromfile(fh, dtype=numpy.float64,
1313 count=self.nd)
1314 self.beta_f = numpy.fromfile(fh, dtype=numpy.float64,
1315 count=1)
1316 self.k_c = numpy.fromfile(fh, dtype=numpy.float64, count=1)
1317
1318 if self.version >= 1.02:
1319 self.color = numpy.fromfile(fh, dtype=numpy.int32,
1320 count=self.np)
1321 else:
1322 self.color = numpy.zeros(self.np, dtype=numpy.int32)
1323
1324 finally:
1325 self.version[0] = VERSION
1326 if fh is not None:
1327 fh.close()
1328
1329 def writebin(self, folder="../input/", verbose=True):
1330 '''
1331 Writes a ``sphere`` binary file to the ``../input/`` folder by default.
1332 The file name will be in the format ``<self.sid>.bin``.
1333
1334 See also :func:`readbin()`.
1335
1336 :param folder: The folder where to place the output binary file
1337 :type folder: str
1338 :param verbose: Show diagnostic information (default=True)
1339 :type verbose: bool
1340 '''
1341 fh = None
1342 try:
1343 targetbin = folder + "/" + self.sid + ".bin"
1344 if verbose:
1345 print("Output file: {0}".format(targetbin))
1346
1347 fh = open(targetbin, "wb")
1348
1349 # Write the current version number
1350 fh.write(self.version.astype(numpy.float64))
1351
1352 # Write the number of dimensions and particles
1353 fh.write(numpy.array(self.nd).astype(numpy.int32))
1354 fh.write(numpy.array(self.np).astype(numpy.uint32))
1355
1356 # Write the time variables
1357 fh.write(self.time_dt.astype(numpy.float64))
1358 fh.write(self.time_current.astype(numpy.float64))
1359 fh.write(self.time_total.astype(numpy.float64))
1360 fh.write(self.time_file_dt.astype(numpy.float64))
1361 fh.write(self.time_step_count.astype(numpy.uint32))
1362
1363 # Read remaining data from binary
1364 fh.write(self.origo.astype(numpy.float64))
1365 fh.write(self.L.astype(numpy.float64))
1366 fh.write(self.num.astype(numpy.uint32))
1367 fh.write(self.periodic.astype(numpy.uint32))
1368 fh.write(self.adaptive.astype(numpy.uint32))
1369
1370 # Per-particle vectors
1371 for i in numpy.arange(self.np):
1372 fh.write(self.x[i, :].astype(numpy.float64))
1373 fh.write(self.radius[i].astype(numpy.float64))
1374
1375 if self.np > 0:
1376 fh.write(self.xyzsum.astype(numpy.float64))
1377
1378 for i in numpy.arange(self.np):
1379 fh.write(self.vel[i, :].astype(numpy.float64))
1380 fh.write(self.fixvel[i].astype(numpy.float64))
1381
1382 if self.np > 0:
1383 fh.write(self.force.astype(numpy.float64))
1384
1385 fh.write(self.angpos.astype(numpy.float64))
1386 fh.write(self.angvel.astype(numpy.float64))
1387 fh.write(self.torque.astype(numpy.float64))
1388
1389 # Per-particle single-value parameters
1390 fh.write(self.es_dot.astype(numpy.float64))
1391 fh.write(self.es.astype(numpy.float64))
1392 fh.write(self.ev_dot.astype(numpy.float64))
1393 fh.write(self.ev.astype(numpy.float64))
1394 fh.write(self.p.astype(numpy.float64))
1395
1396 fh.write(self.g.astype(numpy.float64))
1397 fh.write(self.k_n.astype(numpy.float64))
1398 fh.write(self.k_t.astype(numpy.float64))
1399 fh.write(self.k_r.astype(numpy.float64))
1400 fh.write(self.E.astype(numpy.float64))
1401 fh.write(self.gamma_n.astype(numpy.float64))
1402 fh.write(self.gamma_t.astype(numpy.float64))
1403 fh.write(self.gamma_r.astype(numpy.float64))
1404 fh.write(self.mu_s.astype(numpy.float64))
1405 fh.write(self.mu_d.astype(numpy.float64))
1406 fh.write(self.mu_r.astype(numpy.float64))
1407 fh.write(self.gamma_wn.astype(numpy.float64))
1408 fh.write(self.gamma_wt.astype(numpy.float64))
1409 fh.write(self.mu_ws.astype(numpy.float64))
1410 fh.write(self.mu_wd.astype(numpy.float64))
1411 fh.write(self.rho.astype(numpy.float64))
1412 fh.write(self.contactmodel.astype(numpy.uint32))
1413 fh.write(self.kappa.astype(numpy.float64))
1414 fh.write(self.db.astype(numpy.float64))
1415 fh.write(self.V_b.astype(numpy.float64))
1416
1417 fh.write(numpy.array(self.nw).astype(numpy.uint32))
1418 for i in numpy.arange(self.nw):
1419 fh.write(self.wmode[i].astype(numpy.int32))
1420 for i in numpy.arange(self.nw):
1421 fh.write(self.w_n[i, :].astype(numpy.float64))
1422 fh.write(self.w_x[i].astype(numpy.float64))
1423
1424 for i in numpy.arange(self.nw):
1425 fh.write(self.w_m[i].astype(numpy.float64))
1426 fh.write(self.w_vel[i].astype(numpy.float64))
1427 fh.write(self.w_force[i].astype(numpy.float64))
1428 fh.write(self.w_sigma0[i].astype(numpy.float64))
1429 fh.write(self.w_sigma0_A.astype(numpy.float64))
1430 fh.write(self.w_sigma0_f.astype(numpy.float64))
1431 fh.write(self.w_tau_x.astype(numpy.float64))
1432
1433 fh.write(self.lambda_bar.astype(numpy.float64))
1434 fh.write(numpy.array(self.nb0).astype(numpy.uint32))
1435 fh.write(self.sigma_b.astype(numpy.float64))
1436 fh.write(self.tau_b.astype(numpy.float64))
1437 for i in numpy.arange(self.nb0):
1438 fh.write(self.bonds[i, 0].astype(numpy.uint32))
1439 fh.write(self.bonds[i, 1].astype(numpy.uint32))
1440 fh.write(self.bonds_delta_n.astype(numpy.float64))
1441 fh.write(self.bonds_delta_t.astype(numpy.float64))
1442 fh.write(self.bonds_omega_n.astype(numpy.float64))
1443 fh.write(self.bonds_omega_t.astype(numpy.float64))
1444
1445 if self.fluid:
1446
1447 fh.write(self.cfd_solver.astype(numpy.int32))
1448 fh.write(self.mu.astype(numpy.float64))
1449 for z in numpy.arange(self.num[2]):
1450 for y in numpy.arange(self.num[1]):
1451 for x in numpy.arange(self.num[0]):
1452 fh.write(self.v_f[x, y, z, 0].astype(numpy.float64))
1453 fh.write(self.v_f[x, y, z, 1].astype(numpy.float64))
1454 fh.write(self.v_f[x, y, z, 2].astype(numpy.float64))
1455 fh.write(self.p_f[x, y, z].astype(numpy.float64))
1456 fh.write(self.phi[x, y, z].astype(numpy.float64))
1457 fh.write(self.dphi[x, y, z].astype(numpy.float64)*
1458 self.time_dt*self.ndem)
1459
1460 fh.write(self.rho_f.astype(numpy.float64))
1461 fh.write(self.p_mod_A.astype(numpy.float64))
1462 fh.write(self.p_mod_f.astype(numpy.float64))
1463 fh.write(self.p_mod_phi.astype(numpy.float64))
1464
1465 if self.cfd_solver[0] == 1: # Sides only adjustable with Darcy
1466 fh.write(self.bc_xn.astype(numpy.int32))
1467 fh.write(self.bc_xp.astype(numpy.int32))
1468 fh.write(self.bc_yn.astype(numpy.int32))
1469 fh.write(self.bc_yp.astype(numpy.int32))
1470
1471 fh.write(self.bc_bot.astype(numpy.int32))
1472 fh.write(self.bc_top.astype(numpy.int32))
1473 fh.write(self.free_slip_bot.astype(numpy.int32))
1474 fh.write(self.free_slip_top.astype(numpy.int32))
1475 fh.write(self.bc_bot_flux.astype(numpy.float64))
1476 fh.write(self.bc_top_flux.astype(numpy.float64))
1477
1478 for z in numpy.arange(self.num[2]):
1479 for y in numpy.arange(self.num[1]):
1480 for x in numpy.arange(self.num[0]):
1481 fh.write(self.p_f_constant[x, y, z].astype(
1482 numpy.int32))
1483
1484 # Navier Stokes
1485 if self.cfd_solver[0] == 0:
1486 fh.write(self.gamma.astype(numpy.float64))
1487 fh.write(self.theta.astype(numpy.float64))
1488 fh.write(self.beta.astype(numpy.float64))
1489 fh.write(self.tolerance.astype(numpy.float64))
1490 fh.write(self.maxiter.astype(numpy.uint32))
1491 fh.write(self.ndem.astype(numpy.uint32))
1492
1493 fh.write(self.c_phi.astype(numpy.float64))
1494 fh.write(self.c_v.astype(numpy.float64))
1495 fh.write(self.dt_dem_fac.astype(numpy.float64))
1496
1497 for i in numpy.arange(self.np):
1498 fh.write(self.f_d[i, :].astype(numpy.float64))
1499 for i in numpy.arange(self.np):
1500 fh.write(self.f_p[i, :].astype(numpy.float64))
1501 for i in numpy.arange(self.np):
1502 fh.write(self.f_v[i, :].astype(numpy.float64))
1503 for i in numpy.arange(self.np):
1504 fh.write(self.f_sum[i, :].astype(numpy.float64))
1505
1506 # Darcy
1507 elif self.cfd_solver[0] == 1:
1508
1509 fh.write(self.tolerance.astype(numpy.float64))
1510 fh.write(self.maxiter.astype(numpy.uint32))
1511 fh.write(self.ndem.astype(numpy.uint32))
1512 fh.write(self.c_phi.astype(numpy.float64))
1513 for i in numpy.arange(self.np):
1514 fh.write(self.f_p[i, :].astype(numpy.float64))
1515 fh.write(self.beta_f.astype(numpy.float64))
1516 fh.write(self.k_c.astype(numpy.float64))
1517
1518 else:
1519 raise Exception('Value of cfd_solver not understood (' + \
1520 str(self.cfd_solver[0]) + ')')
1521
1522
1523 fh.write(self.color.astype(numpy.int32))
1524
1525 finally:
1526 if fh is not None:
1527 fh.close()
1528
1529 def writeVTKall(self, cell_centered=True, verbose=True, forces=False):
1530 '''
1531 Writes a VTK file for each simulation output file with particle
1532 information and the fluid grid to the ``../output/`` folder by default.
1533 The file name will be in the format ``<self.sid>.vtu`` and
1534 ``fluid-<self.sid>.vti``. The vtu files can be used to visualize the
1535 particles, and the vti files for visualizing the fluid in ParaView.
1536
1537 After opening the vtu files, the particle fields will show up in the
1538 "Properties" list. Press "Apply" to import all fields into the ParaView
1539 session. The particles are visualized by selecting the imported data in
1540 the "Pipeline Browser". Afterwards, click the "Glyph" button in the
1541 "Common" toolbar, or go to the "Filters" menu, and press "Glyph" from
1542 the "Common" list. Choose "Sphere" as the "Glyph Type", set "Radius" to
1543 1.0, choose "scalar" as the "Scale Mode". Check the "Edit" checkbox, and
1544 set the "Set Scale Factor" to 1.0. The field "Maximum Number of Points"
1545 may be increased if the number of particles exceed the default value.
1546 Finally press "Apply", and the particles will appear in the main window.
1547
1548 The sphere resolution may be adjusted ("Theta resolution", "Phi
1549 resolution") to increase the quality and the computational requirements
1550 of the rendering.
1551
1552 The fluid grid is visualized by opening the vti files, and pressing
1553 "Apply" to import all fluid field properties. To visualize the scalar
1554 fields, such as the pressure, the porosity, the porosity change or the
1555 velocity magnitude, choose "Surface" or "Surface With Edges" as the
1556 "Representation". Choose the desired property as the "Coloring" field.
1557 It may be desirable to show the color bar by pressing the "Show" button,
1558 and "Rescale" to fit the color range limits to the current file. The
1559 coordinate system can be displayed by checking the "Show Axis" field.
1560 All adjustments by default require the "Apply" button to be pressed
1561 before regenerating the view.
1562
1563 The fluid vector fields (e.g. the fluid velocity) can be visualizing by
1564 e.g. arrows. To do this, select the fluid data in the "Pipeline
1565 Browser". Press "Glyph" from the "Common" toolbar, or go to the
1566 "Filters" mennu, and press "Glyph" from the "Common" list. Make sure
1567 that "Arrow" is selected as the "Glyph type", and "Velocity" as the
1568 "Vectors" value. Adjust the "Maximum Number of Points" to be at least as
1569 big as the number of fluid cells in the grid. Press "Apply" to visualize
1570 the arrows.
1571
1572 If several data files are generated for the same simulation (e.g. using
1573 the :func:`writeVTKall()` function), it is able to step the
1574 visualization through time by using the ParaView controls.
1575
1576 :param verbose: Show diagnostic information (default=True)
1577 :type verbose: bool
1578 :param cell_centered: Write fluid values to cell centered positions
1579 (default=true)
1580 :type cell_centered: bool
1581 :param forces: Write contact force files (slow) (default=False)
1582 :type forces: bool
1583 '''
1584 lastfile = status(self.sid)
1585 sb = sim(fluid=self.fluid)
1586 for i in range(lastfile+1):
1587 fn = "../output/{0}.output{1:0=5}.bin".format(self.sid, i)
1588
1589 # check if output VTK file exists and if it is newer than spherebin
1590 fn_vtk = "../output/{0}.{1:0=5}.vtu".format(self.sid, i)
1591 if os.path.isfile(fn_vtk) and \
1592 (os.path.getmtime(fn) < os.path.getmtime(fn_vtk)):
1593 if verbose:
1594 print('skipping ' + fn_vtk +
1595 ': file exists and is newer than ' + fn)
1596 if self.fluid:
1597 fn_vtk = "../output/fluid-{0}.{1:0=5}.vti" \
1598 .format(self.sid, i)
1599 if os.path.isfile(fn_vtk) and \
1600 (os.path.getmtime(fn) < os.path.getmtime(fn_vtk)):
1601 if verbose:
1602 print('skipping ' + fn_vtk +
1603 ': file exists and is newer than ' + fn)
1604 continue
1605 else:
1606 continue
1607
1608 sb.sid = self.sid + ".{:0=5}".format(i)
1609 sb.readbin(fn, verbose=False)
1610 if sb.np > 0:
1611 if i == 0 or i == lastfile:
1612 if i == lastfile:
1613 if verbose:
1614 print("\tto")
1615 sb.writeVTK(verbose=verbose)
1616 if forces:
1617 sb.findContactStresses()
1618 sb.writeVTKforces(verbose=verbose)
1619 else:
1620 sb.writeVTK(verbose=False)
1621 if forces:
1622 sb.findContactStresses()
1623 sb.writeVTKforces(verbose=False)
1624 if self.fluid:
1625 if i == 0 or i == lastfile:
1626 if i == lastfile:
1627 if verbose:
1628 print("\tto")
1629 sb.writeFluidVTK(verbose=verbose,
1630 cell_centered=cell_centered)
1631 else:
1632 sb.writeFluidVTK(verbose=False, cell_centered=cell_centered)
1633
1634 def writeVTK(self, folder='../output/', verbose=True):
1635 '''
1636 Writes a VTK file with particle information to the ``../output/`` folder
1637 by default. The file name will be in the format ``<self.sid>.vtu``.
1638 The vtu files can be used to visualize the particles in ParaView.
1639
1640 After opening the vtu files, the particle fields will show up in the
1641 "Properties" list. Press "Apply" to import all fields into the ParaView
1642 session. The particles are visualized by selecting the imported data in
1643 the "Pipeline Browser". Afterwards, click the "Glyph" button in the
1644 "Common" toolbar, or go to the "Filters" menu, and press "Glyph" from
1645 the "Common" list. Choose "Sphere" as the "Glyph Type", choose "scalar"
1646 as the "Scale Mode". Check the "Edit" checkbox, and set the "Set Scale
1647 Factor" to 1.0. The field "Maximum Number of Points" may be increased if
1648 the number of particles exceed the default value. Finally press "Apply",
1649 and the particles will appear in the main window.
1650
1651 The sphere resolution may be adjusted ("Theta resolution", "Phi
1652 resolution") to increase the quality and the computational requirements
1653 of the rendering. All adjustments by default require the "Apply" button
1654 to be pressed before regenerating the view.
1655
1656 If several vtu files are generated for the same simulation (e.g. using
1657 the :func:`writeVTKall()` function), it is able to step the
1658 visualization through time by using the ParaView controls.
1659
1660 :param folder: The folder where to place the output binary file (default
1661 (default='../output/')
1662 :type folder: str
1663 :param verbose: Show diagnostic information (default=True)
1664 :type verbose: bool
1665 '''
1666
1667 fh = None
1668 try:
1669 targetbin = folder + '/' + self.sid + '.vtu' # unstructured grid
1670 if verbose:
1671 print('Output file: ' + targetbin)
1672
1673 fh = open(targetbin, 'w')
1674
1675 # the VTK data file format is documented in
1676 # http://www.vtk.org/VTK/img/file-formats.pdf
1677
1678 fh.write('<?xml version="1.0"?>\n') # XML header
1679 fh.write('<VTKFile type="UnstructuredGrid" version="0.1" '
1680 + 'byte_order="LittleEndian">\n') # VTK header
1681 fh.write(' <UnstructuredGrid>\n')
1682 fh.write(' <Piece NumberOfPoints="%d" NumberOfCells="0">\n' \
1683 % (self.np))
1684
1685 # Coordinates for each point (positions)
1686 fh.write(' <Points>\n')
1687 fh.write(' <DataArray name="Position [m]" type="Float32" '
1688 + 'NumberOfComponents="3" format="ascii">\n')
1689 fh.write(' ')
1690 for i in range(self.np):
1691 fh.write('%f %f %f ' % (self.x[i, 0], self.x[i, 1], self.x[i, 2]))
1692 fh.write('\n')
1693 fh.write(' </DataArray>\n')
1694 fh.write(' </Points>\n')
1695
1696 ### Data attributes
1697 fh.write(' <PointData Scalars="Diameter [m]" Vectors="vector">\n')
1698
1699 # Radii
1700 fh.write(' <DataArray type="Float32" Name="Diameter" '
1701 + 'format="ascii">\n')
1702 fh.write(' ')
1703 for i in range(self.np):
1704 fh.write('%f ' % (self.radius[i]*2.0))
1705 fh.write('\n')
1706 fh.write(' </DataArray>\n')
1707
1708 # Displacements (xyzsum)
1709 fh.write(' <DataArray type="Float32" Name="Displacement [m]" '
1710 + 'NumberOfComponents="3" format="ascii">\n')
1711 fh.write(' ')
1712 for i in range(self.np):
1713 fh.write('%f %f %f ' % \
1714 (self.xyzsum[i, 0], self.xyzsum[i, 1], self.xyzsum[i, 2]))
1715 fh.write('\n')
1716 fh.write(' </DataArray>\n')
1717
1718 # Velocity
1719 fh.write(' <DataArray type="Float32" Name="Velocity [m/s]" '
1720 + 'NumberOfComponents="3" format="ascii">\n')
1721 fh.write(' ')
1722 for i in range(self.np):
1723 fh.write('%f %f %f ' % \
1724 (self.vel[i, 0], self.vel[i, 1], self.vel[i, 2]))
1725 fh.write('\n')
1726 fh.write(' </DataArray>\n')
1727
1728 if self.fluid:
1729
1730 if self.cfd_solver == 0: # Navier Stokes
1731 # Fluid interaction force
1732 fh.write(' <DataArray type="Float32" '
1733 + 'Name="Fluid force total [N]" '
1734 + 'NumberOfComponents="3" format="ascii">\n')
1735 fh.write(' ')
1736 for i in range(self.np):
1737 fh.write('%f %f %f ' % \
1738 (self.f_sum[i, 0], self.f_sum[i, 1], \
1739 self.f_sum[i, 2]))
1740 fh.write('\n')
1741 fh.write(' </DataArray>\n')
1742
1743 # Fluid drag force
1744 fh.write(' <DataArray type="Float32" '
1745 + 'Name="Fluid drag force [N]" '
1746 + 'NumberOfComponents="3" format="ascii">\n')
1747 fh.write(' ')
1748 for i in range(self.np):
1749 fh.write('%f %f %f ' % \
1750 (self.f_d[i, 0],
1751 self.f_d[i, 1],
1752 self.f_d[i, 2]))
1753 fh.write('\n')
1754 fh.write(' </DataArray>\n')
1755
1756 # Fluid pressure force
1757 fh.write(' <DataArray type="Float32" '
1758 + 'Name="Fluid pressure force [N]" '
1759 + 'NumberOfComponents="3" format="ascii">\n')
1760 fh.write(' ')
1761 for i in range(self.np):
1762 fh.write('%f %f %f ' % \
1763 (self.f_p[i, 0], self.f_p[i, 1], self.f_p[i, 2]))
1764 fh.write('\n')
1765 fh.write(' </DataArray>\n')
1766
1767 if self.cfd_solver == 0: # Navier Stokes
1768 # Fluid viscous force
1769 fh.write(' <DataArray type="Float32" '
1770 + 'Name="Fluid viscous force [N]" '
1771 + 'NumberOfComponents="3" format="ascii">\n')
1772 fh.write(' ')
1773 for i in range(self.np):
1774 fh.write('%f %f %f ' % \
1775 (self.f_v[i, 0],
1776 self.f_v[i, 1],
1777 self.f_v[i, 2]))
1778 fh.write('\n')
1779 fh.write(' </DataArray>\n')
1780
1781 # fixvel
1782 fh.write(' <DataArray type="Float32" Name="FixedVel" '
1783 + 'format="ascii">\n')
1784 fh.write(' ')
1785 for i in range(self.np):
1786 fh.write('%f ' % (self.fixvel[i]))
1787 fh.write('\n')
1788 fh.write(' </DataArray>\n')
1789
1790 # Force
1791 fh.write(' <DataArray type="Float32" Name="Force [N]" '
1792 + 'NumberOfComponents="3" format="ascii">\n')
1793 fh.write(' ')
1794 for i in range(self.np):
1795 fh.write('%f %f %f ' % (self.force[i, 0],
1796 self.force[i, 1],
1797 self.force[i, 2]))
1798 fh.write('\n')
1799 fh.write(' </DataArray>\n')
1800
1801 # Angular Position
1802 fh.write(' <DataArray type="Float32" Name="Angular position'
1803 + '[rad]" '
1804 + 'NumberOfComponents="3" format="ascii">\n')
1805 fh.write(' ')
1806 for i in range(self.np):
1807 fh.write('%f %f %f ' % (self.angpos[i, 0],
1808 self.angpos[i, 1],
1809 self.angpos[i, 2]))
1810 fh.write('\n')
1811 fh.write(' </DataArray>\n')
1812
1813 # Angular Velocity
1814 fh.write(' <DataArray type="Float32" Name="Angular velocity'
1815 + ' [rad/s]" '
1816 + 'NumberOfComponents="3" format="ascii">\n')
1817 fh.write(' ')
1818 for i in range(self.np):
1819 fh.write('%f %f %f ' % (self.angvel[i, 0],
1820 self.angvel[i, 1],
1821 self.angvel[i, 2]))
1822 fh.write('\n')
1823 fh.write(' </DataArray>\n')
1824
1825 # Torque
1826 fh.write(' <DataArray type="Float32" Name="Torque [Nm]" '
1827 + 'NumberOfComponents="3" format="ascii">\n')
1828 fh.write(' ')
1829 for i in range(self.np):
1830 fh.write('%f %f %f ' % (self.torque[i, 0],
1831 self.torque[i, 1],
1832 self.torque[i, 2]))
1833 fh.write('\n')
1834 fh.write(' </DataArray>\n')
1835
1836 # Shear energy rate
1837 fh.write(' <DataArray type="Float32" Name="Shear Energy '
1838 + 'Rate [J/s]" '
1839 + 'format="ascii">\n')
1840 fh.write(' ')
1841 for i in range(self.np):
1842 fh.write('%f ' % (self.es_dot[i]))
1843 fh.write('\n')
1844 fh.write(' </DataArray>\n')
1845
1846 # Shear energy
1847 fh.write(' <DataArray type="Float32" Name="Shear Energy [J]"'
1848 + ' format="ascii">\n')
1849 fh.write(' ')
1850 for i in range(self.np):
1851 fh.write('%f ' % (self.es[i]))
1852 fh.write('\n')
1853 fh.write(' </DataArray>\n')
1854
1855 # Viscous energy rate
1856 fh.write(' <DataArray type="Float32" '
1857 + 'Name="Viscous Energy Rate [J/s]" format="ascii">\n')
1858 fh.write(' ')
1859 for i in range(self.np):
1860 fh.write('%f ' % (self.ev_dot[i]))
1861 fh.write('\n')
1862 fh.write(' </DataArray>\n')
1863
1864 # Shear energy
1865 fh.write(' <DataArray type="Float32" '
1866 + 'Name="Viscous Energy [J]" '
1867 + 'format="ascii">\n')
1868 fh.write(' ')
1869 for i in range(self.np):
1870 fh.write('%f ' % (self.ev[i]))
1871 fh.write('\n')
1872 fh.write(' </DataArray>\n')
1873
1874 # Pressure
1875 fh.write(' <DataArray type="Float32" Name="Pressure [Pa]" '
1876 + 'format="ascii">\n')
1877 fh.write(' ')
1878 for i in range(self.np):
1879 fh.write('%f ' % (self.p[i]))
1880 fh.write('\n')
1881 fh.write(' </DataArray>\n')
1882
1883 # Color
1884 fh.write(' <DataArray type="Int32" Name="Type color" '
1885 + 'format="ascii">\n')
1886 fh.write(' ')
1887 for i in range(self.np):
1888 fh.write('%d ' % (self.color[i]))
1889 fh.write('\n')
1890 fh.write(' </DataArray>\n')
1891
1892 # Footer
1893 fh.write(' </PointData>\n')
1894 fh.write(' <Cells>\n')
1895 fh.write(' <DataArray type="Int32" Name="connectivity" '
1896 + 'format="ascii">\n')
1897 fh.write(' </DataArray>\n')
1898 fh.write(' <DataArray type="Int32" Name="offsets" '
1899 + 'format="ascii">\n')
1900 fh.write(' </DataArray>\n')
1901 fh.write(' <DataArray type="UInt8" Name="types" '
1902 + 'format="ascii">\n')
1903 fh.write(' </DataArray>\n')
1904 fh.write(' </Cells>\n')
1905 fh.write(' </Piece>\n')
1906 fh.write(' </UnstructuredGrid>\n')
1907 fh.write('</VTKFile>')
1908
1909 finally:
1910 if fh is not None:
1911 fh.close()
1912
1913 def writeVTKforces(self, folder='../output/', verbose=True):
1914 '''
1915 Writes a VTK file with particle-interaction information to the
1916 ``../output/`` folder by default. The file name will be in the format
1917 ``<self.sid>.vtp``. The vtp files can be used to visualize the
1918 particle interactions in ParaView. First use the "Cell Data to Point
1919 Data" filter, and afterwards show the contact network with the "Tube"
1920 filter.
1921
1922 :param folder: The folder where to place the output file (default
1923 (default='../output/')
1924 :type folder: str
1925 :param verbose: Show diagnostic information (default=True)
1926 :type verbose: bool
1927 '''
1928
1929 if not py_vtk:
1930 print('Error: vtk module not found, cannot writeVTKforces.')
1931 return
1932
1933 filename = folder + '/forces-' + self.sid + '.vtp' # Polygon data
1934
1935 # points mark the particle centers
1936 points = vtk.vtkPoints()
1937
1938 # lines mark the particle connectivity
1939 lines = vtk.vtkCellArray()
1940
1941 # colors
1942 #colors = vtk.vtkUnsignedCharArray()
1943 #colors.SetNumberOfComponents(3)
1944 #colors.SetName('Colors')
1945 #colors.SetNumberOfTuples(self.overlaps.size)
1946
1947 # scalars
1948 forces = vtk.vtkDoubleArray()
1949 forces.SetName("Force [N]")
1950 forces.SetNumberOfComponents(1)
1951 #forces.SetNumberOfTuples(self.overlaps.size)
1952 forces.SetNumberOfValues(self.overlaps.size)
1953
1954 stresses = vtk.vtkDoubleArray()
1955 stresses.SetName("Stress [Pa]")
1956 stresses.SetNumberOfComponents(1)
1957 stresses.SetNumberOfValues(self.overlaps.size)
1958
1959 for i in numpy.arange(self.overlaps.size):
1960 points.InsertNextPoint(self.x[self.pairs[0, i], :])
1961 points.InsertNextPoint(self.x[self.pairs[1, i], :])
1962 line = vtk.vtkLine()
1963 line.GetPointIds().SetId(0, 2*i) # index of particle 1
1964 line.GetPointIds().SetId(1, 2*i + 1) # index of particle 2
1965 lines.InsertNextCell(line)
1966 #colors.SetTupleValue(i, [100, 100, 100])
1967 forces.SetValue(i, self.f_n_magn[i])
1968 stresses.SetValue(i, self.sigma_contacts[i])
1969
1970 # initalize VTK data structure
1971 polydata = vtk.vtkPolyData()
1972
1973 polydata.SetPoints(points)
1974 polydata.SetLines(lines)
1975 #polydata.GetCellData().SetScalars(colors)
1976 #polydata.GetCellData().SetScalars(forces) # default scalar
1977 polydata.GetCellData().SetScalars(forces) # default scalar
1978 #polydata.GetCellData().AddArray(forces)
1979 polydata.GetCellData().AddArray(stresses)
1980 #polydata.GetPointData().AddArray(stresses)
1981 #polydata.GetPointData().SetScalars(stresses) # default scalar
1982
1983 # write VTK XML image data file
1984 writer = vtk.vtkXMLPolyDataWriter()
1985 writer.SetFileName(filename)
1986 if vtk.VTK_MAJOR_VERSION <= 5:
1987 writer.SetInput(polydata)
1988 else:
1989 writer.SetInputData(polydata)
1990 writer.Write()
1991 #writer.Update()
1992 if verbose:
1993 print('Output file: ' + filename)
1994
1995
1996 def writeFluidVTK(self, folder='../output/', cell_centered=True,
1997 verbose=True):
1998 '''
1999 Writes a VTK file for the fluid grid to the ``../output/`` folder by
2000 default. The file name will be in the format ``fluid-<self.sid>.vti``.
2001 The vti files can be used for visualizing the fluid in ParaView.
2002
2003 The scalars (pressure, porosity, porosity change) and the velocity
2004 vectors are either placed in a grid where the grid corners correspond to
2005 the computational grid center (cell_centered=False). This results in a
2006 grid that doesn't appears to span the simulation domain, and values are
2007 smoothly interpolated on the cell faces. Alternatively, the
2008 visualization grid is equal to the computational grid, and cells face
2009 colors are not interpolated (cell_centered=True, default behavior).
2010
2011 The fluid grid is visualized by opening the vti files, and pressing
2012 "Apply" to import all fluid field properties. To visualize the scalar
2013 fields, such as the pressure, the porosity, the porosity change or the
2014 velocity magnitude, choose "Surface" or "Surface With Edges" as the
2015 "Representation". Choose the desired property as the "Coloring" field.
2016 It may be desirable to show the color bar by pressing the "Show" button,
2017 and "Rescale" to fit the color range limits to the current file. The
2018 coordinate system can be displayed by checking the "Show Axis" field.
2019 All adjustments by default require the "Apply" button to be pressed
2020 before regenerating the view.
2021
2022 The fluid vector fields (e.g. the fluid velocity) can be visualizing by
2023 e.g. arrows. To do this, select the fluid data in the "Pipeline
2024 Browser". Press "Glyph" from the "Common" toolbar, or go to the
2025 "Filters" mennu, and press "Glyph" from the "Common" list. Make sure
2026 that "Arrow" is selected as the "Glyph type", and "Velocity" as the
2027 "Vectors" value. Adjust the "Maximum Number of Points" to be at least as
2028 big as the number of fluid cells in the grid. Press "Apply" to visualize
2029 the arrows.
2030
2031 To visualize the cell-centered data with smooth interpolation, and in
2032 order to visualize fluid vector fields, the cell-centered mesh is
2033 selected in the "Pipeline Browser", and is filtered using "Filters" ->
2034 "Alphabetical" -> "Cell Data to Point Data".
2035
2036 If several data files are generated for the same simulation (e.g. using
2037 the :func:`writeVTKall()` function), it is able to step the
2038 visualization through time by using the ParaView controls.
2039
2040 :param folder: The folder where to place the output binary file (default
2041 (default='../output/')
2042 :type folder: str
2043 :param cell_centered: put scalars and vectors at cell centers (True) or
2044 cell corners (False), (default=True)
2045 :type cell_centered: bool
2046 :param verbose: Show diagnostic information (default=True)
2047 :type verbose: bool
2048 '''
2049 if not py_vtk:
2050 print('Error: vtk module not found, cannot writeFluidVTK.')
2051 return
2052
2053 filename = folder + '/fluid-' + self.sid + '.vti' # image grid
2054
2055 # initalize VTK data structure
2056 grid = vtk.vtkImageData()
2057 dx = (self.L-self.origo)/self.num # cell center spacing
2058 if cell_centered:
2059 grid.SetOrigin(self.origo)
2060 else:
2061 grid.SetOrigin(self.origo + 0.5*dx)
2062 grid.SetSpacing(dx)
2063 if cell_centered:
2064 grid.SetDimensions(self.num + 1) # no. of points in each direction
2065 else:
2066 grid.SetDimensions(self.num) # no. of points in each direction
2067
2068 # array of scalars: hydraulic pressures
2069 pres = vtk.vtkDoubleArray()
2070 pres.SetName("Pressure [Pa]")
2071 pres.SetNumberOfComponents(1)
2072 if cell_centered:
2073 pres.SetNumberOfTuples(grid.GetNumberOfCells())
2074 else:
2075 pres.SetNumberOfTuples(grid.GetNumberOfPoints())
2076
2077 # array of vectors: hydraulic velocities
2078 vel = vtk.vtkDoubleArray()
2079 vel.SetName("Velocity [m/s]")
2080 vel.SetNumberOfComponents(3)
2081 if cell_centered:
2082 vel.SetNumberOfTuples(grid.GetNumberOfCells())
2083 else:
2084 vel.SetNumberOfTuples(grid.GetNumberOfPoints())
2085
2086 # array of scalars: porosities
2087 poros = vtk.vtkDoubleArray()
2088 poros.SetName("Porosity [-]")
2089 poros.SetNumberOfComponents(1)
2090 if cell_centered:
2091 poros.SetNumberOfTuples(grid.GetNumberOfCells())
2092 else:
2093 poros.SetNumberOfTuples(grid.GetNumberOfPoints())
2094
2095 # array of scalars: porosity change
2096 dporos = vtk.vtkDoubleArray()
2097 dporos.SetName("Porosity change [1/s]")
2098 dporos.SetNumberOfComponents(1)
2099 if cell_centered:
2100 dporos.SetNumberOfTuples(grid.GetNumberOfCells())
2101 else:
2102 dporos.SetNumberOfTuples(grid.GetNumberOfPoints())
2103
2104 # array of scalars: Reynold's number
2105 Re_values = self.ReynoldsNumber()
2106 Re = vtk.vtkDoubleArray()
2107 Re.SetName("Reynolds number [-]")
2108 Re.SetNumberOfComponents(1)
2109 if cell_centered:
2110 Re.SetNumberOfTuples(grid.GetNumberOfCells())
2111 else:
2112 Re.SetNumberOfTuples(grid.GetNumberOfPoints())
2113
2114 # Find permeabilities if the Darcy solver is used
2115 if self.cfd_solver[0] == 1:
2116 self.findPermeabilities()
2117 k = vtk.vtkDoubleArray()
2118 k.SetName("Permeability [m*m]")
2119 k.SetNumberOfComponents(1)
2120 if cell_centered:
2121 k.SetNumberOfTuples(grid.GetNumberOfCells())
2122 else:
2123 k.SetNumberOfTuples(grid.GetNumberOfPoints())
2124
2125 self.findHydraulicConductivities()
2126 K = vtk.vtkDoubleArray()
2127 K.SetName("Conductivity [m/s]")
2128 K.SetNumberOfComponents(1)
2129 if cell_centered:
2130 K.SetNumberOfTuples(grid.GetNumberOfCells())
2131 else:
2132 K.SetNumberOfTuples(grid.GetNumberOfPoints())
2133
2134 p_f_constant = vtk.vtkDoubleArray()
2135 p_f_constant.SetName("Constant pressure [-]")
2136 p_f_constant.SetNumberOfComponents(1)
2137 if cell_centered:
2138 p_f_constant.SetNumberOfTuples(grid.GetNumberOfCells())
2139 else:
2140 p_f_constant.SetNumberOfTuples(grid.GetNumberOfPoints())
2141
2142 # insert values
2143 for z in range(self.num[2]):
2144 for y in range(self.num[1]):
2145 for x in range(self.num[0]):
2146 idx = x + self.num[0]*y + self.num[0]*self.num[1]*z
2147 pres.SetValue(idx, self.p_f[x, y, z])
2148 vel.SetTuple(idx, self.v_f[x, y, z, :])
2149 poros.SetValue(idx, self.phi[x, y, z])
2150 dporos.SetValue(idx, self.dphi[x, y, z])
2151 Re.SetValue(idx, Re_values[x, y, z])
2152 if self.cfd_solver[0] == 1:
2153 k.SetValue(idx, self.k[x, y, z])
2154 K.SetValue(idx, self.K[x, y, z])
2155 p_f_constant.SetValue(idx, self.p_f_constant[x, y, z])
2156
2157 # add pres array to grid
2158 if cell_centered:
2159 grid.GetCellData().AddArray(pres)
2160 grid.GetCellData().AddArray(vel)
2161 grid.GetCellData().AddArray(poros)
2162 grid.GetCellData().AddArray(dporos)
2163 grid.GetCellData().AddArray(Re)
2164 if self.cfd_solver[0] == 1:
2165 grid.GetCellData().AddArray(k)
2166 grid.GetCellData().AddArray(K)
2167 grid.GetCellData().AddArray(p_f_constant)
2168 else:
2169 grid.GetPointData().AddArray(pres)
2170 grid.GetPointData().AddArray(vel)
2171 grid.GetPointData().AddArray(poros)
2172 grid.GetPointData().AddArray(dporos)
2173 grid.GetPointData().AddArray(Re)
2174 if self.cfd_solver[0] == 1:
2175 grid.GetPointData().AddArray(k)
2176 grid.GetPointData().AddArray(K)
2177 grid.GetPointData().AddArray(p_f_constant)
2178
2179 # write VTK XML image data file
2180 writer = vtk.vtkXMLImageDataWriter()
2181 writer.SetFileName(filename)
2182 #writer.SetInput(grid) # deprecated from VTK 6
2183 writer.SetInputData(grid)
2184 writer.Update()
2185 if verbose:
2186 print('Output file: ' + filename)
2187
2188 def show(self, coloring=numpy.array([]), resolution=6):
2189 '''
2190 Show a rendering of all particles in a window.
2191
2192 :param coloring: Color the particles from red to white to blue according
2193 to the values in this array.
2194 :type coloring: numpy.array
2195 :param resolution: The resolution of the rendered spheres. Larger values
2196 increase the performance requirements.
2197 :type resolution: int
2198 '''
2199
2200 if not py_vtk:
2201 print('Error: vtk module not found, cannot show scene.')
2202 return
2203
2204 # create a rendering window and renderer
2205 ren = vtk.vtkRenderer()
2206 renWin = vtk.vtkRenderWindow()
2207 renWin.AddRenderer(ren)
2208
2209 # create a renderwindowinteractor
2210 iren = vtk.vtkRenderWindowInteractor()
2211 iren.SetRenderWindow(renWin)
2212
2213 if coloring.any():
2214 #min_value = numpy.min(coloring)
2215 max_value = numpy.max(coloring)
2216 #min_rgb = numpy.array([50, 50, 50])
2217 #max_rgb = numpy.array([255, 255, 255])
2218 #def color(value):
2219 #return (max_rgb - min_rgb) * (value - min_value)
2220
2221 def red(ratio):
2222 return numpy.fmin(1.0, 0.209*ratio**3. - 2.49*ratio**2. + 3.0*ratio
2223 + 0.0109)
2224 def green(ratio):
2225 return numpy.fmin(1.0, -2.44*ratio**2. + 2.15*ratio + 0.369)
2226 def blue(ratio):
2227 return numpy.fmin(1.0, -2.21*ratio**2. + 1.61*ratio + 0.573)
2228
2229 for i in numpy.arange(self.np):
2230
2231 # create source
2232 source = vtk.vtkSphereSource()
2233 source.SetCenter(self.x[i, :])
2234 source.SetRadius(self.radius[i])
2235 source.SetThetaResolution(resolution)
2236 source.SetPhiResolution(resolution)
2237
2238 # mapper
2239 mapper = vtk.vtkPolyDataMapper()
2240 if vtk.VTK_MAJOR_VERSION <= 5:
2241 mapper.SetInput(source.GetOutput())
2242 else:
2243 mapper.SetInputConnection(source.GetOutputPort())
2244
2245 # actor
2246 actor = vtk.vtkActor()
2247 actor.SetMapper(mapper)
2248
2249 # color
2250 if coloring.any():
2251 ratio = coloring[i]/max_value
2252 r, g, b = red(ratio), green(ratio), blue(ratio)
2253 actor.GetProperty().SetColor(r, g, b)
2254
2255 # assign actor to the renderer
2256 ren.AddActor(actor)
2257
2258 ren.SetBackground(0.3, 0.3, 0.3)
2259
2260 # enable user interface interactor
2261 iren.Initialize()
2262 renWin.Render()
2263 iren.Start()
2264
2265 def readfirst(self, verbose=True):
2266 '''
2267 Read the first output file from the ``../output/`` folder, corresponding
2268 to the object simulation id (``self.sid``).
2269
2270 :param verbose: Display diagnostic information (default=True)
2271 :type verbose: bool
2272
2273 See also :func:`readbin()`, :func:`readlast()`, :func:`readsecond`, and
2274 :func:`readstep`.
2275 '''
2276
2277 fn = '../output/' + self.sid + '.output00000.bin'
2278 self.readbin(fn, verbose)
2279
2280 def readsecond(self, verbose=True):
2281 '''
2282 Read the second output file from the ``../output/`` folder,
2283 corresponding to the object simulation id (``self.sid``).
2284
2285 :param verbose: Display diagnostic information (default=True)
2286 :type verbose: bool
2287
2288 See also :func:`readbin()`, :func:`readfirst()`, :func:`readlast()`,
2289 and :func:`readstep`.
2290 '''
2291 fn = '../output/' + self.sid + '.output00001.bin'
2292 self.readbin(fn, verbose)
2293
2294 def readstep(self, step, verbose=True):
2295 '''
2296 Read a output file from the ``../output/`` folder, corresponding
2297 to the object simulation id (``self.sid``).
2298
2299 :param step: The output file number to read, starting from 0.
2300 :type step: int
2301 :param verbose: Display diagnostic information (default=True)
2302 :type verbose: bool
2303
2304 See also :func:`readbin()`, :func:`readfirst()`, :func:`readlast()`,
2305 and :func:`readsecond`.
2306 '''
2307 fn = "../output/{0}.output{1:0=5}.bin".format(self.sid, step)
2308 self.readbin(fn, verbose)
2309
2310 def readlast(self, verbose=True):
2311 '''
2312 Read the last output file from the ``../output/`` folder, corresponding
2313 to the object simulation id (``self.sid``).
2314
2315 :param verbose: Display diagnostic information (default=True)
2316 :type verbose: bool
2317
2318 See also :func:`readbin()`, :func:`readfirst()`, :func:`readsecond`, and
2319 :func:`readstep`.
2320 '''
2321 lastfile = status(self.sid)
2322 fn = "../output/{0}.output{1:0=5}.bin".format(self.sid, lastfile)
2323 self.readbin(fn, verbose)
2324
2325 def readTime(self, time, verbose=True):
2326 '''
2327 Read the output file most closely corresponding to the time given as an
2328 argument.
2329
2330 :param time: The desired current time [s]
2331 :type time: float
2332
2333 See also :func:`readbin()`, :func:`readfirst()`, :func:`readsecond`, and
2334 :func:`readstep`.
2335 '''
2336
2337 self.readfirst(verbose=False)
2338 t_first = self.currentTime()
2339 n_first = self.time_step_count[0]
2340
2341 self.readlast(verbose=False)
2342 t_last = self.currentTime()
2343 n_last = self.time_step_count[0]
2344
2345 if time < t_first or time > t_last:
2346 raise Exception('Error: The specified time {} s is outside the ' +
2347 'range of output files [{}; {}] s.'
2348 .format(time, t_first, t_last))
2349
2350 dt_dn = (t_last - t_first)/(n_last - n_first)
2351 step = int((time - t_first)/dt_dn) + n_first + 1
2352 self.readstep(step, verbose=verbose)
2353
2354 def generateRadii(self, psd='logn', mean=440e-6, variance=8.8e-9,
2355 histogram=False):
2356 '''
2357 Draw random particle radii from a selected probability distribution.
2358 The larger the variance of radii is, the slower the computations will
2359 run. The reason is two-fold: The smallest particle dictates the time
2360 step length, where smaller particles cause shorter time steps. At the
2361 same time, the largest particle determines the sorting cell size, where
2362 larger particles cause larger cells. Larger cells are likely to contain
2363 more particles, causing more contact checks.
2364
2365 :param psd: The particle side distribution. One possible value is
2366 ``logn``, which is a log-normal probability distribution, suitable
2367 for approximating well-sorted, coarse sediments. The other possible
2368 value is ``uni``, which is a uniform distribution from
2369 ``mean - variance`` to ``mean + variance``.
2370 :type psd: str
2371 :param mean: The mean radius [m] (default=440e-6 m)
2372 :type mean: float
2373 :param variance: The variance in the probability distribution
2374 [m].
2375 :type variance: float
2376
2377 See also: :func:`generateBimodalRadii()`.
2378 '''
2379
2380 if psd == 'logn': # Log-normal probability distribution
2381 mu = math.log((mean**2)/math.sqrt(variance+mean**2))
2382 sigma = math.sqrt(math.log(variance/(mean**2)+1))
2383 self.radius = numpy.random.lognormal(mu, sigma, self.np)
2384 elif psd == 'uni': # Uniform distribution
2385 radius_min = mean - variance
2386 radius_max = mean + variance
2387 self.radius = numpy.random.uniform(radius_min, radius_max, self.np)
2388 else:
2389 raise Exception('Particle size distribution type not understood ('
2390 + str(psd) + '). '
2391 + 'Valid values are \'uni\' or \'logn\'')
2392
2393 # Show radii as histogram
2394 if histogram and py_mpl:
2395 fig = plt.figure(figsize=(8, 8))
2396 figtitle = 'Particle size distribution, {0} particles'\
2397 .format(self.np)
2398 fig.text(0.5, 0.95, figtitle, horizontalalignment='center',
2399 fontproperties=FontProperties(size=18))
2400 bins = 20
2401
2402 # Create histogram
2403 plt.hist(self.radius, bins)
2404
2405 # Plot
2406 plt.xlabel('Radii [m]')
2407 plt.ylabel('Count')
2408 plt.axis('tight')
2409 fig.savefig(self.sid + '-psd.png')
2410 fig.clf()
2411
2412 def generateBimodalRadii(self, r_small=0.005, r_large=0.05, ratio=0.2,
2413 verbose=True):
2414 '''
2415 Draw random radii from two distinct sizes.
2416
2417 :param r_small: Radii of small population [m], in ]0;r_large[
2418 :type r_small: float
2419 :param r_large: Radii of large population [m], in ]r_small;inf[
2420 :type r_large: float
2421 :param ratio: Approximate volumetric ratio between the two
2422 populations (large/small).
2423 :type ratio: float
2424
2425 See also: :func:`generateRadii()`.
2426 '''
2427 if r_small >= r_large:
2428 raise Exception("r_large should be larger than r_small")
2429
2430 V_small = V_sphere(r_small)
2431 V_large = V_sphere(r_large)
2432 nlarge = int(V_small/V_large * ratio * self.np) # ignore void volume
2433
2434 self.radius[:] = r_small
2435 self.radius[0:nlarge] = r_large
2436 numpy.random.shuffle(self.radius)
2437
2438 # Test volumetric ratio
2439 V_small_total = V_small * (self.np - nlarge)
2440 V_large_total = V_large * nlarge
2441 if abs(V_large_total/V_small_total - ratio) > 1.0e5:
2442 raise Exception("Volumetric ratio seems wrong")
2443
2444 if verbose:
2445 print("generateBimodalRadii created " + str(nlarge)
2446 + " large particles, and " + str(self.np - nlarge)
2447 + " small")
2448
2449 def checkerboardColors(self, nx=6, ny=6, nz=6):
2450 '''
2451 Assign checkerboard color values to the particles in an orthogonal grid.
2452
2453 :param nx: Number of color values along the x axis
2454 :type nx: int
2455 :param ny: Number of color values along the y ayis
2456 :type ny: int
2457 :param nz: Number of color values along the z azis
2458 :type nz: int
2459 '''
2460 x_min = numpy.min(self.x[:, 0])
2461 x_max = numpy.max(self.x[:, 0])
2462 y_min = numpy.min(self.x[:, 1])
2463 y_max = numpy.max(self.x[:, 1])
2464 z_min = numpy.min(self.x[:, 2])
2465 z_max = numpy.max(self.x[:, 2])
2466 for i in numpy.arange(self.np):
2467 ix = numpy.floor((self.x[i, 0] - x_min)/(x_max/nx))
2468 iy = numpy.floor((self.x[i, 1] - y_min)/(y_max/ny))
2469 iz = numpy.floor((self.x[i, 2] - z_min)/(z_max/nz))
2470 self.color[i] = (-1)**ix + (-1)**iy + (-1)**iz
2471
2472 def contactModel(self, contactmodel):
2473 '''
2474 Define which contact model to use for the tangential component of
2475 particle-particle interactions. The elastic-viscous-frictional contact
2476 model (2) is considered to be the most realistic contact model, while
2477 the viscous-frictional contact model is significantly faster.
2478
2479 :param contactmodel: The type of tangential contact model to use
2480 (visco-frictional=1, elasto-visco-frictional=2)
2481 :type contactmodel: int
2482 '''
2483 self.contactmodel[0] = contactmodel
2484
2485 def wall0iz(self):
2486 '''
2487 Returns the cell index of wall 0 along z.
2488
2489 :returns: z cell index
2490 :return type: int
2491 '''
2492 if self.nw > 0:
2493 return int(self.w_x[0]/(self.L[2]/self.num[2]))
2494 else:
2495 raise Exception('No dynamic top wall present!')
2496
2497 def normalBoundariesXY(self):
2498 '''
2499 Set the x and y boundary conditions to be static walls.
2500
2501 See also :func:`periodicBoundariesXY()` and
2502 :func:`periodicBoundariesX()`
2503 '''
2504 self.periodic[0] = 0
2505
2506 def periodicBoundariesXY(self):
2507 '''
2508 Set the x and y boundary conditions to be periodic.
2509
2510 See also :func:`normalBoundariesXY()` and
2511 :func:`periodicBoundariesX()`
2512 '''
2513 self.periodic[0] = 1
2514
2515 def periodicBoundariesX(self):
2516 '''
2517 Set the x boundary conditions to be periodic.
2518
2519 See also :func:`normalBoundariesXY()` and
2520 :func:`periodicBoundariesXY()`
2521 '''
2522 self.periodic[0] = 2
2523
2524 def adaptiveGrid(self):
2525 '''
2526 Set the height of the fluid grid to automatically readjust to the
2527 height of the granular assemblage, as dictated by the position of the
2528 top wall. This will readjust `self.L[2]` during the simulation to
2529 equal the position of the top wall `self.w_x[0]`.
2530
2531 See also :func:`staticGrid()`
2532 '''
2533 self.adaptive[0] = 1
2534
2535 def staticGrid(self):
2536 '''
2537 Set the height of the fluid grid to be constant as set in `self.L[2]`.
2538
2539 See also :func:`adaptiveGrid()`
2540 '''
2541 self.adaptive[0] = 0
2542
2543 def initRandomPos(self, gridnum=numpy.array([12, 12, 36]), dx=-1.0):
2544 '''
2545 Initialize particle positions in completely random configuration. Radii
2546 *must* be set beforehand. If the x and y boundaries are set as periodic,
2547 the particle centers will be placed all the way to the edge. On regular,
2548 non-periodic boundaries, the particles are restrained at the edges to
2549 make space for their radii within the bounding box.
2550
2551 :param gridnum: The number of sorting cells in each spatial direction
2552 (default=[12, 12, 36])
2553 :type gridnum: numpy.array
2554 :param dx: The cell width in any direction. If the default value is used
2555 (-1), the cell width is calculated to fit the largest particle.
2556 :type dx: float
2557 '''
2558
2559 # Calculate cells in grid
2560 self.num = gridnum
2561 r_max = numpy.max(self.radius)
2562
2563 # Cell configuration
2564 if dx > 0.0:
2565 cellsize = dx
2566 else:
2567 cellsize = 2.1 * numpy.amax(self.radius)
2568
2569 # World size
2570 self.L = self.num * cellsize
2571
2572 # Particle positions randomly distributed without overlap
2573 for i in range(self.np):
2574 overlaps = True
2575 while overlaps:
2576 overlaps = False
2577
2578 # Draw random position
2579 for d in range(self.nd):
2580 self.x[i, d] = (self.L[d] - self.origo[d] - 2*r_max) \
2581 * numpy.random.random_sample() \
2582 + self.origo[d] + r_max
2583
2584 # Check other particles for overlaps
2585 for j in range(i-1):
2586 delta = self.x[i] - self.x[j]
2587 delta_len = math.sqrt(numpy.dot(delta, delta)) \
2588 - (self.radius[i] + self.radius[j])
2589 if delta_len < 0.0:
2590 overlaps = True
2591 print("\rFinding non-overlapping particle positions, "
2592 + "{0} % complete".format(numpy.ceil(i/self.np*100)))
2593
2594 # Print newline
2595 print()
2596
2597
2598 def defineWorldBoundaries(self, L, origo=[0.0, 0.0, 0.0], dx=-1):
2599 '''
2600 Set the boundaries of the world. Particles will only be able to interact
2601 within this domain. With dynamic walls, allow space for expansions.
2602 *Important*: The particle radii have to be set beforehand. The world
2603 edges act as static walls.
2604
2605 :param L: The upper boundary of the domain [m]
2606 :type L: numpy.array
2607 :param origo: The lower boundary of the domain [m]. Negative values
2608 won't work. Default=[0.0, 0.0, 0.0].
2609 :type origo: numpy.array
2610 :param dx: The cell width in any direction. If the default value is used
2611 (-1), the cell width is calculated to fit the largest particle.
2612 :type dx: float
2613 '''
2614
2615 # Cell configuration
2616 if dx > 0.0:
2617 cellsize_min = dx
2618 else:
2619 if self.np < 1:
2620 raise Exception('Error: You need to define dx in ' +
2621 'defineWorldBoundaries if there are no ' +
2622 'particles in the simulation.')
2623 cellsize_min = 2.1 * numpy.amax(self.radius)
2624
2625 # Lower boundary of the sorting grid
2626 self.origo[:] = origo[:]
2627
2628 # Upper boundary of the sorting grid
2629 self.L[:] = L[:]
2630
2631 # Adjust the number of sorting cells along each axis to fit the largest
2632 # particle size and the world size
2633 self.num[0] = numpy.ceil((self.L[0]-self.origo[0])/cellsize_min)
2634 self.num[1] = numpy.ceil((self.L[1]-self.origo[1])/cellsize_min)
2635 self.num[2] = numpy.ceil((self.L[2]-self.origo[2])/cellsize_min)
2636
2637 #if (self.num.any() < 4):
2638 #if (self.num[0] < 4 or self.num[1] < 4 or self.num[2] < 4):
2639 if self.num[0] < 3 or self.num[1] < 3 or self.num[2] < 3:
2640 raise Exception("Error: The grid must be at least 3 cells in each "
2641 + "direction\nGrid: x={}, y={}, z={}\n"
2642 .format(self.num[0], self.num[1], self.num[2])
2643 + "Please increase the world size.")
2644
2645 def initGrid(self, dx=-1):
2646 '''
2647 Initialize grid suitable for the particle positions set previously.
2648 The margin parameter adjusts the distance (in no. of max. radii)
2649 from the particle boundaries.
2650 *Important*: The particle radii have to be set beforehand if the cell
2651 width isn't specified by `dx`.
2652
2653 :param dx: The cell width in any direction. If the default value is used
2654 (-1), the cell width is calculated to fit the largest particle.
2655 :type dx: float
2656 '''
2657
2658 # Cell configuration
2659 if dx > 0.0:
2660 cellsize_min = dx
2661 else:
2662 cellsize_min = 2.1 * numpy.amax(self.radius)
2663 self.num[0] = numpy.ceil((self.L[0]-self.origo[0])/cellsize_min)
2664 self.num[1] = numpy.ceil((self.L[1]-self.origo[1])/cellsize_min)
2665 self.num[2] = numpy.ceil((self.L[2]-self.origo[2])/cellsize_min)
2666
2667 if self.num[0] < 4 or self.num[1] < 4 or self.num[2] < 4:
2668 raise Exception("Error: The grid must be at least 3 cells in each "
2669 + "direction\nGrid: x={}, y={}, z={}"
2670 .format(self.num[0], self.num[1], self.num[2]))
2671
2672 # Put upper wall at top boundary
2673 if self.nw > 0:
2674 self.w_x[0] = self.L[0]
2675
2676 def initGridAndWorldsize(self, margin=2.0):
2677 '''
2678 Initialize grid suitable for the particle positions set previously.
2679 The margin parameter adjusts the distance (in no. of max. radii)
2680 from the particle boundaries. If the upper wall is dynamic, it is placed
2681 at the top boundary of the world.
2682
2683 :param margin: Distance to world boundary in no. of max. particle radii
2684 :type margin: float
2685 '''
2686
2687 # Cell configuration
2688 r_max = numpy.amax(self.radius)
2689
2690 # Max. and min. coordinates of world
2691 self.origo = numpy.array([numpy.amin(self.x[:, 0] - self.radius[:]),
2692 numpy.amin(self.x[:, 1] - self.radius[:]),
2693 numpy.amin(self.x[:, 2] - self.radius[:])]) \
2694 - margin*r_max
2695 self.L = numpy.array([numpy.amax(self.x[:, 0] + self.radius[:]),
2696 numpy.amax(self.x[:, 1] + self.radius[:]),
2697 numpy.amax(self.x[:, 2] + self.radius[:])]) \
2698 + margin*r_max
2699
2700 cellsize_min = 2.1 * r_max
2701 self.num[0] = numpy.ceil((self.L[0]-self.origo[0])/cellsize_min)
2702 self.num[1] = numpy.ceil((self.L[1]-self.origo[1])/cellsize_min)
2703 self.num[2] = numpy.ceil((self.L[2]-self.origo[2])/cellsize_min)
2704
2705 if self.num[0] < 4 or self.num[1] < 4 or self.num[2] < 4:
2706 raise Exception("Error: The grid must be at least 3 cells in each "
2707 + "direction, num=" + str(self.num))
2708
2709 # Put upper wall at top boundary
2710 if self.nw > 0:
2711 self.w_x[0] = self.L[0]
2712
2713 def initGridPos(self, gridnum=numpy.array([12, 12, 36])):
2714 '''
2715 Initialize particle positions in loose, cubic configuration.
2716 ``gridnum`` is the number of cells in the x, y and z directions.
2717 *Important*: The particle radii and the boundary conditions (periodic or
2718 not) for the x and y boundaries have to be set beforehand.
2719
2720 :param gridnum: The number of particles in x, y and z directions
2721 :type gridnum: numpy.array
2722 '''
2723
2724 # Calculate cells in grid
2725 self.num = numpy.asarray(gridnum)
2726
2727 # World size
2728 r_max = numpy.amax(self.radius)
2729 cellsize = 2.1 * r_max
2730 self.L = self.num * cellsize
2731
2732 # Check whether there are enough grid cells
2733 if (self.num[0]*self.num[1]*self.num[2]-(2**3)) < self.np:
2734 print("Error! The grid is not sufficiently large.")
2735 raise NameError('Error! The grid is not sufficiently large.')
2736
2737 gridpos = numpy.zeros(self.nd, dtype=numpy.uint32)
2738
2739 # Make sure grid is sufficiently large if every second level is moved
2740 if self.periodic[0] == 1:
2741 self.num[0] -= 1
2742 self.num[1] -= 1
2743
2744 # Check whether there are enough grid cells
2745 if (self.num[0]*self.num[1]*self.num[2]-(2*3*3)) < self.np:
2746 print("Error! The grid is not sufficiently large.")
2747 raise NameError('Error! The grid is not sufficiently large.')
2748
2749 # Particle positions randomly distributed without overlap
2750 for i in range(self.np):
2751
2752 # Find position in 3d mesh from linear index
2753 gridpos[0] = (i % (self.num[0]))
2754 gridpos[1] = numpy.floor(i/(self.num[0])) % (self.num[0])
2755 gridpos[2] = numpy.floor(i/((self.num[0])*(self.num[1]))) #\
2756 #% ((self.num[0])*(self.num[1]))
2757
2758 for d in range(self.nd):
2759 self.x[i, d] = gridpos[d] * cellsize + 0.5*cellsize
2760
2761 # Allow pushing every 2.nd level out of lateral boundaries
2762 if self.periodic[0] == 1:
2763 # Offset every second level
2764 if gridpos[2] % 2:
2765 self.x[i, 0] += 0.5*cellsize
2766 self.x[i, 1] += 0.5*cellsize
2767
2768 # Readjust grid to correct size
2769 if self.periodic[0] == 1:
2770 self.num[0] += 1
2771 self.num[1] += 1
2772
2773 def initRandomGridPos(self, gridnum=numpy.array([12, 12, 32]),
2774 padding=2.1):
2775 '''
2776 Initialize particle positions in loose, cubic configuration with some
2777 variance. ``gridnum`` is the number of cells in the x, y and z
2778 directions. *Important*: The particle radii and the boundary conditions
2779 (periodic or not) for the x and y boundaries have to be set beforehand.
2780 The world size and grid height (in the z direction) is readjusted to fit
2781 the particle positions.
2782
2783 :param gridnum: The number of particles in x, y and z directions
2784 :type gridnum: numpy.array
2785 :param padding: Increase distance between particles in x, y and z
2786 directions with this multiplier. Large values create more random
2787 packings.
2788 :type padding: float
2789 '''
2790
2791 # Calculate cells in grid
2792 coarsegrid = numpy.floor(numpy.asarray(gridnum)/2)
2793
2794 # World size
2795 r_max = numpy.amax(self.radius)
2796
2797 # Cells in grid 2*size to make space for random offset
2798 cellsize = padding * r_max * 2
2799
2800 # Check whether there are enough grid cells
2801 if ((coarsegrid[0]-1)*(coarsegrid[1]-1)*(coarsegrid[2]-1)) < self.np:
2802 print("Error! The grid is not sufficiently large.")
2803 raise NameError('Error! The grid is not sufficiently large.')
2804
2805 gridpos = numpy.zeros(self.nd, dtype=numpy.uint32)
2806
2807 # Particle positions randomly distributed without overlap
2808 for i in range(self.np):
2809
2810 # Find position in 3d mesh from linear index
2811 gridpos[0] = (i % (coarsegrid[0]))
2812 gridpos[1] = numpy.floor(i/(coarsegrid[0]))%(coarsegrid[1]) # Thanks Horacio!
2813 gridpos[2] = numpy.floor(i/((coarsegrid[0])*(coarsegrid[1])))
2814
2815 # Place particles in grid structure, and randomly adjust the
2816 # positions within the oversized cells (uniform distribution)
2817 for d in range(self.nd):
2818 r = self.radius[i]*1.05
2819 self.x[i, d] = gridpos[d] * cellsize \
2820 + ((cellsize-r) - r) \
2821 * numpy.random.random_sample() + r
2822
2823 # Calculate new grid with cell size equal to max. particle diameter
2824 x_max = numpy.max(self.x[:, 0] + self.radius)
2825 y_max = numpy.max(self.x[:, 1] + self.radius)
2826 z_max = numpy.max(self.x[:, 2] + self.radius)
2827
2828 # Adjust size of world
2829 self.num[0] = numpy.ceil(x_max/cellsize)
2830 self.num[1] = numpy.ceil(y_max/cellsize)
2831 self.num[2] = numpy.ceil(z_max/cellsize)
2832 self.L = self.num * cellsize
2833
2834 def createBondPair(self, i, j, spacing=-0.1):
2835 '''
2836 Bond particles i and j. Particle j is moved adjacent to particle i,
2837 and oriented randomly.
2838
2839 :param i: Index of first particle in bond
2840 :type i: int
2841 :param j: Index of second particle in bond
2842 :type j: int
2843 :param spacing: The inter-particle distance prescribed. Positive
2844 values result in a inter-particle distance, negative equal an
2845 overlap. The value is relative to the sum of the two radii.
2846 :type spacing: float
2847 '''
2848
2849 x_i = self.x[i]
2850 r_i = self.radius[i]
2851 r_j = self.radius[j]
2852 dist_ij = (r_i + r_j)*(1.0 + spacing)
2853
2854 dazi = numpy.random.rand(1) * 360.0 # azimuth
2855 azi = numpy.radians(dazi)
2856 dang = numpy.random.rand(1) * 180.0 - 90.0 # angle
2857 ang = numpy.radians(dang)
2858
2859 x_j = numpy.copy(x_i)
2860 x_j[0] = x_j[0] + dist_ij * numpy.cos(azi) * numpy.cos(ang)
2861 x_j[1] = x_j[1] + dist_ij * numpy.sin(azi) * numpy.cos(ang)
2862 x_j[2] = x_j[2] + dist_ij * numpy.sin(ang) * numpy.cos(azi)
2863 self.x[j] = x_j
2864
2865 if self.x[j, 0] < self.origo[0]:
2866 self.x[j, 0] += x_i[0] - x_j[0]
2867 if self.x[j, 1] < self.origo[1]:
2868 self.x[j, 1] += x_i[1] - x_j[1]
2869 if self.x[j, 2] < self.origo[2]:
2870 self.x[j, 2] += x_i[2] - x_j[2]
2871
2872 if self.x[j, 0] > self.L[0]:
2873 self.x[j, 0] -= abs(x_j[0] - x_i[0])
2874 if self.x[j, 1] > self.L[1]:
2875 self.x[j, 1] -= abs(x_j[1] - x_i[1])
2876 if self.x[j, 2] > self.L[2]:
2877 self.x[j, 2] -= abs(x_j[2] - x_i[2])
2878
2879 self.bond(i, j) # register bond
2880
2881 # Check that the spacing is correct
2882 x_ij = self.x[i] - self.x[j]
2883 x_ij_length = numpy.sqrt(x_ij.dot(x_ij))
2884 if (x_ij_length - dist_ij) > dist_ij*0.01:
2885 print(x_i); print(r_i)
2886 print(x_j); print(r_j)
2887 print(x_ij_length); print(dist_ij)
2888 raise Exception("Error, something went wrong in createBondPair")
2889
2890
2891 def randomBondPairs(self, ratio=0.3, spacing=-0.1):
2892 '''
2893 Bond an amount of particles in two-particle clusters. The particles
2894 should be initialized beforehand. Note: The actual number of bonds is
2895 likely to be somewhat smaller than specified, due to the random
2896 selection algorithm.
2897
2898 :param ratio: The amount of particles to bond, values in ]0.0;1.0]
2899 :type ratio: float
2900 :param spacing: The distance relative to the sum of radii between bonded
2901 particles, neg. values denote an overlap. Values in ]0.0,inf[.
2902 :type spacing: float
2903 '''
2904
2905 bondparticles = numpy.unique(numpy.random.random_integers(0, high=self.np-1,
2906 size=int(self.np*ratio)))
2907 if bondparticles.size % 2 > 0:
2908 bondparticles = bondparticles[:-1].copy()
2909 bondparticles = bondparticles.reshape(int(bondparticles.size/2),
2910 2).copy()
2911
2912 for n in numpy.arange(bondparticles.shape[0]):
2913 self.createBondPair(bondparticles[n, 0], bondparticles[n, 1],
2914 spacing)
2915
2916 def zeroKinematics(self):
2917 '''
2918 Zero all kinematic parameters of the particles. This function is useful
2919 when output from one simulation is reused in another simulation.
2920 '''
2921
2922 self.force = numpy.zeros((self.np, self.nd))
2923 self.torque = numpy.zeros((self.np, self.nd))
2924 self.vel = numpy.zeros(self.np*self.nd, dtype=numpy.float64)\
2925 .reshape(self.np, self.nd)
2926 self.angvel = numpy.zeros(self.np*self.nd, dtype=numpy.float64)\
2927 .reshape(self.np, self.nd)
2928 self.angpos = numpy.zeros(self.np*self.nd, dtype=numpy.float64)\
2929 .reshape(self.np, self.nd)
2930 self.es = numpy.zeros(self.np, dtype=numpy.float64)
2931 self.ev = numpy.zeros(self.np, dtype=numpy.float64)
2932 self.xyzsum = numpy.zeros(self.np*3, dtype=numpy.float64).reshape(self.np, 3)
2933
2934 def adjustUpperWall(self, z_adjust=1.1):
2935 '''
2936 Included for legacy purposes, calls :func:`adjustWall()` with ``idx=0``.
2937
2938 :param z_adjust: Increase the world and grid size by this amount to
2939 allow for wall movement.
2940 :type z_adjust: float
2941 '''
2942
2943 # Initialize upper wall
2944 self.nw = 1
2945 self.wmode = numpy.zeros(1) # fixed BC
2946 self.w_n = numpy.zeros(self.nw*self.nd, dtype=numpy.float64)\
2947 .reshape(self.nw, self.nd)
2948 self.w_n[0, 2] = -1.0
2949 self.w_vel = numpy.zeros(1)
2950 self.w_force = numpy.zeros(1)
2951 self.w_sigma0 = numpy.zeros(1)
2952
2953 self.w_x = numpy.zeros(1)
2954 self.w_m = numpy.zeros(1)
2955 self.adjustWall(idx=0, adjust=z_adjust)
2956
2957 def adjustWall(self, idx, adjust=1.1):
2958 '''
2959 Adjust grid and dynamic wall to max. particle position. The wall
2960 thickness will by standard equal the maximum particle diameter. The
2961 density equals the particle density, and the wall size is equal to the
2962 width and depth of the simulation domain (`self.L[0]` and `self.L[1]`).
2963
2964 :param: idx: The wall to adjust. 0=+z, upper wall (default), 1=-x,
2965 left wall, 2=+x, right wall, 3=-y, front wall, 4=+y, back
2966 wall.
2967 :type idx: int
2968 :param adjust: Increase the world and grid size by this amount to
2969 allow for wall movement.
2970 :type adjust: float
2971 '''
2972
2973 if idx == 0:
2974 dim = 2
2975 elif idx == 1 or idx == 2:
2976 dim = 0
2977 elif idx == 3 or idx == 4:
2978 dim = 1
2979 else:
2980 print("adjustWall: idx value not understood")
2981
2982 xmin = numpy.min(self.x[:, dim] - self.radius)
2983 xmax = numpy.max(self.x[:, dim] + self.radius)
2984
2985 cellsize = self.L[0] / self.num[0]
2986 self.num[dim] = numpy.ceil(((xmax-xmin)*adjust + xmin)/cellsize)
2987 self.L[dim] = (xmax-xmin)*adjust + xmin
2988
2989 # Initialize upper wall
2990 if idx == 0 or idx == 1 or idx == 3:
2991 self.w_x[idx] = numpy.array([xmax])
2992 else:
2993 self.w_x[idx] = numpy.array([xmin])
2994 self.w_m[idx] = self.totalMass()
2995
2996 def consolidate(self, normal_stress=10e3):
2997 '''
2998 Setup consolidation experiment. Specify the upper wall normal stress in
2999 Pascal, default value is 10 kPa.
3000
3001 :param normal_stress: The normal stress to apply from the upper wall
3002 :type normal_stress: float
3003 '''
3004
3005 self.nw = 1
3006
3007 if normal_stress <= 0.0:
3008 raise Exception('consolidate() error: The normal stress should be '
3009 'a positive value, but is ' + str(normal_stress) +
3010 ' Pa')
3011
3012 # Zero the kinematics of all particles
3013 self.zeroKinematics()
3014
3015 # Adjust grid and placement of upper wall
3016 self.adjustUpperWall()
3017
3018 # Set the top wall BC to a value of normal stress
3019 self.wmode = numpy.array([1])
3020 self.w_sigma0 = numpy.ones(1) * normal_stress
3021
3022 # Set top wall to a certain mass corresponding to the selected normal
3023 # stress
3024 #self.w_sigma0 = numpy.zeros(1)
3025 #self.w_m[0] = numpy.abs(normal_stress*self.L[0]*self.L[1]/self.g[2])
3026 self.w_m[0] = self.totalMass()
3027
3028 def uniaxialStrainRate(self, wvel=-0.001):
3029 '''
3030 Setup consolidation experiment. Specify the upper wall velocity in m/s,
3031 default value is -0.001 m/s (i.e. downwards).
3032
3033 :param wvel: Upper wall velocity. Negative values mean that the wall
3034 moves downwards.
3035 :type wvel: float
3036 '''
3037
3038 # zero kinematics
3039 self.zeroKinematics()
3040
3041 # Initialize upper wall
3042 self.adjustUpperWall()
3043 self.wmode = numpy.array([2]) # strain rate BC
3044 self.w_vel = numpy.array([wvel])
3045
3046 def triaxial(self, wvel=-0.001, normal_stress=10.0e3):
3047 '''
3048 Setup triaxial experiment. The upper wall is moved at a fixed velocity
3049 in m/s, default values is -0.001 m/s (i.e. downwards). The side walls
3050 are exerting a defined normal stress.
3051
3052 :param wvel: Upper wall velocity. Negative values mean that the wall
3053 moves downwards.
3054 :type wvel: float
3055 :param normal_stress: The normal stress to apply from the upper wall.
3056 :type normal_stress: float
3057 '''
3058
3059 # zero kinematics
3060 self.zeroKinematics()
3061
3062 # Initialize walls
3063 self.nw = 5 # five dynamic walls
3064 self.wmode = numpy.array([2, 1, 1, 1, 1]) # BCs (vel, stress, stress, ...)
3065 self.w_vel = numpy.array([1, 0, 0, 0, 0]) * wvel
3066 self.w_sigma0 = numpy.array([0, 1, 1, 1, 1]) * normal_stress
3067 self.w_n = numpy.array(([0, 0, -1], [-1, 0, 0],
3068 [1, 0, 0], [0, -1, 0], [0, 1, 0]),
3069 dtype=numpy.float64)
3070 self.w_x = numpy.zeros(5)
3071 self.w_m = numpy.zeros(5)
3072 self.w_force = numpy.zeros(5)
3073 for i in range(5):
3074 self.adjustWall(idx=i)
3075
3076 def shear(self, shear_strain_rate=1.0, shear_stress=False):
3077 '''
3078 Setup shear experiment either by a constant shear rate or a constant
3079 shear stress. The shear strain rate is the shear velocity divided by
3080 the initial height per second. The shear movement is along the positive
3081 x axis. The function zeroes the tangential wall viscosity (gamma_wt) and
3082 the wall friction coefficients (mu_ws, mu_wn).
3083
3084 :param shear_strain_rate: The shear strain rate [-] to use if
3085 shear_stress isn't False.
3086 :type shear_strain_rate: float
3087 :param shear_stress: The shear stress value to use [Pa].
3088 :type shear_stress: float or bool
3089 '''
3090
3091 self.nw = 1
3092
3093 # Find lowest and heighest point
3094 z_min = numpy.min(self.x[:, 2] - self.radius)
3095 z_max = numpy.max(self.x[:, 2] + self.radius)
3096
3097 # the grid cell size is equal to the max. particle diameter
3098 cellsize = self.L[0] / self.num[0]
3099
3100 # make grid one cell heigher to allow dilation
3101 self.num[2] += 1
3102 self.L[2] = self.num[2] * cellsize
3103
3104 # zero kinematics
3105 self.zeroKinematics()
3106
3107 # Adjust grid and placement of upper wall
3108 self.wmode = numpy.array([1])
3109
3110 # Fix horizontal velocity to 0.0 of lowermost particles
3111 d_max_below = numpy.max(self.radius[numpy.nonzero(self.x[:, 2] <
3112 (z_max-z_min)*0.3)])*2.0
3113 I = numpy.nonzero(self.x[:, 2] < (z_min + d_max_below))
3114 self.fixvel[I] = 1
3115 self.angvel[I, 0] = 0.0
3116 self.angvel[I, 1] = 0.0
3117 self.angvel[I, 2] = 0.0
3118 self.vel[I, 0] = 0.0 # x-dim
3119 self.vel[I, 1] = 0.0 # y-dim
3120 self.color[I] = -1
3121
3122 # Fix horizontal velocity to specific value of uppermost particles
3123 d_max_top = numpy.max(self.radius[numpy.nonzero(self.x[:, 2] >
3124 (z_max-z_min)*0.7)])*2.0
3125 I = numpy.nonzero(self.x[:, 2] > (z_max - d_max_top))
3126 self.fixvel[I] = 1
3127 self.angvel[I, 0] = 0.0
3128 self.angvel[I, 1] = 0.0
3129 self.angvel[I, 2] = 0.0
3130 if not shear_stress:
3131 self.vel[I, 0] = (z_max-z_min)*shear_strain_rate
3132 else:
3133 self.vel[I, 0] = 0.0
3134 self.wmode[0] = 3
3135 self.w_tau_x[0] = float(shear_stress)
3136 self.vel[I, 1] = 0.0 # y-dim
3137 self.color[I] = -1
3138
3139 # Set wall tangential viscosity to zero
3140 self.gamma_wt[0] = 0.0
3141
3142 # Set wall friction coefficients to zero
3143 self.mu_ws[0] = 0.0
3144 self.mu_wd[0] = 0.0
3145
3146 def largestFluidTimeStep(self, safety=0.5, v_max=-1.0):
3147 '''
3148 Finds and returns the largest time step in the fluid phase by von
3149 Neumann and Courant-Friedrichs-Lewy analysis given the current
3150 velocities. This ensures stability in the diffusive and advective parts
3151 of the momentum equation.
3152
3153 The value of the time step decreases with increasing fluid viscosity
3154 (`self.mu`), and increases with fluid cell size (`self.L/self.num`)
3155 and fluid velocities (`self.v_f`).
3156
3157 NOTE: The fluid time step with the Darcy solver is an arbitrarily
3158 large value. In practice, this is not a problem since the short
3159 DEM time step is stable for fluid computations.
3160
3161 :param safety: Safety factor which is multiplied to the largest time
3162 step.
3163 :type safety: float
3164 :param v_max: The largest anticipated absolute fluid velocity [m/s]
3165 :type v_max: float
3166
3167 :returns: The largest timestep stable for the current fluid state.
3168 :return type: float
3169 '''
3170
3171 if self.fluid:
3172
3173 # Normalized velocities
3174 v_norm = numpy.empty(self.num[0]*self.num[1]*self.num[2])
3175 idx = 0
3176 for x in numpy.arange(self.num[0]):
3177 for y in numpy.arange(self.num[1]):
3178 for z in numpy.arange(self.num[2]):
3179 v_norm[idx] = numpy.sqrt(self.v_f[x, y, z, :]\
3180 .dot(self.v_f[x, y, z, :]))
3181 idx += 1
3182
3183 v_max_obs = numpy.amax(v_norm)
3184 if v_max_obs == 0:
3185 v_max_obs = 1.0e-7
3186 if v_max < 0.0:
3187 v_max = v_max_obs
3188
3189 dx_min = numpy.min(self.L/self.num)
3190 dt_min_cfl = dx_min/v_max
3191
3192 # Navier-Stokes
3193 if self.cfd_solver[0] == 0:
3194 dt_min_von_neumann = 0.5*dx_min**2/(self.mu[0] + 1.0e-16)
3195
3196 return numpy.min([dt_min_von_neumann, dt_min_cfl])*safety
3197
3198 # Darcy
3199 elif self.cfd_solver[0] == 1:
3200
3201 return dt_min_cfl
3202
3203 '''
3204 # Determine on the base of the diffusivity coefficient
3205 # components
3206 #self.hydraulicPermeability()
3207 #alpha_max = numpy.max(self.k/(self.beta_f*0.9*self.mu))
3208 k_max = 2.7e-10 # hardcoded in darcy.cuh
3209 phi_min = 0.1 # hardcoded in darcy.cuh
3210 alpha_max = k_max/(self.beta_f*phi_min*self.mu)
3211 print(alpha_max)
3212 return safety * 1.0/(2.0*alpha_max)*1.0/(
3213 1.0/(self.dx[0]**2) + \
3214 1.0/(self.dx[1]**2) + \
3215 1.0/(self.dx[2]**2))
3216 '''
3217
3218 '''
3219 # Determine value on the base of the hydraulic conductivity
3220 g = numpy.max(numpy.abs(self.g))
3221
3222 # Bulk modulus of fluid
3223 K = 1.0/self.beta_f[0]
3224
3225 self.hydraulicDiffusivity()
3226
3227 return safety * 1.0/(2.0*self.D)*1.0/( \
3228 1.0/(self.dx[0]**2) + \
3229 1.0/(self.dx[1]**2) + \
3230 1.0/(self.dx[2]**2))
3231 '''
3232
3233 def hydraulicConductivity(self, phi=0.35):
3234 '''
3235 Determine the hydraulic conductivity (K) [m/s] from the permeability
3236 prefactor and a chosen porosity. This value is stored in `self.K_c`.
3237 This function only works for the Darcy solver (`self.cfd_solver == 1`)
3238
3239 :param phi: The porosity to use in the Kozeny-Carman relationship
3240 :type phi: float
3241 :returns: The hydraulic conductivity [m/s]
3242 :return type: float
3243 '''
3244 if self.cfd_solver[0] == 1:
3245 k = self.k_c * phi**3/(1.0 - phi**2)
3246 self.K_c = k*self.rho_f*numpy.abs(self.g[2])/self.mu
3247 return self.K_c[0]
3248 else:
3249 raise Exception('This function only works for the Darcy solver')
3250
3251 def hydraulicPermeability(self):
3252 '''
3253 Determine the hydraulic permeability (k) [m*m] from the Kozeny-Carman
3254 relationship, using the permeability prefactor (`self.k_c`), and the
3255 range of valid porosities set in `src/darcy.cuh`, by default in the
3256 range 0.1 to 0.9.
3257
3258 This function is only valid for the Darcy solver (`self.cfd_solver ==
3259 1`).
3260 '''
3261 if self.cfd_solver[0] == 1:
3262 self.findPermeabilities()
3263 else:
3264 raise Exception('This function only works for the Darcy solver')
3265
3266 def hydraulicDiffusivity(self):
3267 '''
3268 Determine the hydraulic diffusivity (D) [m*m/s]. The result is stored in
3269 `self.D`. This function only works for the Darcy solver
3270 (`self.cfd_solver[0] == 1`)
3271 '''
3272 if self.cfd_solver[0] == 1:
3273 self.hydraulicConductivity()
3274 phi_bar = numpy.mean(self.phi)
3275 self.D = self.K_c/(self.rho_f*self.g[2]
3276 *(self.k_n[0] + phi_bar*self.K))
3277 else:
3278 raise Exception('This function only works for the Darcy solver')
3279
3280 def initTemporal(self, total, current=0.0, file_dt=0.05, step_count=0,
3281 dt=-1, epsilon=0.01):
3282 '''
3283 Set temporal parameters for the simulation. *Important*: Particle radii,
3284 physical parameters, and the optional fluid grid need to be set prior to
3285 these if the computational time step (dt) isn't set explicitly. If the
3286 parameter `dt` is the default value (-1), the function will estimate the
3287 best time step length. The value of the computational time step for the
3288 DEM is checked for stability in the CFD solution if fluid simulation is
3289 included.
3290
3291 :param total: The time at which to end the simulation [s]
3292 :type total: float
3293 :param current: The current time [s] (default=0.0 s)
3294 :type total: float
3295 :param file_dt: The interval between output files [s] (default=0.05 s)
3296 :type total: float
3297 :step_count: The number of the first output file (default=0)
3298 :type step_count: int
3299 :param dt: The computational time step length [s]
3300 :type total: float
3301 :param epsilon: Time step multiplier (default=0.01)
3302 :type epsilon: float
3303 '''
3304
3305 if dt > 0.0:
3306 self.time_dt[0] = dt
3307 if self.np > 0:
3308 print("Warning: Manually specifying the time step length when "
3309 "simulating particles may produce instabilities.")
3310
3311 elif self.np > 0:
3312
3313 r_min = numpy.min(self.radius)
3314 m_min = self.rho * 4.0/3.0*numpy.pi*r_min**3
3315
3316 if self.E > 0.001:
3317 k_max = numpy.max(numpy.pi/2.0*self.E*self.radius)
3318 else:
3319 k_max = numpy.max([self.k_n[:], self.k_t[:]])
3320
3321 # Radjaii et al 2011
3322 self.time_dt[0] = epsilon/(numpy.sqrt(k_max/m_min))
3323
3324 # Zhang and Campbell, 1992
3325 #self.time_dt[0] = 0.075*math.sqrt(m_min/k_max)
3326
3327 # Computational time step (O'Sullivan et al, 2003)
3328 #self.time_dt[0] = 0.17*math.sqrt(m_min/k_max)
3329
3330 elif not self.fluid:
3331 raise Exception('Error: Could not automatically set a time step.')
3332
3333 # Check numerical stability of the fluid phase, by criteria derived
3334 # by von Neumann stability analysis of the diffusion and advection
3335 # terms
3336 if self.fluid:
3337 fluid_time_dt = self.largestFluidTimeStep()
3338 self.time_dt[0] = numpy.min([fluid_time_dt, self.time_dt[0]])
3339
3340 # Time at start
3341 self.time_current[0] = current
3342 self.time_total[0] = total
3343 self.time_file_dt[0] = file_dt
3344 self.time_step_count[0] = step_count
3345
3346 def dry(self):
3347 '''
3348 Set the simulation to be dry (no fluids).
3349
3350 See also :func:`wet()`
3351 '''
3352 self.fluid = False
3353
3354 def wet(self):
3355 '''
3356 Set the simulation to be wet (total fluid saturation).
3357
3358 See also :func:`dry()`
3359 '''
3360 self.fluid = True
3361 self.initFluid()
3362
3363 def initFluid(self, mu=8.9e-4, rho=1.0e3, p=0.0, hydrostatic=False,
3364 cfd_solver=0):
3365 '''
3366 Initialize the fluid arrays and the fluid viscosity. The default value
3367 of ``mu`` equals the dynamic viscosity of water at 25 degrees Celcius.
3368 The value for water at 0 degrees Celcius is 17.87e-4 kg/(m*s).
3369
3370 :param mu: The fluid dynamic viscosity [kg/(m*s)]
3371 :type mu: float
3372 :param rho: The fluid density [kg/(m^3)]
3373 :type rho: float
3374 :param p: The hydraulic pressure to initialize the cells to. If the
3375 parameter `hydrostatic` is set to `True`, this value will apply to
3376 the fluid cells at the top
3377 :param hydrostatic: Initialize the fluid pressures to the hydrostatic
3378 pressure distribution. A pressure gradient with depth is only
3379 created if a gravitational acceleration along :math:`z` previously
3380 has been specified
3381 :type hydrostatic: bool
3382 :param cfd_solver: Solver to use for the computational fluid dynamics.
3383 Accepted values: 0 (Navier Stokes, default) and 1 (Darcy).
3384 :type cfd_solver: int
3385 '''
3386 self.fluid = True
3387 self.mu = numpy.ones(1, dtype=numpy.float64) * mu
3388 self.rho_f = numpy.ones(1, dtype=numpy.float64) * rho
3389
3390 self.p_f = numpy.ones((self.num[0], self.num[1], self.num[2]),
3391 dtype=numpy.float64) * p
3392
3393 if hydrostatic:
3394
3395 dz = self.L[2]/self.num[2]
3396 # Zero pressure gradient from grid top to top wall, linear pressure
3397 # distribution from top wall to grid bottom
3398 if self.nw == 1:
3399 wall0_iz = int(self.w_x[0]/(self.L[2]/self.num[2]))
3400 self.p_f[:, :, wall0_iz:] = p
3401
3402 for iz in numpy.arange(wall0_iz - 1):
3403 z = dz*iz + 0.5*dz
3404 depth = self.w_x[0] - z
3405 self.p_f[:, :, iz] = p + (depth-dz) * rho * -self.g[2]
3406
3407 # Linear pressure distribution from grid top to grid bottom
3408 else:
3409 for iz in numpy.arange(self.num[2] - 1):
3410 z = dz*iz + 0.5*dz
3411 depth = self.L[2] - z
3412 self.p_f[:, :, iz] = p + (depth-dz) * rho * -self.g[2]
3413
3414
3415 self.v_f = numpy.zeros((self.num[0], self.num[1], self.num[2], self.nd),
3416 dtype=numpy.float64)
3417 self.phi = numpy.ones((self.num[0], self.num[1], self.num[2]),
3418 dtype=numpy.float64)
3419 self.dphi = numpy.zeros((self.num[0], self.num[1], self.num[2]),
3420 dtype=numpy.float64)
3421
3422 self.p_mod_A = numpy.zeros(1, dtype=numpy.float64) # Amplitude [Pa]
3423 self.p_mod_f = numpy.zeros(1, dtype=numpy.float64) # Frequency [Hz]
3424 self.p_mod_phi = numpy.zeros(1, dtype=numpy.float64) # Shift [rad]
3425
3426 self.bc_bot = numpy.zeros(1, dtype=numpy.int32)
3427 self.bc_top = numpy.zeros(1, dtype=numpy.int32)
3428 self.free_slip_bot = numpy.ones(1, dtype=numpy.int32)
3429 self.free_slip_top = numpy.ones(1, dtype=numpy.int32)
3430 self.bc_bot_flux = numpy.zeros(1, dtype=numpy.float64)
3431 self.bc_top_flux = numpy.zeros(1, dtype=numpy.float64)
3432
3433 self.p_f_constant = numpy.zeros((self.num[0], self.num[1], self.num[2]),
3434 dtype=numpy.int32)
3435
3436 # Fluid solver type
3437 # 0: Navier Stokes (fluid with inertia)
3438 # 1: Stokes-Darcy (fluid without inertia)
3439 self.cfd_solver = numpy.ones(1)*cfd_solver
3440
3441 if self.cfd_solver[0] == 0:
3442 self.gamma = numpy.array(0.0)
3443 self.theta = numpy.array(1.0)
3444 self.beta = numpy.array(0.0)
3445 self.tolerance = numpy.array(1.0e-3)
3446 self.maxiter = numpy.array(1e4)
3447 self.ndem = numpy.array(1)
3448
3449 self.c_phi = numpy.ones(1, dtype=numpy.float64)
3450 self.c_v = numpy.ones(1, dtype=numpy.float64)
3451 self.dt_dem_fac = numpy.ones(1, dtype=numpy.float64)
3452
3453 self.f_d = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
3454 self.f_p = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
3455 self.f_v = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
3456 self.f_sum = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
3457
3458 elif self.cfd_solver[0] == 1:
3459 self.tolerance = numpy.array(1.0e-3)
3460 self.maxiter = numpy.array(1e4)
3461 self.ndem = numpy.array(1)
3462 self.c_phi = numpy.ones(1, dtype=numpy.float64)
3463 self.f_d = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
3464 self.beta_f = numpy.ones(1, dtype=numpy.float64)*4.5e-10
3465 self.f_p = numpy.zeros((self.np, self.nd), dtype=numpy.float64)
3466 self.k_c = numpy.ones(1, dtype=numpy.float64)*4.6e-10
3467
3468 self.bc_xn = numpy.ones(1, dtype=numpy.int32)*2
3469 self.bc_xp = numpy.ones(1, dtype=numpy.int32)*2
3470 self.bc_yn = numpy.ones(1, dtype=numpy.int32)*2
3471 self.bc_yp = numpy.ones(1, dtype=numpy.int32)*2
3472
3473 else:
3474 raise Exception('Value of cfd_solver not understood (' + \
3475 str(self.cfd_solver[0]) + ')')
3476
3477 def currentTime(self, value=-1):
3478 '''
3479 Get or set the current time. If called without arguments the current
3480 time is returned. If a new time is passed in the 'value' argument, the
3481 time is written to the object.
3482
3483 :param value: The new current time
3484 :type value: float
3485
3486 :returns: The current time
3487 :return type: float
3488 '''
3489 if value != -1:
3490 self.time_current[0] = value
3491 else:
3492 return self.time_current[0]
3493
3494 def setFluidBottomNoFlow(self):
3495 '''
3496 Set the lower boundary of the fluid domain to follow the no-flow
3497 (Neumann) boundary condition with free slip parallel to the boundary.
3498
3499 The default behavior for the boundary is fixed value (Dirichlet), see
3500 :func:`setFluidBottomFixedPressure()`.
3501 '''
3502 self.bc_bot[0] = 1
3503
3504 def setFluidBottomNoFlowNoSlip(self):
3505 '''
3506 Set the lower boundary of the fluid domain to follow the no-flow
3507 (Neumann) boundary condition with no slip parallel to the boundary.
3508
3509 The default behavior for the boundary is fixed value (Dirichlet), see
3510 :func:`setFluidBottomFixedPressure()`.
3511 '''
3512 self.bc_bot[0] = 2
3513
3514 def setFluidBottomFixedPressure(self):
3515 '''
3516 Set the lower boundary of the fluid domain to follow the fixed pressure
3517 value (Dirichlet) boundary condition.
3518
3519 This is the default behavior for the boundary. See also
3520 :func:`setFluidBottomNoFlow()`
3521 '''
3522 self.bc_bot[0] = 0
3523
3524 def setFluidBottomFixedFlux(self, specific_flux):
3525 '''
3526 Define a constant fluid flux normal to the boundary.
3527
3528 The default behavior for the boundary is fixed value (Dirichlet), see
3529 :func:`setFluidBottomFixedPressure()`.
3530
3531 :param specific_flux: Specific flux values across boundary (positive
3532 values upwards), [m/s]
3533 '''
3534 self.bc_bot[0] = 4
3535 self.bc_bot_flux[0] = specific_flux
3536
3537 def setFluidTopNoFlow(self):
3538 '''
3539 Set the upper boundary of the fluid domain to follow the no-flow
3540 (Neumann) boundary condition with free slip parallel to the boundary.
3541
3542 The default behavior for the boundary is fixed value (Dirichlet), see
3543 :func:`setFluidTopFixedPressure()`.
3544 '''
3545 self.bc_top[0] = 1
3546
3547 def setFluidTopNoFlowNoSlip(self):
3548 '''
3549 Set the upper boundary of the fluid domain to follow the no-flow
3550 (Neumann) boundary condition with no slip parallel to the boundary.
3551
3552 The default behavior for the boundary is fixed value (Dirichlet), see
3553 :func:`setFluidTopFixedPressure()`.
3554 '''
3555 self.bc_top[0] = 2
3556
3557 def setFluidTopFixedPressure(self):
3558 '''
3559 Set the upper boundary of the fluid domain to follow the fixed pressure
3560 value (Dirichlet) boundary condition.
3561
3562 This is the default behavior for the boundary. See also
3563 :func:`setFluidTopNoFlow()`
3564 '''
3565 self.bc_top[0] = 0
3566
3567 def setFluidTopFixedFlux(self, specific_flux):
3568 '''
3569 Define a constant fluid flux normal to the boundary.
3570
3571 The default behavior for the boundary is fixed value (Dirichlet), see
3572 :func:`setFluidBottomFixedPressure()`.
3573
3574 :param specific_flux: Specific flux values across boundary (positive
3575 values upwards), [m/s]
3576 '''
3577 self.bc_top[0] = 4
3578 self.bc_top_flux[0] = specific_flux
3579
3580 def setFluidXFixedPressure(self):
3581 '''
3582 Set the X boundaries of the fluid domain to follow the fixed pressure
3583 value (Dirichlet) boundary condition.
3584
3585 This is not the default behavior for the boundary. See also
3586 :func:`setFluidXFixedPressure()`,
3587 :func:`setFluidXNoFlow()`, and
3588 :func:`setFluidXPeriodic()` (default)
3589 '''
3590 self.bc_xn[0] = 0
3591 self.bc_xp[0] = 0
3592
3593 def setFluidXNoFlow(self):
3594 '''
3595 Set the X boundaries of the fluid domain to follow the no-flow
3596 (Neumann) boundary condition.
3597
3598 This is not the default behavior for the boundary. See also
3599 :func:`setFluidXFixedPressure()`,
3600 :func:`setFluidXNoFlow()`, and
3601 :func:`setFluidXPeriodic()` (default)
3602 '''
3603 self.bc_xn[0] = 1
3604 self.bc_xp[0] = 1
3605
3606 def setFluidXPeriodic(self):
3607 '''
3608 Set the X boundaries of the fluid domain to follow the periodic
3609 (cyclic) boundary condition.
3610
3611 This is the default behavior for the boundary. See also
3612 :func:`setFluidXFixedPressure()` and
3613 :func:`setFluidXNoFlow()`
3614 '''
3615 self.bc_xn[0] = 2
3616 self.bc_xp[0] = 2
3617
3618 def setFluidYFixedPressure(self):
3619 '''
3620 Set the Y boundaries of the fluid domain to follow the fixed pressure
3621 value (Dirichlet) boundary condition.
3622
3623 This is not the default behavior for the boundary. See also
3624 :func:`setFluidYNoFlow()` and
3625 :func:`setFluidYPeriodic()` (default)
3626 '''
3627 self.bc_yn[0] = 0
3628 self.bc_yp[0] = 0
3629
3630 def setFluidYNoFlow(self):
3631 '''
3632 Set the Y boundaries of the fluid domain to follow the no-flow
3633 (Neumann) boundary condition.
3634
3635 This is not the default behavior for the boundary. See also
3636 :func:`setFluidYFixedPressure()` and
3637 :func:`setFluidYPeriodic()` (default)
3638 '''
3639 self.bc_yn[0] = 1
3640 self.bc_yp[0] = 1
3641
3642 def setFluidYPeriodic(self):
3643 '''
3644 Set the Y boundaries of the fluid domain to follow the periodic
3645 (cyclic) boundary condition.
3646
3647 This is the default behavior for the boundary. See also
3648 :func:`setFluidYFixedPressure()` and
3649 :func:`setFluidYNoFlow()`
3650 '''
3651 self.bc_yn[0] = 2
3652 self.bc_yp[0] = 2
3653
3654 def setPermeabilityGrainSize(self, verbose=True):
3655 '''
3656 Set the permeability prefactor based on the mean grain size (Damsgaard
3657 et al., 2015, eq. 10).
3658
3659 :param verbose: Print information about the realistic permeabilities
3660 hydraulic conductivities to expect with the chosen permeability
3661 prefactor.
3662 :type verbose: bool
3663 '''
3664 self.setPermeabilityPrefactor(k_c=numpy.mean(self.radius*2.0)**2.0/180.0,
3665 verbose=verbose)
3666
3667 def setPermeabilityPrefactor(self, k_c, verbose=True):
3668 '''
3669 Set the permeability prefactor from Goren et al 2011, eq. 24. The
3670 function will print the limits of permeabilities to be simulated. This
3671 parameter is only used in the Darcy solver.
3672
3673 :param k_c: Permeability prefactor value [m*m]
3674 :type k_c: float
3675 :param verbose: Print information about the realistic permeabilities and
3676 hydraulic conductivities to expect with the chosen permeability
3677 prefactor.
3678 :type verbose: bool
3679 '''
3680 if self.cfd_solver[0] == 1:
3681 self.k_c[0] = k_c
3682 if verbose:
3683 phi = numpy.array([0.1, 0.35, 0.9])
3684 k = self.k_c * phi**3/(1.0 - phi**2)
3685 K = k * self.rho_f*numpy.abs(self.g[2])/self.mu
3686 print('Hydraulic permeability limits for porosity phi=' + \
3687 str(phi) + ':')
3688 print('\tk=' + str(k) + ' m*m')
3689 print('Hydraulic conductivity limits for porosity phi=' + \
3690 str(phi) + ':')
3691 print('\tK=' + str(K) + ' m/s')
3692 else:
3693 raise Exception('setPermeabilityPrefactor() only relevant for the '
3694 'Darcy solver (cfd_solver=1)')
3695
3696 def findPermeabilities(self):
3697 '''
3698 Calculates the hydrological permeabilities from the Kozeny-Carman
3699 relationship. These values are only relevant when the Darcy solver is
3700 used (`self.cfd_solver=1`). The permeability pre-factor `self.k_c`
3701 and the assemblage porosities must be set beforehand. The former values
3702 are set if a file from the `output/` folder is read using
3703 `self.readbin`.
3704 '''
3705 if self.cfd_solver[0] == 1:
3706 phi = numpy.clip(self.phi, 0.1, 0.9)
3707 self.k = self.k_c * phi**3/(1.0 - phi**2)
3708 else:
3709 raise Exception('findPermeabilities() only relevant for the '
3710 'Darcy solver (cfd_solver=1)')
3711
3712 def findHydraulicConductivities(self):
3713 '''
3714 Calculates the hydrological conductivities from the Kozeny-Carman
3715 relationship. These values are only relevant when the Darcy solver is
3716 used (`self.cfd_solver=1`). The permeability pre-factor `self.k_c`
3717 and the assemblage porosities must be set beforehand. The former values
3718 are set if a file from the `output/` folder is read using
3719 `self.readbin`.
3720 '''
3721 if self.cfd_solver[0] == 1:
3722 self.findPermeabilities()
3723 self.K = self.k*self.rho_f*numpy.abs(self.g[2])/self.mu
3724 else:
3725 raise Exception('findPermeabilities() only relevant for the '
3726 'Darcy solver (cfd_solver=1)')
3727
3728 def defaultParams(self, mu_s=0.5, mu_d=0.5, mu_r=0.0, rho=2600, k_n=1.16e9,
3729 k_t=1.16e9, k_r=0, gamma_n=0.0, gamma_t=0.0, gamma_r=0.0,
3730 gamma_wn=0.0, gamma_wt=0.0, capillaryCohesion=0):
3731 '''
3732 Initialize particle parameters to default values.
3733
3734 :param mu_s: The coefficient of static friction between particles [-]
3735 :type mu_s: float
3736 :param mu_d: The coefficient of dynamic friction between particles [-]
3737 :type mu_d: float
3738 :param rho: The density of the particle material [kg/(m^3)]
3739 :type rho: float
3740 :param k_n: The normal stiffness of the particles [N/m]
3741 :type k_n: float
3742 :param k_t: The tangential stiffness of the particles [N/m]
3743 :type k_t: float
3744 :param k_r: The rolling stiffness of the particles [N/rad] *Parameter
3745 not used*
3746 :type k_r: float
3747 :param gamma_n: Particle-particle contact normal viscosity [Ns/m]
3748 :type gamma_n: float
3749 :param gamma_t: Particle-particle contact tangential viscosity [Ns/m]
3750 :type gamma_t: float
3751 :param gamma_r: Particle-particle contact rolling viscosity *Parameter
3752 not used*
3753 :type gamma_r: float
3754 :param gamma_wn: Wall-particle contact normal viscosity [Ns/m]
3755 :type gamma_wn: float
3756 :param gamma_wt: Wall-particle contact tangential viscosity [Ns/m]
3757 :type gamma_wt: float
3758 :param capillaryCohesion: Enable particle-particle capillary cohesion
3759 interaction model (0=no (default), 1=yes)
3760 :type capillaryCohesion: int
3761 '''
3762
3763 # Particle material density, kg/m^3
3764 self.rho = numpy.ones(1, dtype=numpy.float64) * rho
3765
3766
3767 ### Dry granular material parameters
3768
3769 # Contact normal elastic stiffness, N/m
3770 self.k_n = numpy.ones(1, dtype=numpy.float64) * k_n
3771
3772 # Contact shear elastic stiffness (for contactmodel=2), N/m
3773 self.k_t = numpy.ones(1, dtype=numpy.float64) * k_t
3774
3775 # Contact rolling elastic stiffness (for contactmodel=2), N/m
3776 self.k_r = numpy.ones(1, dtype=numpy.float64) * k_r
3777
3778 # Contact normal viscosity. Critical damping: 2*sqrt(m*k_n).
3779 # Normal force component elastic if nu=0.0.
3780 #self.gamma_n=numpy.ones(self.np, dtype=numpy.float64) \
3781 # * nu_frac * 2.0 * math.sqrt(4.0/3.0 * math.pi \
3782 # * numpy.amin(self.radius)**3 \
3783 # * self.rho[0] * self.k_n[0])
3784 self.gamma_n = numpy.ones(1, dtype=numpy.float64) * gamma_n
3785
3786 # Contact shear viscosity, Ns/m
3787 self.gamma_t = numpy.ones(1, dtype=numpy.float64) * gamma_t
3788
3789 # Contact rolling viscosity, Ns/m?
3790 self.gamma_r = numpy.ones(1, dtype=numpy.float64) * gamma_r
3791
3792 # Contact static shear friction coefficient
3793 #self.mu_s = numpy.ones(1, dtype=numpy.float64) * \
3794 #numpy.tan(numpy.radians(ang_s))
3795 self.mu_s = numpy.ones(1, dtype=numpy.float64) * mu_s
3796
3797 # Contact dynamic shear friction coefficient
3798 #self.mu_d = numpy.ones(1, dtype=numpy.float64) * \
3799 #numpy.tan(numpy.radians(ang_d))
3800 self.mu_d = numpy.ones(1, dtype=numpy.float64) * mu_d
3801
3802 # Contact rolling friction coefficient
3803 #self.mu_r = numpy.ones(1, dtype=numpy.float64) * \
3804 #numpy.tan(numpy.radians(ang_r))
3805 self.mu_r = numpy.ones(1, dtype=numpy.float64) * mu_r
3806
3807 # Wall viscosities
3808 self.gamma_wn[0] = gamma_wn # normal
3809 self.gamma_wt[0] = gamma_wt # sliding
3810
3811 # Wall friction coefficients
3812 self.mu_ws = self.mu_s # static
3813 self.mu_wd = self.mu_d # dynamic
3814
3815 ### Parameters related to capillary bonds
3816
3817 # Wettability, 0=perfect
3818 theta = 0.0
3819 if capillaryCohesion == 1:
3820 # Prefactor
3821 self.kappa[0] = 2.0 * math.pi * gamma_t * numpy.cos(theta)
3822 self.V_b[0] = 1e-12 # Liquid volume at bond
3823 else:
3824 self.kappa[0] = 0.0 # Zero capillary force
3825 self.V_b[0] = 0.0 # Zero liquid volume at bond
3826
3827 # Debonding distance
3828 self.db[0] = (1.0 + theta/2.0) * self.V_b**(1.0/3.0)
3829
3830 def setStiffnessNormal(self, k_n):
3831 '''
3832 Set the elastic stiffness (`k_n`) in the normal direction of the
3833 contact.
3834
3835 :param k_n: The elastic stiffness coefficient [N/m]
3836 :type k_n: float
3837 '''
3838 self.k_n[0] = k_n
3839
3840 def setStiffnessTangential(self, k_t):
3841 '''
3842 Set the elastic stiffness (`k_t`) in the tangential direction of the
3843 contact.
3844
3845 :param k_t: The elastic stiffness coefficient [N/m]
3846 :type k_t: float
3847 '''
3848 self.k_t[0] = k_t
3849
3850 def setYoungsModulus(self, E):
3851 '''
3852 Set the elastic Young's modulus (`E`) for the contact model. This
3853 parameter is used over normal stiffness (`k_n`) and tangential
3854 stiffness (`k_t`) when its value is greater than zero. Using this
3855 parameter produces size-invariant behavior.
3856
3857 Example values are ~70e9 Pa for quartz,
3858 http://www.engineeringtoolbox.com/young-modulus-d_417.html
3859
3860 :param E: The elastic modulus [Pa]
3861 :type E: float
3862 '''
3863 self.E[0] = E
3864
3865 def setDampingNormal(self, gamma, over_damping=False):
3866 '''
3867 Set the dampening coefficient (gamma) in the normal direction of the
3868 particle-particle contact model. The function will print the fraction
3869 between the chosen damping and the critical damping value.
3870
3871 :param gamma: The viscous damping constant [N/(m/s)]
3872 :type gamma: float
3873 :param over_damping: Accept overdampening
3874 :type over_damping: boolean
3875
3876 See also: :func:`setDampingTangential(gamma)`
3877 '''
3878 self.gamma_n[0] = gamma
3879 critical_gamma = 2.0*numpy.sqrt(self.smallestMass()*self.k_n[0])
3880 damping_ratio = gamma/critical_gamma
3881 if damping_ratio < 1.0:
3882 print('Info: The system is under-dampened (ratio='
3883 + str(damping_ratio)
3884 + ') in the normal component. \nCritical damping='
3885 + str(critical_gamma) + '. This is ok.')
3886 elif damping_ratio > 1.0:
3887 if over_damping:
3888 print('Warning: The system is over-dampened (ratio='
3889 + str(damping_ratio) + ') in the normal component. '
3890 '\nCritical damping=' + str(critical_gamma) + '.')
3891 else:
3892 raise Exception('Warning: The system is over-dampened (ratio='
3893 + str(damping_ratio) + ') in the normal '
3894 'component.\n'
3895 'Call this function once more with '
3896 '`over_damping=True` if this is what you want.'
3897 '\nCritical damping=' + str(critical_gamma) +
3898 '.')
3899 else:
3900 print('Warning: The system is critically dampened (ratio=' +
3901 str(damping_ratio) + ') in the normal component. '
3902 '\nCritical damping=' + str(critical_gamma) + '.')
3903
3904 def setDampingTangential(self, gamma, over_damping=False):
3905 '''
3906 Set the dampening coefficient (gamma) in the tangential direction of the
3907 particle-particle contact model. The function will print the fraction
3908 between the chosen damping and the critical damping value.
3909
3910 :param gamma: The viscous damping constant [N/(m/s)]
3911 :type gamma: float
3912 :param over_damping: Accept overdampening
3913 :type over_damping: boolean
3914
3915 See also: :func:`setDampingNormal(gamma)`
3916 '''
3917 self.gamma_t[0] = gamma
3918 damping_ratio = gamma/(2.0*numpy.sqrt(self.smallestMass()*self.k_t[0]))
3919 if damping_ratio < 1.0:
3920 print('Info: The system is under-dampened (ratio='
3921 + str(damping_ratio)
3922 + ') in the tangential component. This is ok.')
3923 elif damping_ratio > 1.0:
3924 if over_damping:
3925 print('Warning: The system is over-dampened (ratio='
3926 + str(damping_ratio) + ') in the tangential component.')
3927 else:
3928 raise Exception('Warning: The system is over-dampened (ratio='
3929 + str(damping_ratio) + ') in the tangential '
3930 'component.\n'
3931 'Call this function once more with '
3932 '`over_damping=True` if this is what you want.')
3933 else:
3934 print('Warning: The system is critically dampened (ratio='
3935 + str(damping_ratio) + ') in the tangential component.')
3936
3937 def setStaticFriction(self, mu_s):
3938 '''
3939 Set the static friction coefficient for particle-particle interactions
3940 (`self.mu_s`). This value describes the resistance to a shearing motion
3941 while it is not happenind (contact tangential velocity zero).
3942
3943 :param mu_s: Value of the static friction coefficient, in [0;inf[.
3944 Usually between 0 and 1.
3945 :type mu_s: float
3946
3947 See also: :func:`setDynamicFriction(mu_d)`
3948 '''
3949 self.mu_s[0] = mu_s
3950
3951 def setDynamicFriction(self, mu_d):
3952 '''
3953 Set the dynamic friction coefficient for particle-particle interactions
3954 (`self.mu_d`). This value describes the resistance to a shearing motion
3955 while it is happening (contact tangential velocity larger than 0).
3956 Strain softening can be introduced by having a smaller dynamic
3957 frictional coefficient than the static fricion coefficient. Usually this
3958 value is identical to the static friction coefficient.
3959
3960 :param mu_d: Value of the dynamic friction coefficient, in [0;inf[.
3961 Usually between 0 and 1.
3962 :type mu_d: float
3963
3964 See also: :func:`setStaticFriction(mu_s)`
3965 '''
3966 self.mu_d[0] = mu_d
3967
3968 def setFluidCompressibility(self, beta_f):
3969 '''
3970 Set the fluid adiabatic compressibility [1/Pa]. This value is equal to
3971 `1/K` where `K` is the bulk modulus [Pa]. The value for water is 5.1e-10
3972 for water at 0 degrees Celcius. This parameter is used for the Darcy
3973 solver exclusively.
3974
3975 :param beta_f: The fluid compressibility [1/Pa]
3976 :type beta_f: float
3977
3978 See also: :func:`setFluidDensity()` and :func:`setFluidViscosity()`
3979 '''
3980 if self.cfd_solver[0] == 1:
3981 self.beta_f[0] = beta_f
3982 else:
3983 raise Exception('setFluidCompressibility() only relevant for the '
3984 'Darcy solver (cfd_solver=1)')
3985
3986 def setFluidViscosity(self, mu):
3987 '''
3988 Set the fluid dynamic viscosity [Pa*s]. The value for water is
3989 1.797e-3 at 0 degrees Celcius. This parameter is used for both the Darcy
3990 and Navier-Stokes fluid solver.
3991
3992 :param mu: The fluid dynamic viscosity [Pa*s]
3993 :type mu: float
3994
3995 See also: :func:`setFluidDensity()` and
3996 :func:`setFluidCompressibility()`
3997 '''
3998 self.mu[0] = mu
3999
4000 def setFluidDensity(self, rho_f):
4001 '''
4002 Set the fluid density [kg/(m*m*m)]. The value for water is 1000. This
4003 parameter is used for the Navier-Stokes fluid solver exclusively.
4004
4005 :param rho_f: The fluid density [kg/(m*m*m)]
4006 :type rho_f: float
4007
4008 See also: :func:`setFluidViscosity()` and
4009 :func:`setFluidCompressibility()`
4010 '''
4011 self.rho_f[0] = rho_f
4012
4013 def scaleSize(self, factor):
4014 '''
4015 Scale the positions, linear velocities, forces, torques and radii of all
4016 particles and mobile walls.
4017
4018 :param factor: Spatial scaling factor ]0;inf[
4019 :type factor: float
4020 '''
4021 self.L *= factor
4022 self.x *= factor
4023 self.radius *= factor
4024 self.xyzsum *= factor
4025 self.vel *= factor
4026 self.force *= factor
4027 self.torque *= factor
4028 self.w_x *= factor
4029 self.w_m *= factor
4030 self.w_vel *= factor
4031 self.w_force *= factor
4032
4033 def bond(self, i, j):
4034 '''
4035 Create a bond between particles with index i and j
4036
4037 :param i: Index of first particle in bond
4038 :type i: int
4039 :param j: Index of second particle in bond
4040 :type j: int
4041 '''
4042
4043 self.lambda_bar[0] = 1.0 # Radius multiplier to parallel-bond radii
4044
4045 if not hasattr(self, 'bonds'):
4046 self.bonds = numpy.array([[i, j]], dtype=numpy.uint32)
4047 else:
4048 self.bonds = numpy.vstack((self.bonds, [i, j]))
4049
4050 if not hasattr(self, 'bonds_delta_n'):
4051 self.bonds_delta_n = numpy.array([0.0], dtype=numpy.uint32)
4052 else:
4053 #self.bonds_delta_n = numpy.vstack((self.bonds_delta_n, [0.0]))
4054 self.bonds_delta_n = numpy.append(self.bonds_delta_n, [0.0])
4055
4056 if not hasattr(self, 'bonds_delta_t'):
4057 self.bonds_delta_t = numpy.array([[0.0, 0.0, 0.0]], dtype=numpy.uint32)
4058 else:
4059 self.bonds_delta_t = numpy.vstack((self.bonds_delta_t,
4060 [0.0, 0.0, 0.0]))
4061
4062 if not hasattr(self, 'bonds_omega_n'):
4063 self.bonds_omega_n = numpy.array([0.0], dtype=numpy.uint32)
4064 else:
4065 #self.bonds_omega_n = numpy.vstack((self.bonds_omega_n, [0.0]))
4066 self.bonds_omega_n = numpy.append(self.bonds_omega_n, [0.0])
4067
4068 if not hasattr(self, 'bonds_omega_t'):
4069 self.bonds_omega_t = numpy.array([[0.0, 0.0, 0.0]],
4070 dtype=numpy.uint32)
4071 else:
4072 self.bonds_omega_t = numpy.vstack((self.bonds_omega_t,
4073 [0.0, 0.0, 0.0]))
4074
4075 # Increment the number of bonds with one
4076 self.nb0 += 1
4077
4078 def currentNormalStress(self, type='defined'):
4079 '''
4080 Calculates the current magnitude of the defined or effective top wall
4081 normal stress.
4082
4083 :param type: Find the 'defined' (default) or 'effective' normal stress
4084 :type type: str
4085
4086 :returns: The current top wall normal stress in Pascal
4087 :return type: float
4088 '''
4089 if type == 'defined':
4090 return self.w_sigma0[0] \
4091 + self.w_sigma0_A[0] \
4092 *numpy.sin(2.0*numpy.pi*self.w_sigma0_f[0]\
4093 *self.time_current[0])
4094 elif type == 'effective':
4095 return self.w_force[0]/(self.L[0]*self.L[1])
4096 else:
4097 raise Exception('Normal stress type ' + type + ' not understood')
4098
4099 def surfaceArea(self, idx):
4100 '''
4101 Returns the surface area of a particle.
4102
4103 :param idx: Particle index
4104 :type idx: int
4105 :returns: The surface area of the particle [m^2]
4106 :return type: float
4107 '''
4108 return 4.0*numpy.pi*self.radius[idx]**2
4109
4110 def volume(self, idx):
4111 '''
4112 Returns the volume of a particle.
4113
4114 :param idx: Particle index
4115 :type idx: int
4116 :returns: The volume of the particle [m^3]
4117 :return type: float
4118 '''
4119 return V_sphere(self.radius[idx])
4120
4121 def mass(self, idx):
4122 '''
4123 Returns the mass of a particle.
4124
4125 :param idx: Particle index
4126 :type idx: int
4127 :returns: The mass of the particle [kg]
4128 :return type: float
4129 '''
4130 return self.rho[0]*self.volume(idx)
4131
4132 def totalMass(self):
4133 '''
4134 Returns the total mass of all particles.
4135
4136 :returns: The total mass in [kg]
4137 '''
4138 m = 0.0
4139 for i in range(self.np):
4140 m += self.mass(i)
4141 return m
4142
4143 def smallestMass(self):
4144 '''
4145 Returns the mass of the leightest particle.
4146
4147 :param idx: Particle index
4148 :type idx: int
4149 :returns: The mass of the particle [kg]
4150 :return type: float
4151 '''
4152 return V_sphere(numpy.min(self.radius))
4153
4154 def largestMass(self):
4155 '''
4156 Returns the mass of the heaviest particle.
4157
4158 :param idx: Particle index
4159 :type idx: int
4160 :returns: The mass of the particle [kg]
4161 :return type: float
4162 '''
4163 return V_sphere(numpy.max(self.radius))
4164
4165 def momentOfInertia(self, idx):
4166 '''
4167 Returns the moment of inertia of a particle.
4168
4169 :param idx: Particle index
4170 :type idx: int
4171 :returns: The moment of inertia [kg*m^2]
4172 :return type: float
4173 '''
4174 return 2.0/5.0*self.mass(idx)*self.radius[idx]**2
4175
4176 def kineticEnergy(self, idx):
4177 '''
4178 Returns the (linear) kinetic energy for a particle.
4179
4180 :param idx: Particle index
4181 :type idx: int
4182 :returns: The kinetic energy of the particle [J]
4183 :return type: float
4184 '''
4185 return 0.5*self.mass(idx) \
4186 *numpy.sqrt(numpy.dot(self.vel[idx, :], self.vel[idx, :]))**2
4187
4188 def totalKineticEnergy(self):
4189 '''
4190 Returns the total linear kinetic energy for all particles.
4191
4192 :returns: The kinetic energy of all particles [J]
4193 '''
4194 esum = 0.0
4195 for i in range(self.np):
4196 esum += self.kineticEnergy(i)
4197 return esum
4198
4199 def rotationalEnergy(self, idx):
4200 '''
4201 Returns the rotational energy for a particle.
4202
4203 :param idx: Particle index
4204 :type idx: int
4205 :returns: The rotational kinetic energy of the particle [J]
4206 :return type: float
4207 '''
4208 return 0.5*self.momentOfInertia(idx) \
4209 *numpy.sqrt(numpy.dot(self.angvel[idx, :], self.angvel[idx, :]))**2
4210
4211 def totalRotationalEnergy(self):
4212 '''
4213 Returns the total rotational kinetic energy for all particles.
4214
4215 :returns: The rotational energy of all particles [J]
4216 '''
4217 esum = 0.0
4218 for i in range(self.np):
4219 esum += self.rotationalEnergy(i)
4220 return esum
4221
4222 def viscousEnergy(self, idx):
4223 '''
4224 Returns the viscous dissipated energy for a particle.
4225
4226 :param idx: Particle index
4227 :type idx: int
4228 :returns: The energy lost by the particle by viscous dissipation [J]
4229 :return type: float
4230 '''
4231 return self.ev[idx]
4232
4233 def totalViscousEnergy(self):
4234 '''
4235 Returns the total viscous dissipated energy for all particles.
4236
4237 :returns: The normal viscous energy lost by all particles [J]
4238 :return type: float
4239 '''
4240 esum = 0.0
4241 for i in range(self.np):
4242 esum += self.viscousEnergy(i)
4243 return esum
4244
4245 def frictionalEnergy(self, idx):
4246 '''
4247 Returns the frictional dissipated energy for a particle.
4248
4249 :param idx: Particle index
4250 :type idx: int
4251 :returns: The frictional energy lost of the particle [J]
4252 :return type: float
4253 '''
4254 return self.es[idx]
4255
4256 def totalFrictionalEnergy(self):
4257 '''
4258 Returns the total frictional dissipated energy for all particles.
4259
4260 :returns: The total frictional energy lost of all particles [J]
4261 :return type: float
4262 '''
4263 esum = 0.0
4264 for i in range(self.np):
4265 esum += self.frictionalEnergy(i)
4266 return esum
4267
4268 def energy(self, method):
4269 '''
4270 Calculates the sum of the energy components of all particles.
4271
4272 :param method: The type of energy to return. Possible values are 'pot'
4273 for potential energy [J], 'kin' for kinetic energy [J], 'rot' for
4274 rotational energy [J], 'shear' for energy lost by friction,
4275 'shearrate' for the rate of frictional energy loss [W], 'visc_n' for
4276 viscous losses normal to the contact [J], 'visc_n_rate' for the rate
4277 of viscous losses normal to the contact [W], and finally 'bondpot'
4278 for the potential energy stored in bonds [J]
4279 :type method: str
4280 :returns: The value of the selected energy type
4281 :return type: float
4282 '''
4283
4284 if method == 'pot':
4285 m = numpy.ones(self.np)*4.0/3.0*math.pi*self.radius**3*self.rho
4286 return numpy.sum(m*math.sqrt(numpy.dot(self.g, self.g))*self.x[:, 2])
4287
4288 elif method == 'kin':
4289 m = numpy.ones(self.np)*4.0/3.0*math.pi*self.radius**3*self.rho
4290 esum = 0.0
4291 for i in range(self.np):
4292 esum += 0.5*m[i]*math.sqrt(\
4293 numpy.dot(self.vel[i, :], self.vel[i, :]))**2
4294 return esum
4295
4296 elif method == 'rot':
4297 m = numpy.ones(self.np)*4.0/3.0*math.pi*self.radius**3*self.rho
4298 esum = 0.0
4299 for i in range(self.np):
4300 esum += 0.5*2.0/5.0*m[i]*self.radius[i]**2 \
4301 *math.sqrt(\
4302 numpy.dot(self.angvel[i, :], self.angvel[i, :]))**2
4303 return esum
4304
4305 elif method == 'shear':
4306 return numpy.sum(self.es)
4307
4308 elif method == 'shearrate':
4309 return numpy.sum(self.es_dot)
4310
4311 elif method == 'visc_n':
4312 return numpy.sum(self.ev)
4313
4314 elif method == 'visc_n_rate':
4315 return numpy.sum(self.ev_dot)
4316
4317 elif method == 'bondpot':
4318 if self.nb0 > 0:
4319 R_bar = self.lambda_bar*numpy.minimum(\
4320 self.radius[self.bonds[:, 0]],\
4321 self.radius[self.bonds[:, 1]])
4322 A = numpy.pi*R_bar**2
4323 I = 0.25*numpy.pi*R_bar**4
4324 J = I*2.0
4325 bondpot_fn = numpy.sum(\
4326 0.5*A*self.k_n*numpy.abs(self.bonds_delta_n)**2)
4327 bondpot_ft = numpy.sum(\
4328 0.5*A*self.k_t*numpy.linalg.norm(self.bonds_delta_t)**2)
4329 bondpot_tn = numpy.sum(\
4330 0.5*J*self.k_t*numpy.abs(self.bonds_omega_n)**2)
4331 bondpot_tt = numpy.sum(\
4332 0.5*I*self.k_n*numpy.linalg.norm(self.bonds_omega_t)**2)
4333 return bondpot_fn + bondpot_ft + bondpot_tn + bondpot_tt
4334 else:
4335 return 0.0
4336 else:
4337 raise Exception('Unknownw energy() method "' + method + '"')
4338
4339 def voidRatio(self):
4340 '''
4341 Calculates the current void ratio
4342
4343 :returns: The void ratio (pore volume relative to solid volume)
4344 :return type: float
4345 '''
4346
4347 # Find the bulk volume
4348 V_t = (self.L[0] - self.origo[0]) \
4349 *(self.L[1] - self.origo[1]) \
4350 *(self.w_x[0] - self.origo[2])
4351
4352 # Find the volume of solids
4353 V_s = numpy.sum(4.0/3.0 * math.pi * self.radius**3)
4354
4355 # Return the void ratio
4356 e = (V_t - V_s)/V_s
4357 return e
4358
4359 def bulkPorosity(self, trim=True):
4360 '''
4361 Calculates the bulk porosity of the particle assemblage.
4362
4363 :param trim: Trim the total volume to the smallest axis-parallel cube
4364 containing all particles.
4365 :type trim: bool
4366
4367 :returns: The bulk porosity, in [0:1]
4368 :return type: float
4369 '''
4370
4371 V_total = 0.0
4372 if trim:
4373 min_x = numpy.min(self.x[:, 0] - self.radius)
4374 min_y = numpy.min(self.x[:, 1] - self.radius)
4375 min_z = numpy.min(self.x[:, 2] - self.radius)
4376 max_x = numpy.max(self.x[:, 0] + self.radius)
4377 max_y = numpy.max(self.x[:, 1] + self.radius)
4378 max_z = numpy.max(self.x[:, 2] + self.radius)
4379 V_total = (max_x - min_x)*(max_y - min_y)*(max_z - min_z)
4380
4381 else:
4382 if self.nw == 0:
4383 V_total = self.L[0] * self.L[1] * self.L[2]
4384 elif self.nw == 1:
4385 V_total = self.L[0] * self.L[1] * self.w_x[0]
4386 if V_total <= 0.0:
4387 raise Exception("Could not determine total volume")
4388
4389 # Find the volume of solids
4390 V_solid = numpy.sum(V_sphere(self.radius))
4391 return (V_total - V_solid) / V_total
4392
4393 def porosity(self, slices=10, verbose=False):
4394 '''
4395 Calculates the porosity as a function of depth, by averaging values in
4396 horizontal slabs. Returns porosity values and their corresponding depth.
4397 The values are calculated using the external ``porosity`` program.
4398
4399 :param slices: The number of vertical slabs to find porosities in.
4400 :type slices: int
4401 :param verbose: Show the file name of the temporary file written to
4402 disk
4403 :type verbose: bool
4404 :returns: A 2d array of depths and their averaged porosities
4405 :return type: numpy.array
4406 '''
4407
4408 # Write data as binary
4409 self.writebin(verbose=False)
4410
4411 # Run porosity program on binary
4412 pipe = subprocess.Popen(["../porosity",\
4413 "-s", "{}".format(slices),
4414 "../input/" + self.sid + ".bin"],
4415 stdout=subprocess.PIPE)
4416 output, err = pipe.communicate()
4417
4418 if err:
4419 print(err)
4420 raise Exception("Could not run external 'porosity' program")
4421
4422 # read one line of output at a time
4423 s2 = output.split(b'\n')
4424 depth = []
4425 porosity = []
4426 for row in s2:
4427 if row != '\n' or row != '' or row != ' ': # skip blank lines
4428 s3 = row.split(b'\t')
4429 if s3 != '' and len(s3) == 2: # make sure line has two vals
4430 depth.append(float(s3[0]))
4431 porosity.append(float(s3[1]))
4432
4433 return numpy.array(porosity), numpy.array(depth)
4434
4435 def run(self, verbose=True, hideinputfile=False, dry=False, valgrind=False,
4436 cudamemcheck=False, device=-1):
4437 '''
4438 Start ``sphere`` calculations on the ``sim`` object
4439
4440 :param verbose: Show ``sphere`` output
4441 :type verbose: bool
4442 :param hideinputfile: Hide the file name of the ``sphere`` input file
4443 :type hideinputfile: bool
4444 :param dry: Perform a dry run. Important parameter values are shown by
4445 the ``sphere`` program, and it exits afterwards.
4446 :type dry: bool
4447 :param valgrind: Run the program with ``valgrind`` in order to check
4448 memory leaks in the host code. This causes a significant increase in
4449 computational time.
4450 :type valgrind: bool
4451 :param cudamemcheck: Run the program with ``cudamemcheck`` in order to
4452 check for device memory leaks and errors. This causes a significant
4453 increase in computational time.
4454 :type cudamemcheck: bool
4455 :param device: Specify the GPU device to execute the program on.
4456 If not specified, sphere will use the device with the most CUDA cores.
4457 To see a list of devices, run ``nvidia-smi`` in the system shell.
4458 :type device: int
4459 '''
4460
4461 self.writebin(verbose=False)
4462
4463 quiet = ""
4464 stdout = ""
4465 dryarg = ""
4466 fluidarg = ""
4467 devicearg = ""
4468 valgrindbin = ""
4469 cudamemchk = ""
4470 binary = "sphere"
4471 if not verbose:
4472 quiet = "-q "
4473 if hideinputfile:
4474 stdout = " > /dev/null"
4475 if dry:
4476 dryarg = "--dry "
4477 if valgrind:
4478 valgrindbin = "valgrind -q --track-origins=yes "
4479 if cudamemcheck:
4480 cudamemchk = "cuda-memcheck --leak-check full "
4481 if self.fluid:
4482 fluidarg = "--fluid "
4483 if device != -1:
4484 devicearg = "-d " + str(device) + " "
4485
4486 cmd = "cd ..; " + valgrindbin + cudamemchk + "./" + binary + " " \
4487 + quiet + dryarg + fluidarg + devicearg + \
4488 "input/" + self.sid + ".bin " + stdout
4489 #print(cmd)
4490 status = subprocess.call(cmd, shell=True)
4491
4492 if status != 0:
4493 print("Warning: the sphere run returned with status " + str(status))
4494
4495 def cleanup(self):
4496 '''
4497 Removes the input/output files and images belonging to the object
4498 simulation ID from the ``input/``, ``output/`` and ``img_out/`` folders.
4499 '''
4500 cleanup(self)
4501
4502 def torqueScript(self, email='adc@geo.au.dk', email_alerts='ae',
4503 walltime='24:00:00', queue='qfermi',
4504 cudapath='/com/cuda/4.0.17/cuda',
4505 spheredir='/home/adc/code/sphere',
4506 use_workdir=False, workdir='/scratch'):
4507 '''
4508 Creates a job script for the Torque queue manager for the simulation
4509 object.
4510
4511 :param email: The e-mail address that Torque messages should be sent to
4512 :type email: str
4513 :param email_alerts: The type of Torque messages to send to the e-mail
4514 address. The character 'b' causes a mail to be sent when the
4515 execution begins. The character 'e' causes a mail to be sent when
4516 the execution ends normally. The character 'a' causes a mail to be
4517 sent if the execution ends abnormally. The characters can be written
4518 in any order.
4519 :type email_alerts: str
4520 :param walltime: The maximal allowed time for the job, in the format
4521 'HH:MM:SS'.
4522 :type walltime: str
4523 :param queue: The Torque queue to schedule the job for
4524 :type queue: str
4525 :param cudapath: The path of the CUDA library on the cluster compute
4526 nodes
4527 :type cudapath: str
4528 :param spheredir: The path to the root directory of sphere on the
4529 cluster
4530 :type spheredir: str
4531 :param use_workdir: Use a different working directory than the sphere
4532 folder
4533 :type use_workdir: bool
4534 :param workdir: The working directory during the calculations, if
4535 `use_workdir=True`
4536 :type workdir: str
4537
4538 '''
4539
4540 filename = self.sid + ".sh"
4541 fh = None
4542 try:
4543 fh = open(filename, "w")
4544
4545 fh.write('#!/bin/sh\n')
4546 fh.write('#PBS -N ' + self.sid + '\n')
4547 fh.write('#PBS -l nodes=1:ppn=1\n')
4548 fh.write('#PBS -l walltime=' + walltime + '\n')
4549 fh.write('#PBS -q ' + queue + '\n')
4550 fh.write('#PBS -M ' + email + '\n')
4551 fh.write('#PBS -m ' + email_alerts + '\n')
4552 fh.write('CUDAPATH=' + cudapath + '\n')
4553 fh.write('export PATH=$CUDAPATH/bin:$PATH\n')
4554 fh.write('export LD_LIBRARY_PATH=$CUDAPATH/lib64'
4555 + ':$CUDAPATH/lib:$LD_LIBRARY_PATH\n')
4556 fh.write('echo "`whoami`@`hostname`"\n')
4557 fh.write('echo "Start at `date`"\n')
4558 fh.write('ORIGDIR=' + spheredir + '\n')
4559 if use_workdir:
4560 fh.write('WORKDIR=' + workdir + "/$PBS_JOBID\n")
4561 fh.write('cp -r $ORIGDIR/* $WORKDIR\n')
4562 fh.write('cd $WORKDIR\n')
4563 else:
4564 fh.write('cd ' + spheredir + '\n')
4565 fh.write('cmake . && make\n')
4566 fh.write('./sphere input/' + self.sid + '.bin > /dev/null &\n')
4567 fh.write('wait\n')
4568 if use_workdir:
4569 fh.write('cp $WORKDIR/output/* $ORIGDIR/output/\n')
4570 fh.write('echo "End at `date`"\n')
4571
4572 finally:
4573 if fh is not None:
4574 fh.close()
4575
4576 def render(self, method="pres", max_val=1e3, lower_cutoff=0.0,
4577 graphics_format="png", verbose=True):
4578 '''
4579 Using the built-in ray tracer, render all output files that belong to
4580 the simulation, determined by the simulation id (``sid``).
4581
4582 :param method: The color visualization method to use for the particles.
4583 Possible values are: 'normal': color all particles with the same
4584 color, 'pres': color by pressure, 'vel': color by translational
4585 velocity, 'angvel': color by rotational velocity, 'xdisp': color by
4586 total displacement along the x-axis, 'angpos': color by angular
4587 position.
4588 :type method: str
4589 :param max_val: The maximum value of the color bar
4590 :type max_val: float
4591 :param lower_cutoff: Do not render particles with a value below this
4592 value, of the field selected by ``method``
4593 :type lower_cutoff: float
4594 :param graphics_format: Convert the PPM images generated by the ray
4595 tracer to this image format using Imagemagick
4596 :type graphics_format: str
4597 :param verbose: Show verbose information during ray tracing
4598 :type verbose: bool
4599 '''
4600
4601 print("Rendering {} images with the raytracer".format(self.sid))
4602
4603 quiet = ""
4604 if not verbose:
4605 quiet = "-q"
4606
4607 # Render images using sphere raytracer
4608 if method == "normal":
4609 subprocess.call("cd ..; for F in `ls output/" + self.sid
4610 + "*.bin`; do ./sphere " + quiet
4611 + " --render $F; done", shell=True)
4612 else:
4613 subprocess.call("cd ..; for F in `ls output/" + self.sid
4614 + "*.bin`; do ./sphere " + quiet
4615 + " --method " + method + " {}".format(max_val)
4616 + " -l {}".format(lower_cutoff)
4617 + " --render $F; done", shell=True)
4618
4619 # Convert images to compressed format
4620 if verbose:
4621 print('converting images to ' + graphics_format)
4622 convert(graphics_format=graphics_format)
4623
4624 def video(self, out_folder="./", video_format="mp4",
4625 graphics_folder="../img_out/", graphics_format="png", fps=25,
4626 verbose=False):
4627 '''
4628 Uses ffmpeg to combine images to animation. All images should be
4629 rendered beforehand using :func:`render()`.
4630
4631 :param out_folder: The output folder for the video file
4632 :type out_folder: str
4633 :param video_format: The format of the output video
4634 :type video_format: str
4635 :param graphics_folder: The folder containing the rendered images
4636 :type graphics_folder: str
4637 :param graphics_format: The format of the rendered images
4638 :type graphics_format: str
4639 :param fps: The number of frames per second to use in the video
4640 :type fps: int
4641 :param qscale: The output video quality, in ]0;1]
4642 :type qscale: float
4643 :param bitrate: The bitrate to use in the output video
4644 :type bitrate: int
4645 :param verbose: Show ffmpeg output
4646 :type verbose: bool
4647 '''
4648
4649 video(self.sid, out_folder, video_format, graphics_folder,
4650 graphics_format, fps, verbose)
4651
4652 def shearDisplacement(self):
4653 '''
4654 Calculates and returns the current shear displacement. The displacement
4655 is found by determining the total x-axis displacement of the upper,
4656 fixed particles.
4657
4658 :returns: The total shear displacement [m]
4659 :return type: float
4660
4661 See also: :func:`shearStrain()` and :func:`shearVelocity()`
4662 '''
4663
4664 # Displacement of the upper, fixed particles in the shear direction
4665 #xdisp = self.time_current[0] * self.shearVel()
4666 fixvel = numpy.nonzero(self.fixvel > 0.0)
4667 return numpy.max(self.xyzsum[fixvel, 0])
4668
4669 def shearVelocity(self):
4670 '''
4671 Calculates and returns the current shear velocity. The displacement
4672 is found by determining the total x-axis velocity of the upper,
4673 fixed particles.
4674
4675 :returns: The shear velocity [m/s]
4676 :return type: float
4677
4678 See also: :func:`shearStrainRate()` and :func:`shearDisplacement()`
4679 '''
4680 # Displacement of the upper, fixed particles in the shear direction
4681 #xdisp = self.time_current[0] * self.shearVel()
4682 fixvel = numpy.nonzero(self.fixvel > 0.0)
4683 return numpy.max(self.vel[fixvel, 0])
4684
4685 def shearVel(self):
4686 '''
4687 Alias of :func:`shearVelocity()`
4688 '''
4689 return self.shearVelocity()
4690
4691 def shearStrain(self):
4692 '''
4693 Calculates and returns the current shear strain (gamma) value of the
4694 experiment. The shear strain is found by determining the total x-axis
4695 displacement of the upper, fixed particles.
4696
4697 :returns: The total shear strain [-]
4698 :return type: float
4699
4700 See also: :func:`shearStrainRate()` and :func:`shearVel()`
4701 '''
4702
4703 # Current height
4704 w_x0 = self.w_x[0]
4705
4706 # Displacement of the upper, fixed particles in the shear direction
4707 xdisp = self.shearDisplacement()
4708
4709 # Return shear strain
4710 return xdisp/w_x0
4711
4712 def shearStrainRate(self):
4713 '''
4714 Calculates the shear strain rate (dot(gamma)) value of the experiment.
4715
4716 :returns: The value of dot(gamma)
4717 :return type: float
4718
4719 See also: :func:`shearStrain()` and :func:`shearVel()`
4720 '''
4721 #return self.shearStrain()/self.time_current[1]
4722
4723 # Current height
4724 w_x0 = self.w_x[0]
4725 v = self.shearVelocity()
4726
4727 # Return shear strain rate
4728 return v/w_x0
4729
4730 def inertiaParameterPlanarShear(self):
4731 '''
4732 Returns the value of the inertia parameter $I$ during planar shear
4733 proposed by GDR-MiDi 2004.
4734
4735 :returns: The value of $I$
4736 :return type: float
4737
4738 See also: :func:`shearStrainRate()` and :func:`shearVel()`
4739 '''
4740 return self.shearStrainRate() * numpy.mean(self.radius) \
4741 * numpy.sqrt(self.rho[0]/self.currentNormalStress())
4742
4743 def findOverlaps(self):
4744 '''
4745 Find all particle-particle overlaps by a n^2 contact search, which is
4746 done in C++. The particle pair indexes and the distance of the overlaps
4747 is saved in the object itself as the ``.pairs`` and ``.overlaps``
4748 members.
4749
4750 See also: :func:`findNormalForces()`
4751 '''
4752 self.writebin(verbose=False)
4753 subprocess.call('cd .. && ./sphere --contacts input/' + self.sid
4754 + '.bin > output/' + self.sid + '.contacts.txt',
4755 shell=True)
4756 contactdata = numpy.loadtxt('../output/' + self.sid + '.contacts.txt')
4757 self.pairs = numpy.array((contactdata[:, 0], contactdata[:, 1]),
4758 dtype=numpy.int32)
4759 self.overlaps = numpy.array(contactdata[:, 2])
4760
4761 def findCoordinationNumber(self):
4762 '''
4763 Finds the coordination number (the average number of contacts per
4764 particle). Requires a previous call to :func:`findOverlaps()`. Values
4765 are stored in ``self.coordinationnumber``.
4766 '''
4767 self.coordinationnumber = numpy.zeros(self.np, dtype=numpy.int)
4768 for i in numpy.arange(self.overlaps.size):
4769 self.coordinationnumber[self.pairs[0, i]] += 1
4770 self.coordinationnumber[self.pairs[1, i]] += 1
4771
4772 def findMeanCoordinationNumber(self):
4773 '''
4774 Returns the coordination number (the average number of contacts per
4775 particle). Requires a previous call to :func:`findOverlaps()`
4776
4777 :returns: The mean particle coordination number
4778 :return type: float
4779 '''
4780 return numpy.mean(self.coordinationnumber)
4781
4782 def findNormalForces(self):
4783 '''
4784 Finds all particle-particle overlaps (by first calling
4785 :func:`findOverlaps()`) and calculating the normal magnitude by
4786 multiplying the overlaps with the elastic stiffness ``self.k_n``.
4787
4788 The result is saved in ``self.f_n_magn``.
4789
4790 See also: :func:`findOverlaps()` and :func:`findContactStresses()`
4791 '''
4792 self.findOverlaps()
4793 self.f_n_magn = self.k_n * numpy.abs(self.overlaps)
4794
4795 def contactSurfaceArea(self, i, j, overlap):
4796 '''
4797 Finds the contact surface area of an inter-particle contact.
4798
4799 :param i: Index of first particle
4800 :type i: int or array of ints
4801 :param j: Index of second particle
4802 :type j: int or array of ints
4803 :param d: Overlap distance
4804 :type d: float or array of floats
4805 :returns: Contact area [m*m]
4806 :return type: float or array of floats
4807 '''
4808 r_i = self.radius[i]
4809 r_j = self.radius[j]
4810 d = r_i + r_j + overlap
4811 contact_radius = 1./(2.*d)*((-d + r_i - r_j)*(-d - r_i + r_j)*
4812 (-d + r_i + r_j)*(d + r_i + r_j)
4813 )**0.5
4814 return numpy.pi*contact_radius**2.
4815
4816 def contactParticleArea(self, i, j):
4817 '''
4818 Finds the average area of an two particles in an inter-particle contact.
4819
4820 :param i: Index of first particle
4821 :type i: int or array of ints
4822 :param j: Index of second particle
4823 :type j: int or array of ints
4824 :param d: Overlap distance
4825 :type d: float or array of floats
4826 :returns: Contact area [m*m]
4827 :return type: float or array of floats
4828 '''
4829 r_bar = (self.radius[i] + self.radius[j])*0.5
4830 return numpy.pi*r_bar**2.
4831
4832 def findAllContactSurfaceAreas(self):
4833 '''
4834 Finds the contact surface area of an inter-particle contact. This
4835 function requires a prior call to :func:`findOverlaps()` as it reads
4836 from the ``self.pairs`` and ``self.overlaps`` arrays.
4837
4838 :returns: Array of contact surface areas
4839 :return type: array of floats
4840 '''
4841 return self.contactSurfaceArea(self.pairs[0, :], self.pairs[1, :],
4842 self.overlaps)
4843
4844 def findAllAverageParticlePairAreas(self):
4845 '''
4846 Finds the average area of an inter-particle contact. This
4847 function requires a prior call to :func:`findOverlaps()` as it reads
4848 from the ``self.pairs`` and ``self.overlaps`` arrays.
4849
4850 :returns: Array of contact surface areas
4851 :return type: array of floats
4852 '''
4853 return self.contactParticleArea(self.pairs[0, :], self.pairs[1, :])
4854
4855 def findContactStresses(self, area='average'):
4856 '''
4857 Finds all particle-particle uniaxial normal stresses (by first calling
4858 :func:`findNormalForces()`) and calculating the stress magnitudes by
4859 dividing the normal force magnitude with the average particle area
4860 ('average') or by the contact surface area ('contact').
4861
4862 The result is saved in ``self.sigma_contacts``.
4863
4864 :param area: Area to use: 'average' (default) or 'contact'
4865 :type area: str
4866
4867 See also: :func:`findNormalForces()` and :func:`findOverlaps()`
4868 '''
4869 self.findNormalForces()
4870 if area == 'average':
4871 areas = self.findAllAverageParticlePairAreas()
4872 elif area == 'contact':
4873 areas = self.findAllContactSurfaceAreas()
4874 else:
4875 raise Exception('Contact area type "' + area + '" not understood')
4876
4877 self.sigma_contacts = self.f_n_magn/areas
4878
4879 def findLoadedContacts(self, threshold):
4880 '''
4881 Finds the indices of contact pairs where the contact stress magnitude
4882 exceeds or is equal to a specified threshold value. This function calls
4883 :func:`findContactStresses()`.
4884
4885 :param threshold: Threshold contact stress [Pa]
4886 :type threshold: float
4887 :returns: Array of contact indices
4888 :return type: array of ints
4889 '''
4890 self.findContactStresses()
4891 return numpy.nonzero(self.sigma_contacts >= threshold)
4892
4893 def forcechains(self, lc=200.0, uc=650.0, outformat='png', disp='2d'):
4894 '''
4895 Visualizes the force chains in the system from the magnitude of the
4896 normal contact forces, and produces an image of them. Warning: Will
4897 segfault if no contacts are found.
4898
4899 :param lc: Lower cutoff of contact forces. Contacts below are not
4900 visualized
4901 :type lc: float
4902 :param uc: Upper cutoff of contact forces. Contacts above are
4903 visualized with this value
4904 :type uc: float
4905 :param outformat: Format of output image. Possible values are
4906 'interactive', 'png', 'epslatex', 'epslatex-color'
4907 :type outformat: str
4908 :param disp: Display forcechains in '2d' or '3d'
4909 :type disp: str
4910 '''
4911
4912 self.writebin(verbose=False)
4913
4914 nd = ''
4915 if disp == '2d':
4916 nd = '-2d '
4917
4918 subprocess.call("cd .. && ./forcechains " + nd + "-f " + outformat
4919 + " -lc " + str(lc) + " -uc " + str(uc)
4920 + " input/" + self.sid + ".bin > python/tmp.gp",
4921 shell=True)
4922 subprocess.call("gnuplot tmp.gp && rm tmp.gp", shell=True)
4923
4924
4925 def forcechainsRose(self, lower_limit=0.25, graphics_format='pdf'):
4926 '''
4927 Visualize trend and plunge angles of the strongest force chains in a
4928 rose plot. The plots are saved in the current folder with the name
4929 'fc-<simulation id>-rose.pdf'.
4930
4931 :param lower_limit: Do not visualize force chains below this relative
4932 contact force magnitude, in ]0;1[
4933 :type lower_limit: float
4934 :param graphics_format: Save the plot in this format
4935 :type graphics_format: str
4936 '''
4937 self.writebin(verbose=False)
4938
4939 subprocess.call("cd .. && ./forcechains -f txt input/" + self.sid \
4940 + ".bin > python/fc-tmp.txt", shell=True)
4941
4942 # data will have the shape (numcontacts, 7)
4943 data = numpy.loadtxt("fc-tmp.txt", skiprows=1)
4944
4945 # find the max. value of the normal force
4946 f_n_max = numpy.amax(data[:, 6])
4947
4948 # specify the lower limit of force chains to do statistics on
4949 f_n_lim = lower_limit * f_n_max * 0.6
4950
4951 # find the indexes of these contacts
4952 I = numpy.nonzero(data[:, 6] > f_n_lim)
4953
4954 # loop through these contacts and find the strike and dip of the
4955 # contacts
4956 strikelist = [] # strike direction of the normal vector, [0:360[
4957 diplist = [] # dip of the normal vector, [0:90]
4958 for i in I[0]:
4959
4960 x1 = data[i, 0]
4961 y1 = data[i, 1]
4962 z1 = data[i, 2]
4963 x2 = data[i, 3]
4964 y2 = data[i, 4]
4965 z2 = data[i, 5]
4966
4967 if z1 < z2:
4968 xlower = x1; ylower = y1; zlower = z1
4969 xupper = x2; yupper = y2; zupper = z2
4970 else:
4971 xlower = x2; ylower = y2; zlower = z2
4972 xupper = x1; yupper = y1; zupper = z1
4973
4974 # Vector pointing downwards
4975 dx = xlower - xupper
4976 dy = ylower - yupper
4977 dhoriz = numpy.sqrt(dx**2 + dy**2)
4978
4979 # Find dip angle
4980 diplist.append(math.degrees(math.atan((zupper - zlower)/dhoriz)))
4981
4982 # Find strike angle
4983 if ylower >= yupper: # in first two quadrants
4984 strikelist.append(math.acos(dx/dhoriz))
4985 else:
4986 strikelist.append(2.0*numpy.pi - math.acos(dx/dhoriz))
4987
4988
4989 plt.figure(figsize=[4, 4])
4990 ax = plt.subplot(111, polar=True)
4991 ax.scatter(strikelist, diplist, c='k', marker='+')
4992 ax.set_rmax(90)
4993 ax.set_rticks([])
4994 plt.savefig('fc-' + self.sid + '-rose.' + graphics_format,\
4995 transparent=True)
4996
4997 subprocess.call('rm fc-tmp.txt', shell=True)
4998
4999 def bondsRose(self, graphics_format='pdf'):
5000 '''
5001 Visualize the trend and plunge angles of the bond pairs in a rose plot.
5002 The plot is saved in the current folder as
5003 'bonds-<simulation id>-rose.<graphics_format>'.
5004
5005 :param graphics_format: Save the plot in this format
5006 :type graphics_format: str
5007 '''
5008 if not py_mpl:
5009 print('Error: matplotlib module not found, cannot bondsRose.')
5010 return
5011 # loop through these contacts and find the strike and dip of the
5012 # contacts
5013 strikelist = [] # strike direction of the normal vector, [0:360[
5014 diplist = [] # dip of the normal vector, [0:90]
5015 for n in numpy.arange(self.nb0):
5016
5017 i = self.bonds[n, 0]
5018 j = self.bonds[n, 1]
5019
5020 x1 = self.x[i, 0]
5021 y1 = self.x[i, 1]
5022 z1 = self.x[i, 2]
5023 x2 = self.x[j, 0]
5024 y2 = self.x[j, 1]
5025 z2 = self.x[j, 2]
5026
5027 if z1 < z2:
5028 xlower = x1; ylower = y1; zlower = z1
5029 xupper = x2; yupper = y2; zupper = z2
5030 else:
5031 xlower = x2; ylower = y2; zlower = z2
5032 xupper = x1; yupper = y1; zupper = z1
5033
5034 # Vector pointing downwards
5035 dx = xlower - xupper
5036 dy = ylower - yupper
5037 dhoriz = numpy.sqrt(dx**2 + dy**2)
5038
5039 # Find dip angle
5040 diplist.append(math.degrees(math.atan((zupper - zlower)/dhoriz)))
5041
5042 # Find strike angle
5043 if ylower >= yupper: # in first two quadrants
5044 strikelist.append(math.acos(dx/dhoriz))
5045 else:
5046 strikelist.append(2.0*numpy.pi - math.acos(dx/dhoriz))
5047
5048 plt.figure(figsize=[4, 4])
5049 ax = plt.subplot(111, polar=True)
5050 ax.scatter(strikelist, diplist, c='k', marker='+')
5051 ax.set_rmax(90)
5052 ax.set_rticks([])
5053 plt.savefig('bonds-' + self.sid + '-rose.' + graphics_format,\
5054 transparent=True)
5055
5056 def status(self):
5057 '''
5058 Returns the current simulation status by using the simulation id
5059 (``sid``) as an identifier.
5060
5061 :returns: The number of the last output file written
5062 :return type: int
5063 '''
5064 return status(self.sid)
5065
5066 def momentum(self, idx):
5067 '''
5068 Returns the momentum (m*v) of a particle.
5069
5070 :param idx: The particle index
5071 :type idx: int
5072 :returns: The particle momentum [N*s]
5073 :return type: numpy.array
5074 '''
5075 return self.rho*V_sphere(self.radius[idx])*self.vel[idx, :]
5076
5077 def totalMomentum(self):
5078 '''
5079 Returns the sum of particle momentums.
5080
5081 :returns: The sum of particle momentums (m*v) [N*s]
5082 :return type: numpy.array
5083 '''
5084 m_sum = numpy.zeros(3)
5085 for i in range(self.np):
5086 m_sum += self.momentum(i)
5087 return m_sum
5088
5089 def sheardisp(self, graphics_format='pdf', zslices=32):
5090 '''
5091 Plot the particle x-axis displacement against the original vertical
5092 particle position. The plot is saved in the current directory with the
5093 file name '<simulation id>-sheardisp.<graphics_format>'.
5094
5095 :param graphics_format: Save the plot in this format
5096 :type graphics_format: str
5097 '''
5098 if not py_mpl:
5099 print('Error: matplotlib module not found, cannot sheardisp.')
5100 return
5101
5102 # Bin data and error bars for alternative visualization
5103 h_total = numpy.max(self.x[:, 2]) - numpy.min(self.x[:, 2])
5104 h_slice = h_total / zslices
5105
5106 zpos = numpy.zeros(zslices)
5107 xdisp = numpy.zeros(zslices)
5108 err = numpy.zeros(zslices)
5109
5110 for iz in range(zslices):
5111
5112 # Find upper and lower boundaries of bin
5113 zlower = iz * h_slice
5114 zupper = zlower + h_slice
5115
5116 # Save depth
5117 zpos[iz] = zlower + 0.5*h_slice
5118
5119 # Find particle indexes within that slice
5120 I = numpy.nonzero((self.x[:, 2] > zlower) & (self.x[:, 2] < zupper))
5121
5122 # Save mean x displacement
5123 xdisp[iz] = numpy.mean(self.xyzsum[I, 0])
5124
5125 # Save x displacement standard deviation
5126 err[iz] = numpy.std(self.xyzsum[I, 0])
5127
5128 plt.figure(figsize=[4, 4])
5129 ax = plt.subplot(111)
5130 ax.scatter(self.xyzsum[:, 0], self.x[:, 2], c='gray', marker='+')
5131 ax.errorbar(xdisp, zpos, xerr=err,
5132 c='black', linestyle='-', linewidth=1.4)
5133 ax.set_xlabel("Horizontal particle displacement, [m]")
5134 ax.set_ylabel("Vertical position, [m]")
5135 plt.savefig(self.sid + '-sheardisp.' + graphics_format,
5136 transparent=True)
5137
5138 def porosities(self, graphics_format='pdf', zslices=16):
5139 '''
5140 Plot the averaged porosities with depth. The plot is saved in the format
5141 '<simulation id>-porosity.<graphics_format>'.
5142
5143 :param graphics_format: Save the plot in this format
5144 :type graphics_format: str
5145 :param zslices: The number of points along the vertical axis to sample
5146 the porosity in
5147 :type zslices: int
5148 '''
5149 if not py_mpl:
5150 print('Error: matplotlib module not found, cannot sheardisp.')
5151 return
5152
5153 porosity, depth = self.porosity(zslices)
5154
5155 plt.figure(figsize=[4, 4])
5156 ax = plt.subplot(111)
5157 ax.plot(porosity, depth, c='black', linestyle='-', linewidth=1.4)
5158 ax.set_xlabel('Horizontally averaged porosity, [-]')
5159 ax.set_ylabel('Vertical position, [m]')
5160 plt.savefig(self.sid + '-porositiy.' + graphics_format,
5161 transparent=True)
5162
5163 def thinsection_x1x3(self, x2='center', graphics_format='png', cbmax=None,
5164 arrowscale=0.01, velarrowscale=1.0, slipscale=1.0,
5165 verbose=False):
5166 '''
5167 Produce a 2D image of particles on a x1,x3 plane, intersecting the
5168 second axis at x2. Output is saved as '<sid>-ts-x1x3.txt' in the
5169 current folder.
5170
5171 An upper limit to the pressure color bar range can be set by the
5172 cbmax parameter.
5173
5174 The data can be plotted in gnuplot with:
5175 gnuplot> set size ratio -1
5176 gnuplot> set palette defined (0 "blue", 0.5 "gray", 1 "red")
5177 gnuplot> plot '<sid>-ts-x1x3.txt' with circles palette fs \
5178 transparent solid 0.4 noborder
5179
5180 This function also saves a plot of the inter-particle slip angles.
5181
5182 :param x2: The position along the second axis of the intersecting plane
5183 :type x2: foat
5184 :param graphics_format: Save the slip angle plot in this format
5185 :type graphics_format: str
5186 :param cbmax: The maximal value of the pressure color bar range
5187 :type cbmax: float
5188 :param arrowscale: Scale the rotational arrows by this value
5189 :type arrowscale: float
5190 :param velarrowscale: Scale the translational arrows by this value
5191 :type velarrowscale: float
5192 :param slipscale: Scale the slip arrows by this value
5193 :type slipscale: float
5194 :param verbose: Show function output during calculations
5195 :type verbose: bool
5196 '''
5197
5198 if not py_mpl:
5199 print('Error: matplotlib module not found (thinsection_x1x3).')
5200 return
5201
5202 if x2 == 'center':
5203 x2 = (self.L[1] - self.origo[1]) / 2.0
5204
5205 # Initialize plot circle positionsr, radii and pressures
5206 ilist = []
5207 xlist = []
5208 ylist = []
5209 rlist = []
5210 plist = []
5211 pmax = 0.0
5212 rmax = 0.0
5213 axlist = []
5214 aylist = []
5215 daxlist = []
5216 daylist = []
5217 dvxlist = []
5218 dvylist = []
5219 # Black circle at periphery of particles with angvel[:, 1] > 0.0
5220 cxlist = []
5221 cylist = []
5222 crlist = []
5223
5224 # Loop over all particles, find intersections
5225 for i in range(self.np):
5226
5227 delta = abs(self.x[i, 1] - x2) # distance between centre and plane
5228
5229 if delta < self.radius[i]: # if the sphere intersects the plane
5230
5231 # Store particle index
5232 ilist.append(i)
5233
5234 # Store position on plane
5235 xlist.append(self.x[i, 0])
5236 ylist.append(self.x[i, 2])
5237
5238 # Store radius of intersection
5239 r_circ = math.sqrt(self.radius[i]**2 - delta**2)
5240 if r_circ > rmax:
5241 rmax = r_circ
5242 rlist.append(r_circ)
5243
5244 # Store pos. and radius if it is spinning around pos. y
5245 if self.angvel[i, 1] > 0.0:
5246 cxlist.append(self.x[i, 0])
5247 cylist.append(self.x[i, 2])
5248 crlist.append(r_circ)
5249
5250 # Store pressure
5251 pval = self.p[i]
5252 if cbmax != None:
5253 if pval > cbmax:
5254 pval = cbmax
5255 plist.append(pval)
5256
5257 # Store rotational velocity data for arrows
5258 # Save two arrows per particle
5259 axlist.append(self.x[i, 0]) # x starting point of arrow
5260 axlist.append(self.x[i, 0]) # x starting point of arrow
5261
5262 # y starting point of arrow
5263 aylist.append(self.x[i, 2] + r_circ*0.5)
5264
5265 # y starting point of arrow
5266 aylist.append(self.x[i, 2] - r_circ*0.5)
5267
5268 # delta x for arrow end point
5269 daxlist.append(self.angvel[i, 1]*arrowscale)
5270
5271 # delta x for arrow end point
5272 daxlist.append(-self.angvel[i, 1]*arrowscale)
5273 daylist.append(0.0) # delta y for arrow end point
5274 daylist.append(0.0) # delta y for arrow end point
5275
5276 # Store linear velocity data
5277
5278 # delta x for arrow end point
5279 dvxlist.append(self.vel[i, 0]*velarrowscale)
5280
5281 # delta y for arrow end point
5282 dvylist.append(self.vel[i, 2]*velarrowscale)
5283
5284 if r_circ > self.radius[i]:
5285 raise Exception("Error, circle radius is larger than the "
5286 "particle radius")
5287 if self.p[i] > pmax:
5288 pmax = self.p[i]
5289
5290 if verbose:
5291 print("Max. pressure of intersecting spheres: " + str(pmax) + " Pa")
5292 if cbmax != None:
5293 print("Value limited to: " + str(cbmax) + " Pa")
5294
5295 # Save circle data
5296 filename = '../gnuplot/data/' + self.sid + '-ts-x1x3.txt'
5297 fh = None
5298 try:
5299 fh = open(filename, 'w')
5300
5301 for (x, y, r, p) in zip(xlist, ylist, rlist, plist):
5302 fh.write("{}\t{}\t{}\t{}\n".format(x, y, r, p))
5303
5304 finally:
5305 if fh is not None:
5306 fh.close()
5307
5308 # Save circle data for articles spinning with pos. y
5309 filename = '../gnuplot/data/' + self.sid + '-ts-x1x3-circ.txt'
5310 fh = None
5311 try:
5312 fh = open(filename, 'w')
5313
5314 for (x, y, r) in zip(cxlist, cylist, crlist):
5315 fh.write("{}\t{}\t{}\n".format(x, y, r))
5316
5317 finally:
5318 if fh is not None:
5319 fh.close()
5320
5321 # Save angular velocity data. The arrow lengths are normalized to max.
5322 # radius
5323 # Output format: x, y, deltax, deltay
5324 # gnuplot> plot '-' using 1:2:3:4 with vectors head filled lt 2
5325 filename = '../gnuplot/data/' + self.sid + '-ts-x1x3-arrows.txt'
5326 fh = None
5327 try:
5328 fh = open(filename, 'w')
5329
5330 for (ax, ay, dax, day) in zip(axlist, aylist, daxlist, daylist):
5331 fh.write("{}\t{}\t{}\t{}\n".format(ax, ay, dax, day))
5332
5333 finally:
5334 if fh is not None:
5335 fh.close()
5336
5337 # Save linear velocity data
5338 # Output format: x, y, deltax, deltay
5339 # gnuplot> plot '-' using 1:2:3:4 with vectors head filled lt 2
5340 filename = '../gnuplot/data/' + self.sid + '-ts-x1x3-velarrows.txt'
5341 fh = None
5342 try:
5343 fh = open(filename, 'w')
5344
5345 for (x, y, dvx, dvy) in zip(xlist, ylist, dvxlist, dvylist):
5346 fh.write("{}\t{}\t{}\t{}\n".format(x, y, dvx, dvy))
5347
5348 finally:
5349 if fh is not None:
5350 fh.close()
5351
5352 # Check whether there are slips between the particles intersecting the
5353 # plane
5354 sxlist = []
5355 sylist = []
5356 dsxlist = []
5357 dsylist = []
5358 anglelist = [] # angle of the slip vector
5359 slipvellist = [] # velocity of the slip
5360 for i in ilist:
5361
5362 # Loop through other particles, and check whether they are in
5363 # contact
5364 for j in ilist:
5365 #if i < j:
5366 if i != j:
5367
5368 # positions
5369 x_i = self.x[i, :]
5370 x_j = self.x[j, :]
5371
5372 # radii
5373 r_i = self.radius[i]
5374 r_j = self.radius[j]
5375
5376 # Inter-particle vector
5377 x_ij = x_i - x_j
5378 x_ij_length = numpy.sqrt(x_ij.dot(x_ij))
5379
5380 # Check for overlap
5381 if x_ij_length - (r_i + r_j) < 0.0:
5382
5383 # contact plane normal vector
5384 n_ij = x_ij / x_ij_length
5385
5386 vel_i = self.vel[i, :]
5387 vel_j = self.vel[j, :]
5388 angvel_i = self.angvel[i, :]
5389 angvel_j = self.angvel[j, :]
5390
5391 # Determine the tangential contact surface velocity in
5392 # the x,z plane
5393 dot_delta = (vel_i - vel_j) \
5394 + r_i * numpy.cross(n_ij, angvel_i) \
5395 + r_j * numpy.cross(n_ij, angvel_j)
5396
5397 # Subtract normal component to get tangential velocity
5398 dot_delta_n = n_ij * numpy.dot(dot_delta, n_ij)
5399 dot_delta_t = dot_delta - dot_delta_n
5400
5401 # Save slip velocity data for gnuplot
5402 if dot_delta_t[0] != 0.0 or dot_delta_t[2] != 0.0:
5403
5404 # Center position of the contact
5405 cpos = x_i - x_ij * 0.5
5406
5407 sxlist.append(cpos[0])
5408 sylist.append(cpos[2])
5409 dsxlist.append(dot_delta_t[0] * slipscale)
5410 dsylist.append(dot_delta_t[2] * slipscale)
5411 #anglelist.append(math.degrees(\
5412 #math.atan(dot_delta_t[2]/dot_delta_t[0])))
5413 anglelist.append(\
5414 math.atan(dot_delta_t[2]/dot_delta_t[0]))
5415 slipvellist.append(\
5416 numpy.sqrt(dot_delta_t.dot(dot_delta_t)))
5417
5418
5419 # Write slip lines to text file
5420 filename = '../gnuplot/data/' + self.sid + '-ts-x1x3-slips.txt'
5421 fh = None
5422 try:
5423 fh = open(filename, 'w')
5424
5425 for (sx, sy, dsx, dsy) in zip(sxlist, sylist, dsxlist, dsylist):
5426 fh.write("{}\t{}\t{}\t{}\n".format(sx, sy, dsx, dsy))
5427
5428 finally:
5429 if fh is not None:
5430 fh.close()
5431
5432 # Plot thinsection with gnuplot script
5433 gamma = self.shearStrain()
5434 subprocess.call('''cd ../gnuplot/scripts && gnuplot -e "sid='{}'; ''' \
5435 + '''gamma='{:.4}'; xmin='{}'; xmax='{}'; ymin='{}'; ''' \
5436 + '''ymax='{}'" plotts.gp'''.format(\
5437 self.sid, self.shearStrain(), self.origo[0], self.L[0], \
5438 self.origo[2], self.L[2]), shell=True)
5439
5440 # Find all particles who have a slip velocity higher than slipvel
5441 slipvellimit = 0.01
5442 slipvels = numpy.nonzero(numpy.array(slipvellist) > slipvellimit)
5443
5444 # Bin slip angle data for histogram
5445 binno = 36/2
5446 hist_ang, bins_ang = numpy.histogram(numpy.array(anglelist)[slipvels],\
5447 bins=binno, density=False)
5448 center_ang = (bins_ang[:-1] + bins_ang[1:]) / 2.0
5449
5450 center_ang_mirr = numpy.concatenate((center_ang, center_ang + math.pi))
5451 hist_ang_mirr = numpy.tile(hist_ang, 2)
5452
5453 # Write slip angles to text file
5454 #numpy.savetxt(self.sid + '-ts-x1x3-slipangles.txt', zip(center_ang,\
5455 #hist_ang), fmt="%f\t%f")
5456
5457 fig = plt.figure()
5458 ax = fig.add_subplot(111, polar=True)
5459 ax.bar(center_ang_mirr, hist_ang_mirr, width=30.0/180.0)
5460 fig.savefig('../img_out/' + self.sid + '-ts-x1x3-slipangles.' +
5461 graphics_format)
5462 fig.clf()
5463
5464 def plotContacts(self, graphics_format='png', figsize=[4, 4], title=None,
5465 lower_limit=0.0, upper_limit=1.0, alpha=1.0,
5466 return_data=False, outfolder='.',
5467 f_min=None, f_max=None, histogram=True,
5468 forcechains=True):
5469 '''
5470 Plot current contact orientations on polar plot
5471
5472 :param lower_limit: Do not visualize force chains below this relative
5473 contact force magnitude, in ]0;1[
5474 :type lower_limit: float
5475 :param upper_limit: Visualize force chains above this relative
5476 contact force magnitude but cap color bar range, in ]0;1[
5477 :type upper_limit: float
5478 :param graphics_format: Save the plot in this format
5479 :type graphics_format: str
5480 '''
5481
5482 if not py_mpl:
5483 print('Error: matplotlib module not found (plotContacts).')
5484 return
5485
5486 self.writebin(verbose=False)
5487
5488 subprocess.call("cd .. && ./forcechains -f txt input/" + self.sid \
5489 + ".bin > python/contacts-tmp.txt", shell=True)
5490
5491 # data will have the shape (numcontacts, 7)
5492 data = numpy.loadtxt('contacts-tmp.txt', skiprows=1)
5493
5494 # find the max. value of the normal force
5495 f_n_max = numpy.amax(data[:, 6])
5496
5497 # specify the lower limit of force chains to do statistics on
5498 f_n_lim = lower_limit * f_n_max
5499
5500 if f_min:
5501 f_n_lim = f_min
5502 if f_max:
5503 f_n_max = f_max
5504
5505 # find the indexes of these contacts
5506 I = numpy.nonzero(data[:, 6] >= f_n_lim)
5507
5508 # loop through these contacts and find the strike and dip of the
5509 # contacts
5510
5511 # strike direction of the normal vector, [0:360[
5512 strikelist = numpy.empty(len(I[0]))
5513 diplist = numpy.empty(len(I[0])) # dip of the normal vector, [0:90]
5514 forcemagnitude = data[I, 6]
5515 j = 0
5516 for i in I[0]:
5517
5518 x1 = data[i, 0]
5519 y1 = data[i, 1]
5520 z1 = data[i, 2]
5521 x2 = data[i, 3]
5522 y2 = data[i, 4]
5523 z2 = data[i, 5]
5524
5525 if z1 < z2:
5526 xlower = x1; ylower = y1; zlower = z1
5527 xupper = x2; yupper = y2; zupper = z2
5528 else:
5529 xlower = x2; ylower = y2; zlower = z2
5530 xupper = x1; yupper = y1; zupper = z1
5531
5532 # Vector pointing downwards
5533 dx = xlower - xupper
5534 dy = ylower - yupper
5535 dhoriz = numpy.sqrt(dx**2 + dy**2)
5536
5537 # Find dip angle
5538 diplist[j] = numpy.degrees(numpy.arctan((zupper - zlower)/dhoriz))
5539
5540 # Find strike angle
5541 if ylower >= yupper: # in first two quadrants
5542 strikelist[j] = numpy.arccos(dx/dhoriz)
5543 else:
5544 strikelist[j] = 2.0*numpy.pi - numpy.arccos(dx/dhoriz)
5545
5546 j += 1
5547
5548 fig = plt.figure(figsize=figsize)
5549 ax = plt.subplot(111, polar=True)
5550 cs = ax.scatter(strikelist, 90. - diplist, marker='o',
5551 c=forcemagnitude,
5552 s=forcemagnitude/f_n_max*40.,
5553 alpha=alpha,
5554 edgecolors='none',
5555 vmin=f_n_max*lower_limit,
5556 vmax=f_n_max*upper_limit,
5557 cmap=matplotlib.cm.get_cmap('afmhot_r'))
5558 plt.colorbar(cs, extend='max')
5559
5560 # plot defined max compressive stress from tau/N ratio
5561 ax.scatter(0., # prescribed stress
5562 numpy.degrees(numpy.arctan(self.shearStress('defined')/
5563 self.currentNormalStress('defined'))),
5564 marker='o', c='none', edgecolor='blue', s=300)
5565 ax.scatter(0., # actual stress
5566 numpy.degrees(numpy.arctan(self.shearStress('effective')/
5567 self.currentNormalStress('effective'))),
5568 marker='+', color='blue', s=300)
5569
5570 ax.set_rmax(90)
5571 ax.set_rticks([])
5572
5573 if title:
5574 plt.title(title)
5575 else:
5576 plt.title('t={:.2f} s'.format(self.currentTime()))
5577
5578 #plt.tight_layout()
5579 plt.savefig(outfolder + '/contacts-' + self.sid + '-' + \
5580 str(self.time_step_count[0]) + '.' + \
5581 graphics_format,\
5582 transparent=False)
5583
5584 subprocess.call('rm contacts-tmp.txt', shell=True)
5585
5586 fig.clf()
5587 if histogram:
5588 #hist, bins = numpy.histogram(datadata[:, 6], bins=10)
5589 _, _, _ = plt.hist(data[:, 6], alpha=0.75, facecolor='gray')
5590 #plt.xlabel('$\\boldsymbol{f}_\text{n}$ [N]')
5591 plt.yscale('log', nonposy='clip')
5592 plt.xlabel('Contact load [N]')
5593 plt.ylabel('Count $N$')
5594 plt.grid(True)
5595 plt.savefig(outfolder + '/contacts-hist-' + self.sid + '-' + \
5596 str(self.time_step_count[0]) + '.' + \
5597 graphics_format,\
5598 transparent=False)
5599 plt.clf()
5600
5601 # angle: 0 when vertical, 90 when horizontal
5602 #hist, bins = numpy.histogram(datadata[:, 6], bins=10)
5603 _, _, _ = plt.hist(90. - diplist, bins=range(0, 100, 10),
5604 alpha=0.75, facecolor='gray')
5605 theta_sigma1 = numpy.degrees(numpy.arctan(
5606 self.currentNormalStress('defined')/\
5607 self.shearStress('defined')))
5608 plt.axvline(90. - theta_sigma1, color='k', linestyle='dashed',
5609 linewidth=1)
5610 plt.xlim([0, 90.])
5611 plt.ylim([0, self.np/10])
5612 #plt.xlabel('$\\boldsymbol{f}_\text{n}$ [N]')
5613 plt.xlabel('Contact angle [deg]')
5614 plt.ylabel('Count $N$')
5615 plt.grid(True)
5616 plt.savefig(outfolder + '/dip-' + self.sid + '-' + \
5617 str(self.time_step_count[0]) + '.' + \
5618 graphics_format,\
5619 transparent=False)
5620 plt.clf()
5621
5622 if forcechains:
5623
5624 #color = matplotlib.cm.spectral(data[:, 6]/f_n_max)
5625 for i in I[0]:
5626
5627 x1 = data[i, 0]
5628 #y1 = data[i, 1]
5629 z1 = data[i, 2]
5630 x2 = data[i, 3]
5631 #y2 = data[i, 4]
5632 z2 = data[i, 5]
5633 f_n = data[i, 6]
5634
5635 lw_max = 1.0
5636 if f_n >= f_n_max:
5637 lw = lw_max
5638 else:
5639 lw = (f_n - f_n_lim)/(f_n_max - f_n_lim)*lw_max
5640
5641 #print lw
5642 plt.plot([x1, x2], [z1, z2], '-k', linewidth=lw)
5643
5644 axfc1 = plt.gca()
5645 axfc1.spines['right'].set_visible(False)
5646 axfc1.spines['left'].set_visible(False)
5647 # Only show ticks on the left and bottom spines
5648 axfc1.xaxis.set_ticks_position('none')
5649 axfc1.yaxis.set_ticks_position('none')
5650 #axfc1.set_xticklabels([])
5651 #axfc1.set_yticklabels([])
5652 axfc1.set_xlim([self.origo[0], self.L[0]])
5653 axfc1.set_ylim([self.origo[2], self.L[2]])
5654 axfc1.set_aspect('equal')
5655
5656 plt.xlabel('$x$ [m]')
5657 plt.ylabel('$z$ [m]')
5658 plt.grid(False)
5659 plt.savefig(outfolder + '/fc-' + self.sid + '-' + \
5660 str(self.time_step_count[0]) + '.' + \
5661 graphics_format,\
5662 transparent=False)
5663
5664 plt.close()
5665
5666 if return_data:
5667 return data, strikelist, diplist, forcemagnitude, alpha, f_n_max
5668
5669 def plotFluidPressuresY(self, y=-1, graphics_format='png', verbose=True):
5670 '''
5671 Plot fluid pressures in a plane normal to the second axis.
5672 The plot is saved in the current folder with the format
5673 'p_f-<simulation id>-y<y value>.<graphics_format>'.
5674
5675 :param y: Plot pressures in fluid cells with these y axis values. If
5676 this value is -1, the center y position is used.
5677 :type y: int
5678 :param graphics_format: Save the plot in this format
5679 :type graphics_format: str
5680 :param verbose: Print output filename after saving
5681 :type verbose: bool
5682
5683 See also: :func:`writeFluidVTK()` and :func:`plotFluidPressuresZ()`
5684 '''
5685
5686 if not py_mpl:
5687 print('Error: matplotlib module not found (plotFluidPressuresY).')
5688 return
5689
5690 if y == -1:
5691 y = self.num[1]/2
5692
5693 plt.figure(figsize=[8, 8])
5694 plt.title('Fluid pressures')
5695 imgplt = plt.imshow(self.p_f[:, y, :].T, origin='lower')
5696 imgplt.set_interpolation('nearest')
5697 #imgplt.set_interpolation('bicubic')
5698 #imgplt.set_cmap('hot')
5699 plt.xlabel('$x_1$')
5700 plt.ylabel('$x_3$')
5701 plt.colorbar()
5702 filename = 'p_f-' + self.sid + '-y' + str(y) + '.' + graphics_format
5703 plt.savefig(filename, transparent=False)
5704 if verbose:
5705 print('saved to ' + filename)
5706 plt.clf()
5707 plt.close()
5708
5709 def plotFluidPressuresZ(self, z=-1, graphics_format='png', verbose=True):
5710 '''
5711 Plot fluid pressures in a plane normal to the third axis.
5712 The plot is saved in the current folder with the format
5713 'p_f-<simulation id>-z<z value>.<graphics_format>'.
5714
5715 :param z: Plot pressures in fluid cells with these z axis values. If
5716 this value is -1, the center z position is used.
5717 :type z: int
5718 :param graphics_format: Save the plot in this format
5719 :type graphics_format: str
5720 :param verbose: Print output filename after saving
5721 :type verbose: bool
5722
5723 See also: :func:`writeFluidVTK()` and :func:`plotFluidPressuresY()`
5724 '''
5725
5726 if not py_mpl:
5727 print('Error: matplotlib module not found (plotFluidPressuresZ).')
5728 return
5729
5730 if z == -1:
5731 z = self.num[2]/2
5732
5733 plt.figure(figsize=[8, 8])
5734 plt.title('Fluid pressures')
5735 imgplt = plt.imshow(self.p_f[:, :, z].T, origin='lower')
5736 imgplt.set_interpolation('nearest')
5737 #imgplt.set_interpolation('bicubic')
5738 #imgplt.set_cmap('hot')
5739 plt.xlabel('$x_1$')
5740 plt.ylabel('$x_2$')
5741 plt.colorbar()
5742 filename = 'p_f-' + self.sid + '-z' + str(z) + '.' + graphics_format
5743 plt.savefig(filename, transparent=False)
5744 if verbose:
5745 print('saved to ' + filename)
5746 plt.clf()
5747 plt.close()
5748
5749 def plotFluidVelocitiesY(self, y=-1, graphics_format='png', verbose=True):
5750 '''
5751 Plot fluid velocities in a plane normal to the second axis.
5752 The plot is saved in the current folder with the format
5753 'v_f-<simulation id>-z<z value>.<graphics_format>'.
5754
5755 :param y: Plot velocities in fluid cells with these y axis values. If
5756 this value is -1, the center y position is used.
5757 :type y: int
5758 :param graphics_format: Save the plot in this format
5759 :type graphics_format: str
5760 :param verbose: Print output filename after saving
5761 :type verbose: bool
5762
5763 See also: :func:`writeFluidVTK()` and :func:`plotFluidVelocitiesZ()`
5764 '''
5765
5766 if not py_mpl:
5767 print('Error: matplotlib module not found (plotFluidVelocitiesY).')
5768 return
5769
5770 if y == -1:
5771 y = self.num[1]/2
5772
5773 plt.title('Fluid velocities')
5774 plt.figure(figsize=[8, 8])
5775
5776 plt.subplot(131)
5777 imgplt = plt.imshow(self.v_f[:, y, :, 0].T, origin='lower')
5778 imgplt.set_interpolation('nearest')
5779 #imgplt.set_interpolation('bicubic')
5780 #imgplt.set_cmap('hot')
5781 plt.title("$v_1$")
5782 plt.xlabel('$x_1$')
5783 plt.ylabel('$x_3$')
5784 plt.colorbar(orientation='horizontal')
5785
5786 plt.subplot(132)
5787 imgplt = plt.imshow(self.v_f[:, y, :, 1].T, origin='lower')
5788 imgplt.set_interpolation('nearest')
5789 #imgplt.set_interpolation('bicubic')
5790 #imgplt.set_cmap('hot')
5791 plt.title("$v_2$")
5792 plt.xlabel('$x_1$')
5793 plt.ylabel('$x_3$')
5794 plt.colorbar(orientation='horizontal')
5795
5796 plt.subplot(133)
5797 imgplt = plt.imshow(self.v_f[:, y, :, 2].T, origin='lower')
5798 imgplt.set_interpolation('nearest')
5799 #imgplt.set_interpolation('bicubic')
5800 #imgplt.set_cmap('hot')
5801 plt.title("$v_3$")
5802 plt.xlabel('$x_1$')
5803 plt.ylabel('$x_3$')
5804 plt.colorbar(orientation='horizontal')
5805
5806 filename = 'v_f-' + self.sid + '-y' + str(y) + '.' + graphics_format
5807 plt.savefig(filename, transparent=False)
5808 if verbose:
5809 print('saved to ' + filename)
5810 plt.clf()
5811 plt.close()
5812
5813 def plotFluidVelocitiesZ(self, z=-1, graphics_format='png', verbose=True):
5814 '''
5815 Plot fluid velocities in a plane normal to the third axis.
5816 The plot is saved in the current folder with the format
5817 'v_f-<simulation id>-z<z value>.<graphics_format>'.
5818
5819 :param z: Plot velocities in fluid cells with these z axis values. If
5820 this value is -1, the center z position is used.
5821 :type z: int
5822 :param graphics_format: Save the plot in this format
5823 :type graphics_format: str
5824 :param verbose: Print output filename after saving
5825 :type verbose: bool
5826
5827 See also: :func:`writeFluidVTK()` and :func:`plotFluidVelocitiesY()`
5828 '''
5829 if not py_mpl:
5830 print('Error: matplotlib module not found (plotFluidVelocitiesZ).')
5831 return
5832
5833 if z == -1:
5834 z = self.num[2]/2
5835
5836 plt.title("Fluid velocities")
5837 plt.figure(figsize=[8, 8])
5838
5839 plt.subplot(131)
5840 imgplt = plt.imshow(self.v_f[:, :, z, 0].T, origin='lower')
5841 imgplt.set_interpolation('nearest')
5842 #imgplt.set_interpolation('bicubic')
5843 #imgplt.set_cmap('hot')
5844 plt.title("$v_1$")
5845 plt.xlabel('$x_1$')
5846 plt.ylabel('$x_2$')
5847 plt.colorbar(orientation='horizontal')
5848
5849 plt.subplot(132)
5850 imgplt = plt.imshow(self.v_f[:, :, z, 1].T, origin='lower')
5851 imgplt.set_interpolation('nearest')
5852 #imgplt.set_interpolation('bicubic')
5853 #imgplt.set_cmap('hot')
5854 plt.title("$v_2$")
5855 plt.xlabel('$x_1$')
5856 plt.ylabel('$x_2$')
5857 plt.colorbar(orientation='horizontal')
5858
5859 plt.subplot(133)
5860 imgplt = plt.imshow(self.v_f[:, :, z, 2].T, origin='lower')
5861 imgplt.set_interpolation('nearest')
5862 #imgplt.set_interpolation('bicubic')
5863 #imgplt.set_cmap('hot')
5864 plt.title("$v_3$")
5865 plt.xlabel('$x_1$')
5866 plt.ylabel('$x_2$')
5867 plt.colorbar(orientation='horizontal')
5868
5869 filename = 'v_f-' + self.sid + '-z' + str(z) + '.' + graphics_format
5870 plt.savefig(filename, transparent=False)
5871 if verbose:
5872 print('saved to ' + filename)
5873 plt.clf()
5874 plt.close()
5875
5876 def plotFluidDiffAdvPresZ(self, graphics_format='png', verbose=True):
5877 '''
5878 Compare contributions to the velocity from diffusion and advection,
5879 assuming the flow is 1D along the z-axis, phi=1, and dphi=0. This
5880 solution is analog to the predicted velocity and not constrained by the
5881 conservation of mass. The plot is saved in the output folder with the
5882 name format '<simulation id>-diff_adv-t=<current time>s-mu=<dynamic
5883 viscosity>Pa-s.<graphics_format>'.
5884
5885 :param graphics_format: Save the plot in this format
5886 :type graphics_format: str
5887 :param verbose: Print output filename after saving
5888 :type verbose: bool
5889 '''
5890 if not py_mpl:
5891 print('Error: matplotlib module not found (plotFluidDiffAdvPresZ).')
5892 return
5893
5894 # The v_z values are read from self.v_f[0, 0, :, 2]
5895 dz = self.L[2]/self.num[2]
5896 rho = self.rho_f
5897
5898 # Central difference gradients
5899 dvz_dz = (self.v_f[0, 0, 1:, 2] - self.v_f[0, 0, :-1, 2])/(2.0*dz)
5900 dvzvz_dz = (self.v_f[0, 0, 1:, 2]**2 - self.v_f[0, 0, :-1, 2]**2)\
5901 /(2.0*dz)
5902
5903 # Diffusive contribution to velocity change
5904 dvz_diff = 2.0*self.mu/rho*dvz_dz*self.time_dt
5905
5906 # Advective contribution to velocity change
5907 dvz_adv = dvzvz_dz*self.time_dt
5908
5909 # Pressure gradient
5910 dp_dz = (self.p_f[0, 0, 1:] - self.p_f[0, 0, :-1])/(2.0*dz)
5911
5912 cellno = numpy.arange(1, self.num[2])
5913
5914 fig = plt.figure()
5915 titlesize = 12
5916
5917 plt.subplot(1, 3, 1)
5918 plt.title('Pressure', fontsize=titlesize)
5919 plt.ylabel('$i_z$')
5920 plt.xlabel('$p_z$')
5921 plt.plot(self.p_f[0, 0, :], numpy.arange(self.num[2]))
5922 plt.grid()
5923
5924 plt.subplot(1, 3, 2)
5925 plt.title('Pressure gradient', fontsize=titlesize)
5926 plt.ylabel('$i_z$')
5927 plt.xlabel('$\Delta p_z$')
5928 plt.plot(dp_dz, cellno)
5929 plt.grid()
5930
5931 plt.subplot(1, 3, 3)
5932 plt.title('Velocity prediction terms', fontsize=titlesize)
5933 plt.ylabel('$i_z$')
5934 plt.xlabel('$\Delta v_z$')
5935 plt.plot(dvz_diff, cellno, label='Diffusion')
5936 plt.plot(dvz_adv, cellno, label='Advection')
5937 plt.plot(dvz_diff+dvz_adv, cellno, '--', label='Sum')
5938 leg = plt.legend(loc='best', prop={'size':8})
5939 leg.get_frame().set_alpha(0.5)
5940 plt.grid()
5941
5942 plt.tight_layout()
5943 filename = '../output/{}-diff_adv-t={:.2e}s-mu={:.2e}Pa-s.{}'\
5944 .format(self.sid, self.time_current[0], self.mu[0],
5945 graphics_format)
5946 plt.savefig(filename)
5947 if verbose:
5948 print('saved to ' + filename)
5949 plt.clf()
5950 plt.close(fig)
5951
5952 def ReynoldsNumber(self):
5953 '''
5954 Estimate the per-cell Reynolds number by: Re=rho * ||v_f|| * dx/mu.
5955 This value is returned and also stored in `self.Re`.
5956
5957 :returns: Reynolds number
5958 :return type: Numpy array with dimensions like the fluid grid
5959 '''
5960
5961 # find magnitude of fluid velocity vectors
5962 self.v_f_magn = numpy.empty_like(self.p_f)
5963 for z in numpy.arange(self.num[2]):
5964 for y in numpy.arange(self.num[1]):
5965 for x in numpy.arange(self.num[0]):
5966 self.v_f_magn[x, y, z] = \
5967 self.v_f[x, y, z, :].dot(self.v_f[x, y, z, :])
5968
5969 Re = self.rho_f*self.v_f_magn*self.L[0]/self.num[0]/(self.mu + \
5970 1.0e-16)
5971 return Re
5972
5973 def plotLoadCurve(self, graphics_format='png', verbose=True):
5974 '''
5975 Plot the load curve (log time vs. upper wall movement). The plot is
5976 saved in the current folder with the file name
5977 '<simulation id>-loadcurve.<graphics_format>'.
5978 The consolidation coefficient calculations are done on the base of
5979 Bowles 1992, p. 129--139, using the "Casagrande" method.
5980 It is assumed that the consolidation has stopped at the end of the
5981 simulation (i.e. flat curve).
5982
5983 :param graphics_format: Save the plot in this format
5984 :type graphics_format: str
5985 :param verbose: Print output filename after saving
5986 :type verbose: bool
5987 '''
5988 if not py_mpl:
5989 print('Error: matplotlib module not found (plotLoadCurve).')
5990 return
5991
5992 t = numpy.empty(self.status())
5993 H = numpy.empty_like(t)
5994 sb = sim(self.sid, fluid=self.fluid)
5995 sb.readfirst(verbose=False)
5996 for i in numpy.arange(1, self.status()+1):
5997 sb.readstep(i, verbose=False)
5998 if i == 0:
5999 load = sb.w_sigma0[0]
6000 t[i-1] = sb.time_current[0]
6001 H[i-1] = sb.w_x[0]
6002
6003 # find consolidation parameters
6004 H0 = H[0]
6005 H100 = H[-1]
6006 H50 = (H0 + H100)/2.0
6007 T50 = 0.197 # case I
6008
6009 # find the time where 50% of the consolidation (H50) has happened by
6010 # linear interpolation. The values in H are expected to be
6011 # monotonically decreasing. See Numerical Recipies p. 115
6012 i_lower = 0
6013 i_upper = self.status()-1
6014 while i_upper - i_lower > 1:
6015 i_midpoint = int((i_upper + i_lower)/2)
6016 if H50 < H[i_midpoint]:
6017 i_lower = i_midpoint
6018 else:
6019 i_upper = i_midpoint
6020 t50 = t[i_lower] + (t[i_upper] - t[i_lower]) * \
6021 (H50 - H[i_lower])/(H[i_upper] - H[i_lower])
6022
6023 c_coeff = T50*H50**2.0/(t50)
6024 if self.fluid:
6025 e = numpy.mean(sb.phi[:, :, 3:-8]) # ignore boundaries
6026 else:
6027 e = sb.voidRatio()
6028
6029 phi_bar = e
6030 fig = plt.figure()
6031 plt.xlabel('Time [s]')
6032 plt.ylabel('Height [m]')
6033 plt.title('$c_v$=%.2e m$^2$ s$^{-1}$ at %.1f kPa and $e$=%.2f' \
6034 % (c_coeff, sb.w_sigma0[0]/1000.0, e))
6035 plt.semilogx(t, H, '+-')
6036 plt.axhline(y=H0, color='gray')
6037 plt.axhline(y=H50, color='gray')
6038 plt.axhline(y=H100, color='gray')
6039 plt.axvline(x=t50, color='red')
6040 plt.grid()
6041 filename = self.sid + '-loadcurve.' + graphics_format
6042 plt.savefig(filename)
6043 if verbose:
6044 print('saved to ' + filename)
6045 plt.clf()
6046 plt.close(fig)
6047
6048 def convergence(self):
6049 '''
6050 Read the convergence evolution in the CFD solver. The values are stored
6051 in `self.conv` with iteration number in the first column and iteration
6052 count in the second column.
6053
6054 See also: :func:`plotConvergence()`
6055 '''
6056 return numpy.loadtxt('../output/' + self.sid + '-conv.log', dtype=numpy.int32)
6057
6058 def plotConvergence(self, graphics_format='png', verbose=True):
6059 '''
6060 Plot the convergence evolution in the CFD solver. The plot is saved
6061 in the output folder with the file name
6062 '<simulation id>-conv.<graphics_format>'.
6063
6064 :param graphics_format: Save the plot in this format
6065 :type graphics_format: str
6066 :param verbose: Print output filename after saving
6067 :type verbose: bool
6068
6069 See also: :func:`convergence()`
6070 '''
6071 if not py_mpl:
6072 print('Error: matplotlib module not found (plotConvergence).')
6073 return
6074
6075 fig = plt.figure()
6076 conv = self.convergence()
6077
6078 plt.title('Convergence evolution in CFD solver in "' + self.sid + '"')
6079 plt.xlabel('Time step')
6080 plt.ylabel('Jacobi iterations')
6081 plt.plot(conv[:, 0], conv[:, 1])
6082 plt.grid()
6083 filename = self.sid + '-conv.' + graphics_format
6084 plt.savefig(filename)
6085 if verbose:
6086 print('saved to ' + filename)
6087 plt.clf()
6088 plt.close(fig)
6089
6090 def plotSinFunction(self, baseval, A, f, phi=0.0, xlabel='$t$ [s]',
6091 ylabel='$y$', plotstyle='.', outformat='png',
6092 verbose=True):
6093 '''
6094 Plot the values of a sinusoidal modulated base value. Saves the output
6095 as a plot in the current folder.
6096 The time values will range from `self.time_current` to
6097 `self.time_total`.
6098
6099 :param baseval: The center value which the sinusoidal fluctuations are
6100 modulating
6101 :type baseval: float
6102 :param A: The fluctuation amplitude
6103 :type A: float
6104 :param phi: The phase shift [s]
6105 :type phi: float
6106 :param xlabel: The label for the x axis
6107 :type xlabel: str
6108 :param ylabel: The label for the y axis
6109 :type ylabel: str
6110 :param plotstyle: Matplotlib-string for specifying plotting style
6111 :type plotstyle: str
6112 :param outformat: File format of the output plot
6113 :type outformat: str
6114 :param verbose: Print output filename after saving
6115 :type verbose: bool
6116 '''
6117 if not py_mpl:
6118 print('Error: matplotlib module not found (plotSinFunction).')
6119 return
6120
6121 fig = plt.figure(figsize=[8, 6])
6122 steps_left = (self.time_total[0] - self.time_current[0]) \
6123 /self.time_file_dt[0]
6124 t = numpy.linspace(self.time_current[0], self.time_total[0], steps_left)
6125 f = baseval + A*numpy.sin(2.0*numpy.pi*f*t + phi)
6126 plt.plot(t, f, plotstyle)
6127 plt.grid()
6128 plt.xlabel(xlabel)
6129 plt.ylabel(ylabel)
6130 plt.tight_layout()
6131 filename = self.sid + '-sin.' + outformat
6132 plt.savefig(filename)
6133 if verbose:
6134 print(filename)
6135 plt.clf()
6136 plt.close(fig)
6137
6138 def setTopWallNormalStressModulation(self, A, f, plot=False):
6139 '''
6140 Set the parameters for the sine wave modulating the normal stress
6141 at the top wall. Note that a cos-wave is obtained with phi=pi/2.
6142
6143 :param A: Fluctuation amplitude [Pa]
6144 :type A: float
6145 :param f: Fluctuation frequency [Hz]
6146 :type f: float
6147 :param plot: Show a plot of the resulting modulation
6148 :type plot: bool
6149
6150 See also: :func:`setFluidPressureModulation()` and
6151 :func:`disableTopWallNormalStressModulation()`
6152 '''
6153 self.w_sigma0_A[0] = A
6154 self.w_sigma0_f[0] = f
6155
6156 if plot and py_mpl:
6157 self.plotSinFunction(self.w_sigma0[0], A, f, phi=0.0,
6158 xlabel='$t$ [s]', ylabel='$\\sigma_0$ [Pa]')
6159
6160 def disableTopWallNormalStressModulation(self):
6161 '''
6162 Set the parameters for the sine wave modulating the normal stress
6163 at the top dynamic wall to zero.
6164
6165 See also: :func:`setTopWallNormalStressModulation()`
6166 '''
6167 self.setTopWallNormalStressModulation(A=0.0, f=0.0)
6168
6169 def setFluidPressureModulation(self, A, f, phi=0.0, plot=False):
6170 '''
6171 Set the parameters for the sine wave modulating the fluid pressures
6172 at the top boundary. Note that a cos-wave is obtained with phi=pi/2.
6173
6174 :param A: Fluctuation amplitude [Pa]
6175 :type A: float
6176 :param f: Fluctuation frequency [Hz]
6177 :type f: float
6178 :param phi: Fluctuation phase shift (default=0.0) [rad]
6179 :type phi: float
6180 :param plot: Show a plot of the resulting modulation
6181 :type plot: bool
6182
6183 See also: :func:`setTopWallNormalStressModulation()` and
6184 :func:`disableFluidPressureModulation()`
6185 '''
6186 self.p_mod_A[0] = A
6187 self.p_mod_f[0] = f
6188 self.p_mod_phi[0] = phi
6189
6190 if plot:
6191 self.plotSinFunction(self.p_f[0, 0, -1], A, f, phi=0.0,
6192 xlabel='$t$ [s]', ylabel='$p_f$ [kPa]')
6193
6194 def disableFluidPressureModulation(self):
6195 '''
6196 Set the parameters for the sine wave modulating the fluid pressures
6197 at the top boundary to zero.
6198
6199 See also: :func:`setFluidPressureModulation()`
6200 '''
6201 self.setFluidPressureModulation(A=0.0, f=0.0)
6202
6203 def plotPrescribedFluidPressures(self, graphics_format='png',
6204 verbose=True):
6205 '''
6206 Plot the prescribed fluid pressures through time that may be
6207 modulated through the class parameters p_mod_A, p_mod_f, and p_mod_phi.
6208 The plot is saved in the output folder with the file name
6209 '<simulation id>-pres.<graphics_format>'.
6210 '''
6211 if not py_mpl:
6212 print('Error: matplotlib module not found ' +
6213 '(plotPrescribedFluidPressures).')
6214 return
6215
6216 fig = plt.figure()
6217
6218 plt.title('Prescribed fluid pressures at the top in "' + self.sid + '"')
6219 plt.xlabel('Time [s]')
6220 plt.ylabel('Pressure [Pa]')
6221 t = numpy.linspace(0, self.time_total, self.time_total/self.time_file_dt)
6222 p = self.p_f[0, 0, -1] + self.p_mod_A * \
6223 numpy.sin(2.0*numpy.pi*self.p_mod_f*t + self.p_mod_phi)
6224 plt.plot(t, p, '.-')
6225 plt.grid()
6226 filename = '../output/' + self.sid + '-pres.' + graphics_format
6227 plt.savefig(filename)
6228 if verbose:
6229 print('saved to ' + filename)
6230 plt.clf()
6231 plt.close(fig)
6232
6233 def acceleration(self, idx=-1):
6234 '''
6235 Returns the acceleration of one or more particles, selected by their
6236 index. If the index is equal to -1 (default value), all accelerations
6237 are returned.
6238
6239 :param idx: Index or index range of particles
6240 :type idx: int, list or numpy.array
6241 :returns: n-by-3 matrix of acceleration(s)
6242 :return type: numpy.array
6243 '''
6244 if idx == -1:
6245 idx = range(self.np)
6246 return self.force[idx, :]/(V_sphere(self.radius[idx])*self.rho[0]) + \
6247 self.g
6248
6249 def setGamma(self, gamma):
6250 '''
6251 Gamma is a fluid solver parameter, used for smoothing the pressure
6252 values. The epsilon (pressure) values are smoothed by including the
6253 average epsilon value of the six closest (face) neighbor cells. This
6254 parameter should be in the range [0.0;1.0[. The higher the value, the
6255 more averaging is introduced. A value of 0.0 disables all averaging.
6256
6257 The default and recommended value is 0.0.
6258
6259 :param theta: The smoothing parameter value
6260 :type theta: float
6261
6262 Other solver parameter setting functions: :func:`setTheta()`,
6263 :func:`setBeta()`, :func:`setTolerance()`,
6264 :func:`setDEMstepsPerCFDstep()` and :func:`setMaxIterations()`
6265 '''
6266 self.gamma = numpy.asarray(gamma)
6267
6268 def setTheta(self, theta):
6269 '''
6270 Theta is a fluid solver under-relaxation parameter, used in solution of
6271 Poisson equation. The value should be within the range ]0.0;1.0]. At a
6272 value of 1.0, the new estimate of epsilon values is used exclusively. At
6273 lower values, a linear interpolation between new and old values is used.
6274 The solution typically converges faster with a value of 1.0, but
6275 instabilities may be avoided with lower values.
6276
6277 The default and recommended value is 1.0.
6278
6279 :param theta: The under-relaxation parameter value
6280 :type theta: float
6281
6282 Other solver parameter setting functions: :func:`setGamma()`,
6283 :func:`setBeta()`, :func:`setTolerance()`,
6284 :func:`setDEMstepsPerCFDstep()` and :func:`setMaxIterations()`
6285 '''
6286 self.theta = numpy.asarray(theta)
6287
6288
6289 def setBeta(self, beta):
6290 '''
6291 Beta is a fluid solver parameter, used in velocity prediction and
6292 pressure iteration 1.0: Use old pressures for fluid velocity prediction
6293 (see Langtangen et al. 2002) 0.0: Do not use old pressures for fluid
6294 velocity prediction (Chorin's original projection method, see Chorin
6295 (1968) and "Projection method (fluid dynamics)" page on Wikipedia. The
6296 best results precision and performance-wise are obtained by using a beta
6297 of 0 and a low tolerance criteria value.
6298
6299 The default and recommended value is 0.0.
6300
6301 Other solver parameter setting functions: :func:`setGamma()`,
6302 :func:`setTheta()`, :func:`setTolerance()`,
6303 :func:`setDEMstepsPerCFDstep()` and
6304 :func:`setMaxIterations()`
6305 '''
6306 self.beta = numpy.asarray(beta)
6307
6308 def setTolerance(self, tolerance):
6309 '''
6310 A fluid solver parameter, the value of the tolerance parameter denotes
6311 the required value of the maximum normalized residual for the fluid
6312 solver.
6313
6314 The default and recommended value is 1.0e-3.
6315
6316 :param tolerance: The tolerance criteria for the maximal normalized
6317 residual
6318 :type tolerance: float
6319
6320 Other solver parameter setting functions: :func:`setGamma()`,
6321 :func:`setTheta()`, :func:`setBeta()`, :func:`setDEMstepsPerCFDstep()` and
6322 :func:`setMaxIterations()`
6323 '''
6324 self.tolerance = numpy.asarray(tolerance)
6325
6326 def setMaxIterations(self, maxiter):
6327 '''
6328 A fluid solver parameter, the value of the maxiter parameter denotes the
6329 maximal allowed number of fluid solver iterations before ending the
6330 fluid solver loop prematurely. The residual values are at that point not
6331 fulfilling the tolerance criteria. The parameter is included to avoid
6332 infinite hangs.
6333
6334 The default and recommended value is 1e4.
6335
6336 :param maxiter: The maximum number of Jacobi iterations in the fluid
6337 solver
6338 :type maxiter: int
6339
6340 Other solver parameter setting functions: :func:`setGamma()`,
6341 :func:`setTheta()`, :func:`setBeta()`, :func:`setDEMstepsPerCFDstep()`
6342 and :func:`setTolerance()`
6343 '''
6344 self.maxiter = numpy.asarray(maxiter)
6345
6346 def setDEMstepsPerCFDstep(self, ndem):
6347 '''
6348 A fluid solver parameter, the value of the maxiter parameter denotes the
6349 number of DEM time steps to be performed per CFD time step.
6350
6351 The default value is 1.
6352
6353 :param ndem: The DEM/CFD time step ratio
6354 :type ndem: int
6355
6356 Other solver parameter setting functions: :func:`setGamma()`,
6357 :func:`setTheta()`, :func:`setBeta()`, :func:`setTolerance()` and
6358 :func:`setMaxIterations()`.
6359 '''
6360 self.ndem = numpy.asarray(ndem)
6361
6362 def shearStress(self, type='effective'):
6363 '''
6364 Calculates the sum of shear stress values measured on any moving
6365 particles with a finite and fixed velocity.
6366
6367 :param type: Find the 'defined' or 'effective' (default) shear stress
6368 :type type: str
6369
6370 :returns: The shear stress in Pa
6371 :return type: numpy.array
6372 '''
6373
6374 if type == 'defined':
6375 return self.w_tau_x[0]
6376
6377 elif type == 'effective':
6378
6379 fixvel = numpy.nonzero(self.fixvel > 0.0)
6380 force = numpy.zeros(3)
6381
6382 # Summation of shear stress contributions
6383 for i in fixvel[0]:
6384 if self.vel[i, 0] > 0.0:
6385 force += -self.force[i, :]
6386
6387 return force[0]/(self.L[0]*self.L[1])
6388
6389 else:
6390 raise Exception('Shear stress type ' + type + ' not understood')
6391
6392
6393 def visualize(self, method='energy', savefig=True, outformat='png',
6394 figsize=False, pickle=False, xlim=False, firststep=0,
6395 f_min=None, f_max=None, cmap=None, smoothing=0,
6396 smoothing_window='hanning'):
6397 '''
6398 Visualize output from the simulation, where the temporal progress is
6399 of interest. The output will be saved in the current folder with a name
6400 combining the simulation id of the simulation, and the visualization
6401 method.
6402
6403 :param method: The type of plot to render. Possible values are 'energy',
6404 'walls', 'triaxial', 'inertia', 'mean-fluid-pressure',
6405 'fluid-pressure', 'shear', 'shear-displacement', 'porosity',
6406 'rate-dependence', 'contacts'
6407 :type method: str
6408 :param savefig: Save the image instead of showing it on screen
6409 :type savefig: bool
6410 :param outformat: The output format of the plot data. This can be an
6411 image format, or in text ('txt').
6412 :param figsize: Specify output figure size in inches
6413 :type figsize: array
6414 :param pickle: Save all figure content as a Python pickle file. It can
6415 be opened later using `fig=pickle.load(open('file.pickle','rb'))`.
6416 :type pickle: bool
6417 :param xlim: Set custom limits to the x axis. If not specified, the x
6418 range will correspond to the entire data interval.
6419 :type xlim: array
6420 :param firststep: The first output file step to read (default: 0)
6421 :type firststep: int
6422 :param cmap: Choose custom color map, e.g.
6423 `cmap=matplotlib.cm.get_cmap('afmhot')`
6424 :type cmap: matplotlib.colors.LinearSegmentedColormap
6425 :param smoothing: Apply smoothing across a number of output files to the
6426 `method='shear'` plot. A value of less than 3 means that no
6427 smoothing occurs.
6428 :type smoothing: int
6429 :param smoothing_window: Type of smoothing to use when `smoothing >= 3`.
6430 Valid values are 'flat', 'hanning' (default), 'hamming', 'bartlett',
6431 and 'blackman'.
6432 :type smoothing_window: str
6433 '''
6434
6435 lastfile = self.status()
6436 sb = sim(sid=self.sid, np=self.np, nw=self.nw, fluid=self.fluid)
6437
6438 if not py_mpl:
6439 print('Error: matplotlib module not found (visualize).')
6440 return
6441
6442 ### Plotting
6443 if outformat != 'txt':
6444 if figsize:
6445 fig = plt.figure(figsize=figsize)
6446 else:
6447 fig = plt.figure(figsize=(8, 8))
6448
6449 if method == 'energy':
6450 if figsize:
6451 fig = plt.figure(figsize=figsize)
6452 else:
6453 fig = plt.figure(figsize=(20, 8))
6454
6455 # Allocate arrays
6456 t = numpy.zeros(lastfile-firststep + 1)
6457 Epot = numpy.zeros_like(t)
6458 Ekin = numpy.zeros_like(t)
6459 Erot = numpy.zeros_like(t)
6460 Es = numpy.zeros_like(t)
6461 Ev = numpy.zeros_like(t)
6462 Es_dot = numpy.zeros_like(t)
6463 Ev_dot = numpy.zeros_like(t)
6464 Ebondpot = numpy.zeros_like(t)
6465 Esum = numpy.zeros_like(t)
6466
6467 # Read energy values from simulation binaries
6468 for i in numpy.arange(firststep, lastfile+1):
6469 sb.readstep(i, verbose=False)
6470
6471 Epot[i] = sb.energy("pot")
6472 Ekin[i] = sb.energy("kin")
6473 Erot[i] = sb.energy("rot")
6474 Es[i] = sb.energy("shear")
6475 Ev[i] = sb.energy("visc_n")
6476 Es_dot[i] = sb.energy("shearrate")
6477 Ev_dot[i] = sb.energy("visc_n_rate")
6478 Ebondpot[i] = sb.energy("bondpot")
6479 Esum[i] = Epot[i] + Ekin[i] + Erot[i] + Es[i] + Ev[i] +\
6480 Ebondpot[i]
6481 t[i] = sb.currentTime()
6482
6483
6484 if outformat != 'txt':
6485 # Potential energy
6486 ax1 = plt.subplot2grid((2, 5), (0, 0))
6487 ax1.set_xlabel('Time [s]')
6488 ax1.set_ylabel('Total potential energy [J]')
6489 ax1.plot(t, Epot, '+-')
6490 ax1.grid()
6491
6492 # Kinetic energy
6493 ax2 = plt.subplot2grid((2, 5), (0, 1))
6494 ax2.set_xlabel('Time [s]')
6495 ax2.set_ylabel('Total kinetic energy [J]')
6496 ax2.plot(t, Ekin, '+-')
6497 ax2.grid()
6498
6499 # Rotational energy
6500 ax3 = plt.subplot2grid((2, 5), (0, 2))
6501 ax3.set_xlabel('Time [s]')
6502 ax3.set_ylabel('Total rotational energy [J]')
6503 ax3.plot(t, Erot, '+-')
6504 ax3.grid()
6505
6506 # Bond energy
6507 ax4 = plt.subplot2grid((2, 5), (0, 3))
6508 ax4.set_xlabel('Time [s]')
6509 ax4.set_ylabel('Bond energy [J]')
6510 ax4.plot(t, Ebondpot, '+-')
6511 ax4.grid()
6512
6513 # Total energy
6514 ax5 = plt.subplot2grid((2, 5), (0, 4))
6515 ax5.set_xlabel('Time [s]')
6516 ax5.set_ylabel('Total energy [J]')
6517 ax5.plot(t, Esum, '+-')
6518 ax5.grid()
6519
6520 # Shear energy rate
6521 ax6 = plt.subplot2grid((2, 5), (1, 0))
6522 ax6.set_xlabel('Time [s]')
6523 ax6.set_ylabel('Frictional dissipation rate [W]')
6524 ax6.plot(t, Es_dot, '+-')
6525 ax6.grid()
6526
6527 # Shear energy
6528 ax7 = plt.subplot2grid((2, 5), (1, 1))
6529 ax7.set_xlabel('Time [s]')
6530 ax7.set_ylabel('Total frictional dissipation [J]')
6531 ax7.plot(t, Es, '+-')
6532 ax7.grid()
6533
6534 # Visc_n energy rate
6535 ax8 = plt.subplot2grid((2, 5), (1, 2))
6536 ax8.set_xlabel('Time [s]')
6537 ax8.set_ylabel('Viscous dissipation rate [W]')
6538 ax8.plot(t, Ev_dot, '+-')
6539 ax8.grid()
6540
6541 # Visc_n energy
6542 ax9 = plt.subplot2grid((2, 5), (1, 3))
6543 ax9.set_xlabel('Time [s]')
6544 ax9.set_ylabel('Total viscous dissipation [J]')
6545 ax9.plot(t, Ev, '+-')
6546 ax9.grid()
6547
6548 # Combined view
6549 ax10 = plt.subplot2grid((2, 5), (1, 4))
6550 ax10.set_xlabel('Time [s]')
6551 ax10.set_ylabel('Energy [J]')
6552 ax10.plot(t, Epot, '+-g')
6553 ax10.plot(t, Ekin, '+-b')
6554 ax10.plot(t, Erot, '+-r')
6555 ax10.legend(('$\sum E_{pot}$', '$\sum E_{kin}$',
6556 '$\sum E_{rot}$'), 'upper right', shadow=True)
6557 ax10.grid()
6558
6559 if xlim:
6560 ax1.set_xlim(xlim)
6561 ax2.set_xlim(xlim)
6562 ax3.set_xlim(xlim)
6563 ax4.set_xlim(xlim)
6564 ax5.set_xlim(xlim)
6565 ax6.set_xlim(xlim)
6566 ax7.set_xlim(xlim)
6567 ax8.set_xlim(xlim)
6568 ax9.set_xlim(xlim)
6569 ax10.set_xlim(xlim)
6570
6571 fig.tight_layout()
6572
6573 elif method == 'walls':
6574
6575 # Read energy values from simulation binaries
6576 for i in numpy.arange(firststep, lastfile+1):
6577 sb.readstep(i, verbose=False)
6578
6579 # Allocate arrays on first run
6580 if i == firststep:
6581 wforce = numpy.zeros((lastfile+1)*sb.nw,\
6582 dtype=numpy.float64).reshape((lastfile+1), sb.nw)
6583 wvel = numpy.zeros((lastfile+1)*sb.nw,\
6584 dtype=numpy.float64).reshape((lastfile+1), sb.nw)
6585 wpos = numpy.zeros((lastfile+1)*sb.nw,\
6586 dtype=numpy.float64).reshape((lastfile+1), sb.nw)
6587 wsigma0 = numpy.zeros((lastfile+1)*sb.nw,\
6588 dtype=numpy.float64).reshape((lastfile+1), sb.nw)
6589 maxpos = numpy.zeros((lastfile+1), dtype=numpy.float64)
6590 logstress = numpy.zeros((lastfile+1), dtype=numpy.float64)
6591 voidratio = numpy.zeros((lastfile+1), dtype=numpy.float64)
6592
6593 wforce[i] = sb.w_force[0]
6594 wvel[i] = sb.w_vel[0]
6595 wpos[i] = sb.w_x[0]
6596 wsigma0[i] = sb.w_sigma0[0]
6597 maxpos[i] = numpy.max(sb.x[:, 2]+sb.radius)
6598 logstress[i] = numpy.log((sb.w_force[0]/(sb.L[0]*sb.L[1]))/1000.0)
6599 voidratio[i] = sb.voidRatio()
6600
6601 t = numpy.linspace(0.0, sb.time_current, lastfile+1)
6602
6603 # Plotting
6604 if outformat != 'txt':
6605 # linear plot of time vs. wall position
6606 ax1 = plt.subplot2grid((2, 2), (0, 0))
6607 ax1.set_xlabel('Time [s]')
6608 ax1.set_ylabel('Position [m]')
6609 ax1.plot(t, wpos, '+-', label="upper wall")
6610 ax1.plot(t, maxpos, '+-', label="heighest particle")
6611 ax1.legend()
6612 ax1.grid()
6613
6614 #ax2 = plt.subplot2grid((2, 2), (1, 0))
6615 #ax2.set_xlabel('Time [s]')
6616 #ax2.set_ylabel('Force [N]')
6617 #ax2.plot(t, wforce, '+-')
6618
6619 # semilog plot of log stress vs. void ratio
6620 ax2 = plt.subplot2grid((2, 2), (1, 0))
6621 ax2.set_xlabel('log deviatoric stress [kPa]')
6622 ax2.set_ylabel('Void ratio [-]')
6623 ax2.plot(logstress, voidratio, '+-')
6624 ax2.grid()
6625
6626 # linear plot of time vs. wall velocity
6627 ax3 = plt.subplot2grid((2, 2), (0, 1))
6628 ax3.set_xlabel('Time [s]')
6629 ax3.set_ylabel('Velocity [m/s]')
6630 ax3.plot(t, wvel, '+-')
6631 ax3.grid()
6632
6633 # linear plot of time vs. deviatoric stress
6634 ax4 = plt.subplot2grid((2, 2), (1, 1))
6635 ax4.set_xlabel('Time [s]')
6636 ax4.set_ylabel('Deviatoric stress [Pa]')
6637 ax4.plot(t, wsigma0, '+-', label="$\sigma_0$")
6638 ax4.plot(t, wforce/(sb.L[0]*sb.L[1]), '+-', label="$\sigma'$")
6639 ax4.legend(loc=4)
6640 ax4.grid()
6641
6642 if xlim:
6643 ax1.set_xlim(xlim)
6644 ax2.set_xlim(xlim)
6645 ax3.set_xlim(xlim)
6646 ax4.set_xlim(xlim)
6647
6648 elif method == 'triaxial':
6649
6650 # Read energy values from simulation binaries
6651 for i in numpy.arange(firststep, lastfile+1):
6652 sb.readstep(i, verbose=False)
6653
6654 vol = (sb.w_x[0]-sb.origo[2]) * (sb.w_x[1]-sb.w_x[2]) \
6655 * (sb.w_x[3] - sb.w_x[4])
6656
6657 # Allocate arrays on first run
6658 if i == firststep:
6659 axial_strain = numpy.zeros(lastfile+1, dtype=numpy.float64)
6660 deviatoric_stress =\
6661 numpy.zeros(lastfile+1, dtype=numpy.float64)
6662 volumetric_strain =\
6663 numpy.zeros(lastfile+1, dtype=numpy.float64)
6664
6665 w0pos0 = sb.w_x[0]
6666 vol0 = vol
6667
6668 sigma1 = sb.w_force[0]/\
6669 ((sb.w_x[1]-sb.w_x[2])*(sb.w_x[3]-sb.w_x[4]))
6670
6671 axial_strain[i] = (w0pos0 - sb.w_x[0])/w0pos0
6672 volumetric_strain[i] = (vol0-vol)/vol0
6673 deviatoric_stress[i] = sigma1 / sb.w_sigma0[1]
6674
6675 #print(lastfile)
6676 #print(axial_strain)
6677 #print(deviatoric_stress)
6678 #print(volumetric_strain)
6679
6680 # Plotting
6681 if outformat != 'txt':
6682
6683 # linear plot of deviatoric stress
6684 ax1 = plt.subplot2grid((2, 1), (0, 0))
6685 ax1.set_xlabel('Axial strain, $\gamma_1$, [-]')
6686 ax1.set_ylabel('Deviatoric stress, $\sigma_1 - \sigma_3$, [Pa]')
6687 ax1.plot(axial_strain, deviatoric_stress, '+-')
6688 #ax1.legend()
6689 ax1.grid()
6690
6691 #ax2 = plt.subplot2grid((2, 2), (1, 0))
6692 #ax2.set_xlabel('Time [s]')
6693 #ax2.set_ylabel('Force [N]')
6694 #ax2.plot(t, wforce, '+-')
6695
6696 # semilog plot of log stress vs. void ratio
6697 ax2 = plt.subplot2grid((2, 1), (1, 0))
6698 ax2.set_xlabel('Axial strain, $\gamma_1$ [-]')
6699 ax2.set_ylabel('Volumetric strain, $\gamma_v$, [-]')
6700 ax2.plot(axial_strain, volumetric_strain, '+-')
6701 ax2.grid()
6702
6703 if xlim:
6704 ax1.set_xlim(xlim)
6705 ax2.set_xlim(xlim)
6706
6707 elif method == 'shear':
6708
6709 # Read stress values from simulation binaries
6710 for i in numpy.arange(firststep, lastfile+1):
6711 sb.readstep(i, verbose=False)
6712
6713 # First iteration: Allocate arrays and find constant values
6714 if i == firststep:
6715 # Shear displacement
6716 xdisp = numpy.zeros(lastfile+1, dtype=numpy.float64)
6717
6718 # Normal stress
6719 sigma_eff = numpy.zeros(lastfile+1, dtype=numpy.float64)
6720
6721 # Normal stress
6722 sigma_def = numpy.zeros(lastfile+1, dtype=numpy.float64)
6723
6724 # Shear stress
6725 tau = numpy.zeros(lastfile+1, dtype=numpy.float64)
6726
6727 # Upper wall position
6728 dilation = numpy.zeros(lastfile+1, dtype=numpy.float64)
6729
6730 # Peak shear stress
6731 tau_p = 0.0
6732
6733 # Shear strain value of peak sh. stress
6734 tau_p_shearstrain = 0.0
6735
6736 fixvel = numpy.nonzero(sb.fixvel > 0.0)
6737 #fixvel_upper = numpy.nonzero(sb.vel[fixvel, 0] > 0.0)
6738 shearvel = sb.vel[fixvel, 0].max()
6739 w_x0 = sb.w_x[0] # Original height
6740 A = sb.L[0] * sb.L[1] # Upper surface area
6741
6742 if i == firststep+1:
6743 w_x0 = sb.w_x[0] # Original height
6744
6745 # Summation of shear stress contributions
6746 for j in fixvel[0]:
6747 if sb.vel[j, 0] > 0.0:
6748 tau[i] += -sb.force[j, 0]/A
6749
6750 if i > 0:
6751 xdisp[i] = xdisp[i-1] + sb.time_file_dt[0]*shearvel
6752 sigma_eff[i] = sb.w_force[0]/A
6753 sigma_def[i] = sb.w_sigma0[0]
6754
6755 # dilation in meters
6756 #dilation[i] = sb.w_x[0] - w_x0
6757
6758 # dilation in percent
6759 #dilation[i] = (sb.w_x[0] - w_x0)/w_x0 * 100.0 # dilation in percent
6760
6761 # dilation in number of mean particle diameters
6762 d_bar = numpy.mean(self.radius)*2.0
6763 if numpy.isnan(d_bar):
6764 print('No radii in self.radius, attempting to read first '
6765 + 'file')
6766 self.readfirst()
6767 d_bar = numpy.mean(self.radius)*2.0
6768 dilation[i] = (sb.w_x[0] - w_x0)/d_bar
6769
6770 # Test if this was the max. shear stress
6771 if tau[i] > tau_p:
6772 tau_p = tau[i]
6773 tau_p_shearstrain = xdisp[i]/w_x0
6774
6775 shear_strain = xdisp/w_x0
6776
6777 # Copy values so they can be modified during smoothing
6778 shear_strain_smooth = shear_strain
6779 tau_smooth = tau
6780 sigma_def_smooth = sigma_def
6781
6782 # Optionally smooth the shear stress
6783 if smoothing > 2:
6784
6785 if smoothing_window not in ['flat', 'hanning', 'hamming',
6786 'bartlett', 'blackman']:
6787 raise ValueError
6788
6789 s = numpy.r_[2*tau[0]-tau[smoothing:1:-1], tau,
6790 2*tau[-1]-tau[-1:-smoothing:-1]]
6791
6792 if smoothing_window == 'flat': # moving average
6793 w = numpy.ones(smoothing, 'd')
6794 else:
6795 w = getattr(self.np, smoothing_window)(smoothing)
6796 y = numpy.convolve(w/w.sum(), s, mode='same')
6797 tau_smooth = y[smoothing-1:-smoothing+1]
6798
6799 # Plot stresses
6800 if outformat != 'txt':
6801 shearinfo = "$\\tau_p$={:.3} Pa at $\gamma$={:.3}".format(\
6802 tau_p, tau_p_shearstrain)
6803 fig.text(0.01, 0.01, shearinfo, horizontalalignment='left',
6804 fontproperties=FontProperties(size=14))
6805 ax1 = plt.subplot2grid((2, 1), (0, 0))
6806 ax1.set_xlabel('Shear strain [-]')
6807 ax1.set_ylabel('Shear friction $\\tau/\\sigma_0$ [-]')
6808 if smoothing > 2:
6809 ax1.plot(shear_strain_smooth[1:-(smoothing+1)/2],
6810 tau_smooth[1:-(smoothing+1)/2] /
6811 sigma_def_smooth[1:-(smoothing+1)/2],
6812 '-', label="$\\tau/\\sigma_0$")
6813 else:
6814 ax1.plot(shear_strain[1:],\
6815 tau[1:]/sigma_def[1:],\
6816 '-', label="$\\tau/\\sigma_0$")
6817 ax1.grid()
6818
6819 # Plot dilation
6820 ax2 = plt.subplot2grid((2, 1), (1, 0))
6821 ax2.set_xlabel('Shear strain [-]')
6822 ax2.set_ylabel('Dilation, $\Delta h/(2\\bar{r})$ [m]')
6823 if smoothing > 2:
6824 ax2.plot(shear_strain_smooth[1:-(smoothing+1)/2],
6825 dilation[1:-(smoothing+1)/2], '-')
6826 else:
6827 ax2.plot(shear_strain, dilation, '-')
6828 ax2.grid()
6829
6830 if xlim:
6831 ax1.set_xlim(xlim)
6832 ax2.set_xlim(xlim)
6833
6834 fig.tight_layout()
6835
6836 else:
6837 # Write values to textfile
6838 filename = "shear-stresses-{0}.txt".format(self.sid)
6839 #print("Writing stress data to " + filename)
6840 fh = None
6841 try:
6842 fh = open(filename, "w")
6843 for i in numpy.arange(firststep, lastfile+1):
6844 # format: shear distance [mm], sigma [kPa], tau [kPa],
6845 # Dilation [%]
6846 fh.write("{0}\t{1}\t{2}\t{3}\n"
6847 .format(xdisp[i], sigma_eff[i]/1000.0,
6848 tau[i]/1000.0, dilation[i]))
6849 finally:
6850 if fh is not None:
6851 fh.close()
6852
6853 elif method == 'shear-displacement':
6854
6855 time = numpy.zeros(lastfile+1, dtype=numpy.float64)
6856 # Read stress values from simulation binaries
6857 for i in numpy.arange(firststep, lastfile+1):
6858 sb.readstep(i, verbose=False)
6859
6860 # First iteration: Allocate arrays and find constant values
6861 if i == firststep:
6862
6863 # Shear displacement
6864 xdisp = numpy.zeros(lastfile+1, dtype=numpy.float64)
6865
6866 # Normal stress
6867 sigma_eff = numpy.zeros(lastfile+1, dtype=numpy.float64)
6868
6869 # Normal stress
6870 sigma_def = numpy.zeros(lastfile+1, dtype=numpy.float64)
6871
6872 # Shear stress
6873 tau_eff = numpy.zeros(lastfile+1, dtype=numpy.float64)
6874
6875 # Upper wall position
6876 dilation = numpy.zeros(lastfile+1, dtype=numpy.float64)
6877
6878 # Mean porosity
6879 phi_bar = numpy.zeros(lastfile+1, dtype=numpy.float64)
6880
6881 # Mean fluid pressure
6882 p_f_bar = numpy.zeros(lastfile+1, dtype=numpy.float64)
6883 p_f_top = numpy.zeros(lastfile+1, dtype=numpy.float64)
6884
6885 # Upper wall position
6886 tau_p = 0.0 # Peak shear stress
6887 # Shear strain value of peak sh. stress
6888 tau_p_shearstrain = 0.0
6889
6890 fixvel = numpy.nonzero(sb.fixvel > 0.0)
6891 #fixvel_upper=numpy.nonzero(sb.vel[fixvel, 0] > 0.0)
6892 w_x0 = sb.w_x[0] # Original height
6893 A = sb.L[0]*sb.L[1] # Upper surface area
6894
6895 d_bar = numpy.mean(sb.radius)*2.0
6896
6897 # Shear velocity
6898 v = numpy.zeros(lastfile+1, dtype=numpy.float64)
6899
6900 time[i] = sb.time_current[0]
6901
6902 if i == firststep+1:
6903 w_x0 = sb.w_x[0] # Original height
6904
6905 # Summation of shear stress contributions
6906 for j in fixvel[0]:
6907 if sb.vel[j, 0] > 0.0:
6908 tau_eff[i] += -sb.force[j, 0]/A
6909
6910 if i > 0:
6911 xdisp[i] = sb.xyzsum[fixvel, 0].max()
6912 v[i] = sb.vel[fixvel, 0].max()
6913
6914 sigma_eff[i] = sb.w_force[0]/A
6915 sigma_def[i] = sb.currentNormalStress()
6916
6917 # dilation in number of mean particle diameters
6918 dilation[i] = (sb.w_x[0] - w_x0)/d_bar
6919
6920 wall0_iz = int(sb.w_x[0]/(sb.L[2]/sb.num[2]))
6921
6922 if self.fluid:
6923 if i > 0:
6924 phi_bar[i] = numpy.mean(sb.phi[:, :, 0:wall0_iz])
6925 if i == firststep+1:
6926 phi_bar[0] = phi_bar[1]
6927 p_f_bar[i] = numpy.mean(sb.p_f[:, :, 0:wall0_iz])
6928 p_f_top[i] = sb.p_f[0, 0, -1]
6929
6930 # Test if this was the max. shear stress
6931 if tau_eff[i] > tau_p:
6932 tau_p = tau_eff[i]
6933 tau_p_shearstrain = xdisp[i]/w_x0
6934
6935 shear_strain = xdisp/w_x0
6936
6937 # Plot stresses
6938 if outformat != 'txt':
6939 if figsize:
6940 fig = plt.figure(figsize=figsize)
6941 else:
6942 fig = plt.figure(figsize=(8, 12))
6943
6944 # Upper plot
6945 ax1 = plt.subplot(3, 1, 1)
6946 ax1.plot(time, xdisp, 'k', label='Displacement')
6947 ax1.set_ylabel('Horizontal displacement [m]')
6948
6949 ax2 = ax1.twinx()
6950
6951 #ax2color = '#666666'
6952 ax2color = 'blue'
6953 if self.fluid:
6954 ax2.plot(time, phi_bar, color=ax2color, label='Porosity')
6955 ax2.set_ylabel('Mean porosity $\\bar{\\phi}$ [-]')
6956 else:
6957 ax2.plot(time, dilation, color=ax2color, label='Dilation')
6958 ax2.set_ylabel('Dilation, $\Delta h/(2\\bar{r})$ [-]')
6959 for tl in ax2.get_yticklabels():
6960 tl.set_color(ax2color)
6961
6962 # Middle plot
6963 ax5 = plt.subplot(3, 1, 2, sharex=ax1)
6964 ax5.semilogy(time[1:], v[1:], label='Shear velocity')
6965 ax5.set_ylabel('Shear velocity [ms$^{-1}$]')
6966
6967 # shade stick periods
6968 collection = \
6969 matplotlib.collections.BrokenBarHCollection.span_where(
6970 time, ymin=1.0e-7, ymax=1.0,
6971 where=numpy.isclose(v, 0.0),
6972 facecolor='black', alpha=0.2,
6973 linewidth=0)
6974 ax5.add_collection(collection)
6975
6976 # Lower plot
6977 ax3 = plt.subplot(3, 1, 3, sharex=ax1)
6978 if sb.w_sigma0_A > 1.0e-3:
6979 lns0 = ax3.plot(time, sigma_def/1000.0,
6980 '-k', label="$\\sigma_0$")
6981 lns1 = ax3.plot(time, sigma_eff/1000.0,
6982 '--k', label="$\\sigma'$")
6983 lns2 = ax3.plot(time, numpy.ones_like(time)*sb.w_tau_x/1000.0,
6984 '-r', label="$\\tau$")
6985 lns3 = ax3.plot(time, tau_eff/1000.0,
6986 '--r', label="$\\tau'$")
6987 ax3.set_ylabel('Stress [kPa]')
6988 else:
6989 ax3.plot(time, tau_eff/sb.w_sigma0[0],
6990 '-k', label="$Shear friction$")
6991 ax3.plot([0, time[-1]],
6992 [sb.w_tau_x/sigma_def, sb.w_tau_x/sigma_def],
6993 '--k', label="$Applied shear friction$")
6994 ax3.set_ylabel('Shear friction $\\tau\'/\\sigma_0$ [-]')
6995 # axis limits
6996 ax3.set_ylim([sb.w_tau_x/sigma_def[0]*0.5,
6997 sb.w_tau_x/sigma_def[0]*1.5])
6998
6999 if self.fluid:
7000 ax4 = ax3.twinx()
7001 #ax4color = '#666666'
7002 ax4color = ax2color
7003 lns4 = ax4.plot(time, p_f_top/1000.0, '-', color=ax4color,
7004 label='$p_\\text{f}^\\text{forcing}$')
7005 lns5 = ax4.plot(time, p_f_bar/1000.0, '--', color=ax4color,
7006 label='$\\bar{p}_\\text{f}$')
7007 ax4.set_ylabel('Mean fluid pressure '
7008 + '$\\bar{p_\\text{f}}$ [kPa]')
7009 for tl in ax4.get_yticklabels():
7010 tl.set_color(ax4color)
7011 if sb.w_sigma0_A > 1.0e-3:
7012 #ax4.legend(loc='upper right')
7013 lns = lns0+lns1+lns2+lns3+lns4+lns5
7014 labs = [l.get_label() for l in lns]
7015 ax4.legend(lns, labs, loc='upper right',
7016 fancybox=True, framealpha=legend_alpha)
7017 if xlim:
7018 ax4.set_xlim(xlim)
7019
7020 # aesthetics
7021 ax3.set_xlabel('Time [s]')
7022
7023 ax1.grid()
7024 ax3.grid()
7025 ax5.grid()
7026
7027 if xlim:
7028 ax1.set_xlim(xlim)
7029 ax2.set_xlim(xlim)
7030 ax3.set_xlim(xlim)
7031 ax5.set_xlim(xlim)
7032
7033 plt.setp(ax1.get_xticklabels(), visible=False)
7034 plt.setp(ax5.get_xticklabels(), visible=False)
7035 fig.tight_layout()
7036 plt.subplots_adjust(hspace=0.05)
7037
7038 elif method == 'rate-dependence':
7039
7040 if figsize:
7041 fig = plt.figure(figsize=figsize)
7042 else:
7043 fig = plt.figure(figsize=(8, 6))
7044
7045 tau = numpy.empty(sb.status())
7046 N = numpy.empty(sb.status())
7047 #v = numpy.empty(sb.status())
7048 shearstrainrate = numpy.empty(sb.status())
7049 shearstrain = numpy.empty(sb.status())
7050 for i in numpy.arange(firststep, sb.status()):
7051 sb.readstep(i+1, verbose=False)
7052 #tau = sb.shearStress()
7053 tau[i] = sb.w_tau_x # defined shear stress
7054 N[i] = sb.currentNormalStress() # defined normal stress
7055 shearstrainrate[i] = sb.shearStrainRate()
7056 shearstrain[i] = sb.shearStrain()
7057
7058 # remove nonzero sliding velocities and their associated values
7059 idx = numpy.nonzero(shearstrainrate)
7060 shearstrainrate_nonzero = shearstrainrate[idx]
7061 tau_nonzero = tau[idx]
7062 N_nonzero = N[idx]
7063 shearstrain_nonzero = shearstrain[idx]
7064
7065 ax1 = plt.subplot(111)
7066 #ax1.semilogy(N/1000., v)
7067 #ax1.semilogy(tau_nonzero/N_nonzero, v_nonzero, '+k')
7068 #ax1.plot(tau/N, v, '.')
7069 friction = tau_nonzero/N_nonzero
7070 #CS = ax1.scatter(friction, v_nonzero, c=shearstrain_nonzero,
7071 #linewidth=0)
7072 if cmap:
7073 CS = ax1.scatter(friction, shearstrainrate_nonzero,
7074 c=shearstrain_nonzero, linewidth=0.1,
7075 cmap=cmap)
7076 else:
7077 CS = ax1.scatter(friction, shearstrainrate_nonzero,
7078 c=shearstrain_nonzero, linewidth=0.1,
7079 cmap=matplotlib.cm.get_cmap('afmhot'))
7080 ax1.set_yscale('log')
7081 x_min = numpy.floor(numpy.min(friction))
7082 x_max = numpy.ceil(numpy.max(friction))
7083 ax1.set_xlim([x_min, x_max])
7084 y_min = numpy.min(shearstrainrate_nonzero)*0.5
7085 y_max = numpy.max(shearstrainrate_nonzero)*2.0
7086 ax1.set_ylim([y_min, y_max])
7087
7088 cb = plt.colorbar(CS)
7089 cb.set_label('Shear strain $\\gamma$ [-]')
7090
7091 ax1.set_xlabel('Friction $\\tau/N$ [-]')
7092 ax1.set_ylabel('Shear strain rate $\\dot{\\gamma}$ [s$^{-1}$]')
7093
7094 elif method == 'inertia':
7095
7096 t = numpy.zeros(sb.status())
7097 I = numpy.zeros(sb.status())
7098
7099 for i in numpy.arange(firststep, sb.status()):
7100 sb.readstep(i, verbose=False)
7101 t[i] = sb.currentTime()
7102 I[i] = sb.inertiaParameterPlanarShear()
7103
7104 # Plotting
7105 if outformat != 'txt':
7106
7107 if xlim:
7108 ax1.set_xlim(xlim)
7109
7110 # linear plot of deviatoric stress
7111 ax1 = plt.subplot2grid((1, 1), (0, 0))
7112 ax1.set_xlabel('Time $t$ [s]')
7113 ax1.set_ylabel('Inertia parameter $I$ [-]')
7114 ax1.semilogy(t, I)
7115 #ax1.legend()
7116 ax1.grid()
7117
7118 elif method == 'mean-fluid-pressure':
7119
7120 # Read pressure values from simulation binaries
7121 for i in numpy.arange(firststep, lastfile+1):
7122 sb.readstep(i, verbose=False)
7123
7124 # Allocate arrays on first run
7125 if i == firststep:
7126 p_mean = numpy.zeros(lastfile+1, dtype=numpy.float64)
7127
7128 p_mean[i] = numpy.mean(sb.p_f)
7129
7130 t = numpy.linspace(0.0, sb.time_current, lastfile+1)
7131
7132 # Plotting
7133 if outformat != 'txt':
7134
7135 if xlim:
7136 ax1.set_xlim(xlim)
7137
7138 # linear plot of deviatoric stress
7139 ax1 = plt.subplot2grid((1, 1), (0, 0))
7140 ax1.set_xlabel('Time $t$, [s]')
7141 ax1.set_ylabel('Mean fluid pressure, $\\bar{p}_f$, [kPa]')
7142 ax1.plot(t, p_mean/1000.0, '+-')
7143 #ax1.legend()
7144 ax1.grid()
7145
7146 elif method == 'fluid-pressure':
7147
7148 if figsize:
7149 fig = plt.figure(figsize=figsize)
7150 else:
7151 fig = plt.figure(figsize=(8, 6))
7152
7153 sb.readfirst(verbose=False)
7154
7155 # cell midpoint cell positions
7156 zpos_c = numpy.zeros(sb.num[2])
7157 dz = sb.L[2]/sb.num[2]
7158 for i in numpy.arange(sb.num[2]):
7159 zpos_c[i] = i*dz + 0.5*dz
7160
7161 shear_strain = numpy.zeros(sb.status())
7162 pres = numpy.zeros((sb.num[2], sb.status()))
7163
7164 # Read pressure values from simulation binaries
7165 for i in numpy.arange(firststep, sb.status()):
7166 sb.readstep(i, verbose=False)
7167 pres[:, i] = numpy.average(numpy.average(sb.p_f, axis=0), axis=0)
7168 shear_strain[i] = sb.shearStrain()
7169 t = numpy.linspace(0.0, sb.time_current, lastfile+1)
7170
7171 # Plotting
7172 if outformat != 'txt':
7173
7174 ax = plt.subplot(1, 1, 1)
7175
7176 pres /= 1000.0 # Pa to kPa
7177
7178 if xlim:
7179 sb.readstep(10, verbose=False)
7180 gamma_per_i = sb.shearStrain()/10.0
7181 i_min = int(xlim[0]/gamma_per_i)
7182 i_max = int(xlim[1]/gamma_per_i)
7183 pres = pres[:, i_min:i_max]
7184 else:
7185 i_min = 0
7186 i_max = sb.status()
7187 # use largest difference in p from 0 as +/- limit on colormap
7188 #print i_min, i_max
7189 p_ext = numpy.max(numpy.abs(pres))
7190
7191 if sb.wmode[0] == 3:
7192 x = t
7193 else:
7194 x = shear_strain
7195 if xlim:
7196 x = x[i_min:i_max]
7197 if cmap:
7198 im1 = ax.pcolormesh(x, zpos_c, pres, cmap=cmap,
7199 vmin=-p_ext, vmax=p_ext,
7200 rasterized=True)
7201 else:
7202 im1 = ax.pcolormesh(x, zpos_c, pres,
7203 cmap=matplotlib.cm.get_cmap('RdBu_r'),
7204 vmin=-p_ext, vmax=p_ext,
7205 rasterized=True)
7206 ax.set_xlim([0, numpy.max(x)])
7207 if sb.w_x[0] < sb.L[2]:
7208 ax.set_ylim([zpos_c[0], sb.w_x[0]])
7209 else:
7210 ax.set_ylim([zpos_c[0], zpos_c[-1]])
7211 if sb.wmode[0] == 3:
7212 ax.set_xlabel('Time $t$ [s]')
7213 else:
7214 ax.set_xlabel('Shear strain $\\gamma$ [-]')
7215 ax.set_ylabel('Vertical position $z$ [m]')
7216
7217 if xlim:
7218 ax.set_xlim([x[0], x[-1]])
7219
7220 # for article2
7221 ax.set_ylim([zpos_c[0], zpos_c[9]])
7222
7223 cb = plt.colorbar(im1)
7224 cb.set_label('$p_\\text{f}$ [kPa]')
7225 cb.solids.set_rasterized(True)
7226 plt.tight_layout()
7227
7228 elif method == 'porosity':
7229
7230 sb.readfirst(verbose=False)
7231 if not sb.fluid:
7232 raise Exception('Porosities can only be visualized in wet ' +
7233 'simulations')
7234
7235 wall0_iz = int(sb.w_x[0]/(sb.L[2]/sb.num[2]))
7236
7237 # cell midpoint cell positions
7238 zpos_c = numpy.zeros(sb.num[2])
7239 dz = sb.L[2]/sb.num[2]
7240 for i in numpy.arange(firststep, sb.num[2]):
7241 zpos_c[i] = i*dz + 0.5*dz
7242
7243 shear_strain = numpy.zeros(sb.status())
7244 poros = numpy.zeros((sb.num[2], sb.status()))
7245
7246 # Read pressure values from simulation binaries
7247 for i in numpy.arange(firststep, sb.status()):
7248 sb.readstep(i, verbose=False)
7249 poros[:, i] = numpy.average(numpy.average(sb.phi, axis=0), axis=0)
7250 shear_strain[i] = sb.shearStrain()
7251 t = numpy.linspace(0.0, sb.time_current, lastfile+1)
7252
7253 # Plotting
7254 if outformat != 'txt':
7255
7256 ax = plt.subplot(1, 1, 1)
7257
7258 poros_max = numpy.max(poros[0:wall0_iz-1, 1:])
7259 poros_min = numpy.min(poros)
7260
7261 if sb.wmode[0] == 3:
7262 x = t
7263 else:
7264 x = shear_strain
7265 if cmap:
7266 im1 = ax.pcolormesh(x, zpos_c, poros,
7267 cmap=cmap,
7268 vmin=poros_min, vmax=poros_max,
7269 rasterized=True)
7270 else:
7271 im1 = ax.pcolormesh(x, zpos_c, poros,
7272 cmap=matplotlib.cm.get_cmap('Blues_r'),
7273 vmin=poros_min, vmax=poros_max,
7274 rasterized=True)
7275 ax.set_xlim([0, numpy.max(x)])
7276 if sb.w_x[0] < sb.L[2]:
7277 ax.set_ylim([zpos_c[0], sb.w_x[0]])
7278 else:
7279 ax.set_ylim([zpos_c[0], zpos_c[-1]])
7280 if sb.wmode[0] == 3:
7281 ax.set_xlabel('Time $t$ [s]')
7282 else:
7283 ax.set_xlabel('Shear strain $\\gamma$ [-]')
7284 ax.set_ylabel('Vertical position $z$ [m]')
7285
7286 if xlim:
7287 ax.set_xlim(xlim)
7288
7289 cb = plt.colorbar(im1)
7290 cb.set_label('Mean horizontal porosity $\\bar{\phi}$ [-]')
7291 cb.solids.set_rasterized(True)
7292 plt.tight_layout()
7293 plt.subplots_adjust(wspace=.05)
7294
7295 elif method == 'contacts':
7296
7297 for i in numpy.arange(sb.status()+1):
7298 fn = "../output/{0}.output{1:0=5}.bin".format(self.sid, i)
7299 sb.sid = self.sid + ".{:0=5}".format(i)
7300 sb.readbin(fn, verbose=True)
7301 if f_min and f_max:
7302 sb.plotContacts(lower_limit=0.25, upper_limit=0.75,
7303 outfolder='../img_out/',
7304 f_min=f_min, f_max=f_max,
7305 title="t={:.2f} s, $N$={:.0f} kPa"
7306 .format(sb.currentTime(),
7307 sb.currentNormalStress('defined')
7308 /1000.))
7309 else:
7310 sb.plotContacts(lower_limit=0.25, upper_limit=0.75,
7311 title="t={:.2f} s, $N$={:.0f} kPa"
7312 .format(sb.currentTime(),
7313 sb.currentNormalStress('defined')
7314 /1000.), outfolder='../img_out/')
7315
7316 # render images to movie
7317 subprocess.call('cd ../img_out/ && ' +
7318 'ffmpeg -sameq -i {}.%05d-contacts.png '
7319 .format(self.sid) +
7320 '{}-contacts.mp4'.format(self.sid),
7321 shell=True)
7322
7323 else:
7324 print("Visualization type '" + method + "' not understood")
7325 return
7326
7327 # Optional save of figure content
7328 filename = ''
7329 if xlim:
7330 filename = '{0}-{1}-{3}.{2}'.format(self.sid, method, outformat,
7331 xlim[-1])
7332 else:
7333 filename = '{0}-{1}.{2}'.format(self.sid, method, outformat)
7334 if pickle:
7335 pl.dump(fig, file(filename + '.pickle', 'w'))
7336
7337 # Optional save of figure
7338 if outformat != 'txt':
7339 if savefig:
7340 fig.savefig(filename)
7341 print(filename)
7342 fig.clf()
7343 plt.close()
7344 else:
7345 plt.show()
7346
7347
7348 def convert(graphics_format='png', folder='../img_out', remove_ppm=False):
7349 '''
7350 Converts all PPM images in img_out to graphics_format using ImageMagick. All
7351 PPM images are subsequently removed if `remove_ppm` is `True`.
7352
7353 :param graphics_format: Convert the images to this format
7354 :type graphics_format: str
7355 :param folder: The folder containing the PPM images to convert
7356 :type folder: str
7357 :param remove_ppm: Remove ALL ppm files in `folder` after conversion
7358 :type remove_ppm: bool
7359 '''
7360
7361 #quiet = ' > /dev/null'
7362 quiet = ''
7363 # Convert images
7364 subprocess.call('for F in ' + folder \
7365 + '/*.ppm ; do BASE=`basename $F .ppm`; convert $F ' \
7366 + folder + '/$BASE.' + graphics_format + ' ' \
7367 + quiet + ' ; done', shell=True)
7368
7369 # Remove PPM files
7370 if remove_ppm:
7371 subprocess.call('rm ' + folder + '/*.ppm', shell=True)
7372
7373 def render(binary, method='pres', max_val=1e3, lower_cutoff=0.0,
7374 graphics_format='png', verbose=True):
7375 '''
7376 Render target binary using the ``sphere`` raytracer.
7377
7378 :param method: The color visualization method to use for the particles.
7379 Possible values are: 'normal': color all particles with the same
7380 color, 'pres': color by pressure, 'vel': color by translational
7381 velocity, 'angvel': color by rotational velocity, 'xdisp': color by
7382 total displacement along the x-axis, 'angpos': color by angular
7383 position.
7384 :type method: str
7385 :param max_val: The maximum value of the color bar
7386 :type max_val: float
7387 :param lower_cutoff: Do not render particles with a value below this
7388 value, of the field selected by ``method``
7389 :type lower_cutoff: float
7390 :param graphics_format: Convert the PPM images generated by the ray
7391 tracer to this image format using Imagemagick
7392 :type graphics_format: str
7393 :param verbose: Show verbose information during ray tracing
7394 :type verbose: bool
7395 '''
7396 quiet = ''
7397 if not verbose:
7398 quiet = '-q'
7399
7400 # Render images using sphere raytracer
7401 if method == 'normal':
7402 subprocess.call('cd .. ; ./sphere ' + quiet + \
7403 ' --render ' + binary, shell=True)
7404 else:
7405 subprocess.call('cd .. ; ./sphere ' + quiet + \
7406 ' --method ' + method + ' {}'.format(max_val) + \
7407 ' -l {}'.format(lower_cutoff) + \
7408 ' --render ' + binary, shell=True)
7409
7410 # Convert images to compressed format
7411 if verbose:
7412 print('converting to ' + graphics_format)
7413 convert(graphics_format)
7414
7415 def video(project, out_folder='./', video_format='mp4',
7416 graphics_folder='../img_out/', graphics_format='png', fps=25,
7417 verbose=True):
7418 '''
7419 Uses ffmpeg to combine images to animation. All images should be
7420 rendered beforehand using :func:`render()`.
7421
7422 :param project: The simulation id of the project to render
7423 :type project: str
7424 :param out_folder: The output folder for the video file
7425 :type out_folder: str
7426 :param video_format: The format of the output video
7427 :type video_format: str
7428 :param graphics_folder: The folder containing the rendered images
7429 :type graphics_folder: str
7430 :param graphics_format: The format of the rendered images
7431 :type graphics_format: str
7432 :param fps: The number of frames per second to use in the video
7433 :type fps: int
7434 :param qscale: The output video quality, in ]0;1]
7435 :type qscale: float
7436 :param bitrate: The bitrate to use in the output video
7437 :type bitrate: int
7438 :param verbose: Show ffmpeg output
7439 :type verbose: bool
7440 '''
7441 # Possible loglevels:
7442 # quiet, panic, fatal, error, warning, info, verbose, debug
7443 loglevel = 'info'
7444 if not verbose:
7445 loglevel = 'error'
7446
7447 outfile = out_folder + '/' + project + '.' + video_format
7448 subprocess.call('ffmpeg -loglevel ' + loglevel + ' '
7449 + '-i ' + graphics_folder + project + '.output%05d.'
7450 + graphics_format
7451 + ' -c:v libx264 -profile:v high -pix_fmt yuv420p -g 30'
7452 + ' -r {} -y '.format(fps)
7453 + outfile, shell=True)
7454 if verbose:
7455 print('saved to ' + outfile)
7456
7457 def thinsectionVideo(project, out_folder="./", video_format="mp4", fps=25,
7458 qscale=1, bitrate=1800, verbose=False):
7459 '''
7460 Uses ffmpeg to combine thin section images to an animation. This function
7461 will implicity render the thin section images beforehand.
7462
7463 :param project: The simulation id of the project to render
7464 :type project: str
7465 :param out_folder: The output folder for the video file
7466 :type out_folder: str
7467 :param video_format: The format of the output video
7468 :type video_format: str
7469 :param fps: The number of frames per second to use in the video
7470 :type fps: int
7471 :param qscale: The output video quality, in ]0;1]
7472 :type qscale: float
7473 :param bitrate: The bitrate to use in the output video
7474 :type bitrate: int
7475 :param verbose: Show ffmpeg output
7476 :type verbose: bool
7477 '''
7478 ''' Use ffmpeg to combine thin section images to animation.
7479 This function will start off by rendering the images.
7480 '''
7481
7482 # Render thin section images (png)
7483 lastfile = status(project)
7484 sb = sim(fluid=False)
7485 for i in range(lastfile+1):
7486 fn = "../output/{0}.output{1:0=5}.bin".format(project, i)
7487 sb.sid = project + ".output{:0=5}".format(i)
7488 sb.readbin(fn, verbose=False)
7489 sb.thinsection_x1x3(cbmax=sb.w_sigma0[0]*4.0)
7490
7491 # Combine images to animation
7492 # Possible loglevels:
7493 # quiet, panic, fatal, error, warning, info, verbose, debug
7494 loglevel = "info"
7495 if not verbose:
7496 loglevel = "error"
7497
7498 subprocess.call("ffmpeg -qscale {0} -r {1} -b {2} -y ".format(\
7499 qscale, fps, bitrate)
7500 + "-loglevel " + loglevel + " "
7501 + "-i ../img_out/" + project + ".output%05d-ts-x1x3.png "
7502 + "-vf 'crop=((in_w/2)*2):((in_h/2)*2)' " \
7503 + out_folder + "/" + project + "-ts-x1x3." + video_format,
7504 shell=True)
7505
7506 def run(binary, verbose=True, hideinputfile=False):
7507 '''
7508 Execute ``sphere`` with target binary file as input.
7509
7510 :param binary: Input file for ``sphere``
7511 :type binary: str
7512 :param verbose: Show ``sphere`` output
7513 :type verbose: bool
7514 :param hideinputfile: Hide the input file
7515 :type hideinputfile: bool
7516 '''
7517
7518 quiet = ''
7519 stdout = ''
7520 if not verbose:
7521 quiet = '-q'
7522 if hideinputfile:
7523 stdout = ' > /dev/null'
7524 subprocess.call('cd ..; ./sphere ' + quiet + ' ' + binary + ' ' + stdout, \
7525 shell=True)
7526
7527 def torqueScriptParallel3(obj1, obj2, obj3, email='adc@geo.au.dk',
7528 email_alerts='ae', walltime='24:00:00',
7529 queue='qfermi', cudapath='/com/cuda/4.0.17/cuda',
7530 spheredir='/home/adc/code/sphere',
7531 use_workdir=False,
7532 workdir='/scratch'):
7533 '''
7534 Create job script for the Torque queue manager for three binaries,
7535 executed in parallel, ideally on three GPUs.
7536
7537 :param email: The e-mail address that Torque messages should be sent to
7538 :type email: str
7539 :param email_alerts: The type of Torque messages to send to the e-mail
7540 address. The character 'b' causes a mail to be sent when the
7541 execution begins. The character 'e' causes a mail to be sent when
7542 the execution ends normally. The character 'a' causes a mail to be
7543 sent if the execution ends abnormally. The characters can be written
7544 in any order.
7545 :type email_alerts: str
7546 :param walltime: The maximal allowed time for the job, in the format
7547 'HH:MM:SS'.
7548 :type walltime: str
7549 :param queue: The Torque queue to schedule the job for
7550 :type queue: str
7551 :param cudapath: The path of the CUDA library on the cluster compute nodes
7552 :type cudapath: str
7553 :param spheredir: The path to the root directory of sphere on the cluster
7554 :type spheredir: str
7555 :param use_workdir: Use a different working directory than the sphere folder
7556 :type use_workdir: bool
7557 :param workdir: The working directory during the calculations, if
7558 `use_workdir=True`
7559 :type workdir: str
7560
7561 :returns: The filename of the script
7562 :return type: str
7563
7564 See also :func:`torqueScript()`
7565 '''
7566
7567 filename = obj1.sid + '_' + obj2.sid + '_' + obj3.sid + '.sh'
7568
7569 fh = None
7570 try:
7571 fh = open(filename, "w")
7572
7573 fh.write('#!/bin/sh\n')
7574 fh.write('#PBS -N ' + obj1.sid + '_' + obj2.sid + '_' + obj3.sid + '\n')
7575 fh.write('#PBS -l nodes=1:ppn=1\n')
7576 fh.write('#PBS -l walltime=' + walltime + '\n')
7577 fh.write('#PBS -q ' + queue + '\n')
7578 fh.write('#PBS -M ' + email + '\n')
7579 fh.write('#PBS -m ' + email_alerts + '\n')
7580 fh.write('CUDAPATH=' + cudapath + '\n')
7581 fh.write('export PATH=$CUDAPATH/bin:$PATH\n')
7582 fh.write('export LD_LIBRARY_PATH=$CUDAPATH/lib64')
7583 fh.write(':$CUDAPATH/lib:$LD_LIBRARY_PATH\n')
7584 fh.write('echo "`whoami`@`hostname`"\n')
7585 fh.write('echo "Start at `date`"\n')
7586 if use_workdir:
7587 fh.write('ORIGDIR=' + spheredir + '\n')
7588 fh.write('WORKDIR=' + workdir + "/$PBS_JOBID\n")
7589 fh.write('cp -r $ORIGDIR/* $WORKDIR\n')
7590 fh.write('cd $WORKDIR\n')
7591 else:
7592 fh.write('cd ' + spheredir + '\n')
7593 fh.write('cmake . && make\n')
7594 fh.write('./sphere input/' + obj1.sid + '.bin > /dev/null &\n')
7595 fh.write('./sphere input/' + obj2.sid + '.bin > /dev/null &\n')
7596 fh.write('./sphere input/' + obj3.sid + '.bin > /dev/null &\n')
7597 fh.write('wait\n')
7598 if use_workdir:
7599 fh.write('cp $WORKDIR/output/* $ORIGDIR/output/\n')
7600 fh.write('echo "End at `date`"\n')
7601 return filename
7602
7603 finally:
7604 if fh is not None:
7605 fh.close()
7606
7607 def status(project):
7608 '''
7609 Check the status.dat file for the target project, and return the last output
7610 file number.
7611
7612 :param project: The simulation id of the target project
7613 :type project: str
7614
7615 :returns: The last output file written in the simulation calculations
7616 :return type: int
7617 '''
7618
7619 fh = None
7620 try:
7621 filepath = "../output/{0}.status.dat".format(project)
7622 fh = open(filepath)
7623 data = fh.read()
7624 return int(data.split()[2]) # Return last file number
7625 finally:
7626 if fh is not None:
7627 fh.close()
7628
7629 def cleanup(sb):
7630 '''
7631 Removes the input/output files and images belonging to the object simulation
7632 ID from the ``input/``, ``output/`` and ``img_out/`` folders.
7633
7634 :param sb: A sphere.sim object
7635 :type sb: sim
7636 '''
7637 subprocess.call("rm -f ../input/" + sb.sid + ".bin", shell=True)
7638 subprocess.call("rm -f ../output/" + sb.sid + ".*.bin", shell=True)
7639 subprocess.call("rm -f ../img_out/" + sb.sid + ".*", shell=True)
7640 subprocess.call("rm -f ../output/" + sb.sid + ".status.dat", shell=True)
7641 subprocess.call("rm -f ../output/" + sb.sid + ".*.vtu", shell=True)
7642 subprocess.call("rm -f ../output/fluid-" + sb.sid + ".*.vti", shell=True)
7643 subprocess.call("rm -f ../output/" + sb.sid + "-conv.png", shell=True)
7644 subprocess.call("rm -f ../output/" + sb.sid + "-conv.log", shell=True)
7645
7646 def V_sphere(r):
7647 '''
7648 Calculates the volume of a sphere with radius r
7649
7650 :returns: The sphere volume [m^3]
7651 :return type: float
7652 '''
7653 return 4.0/3.0 * math.pi * r**3.0