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