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