t1d-channel.py - granular-channel-hydro - subglacial hydrology model for sedimentary channels
 (HTM) git clone git://src.adamsgaard.dk/granular-channel-hydro
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
       t1d-channel.py (12863B)
       ---
            1 #!/usr/bin/env python
            2 
            3 # # ABOUT THIS FILE
            4 # The following script uses basic Python and Numpy functionality to solve the
            5 # coupled systems of equations describing subglacial channel development in
            6 # soft beds as presented in `Damsgaard et al. "Sediment behavior controls
            7 # equilibrium width of subglacial channels"`, accepted for publicaiton in
            8 # Journal of Glaciology.
            9 #
           10 # High performance is not the goal for this implementation, which is instead
           11 # intended as a heavily annotated example on the solution procedure without
           12 # relying on solver libraries, suitable for low-level languages like C, Fortran
           13 # or CUDA.
           14 #
           15 # License: GNU Public License v3+ (see LICENSE.md)
           16 # Author: Anders Damsgaard, andersd@princeton.edu, https://adamsgaard.dk
           17 
           18 import numpy
           19 import matplotlib.pyplot as plt
           20 import sys
           21 
           22 
           23 # # Model parameters
           24 Ns = 25              # Number of nodes [-]
           25 Ls = 1e3             # Model length [m]
           26 total_days = 2.     # Total simulation time [d]
           27 t_end = 24.*60.*60.*total_days  # Total simulation time [s]
           28 tol_S = 1e-2         # Tolerance criteria for the norm. max. residual for S
           29 tol_Q = 1e-2         # Tolerance criteria for the norm. max. residual for Q
           30 tol_P_c = 1e-2       # Tolerance criteria for the norm. max. residual for P_c
           31 max_iter = 1e2*Ns    # Maximum number of solver iterations before failure
           32 print_output_convergence = False      # Display convergence in nested loops
           33 print_output_convergence_main = True  # Display convergence in main loop
           34 safety = 0.01        # Safety factor ]0;1] for adaptive timestepping
           35 plot_interval = 20   # Time steps between plots
           36 plot_during_iterations = False        # Generate plots for intermediate results
           37 
           38 # Physical parameters
           39 rho_w = 1000.        # Water density [kg/m^3]
           40 rho_i = 910.         # Ice density [kg/m^3]
           41 rho_s = 2600.        # Sediment density [kg/m^3]
           42 g = 9.8              # Gravitational acceleration [m/s^2]
           43 theta = 30.          # Angle of internal friction in sediment [deg]
           44 D = 1.15e-3          # Mean grain size [m], Lajeuness et al 2010, series 1
           45 tau_c = 0.016        # Critical Shields stress, Lajeunesse et al 2010, series 1
           46 
           47 # Boundary conditions
           48 P_terminus = 0.      # Water pressure at terminus [Pa]
           49 m_dot = numpy.linspace(1e-6, 1e-5, Ns-1)  # Water source term [m/s]
           50 Q_upstream = 1e-3    # Water influx upstream (must be larger than 0) [m^3/s]
           51 
           52 # Channel hydraulic properties
           53 manning = 0.1          # Manning roughness coefficient [m^{-1/3} s]
           54 friction_factor = 0.1  # Darcy-Weisbach friction factor [-]
           55 
           56 # Channel growth-limit parameters
           57 c_1 = -0.118         # [m/kPa]
           58 c_2 = 4.60           # [m]
           59 
           60 # Minimum channel size [m^2], must be bigger than 0
           61 S_min = 1e-2
           62 
           63 
           64 # # Initialize model arrays
           65 # Node positions, terminus at Ls
           66 s = numpy.linspace(0., Ls, Ns)
           67 ds = s[1:] - s[:-1]
           68 
           69 # Ice thickness [m]
           70 H = 6.*(numpy.sqrt(Ls - s + 5e3) - numpy.sqrt(5e3)) + 1.0
           71 
           72 # Bed topography [m]
           73 b = numpy.zeros_like(H)
           74 
           75 N = H*0.1*rho_i*g  # Initial effective stress [Pa]
           76 
           77 # Initialize arrays for channel segments between nodes
           78 S = numpy.ones(len(s) - 1)*0.1   # Cross-sect. area of channel segments [m^2]
           79 S_max = numpy.zeros_like(S)  # Max. channel size [m^2]
           80 dSdt = numpy.zeros_like(S)   # Transient in channel cross-sect. area [m^2/s]
           81 W = S/numpy.tan(numpy.deg2rad(theta))  # Assuming no channel floor wedge
           82 Q = numpy.zeros_like(S)      # Water flux in channel segments [m^3/s]
           83 Q_s = numpy.zeros_like(S)    # Sediment flux in channel segments [m^3/s]
           84 P_c = numpy.zeros_like(S)    # Effective pressure in channel segments [Pa]
           85 P_w = numpy.zeros_like(S)    # Effective pressure in channel segments [Pa]
           86 tau = numpy.zeros_like(S)    # Avg. shear stress from current [Pa]
           87 porosity = numpy.ones_like(S)*0.3  # Sediment porosity [-]
           88 res = numpy.zeros_like(S)   # Solution residual during solver iterations
           89 
           90 
           91 # # Helper functions
           92 def gradient(arr, arr_x):
           93     # Central difference gradient of an array ``arr`` with node positions at
           94     # ``arr_x``.
           95     return (arr[1:] - arr[:-1])/(arr_x[1:] - arr_x[:-1])
           96 
           97 
           98 def avg_midpoint(arr):
           99     # Averaged value of neighboring array elements
          100     return (arr[:-1] + arr[1:])/2.
          101 
          102 
          103 def channel_hydraulic_roughness(manning, S, W, theta):
          104     # Determine hydraulic roughness assuming that the channel has no channel
          105     # floor wedge.
          106     l = W + W/numpy.cos(numpy.deg2rad(theta))  # wetted perimeter
          107     return manning**2.*(l**2./S)**(2./3.)
          108 
          109 
          110 def channel_shear_stress(Q, S):
          111     # Determine mean wall shear stress from Darcy-Weisbach friction loss
          112     u_bar = Q/S
          113     return 1./8.*friction_factor*rho_w*u_bar**2.
          114 
          115 
          116 def channel_sediment_flux(tau, W):
          117     # Meyer-Peter and Mueller 1948
          118     # tau: Shear stress by water flow
          119     # W: Channel width
          120 
          121     # Non-dimensionalize shear stress
          122     shields_stress = tau/((rho_s - rho_w)*g*D)
          123 
          124     stress_excess = shields_stress - tau_c
          125     stress_excess[stress_excess < 0.] = 0.
          126     return 8.*stress_excess**(3./2.)*W \
          127         * numpy.sqrt((rho_s - rho_w)/rho_w*g*D**3.)
          128 
          129 
          130 def channel_growth_rate_sedflux(Q_s, porosity, s_c):
          131     # Damsgaard et al, in prep
          132     return 1./porosity[1:] * gradient(Q_s, s_c)
          133 
          134 
          135 def update_channel_size_with_limit(S, S_old, dSdt, dt, P_c):
          136     # Damsgaard et al, in prep
          137     S_max = numpy.maximum(
          138         numpy.maximum(
          139             1./4.*(c_1*numpy.maximum(P_c, 0.)/1000. + c_2), 0.)**2. *
          140         numpy.tan(numpy.deg2rad(theta)), S_min)
          141     S = numpy.maximum(numpy.minimum(S_old + dSdt*dt, S_max), S_min)
          142     W = S/numpy.tan(numpy.deg2rad(theta))  # Assume no channel floor wedge
          143     dSdt = S - S_old   # adjust dSdt for clipping due to channel size limits
          144     return S, W, S_max, dSdt
          145 
          146 
          147 def flux_solver(m_dot, ds):
          148     # Iteratively find new water fluxes
          149     it = 0
          150     max_res = 1e9  # arbitrary large value
          151 
          152     # Iteratively find solution, do not settle for less iterations than the
          153     # number of nodes
          154     while max_res > tol_Q:
          155 
          156         Q_old = Q.copy()
          157         # dQ/ds = m_dot  ->  Q_out = m*delta(s) + Q_in
          158         # Upwind information propagation (upwind)
          159         Q[0] = Q_upstream
          160         Q[1:] = m_dot[1:]*ds[1:] + Q[:-1]
          161         max_res = numpy.max(numpy.abs((Q - Q_old)/(Q + 1e-16)))
          162 
          163         if print_output_convergence:
          164             print('it = {}: max_res = {}'.format(it, max_res))
          165 
          166         # import ipdb; ipdb.set_trace()
          167         if it >= max_iter:
          168             raise Exception('t = {}, step = {}: '.format(time, step) +
          169                             'Iterative solution not found for Q')
          170         it += 1
          171 
          172     return Q
          173 
          174 
          175 def pressure_solver(psi, f, Q, S):
          176     # Iteratively find new water pressures
          177     # dP_c/ds = f*rho_w*g*Q^2/S^{8/3} - psi  (Kingslake and Ng 2013)
          178 
          179     it = 0
          180     max_res = 1e9  # arbitrary large value
          181     while max_res > tol_P_c:
          182 
          183         P_c_old = P_c.copy()
          184 
          185         # Dirichlet BC (fixed pressure) at terminus
          186         P_c[-1] = rho_i*g*H_c[-1] - P_terminus
          187 
          188         P_c[:-1] = P_c[1:] \
          189             + psi[:-1]*ds[:-1] \
          190             - f[:-1]*rho_w*g*Q[:-1]*numpy.abs(Q[:-1]) \
          191             / (S[:-1]**(8./3.))*ds[:-1]
          192 
          193         max_res = numpy.max(numpy.abs((P_c - P_c_old)/(P_c + 1e-16)))
          194 
          195         if print_output_convergence:
          196             print('it = {}: max_res = {}'.format(it, max_res))
          197 
          198         if it >= max_iter:
          199             raise Exception('t = {}, step = {}:'.format(time, step) +
          200                             'Iterative solution not found for P_c')
          201         it += 1
          202 
          203     return P_c
          204 
          205 
          206 def plot_state(step, time, S_, S_max_, title=False):
          207     # Plot parameters along profile
          208     fig = plt.gcf()
          209     fig.set_size_inches(3.3*1.1, 3.3*1.1*1.5)
          210 
          211     ax_Pa = plt.subplot(3, 1, 1)  # axis with Pascals as y-axis unit
          212     ax_Pa.plot(s_c/1000., P_c/1e6, '-k', label='$P_c$')
          213     ax_Pa.plot(s_c/1000., H_c*rho_i*g/1e6, '--r', label='$P_i$')
          214     ax_Pa.plot(s_c/1000., P_w/1e6, ':y', label='$P_w$')
          215 
          216     ax_m3s = ax_Pa.twinx()  # axis with m3/s as y-axis unit
          217     ax_m3s.plot(s_c/1000., Q, '.-b', label='$Q$')
          218 
          219     if title:
          220         plt.title('Day: {:.3}'.format(time/(60.*60.*24.)))
          221     ax_Pa.legend(loc=2)
          222     ax_m3s.legend(loc=1)
          223     ax_Pa.set_ylabel('[MPa]')
          224     ax_m3s.set_ylabel('[m$^3$/s]')
          225 
          226     # ax_m3s_sed = plt.subplot(3, 1, 2, sharex=ax_Pa)
          227     ax_m3s_sed_blank = plt.subplot(3, 1, 2, sharex=ax_Pa)
          228     ax_m3s_sed_blank.get_yaxis().set_visible(False)
          229     ax_m3s_sed = ax_m3s_sed_blank.twinx()
          230     #ax_m3s_sed.plot(s_c/1000., Q_s, '-', label='$Q_{s}$')
          231     ax_m3s_sed.plot(s_c/1000., Q_s*1000., '-', label='$Q_{s}$')
          232     ax_m3s_sed.legend(loc=2)
          233 
          234     ax_m2 = plt.subplot(3, 1, 3, sharex=ax_Pa)
          235     ax_m2.plot(s_c/1000., S_, '-k', label='$S$')
          236     ax_m2.plot(s_c/1000., S_max_, '--', color='#666666', label='$S_{max}$')
          237 
          238     ax_m2s = ax_m2.twinx()
          239     #ax_m2s.plot(s_c/1000., dSdt, ':', label='$dS/dt$')
          240     ax_m2s.plot(s_c/1000., dSdt*1000., ':', label='$dS/dt$')
          241 
          242     ax_m2.legend(loc=2)
          243     ax_m2s.legend(loc=3)
          244     ax_m2.set_xlabel('$s$ [km]')
          245     ax_m2.set_ylabel('[m$^2$]')
          246     #ax_m3s_sed.set_ylabel('[m$^3$/s]')
          247     #ax_m2s.set_ylabel('[m$^2$/s]')
          248     ax_m3s_sed.set_ylabel('[mm$^3$/s]')
          249     ax_m2s.set_ylabel('[mm$^2$/s]')
          250 
          251     # use scientific notation for m3s and m2s axes
          252     #ax_m3s_sed.get_yaxis().set_major_formatter(plt.LogFormatter(10,
          253     #labelOnlyBase=False))
          254     #ax_m2s.get_yaxis().set_major_formatter(plt.LogFormatter(10,
          255     #labelOnlyBase=False))
          256 
          257     ax_Pa.set_xlim([s.min()/1000., s.max()/1000.])
          258 
          259     plt.setp(ax_Pa.get_xticklabels(), visible=False)
          260     plt.tight_layout()
          261     if step == -1:
          262         plt.savefig('chan-0.init.pdf')
          263     else:
          264         plt.savefig('chan-' + str(step) + '.pdf')
          265     plt.clf()
          266     plt.close()
          267 
          268 
          269 def find_new_timestep(ds, Q, Q_s, S):
          270     # Determine the timestep using the Courant-Friedrichs-Lewy condition
          271     dt = safety*numpy.minimum(60.*60.*24.,
          272                               numpy.min(numpy.abs(ds/(Q*S),
          273                                                   ds/(Q_s*S)+1e-16)))
          274 
          275     if dt < 1.0:
          276         raise Exception('Error: Time step less than 1 second at step '
          277                         + '{}, time '.format(step)
          278                         + '{:.3} s/{:.3} d'.format(time, time/(60.*60.*24.)))
          279 
          280     return dt
          281 
          282 
          283 def print_status_to_stdout(step, time, dt):
          284     sys.stdout.write('\rstep = {}, '.format(step) +
          285                      't = {:.2} s or {:.4} d, dt = {:.2} s         '
          286                      .format(time, time/(60.*60.*24.), dt))
          287     sys.stdout.flush()
          288 
          289 
          290 # Initialize remaining values before entering time loop
          291 s_c = avg_midpoint(s)  # Channel section midpoint coordinates [m]
          292 H_c = avg_midpoint(H)
          293 
          294 # Water-pressure gradient from geometry [Pa/m]
          295 psi = -rho_i*g*gradient(H, s) - (rho_w - rho_i)*g*gradient(b, s)
          296 
          297 # Prepare figure object for plotting during the simulation
          298 fig = plt.figure('channel')
          299 plot_state(-1, 0.0, S, S_max)
          300 
          301 
          302 # # Time loop
          303 time = 0.
          304 step = 0
          305 while time <= t_end:
          306 
          307     # Determine time step length from water flux
          308     dt = find_new_timestep(ds, Q, Q_s, S)
          309 
          310     # Display current simulation status
          311     print_status_to_stdout(step, time, dt)
          312 
          313     it = 0
          314 
          315     # Initialize the maximum normalized residual for S to an arbitrary large
          316     # value
          317     max_res = 1e9
          318 
          319     S_old = S.copy()
          320     # Iteratively find solution with the Jacobi relaxation method
          321     while max_res > tol_S:
          322 
          323         S_prev_it = S.copy()
          324 
          325         # Find new water fluxes consistent with mass conservation and local
          326         # meltwater production (m_dot)
          327         Q = flux_solver(m_dot, ds)
          328 
          329         # Find average shear stress from water flux for each channel segment
          330         tau = channel_shear_stress(Q, S)
          331 
          332         # Determine sediment flux
          333         Q_s = channel_sediment_flux(tau, W)
          334 
          335         # Determine change in channel size for each channel segment.
          336         # Use backward differences and assume dS/dt=0 in first segment.
          337         dSdt[1:] = channel_growth_rate_sedflux(Q_s, porosity, s_c)
          338 
          339         # Update channel cross-sectional area and width according to growth
          340         # rate and size limit for each channel segment
          341         S, W, S_max, dSdt = \
          342             update_channel_size_with_limit(S, S_old, dSdt, dt, P_c)
          343 
          344         f = channel_hydraulic_roughness(manning, S, W, theta)
          345 
          346         # Find new effective pressures consistent with the flow law and water
          347         # pressures in channel segments
          348         P_c = pressure_solver(psi, f, Q, S)
          349         P_w = rho_i*g*H_c - P_c
          350 
          351         if plot_during_iterations:
          352             plot_state(step + it/1e4, time, S, S_max)
          353 
          354         # Find new maximum normalized residual value
          355         max_res = numpy.max(numpy.abs((S - S_prev_it)/(S + 1e-16)))
          356         if print_output_convergence_main:
          357             print('it = {}: max_res = {}'.format(it, max_res))
          358 
          359         # import ipdb; ipdb.set_trace()
          360         if it >= max_iter:
          361             raise Exception('t = {}, step = {}: '.format(time, step) +
          362                             'Iterative solution not found')
          363         it += 1
          364 
          365     # Generate an output figure for every n time steps
          366     if step % plot_interval == 0:
          367         plot_state(step, time, S, S_max)
          368 
          369     # Update time
          370     time += dt
          371     step += 1