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