Skip to content

Commit 05eeb44

Browse files
authored
Merge branch 'Sierd-patch-1' into main
2 parents 3e7607d + 9ccc733 commit 05eeb44

File tree

5 files changed

+454
-919
lines changed

5 files changed

+454
-919
lines changed

aeolis/avalanching.py

Lines changed: 132 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
# package modules
3434
from aeolis.utils import *
3535

36+
from numba import njit
37+
3638
# initialize logger
3739
logger = logging.getLogger(__name__)
3840

@@ -60,10 +62,10 @@ def angele_of_repose(s,p):
6062
# comment Lisa: dependence on moisture content is not yet implemented
6163
# Can we do something with theta dependent on vegetation cover (larger rhoveg = larger theta?)
6264

63-
theta_stat = p['theta_stat']
65+
# theta_stat = p['theta_stat']
6466
theta_dyn = p['theta_dyn']
6567

66-
s['theta_stat'] = theta_stat
68+
# s['theta_stat'] = theta_stat
6769
s['theta_dyn'] = theta_dyn
6870

6971
return s
@@ -92,176 +94,150 @@ def avalanche(s, p):
9294
'''
9395

9496
if p['process_avalanche']:
97+
nx = p['nx'] + 1
98+
ny = p['ny'] + 1
9599

96-
nx = p['nx']+1
97-
ny = p['ny']+1
98-
99-
#parameters
100-
101-
tan_stat = np.tan(np.deg2rad(s['theta_stat']))
100+
# parameters - only dynamic angle used in loop for now.
101+
# Static angle can be used for more complex criteria in later
102+
# tan_stat = np.tan(np.deg2rad(s['theta_stat']))
102103
tan_dyn = np.tan(np.deg2rad(s['theta_dyn']))
103-
104104

105-
106-
E = 0.2
107-
108-
grad_h_down = np.zeros((ny,nx,4))
109-
flux_down = np.zeros((ny,nx,4))
110-
slope_diff = np.zeros((ny,nx))
111-
grad_h = np.zeros((ny,nx))
105+
E = 0.1
112106

113107
max_iter_ava = p['max_iter_ava']
114-
115-
max_grad_h, grad_h, grad_h_down = calc_gradients(s['zb'], nx, ny, s['ds'], s['dn'], s['zne'])
116-
117-
s['gradh'] = grad_h.copy()
118-
119-
initiate_avalanche = (max_grad_h > tan_stat)
120-
121-
if initiate_avalanche:
122-
123-
for i in range(0,max_iter_ava):
124-
125-
grad_h_down *= 0.
126-
flux_down *= 0.
127-
slope_diff *= 0.
128-
grad_h *= 0.
129-
130-
max_grad_h, grad_h, grad_h_down = calc_gradients(s['zb'], nx, ny, s['ds'], s['dn'], s[ 'zne'])
131-
132-
if max_grad_h < tan_dyn:
133-
break
134-
135-
# Calculation of flux
136-
137-
grad_h_nonerod = (s['zb'] - s['zne']) / s['ds'] # HAS TO BE ADJUSTED!
138-
139-
ix = np.logical_and(grad_h > tan_dyn, grad_h_nonerod > 0)
140-
slope_diff[ix] = np.tanh(grad_h[ix]) - np.tanh(0.9*tan_dyn)
141-
142-
ix = grad_h_nonerod < grad_h - tan_dyn
143-
slope_diff[ix] = np.tanh(grad_h_nonerod[ix])
144-
145-
ix = grad_h != 0
146-
147-
if ny == 1:
148-
#1D interpretation
149-
flux_down[:,:,0][ix] = slope_diff[ix] * grad_h_down[:,:,0][ix] / grad_h[ix]
150-
flux_down[:,:,2][ix] = slope_diff[ix] * grad_h_down[:,:,2][ix] / grad_h[ix]
151-
152-
# Calculation of change in bed level
153-
154-
q_in = np.zeros((ny,nx))
155-
156-
q_out = 0.5*np.abs(flux_down[:,:,0]) + 0.5*np.abs(flux_down[:,:,2])
157-
158-
q_in[0,1:-1] = 0.5*(np.maximum(flux_down[0,:-2,0],0.) \
159-
- np.minimum(flux_down[0,2:,0],0.) \
160-
+ np.maximum(flux_down[0,2:,2],0.) \
161-
- np.minimum(flux_down[0,:-2,2],0.))
162-
else:
163-
# 2D interpretation
164-
flux_down[:,:,0][ix] = slope_diff[ix] * grad_h_down[:,:,0][ix] / grad_h[ix]
165-
flux_down[:,:,1][ix] = slope_diff[ix] * grad_h_down[:,:,1][ix] / grad_h[ix]
166-
flux_down[:,:,2][ix] = slope_diff[ix] * grad_h_down[:,:,2][ix] / grad_h[ix]
167-
flux_down[:,:,3][ix] = slope_diff[ix] * grad_h_down[:,:,3][ix] / grad_h[ix]
168-
169-
# Calculation of change in bed level
170-
171-
q_in = np.zeros((ny,nx))
172-
173-
q_out = 0.5*np.abs(flux_down[:,:,0]) + 0.5* np.abs(flux_down[:,:,1]) + 0.5*np.abs(flux_down[:,:,2]) + 0.5* np.abs(flux_down[:,:,3])
174-
175-
q_in[1:-1,1:-1] = 0.5*(np.maximum(flux_down[1:-1,:-2,0],0.) \
176-
- np.minimum(flux_down[1:-1,2:,0],0.) \
177-
+ np.maximum(flux_down[:-2,1:-1,1],0.) \
178-
- np.minimum(flux_down[2:,1:-1,1],0.) \
179-
180-
+ np.maximum(flux_down[1:-1,2:,2],0.) \
181-
- np.minimum(flux_down[1:-1,:-2,2],0.) \
182-
+ np.maximum(flux_down[2:,1:-1,3],0.) \
183-
- np.minimum(flux_down[:-2,1:-1,3],0.))
184-
185-
s['zb'] += E * (q_in - q_out)
186108

109+
# call the njit-compiled loop for performance
110+
zb, grad_h = avalanche_loop(
111+
s['zb'].copy(), s['zne'], s['ds'], s['dn'], nx, ny, E, max_iter_ava, tan_dyn
112+
)
113+
187114
# Ensure water level is up-to-date with bed level
188-
s['zs'] = s['SWL'].copy()
115+
s['zb'] = zb
116+
s['gradh'] = grad_h
117+
s['zs'] = s['SWL']
189118
ix = (s['zb'] > s['zs'])
190119
s['zs'][ix] = s['zb'][ix]
191-
return s
192120

121+
return s
193122

194-
def calc_gradients(zb, nx, ny, ds, dn, zne):
195-
'''Calculates the downslope gradients in the bed that are needed for
196-
avalanching module
197-
198-
Parameters
199-
----------
200-
123+
@njit(cache=True)
124+
def avalanche_loop(zb, zne, ds, dn, nx, ny, E, max_iter_ava, tan_dyn):
125+
# Rewritten to use explicit loops and avoid numpy boolean indexing
126+
# Allocate temporaries once
127+
grad_h_down = np.zeros((ny, nx, 2))
128+
flux_down = np.zeros((ny, nx, 2))
129+
slope_diff = np.zeros((ny, nx))
130+
grad_h = np.zeros((ny, nx))
131+
for it in range(max_iter_ava):
132+
# Reset temporaries to zero
133+
grad_h_down.fill(0)
134+
flux_down.fill(0)
135+
slope_diff.fill(0)
136+
grad_h.fill(0)
137+
138+
# first calculate the downslope gradients to see if there is avalanching
139+
# Compute downslope gradients grad_h_down (ny,nx,2), grad_h (ny,nx), and max_grad_h
140+
# Initialize
141+
max_grad_h = 0.0
142+
143+
# Directions: 0 => +X, 1 => +Y
144+
for i in range(ny):
145+
for j in range(nx):
146+
# disable avalanching where zne >= zb
147+
if zne[i, j] >= zb[i, j]:
148+
continue
149+
else:
150+
center = zb[i, j]
151+
# +X direction
152+
g0 = 0.0
153+
# Handle boundaries: set gradient to zero at edges
154+
if j == 0 or j == nx - 1:
155+
grad_h_down[i, j, 0] = 0.0
156+
else:
157+
right = zb[i, j + 1]
158+
left = zb[i, j - 1]
159+
if not ((right > center) and (left > center)):
160+
if right > left:
161+
g0 = left - center
162+
else:
163+
g0 = center - right
164+
grad_h_down[i, j, 0] = g0 / ds[i, j]
165+
166+
# +Y direction
167+
g1 = 0.0
168+
if i == 0 or i == ny - 1:
169+
grad_h_down[i, j, 1] = 0.0
170+
else:
171+
down = zb[i + 1, j]
172+
up = zb[i - 1, j]
173+
if not ((down > center) and (up > center)):
174+
if down > up:
175+
g1 = up - center
176+
else:
177+
g1 = center - down
178+
grad_h_down[i, j, 1] = g1 / dn[i, j]
179+
180+
# gradient magnitude and maximum
181+
gh2 = grad_h_down[i, j, 0] * grad_h_down[i, j, 0] + grad_h_down[i, j, 1] * grad_h_down[i, j, 1]
182+
gh = np.sqrt(gh2)
183+
grad_h[i, j] = gh
184+
# derive maximum slope
185+
if gh > max_grad_h:
186+
max_grad_h = gh
187+
188+
# ok now the gradients are calculated
189+
# these are max_grad_h, grad_h, grad_h_down
190+
# check for stopping criterion
191+
if max_grad_h < tan_dyn:
192+
break
201193

202-
Returns
203-
-------
204-
np.ndarray
205-
Downslope gradients in 4 different directions (nx*ny, 4)
206-
'''
207-
208-
grad_h_down = np.zeros((ny,nx,4))
209-
210-
# Calculation of slope (positive x-direction)
211-
grad_h_down[:,1:-1,0] = zb[:,1:-1] - zb[:,2:]
212-
ix = zb[:,2:] > zb[:,:-2]
213-
grad_h_down[:,1:-1,0][ix] = - (zb[:,1:-1][ix] - zb[:,:-2][ix])
214-
ix = np.logical_and(zb[:,2:]>zb[:,1:-1], zb[:,:-2]>zb[:,1:-1])
215-
grad_h_down[:,1:-1,0][ix] = 0.
216-
217-
# Calculation of slope (positive y-direction)
218-
grad_h_down[1:-1,:,1] = zb[1:-1,:] - zb[2:,:]
219-
ix = zb[2:,:] > zb[:-2,:]
220-
grad_h_down[1:-1,:,1][ix] = - (zb[1:-1,:][ix] - zb[:-2,:][ix])
221-
ix = np.logical_and(zb[2:,:]>zb[1:-1,:], zb[:-2,:]>zb[1:-1,:])
222-
grad_h_down[1:-1,:,1][ix] = 0.
223-
224-
# Calculation of slope (negative x-direction)
225-
grad_h_down[:,1:-1,2] = zb[:,1:-1] - zb[:,:-2]
226-
ix = zb[:,:-2] > zb[:,2:]
227-
grad_h_down[:,1:-1,2][ix] = - (zb[:,1:-1][ix] - zb[:,2:][ix])
228-
ix = np.logical_and(zb[:,:-2]>zb[:,1:-1], zb[:,2:]>zb[:,1:-1])
229-
grad_h_down[:,1:-1,2][ix] = 0.
230-
231-
# Calculation of slope (negative y-direction)
232-
grad_h_down[1:-1,:,3] = zb[1:-1,:] - zb[:-2,:]
233-
ix = zb[:-2,:] > zb[2:,:]
234-
grad_h_down[1:-1,:,3][ix] = - (zb[1:-1,:][ix] - zb[2:,:][ix])
235-
ix = np.logical_and(zb[:-2,:]>zb[1:-1,:], zb[2:,:]>zb[1:-1,:])
236-
grad_h_down[1:-1,:,3][ix] = 0.
237-
238-
if ny == 1:
239-
#1D interpretation
240-
grad_h_down[:,0,:] = 0
241-
grad_h_down[:,-1,:] = 0
242-
243-
else:
244-
# 2D interpretation
245-
grad_h_down[:,0,:] = 0
246-
grad_h_down[:,-1,:] = 0
247-
grad_h_down[0,:,:] = 0
248-
grad_h_down[-1,:,:] = 0
194+
# we continue to compute fluxes and update zb
249195

250-
grad_h_down[:,:,0] /= ds
251-
grad_h_down[:,:,1] /= dn
252-
grad_h_down[:,:,2] /= ds
253-
grad_h_down[:,:,3] /= dn
196+
# compute grad_h_nonerod and slope_diff per cell using explicit loops
197+
for i in range(ny):
198+
for j in range(nx):
199+
if grad_h[i, j] > tan_dyn:
200+
slope_diff[i, j] = np.tanh(grad_h[i, j]) - np.tanh(0.9 * tan_dyn)
201+
flux_down[i, j, 0] = slope_diff[i, j] * grad_h_down[i, j, 0]# / grad_h[i, j]
202+
flux_down[i, j, 1] = slope_diff[i, j] * grad_h_down[i, j, 1]# / grad_h[i, j]
203+
204+
# Build q_in and q_out from 2-component flux representation
205+
f_x = flux_down[:, :, 0]
206+
f_y = flux_down[:, :, 1]
254207

255-
grad_h2 = 0.5*grad_h_down[:,:,0]**2 + 0.5*grad_h_down[:,:,1]**2 + 0.5*grad_h_down[:,:,2]**2 + 0.5*grad_h_down[:,:,3]**2
208+
# preserve sign: compute positive outgoing components per direction
209+
out_east = np.maximum(f_x, 0.0)
210+
out_south = np.maximum(f_y, 0.0)
256211

257-
if 0: #Sierd_com; to be changed in future release
258-
ix = zb < zne + 0.005
259-
grad_h2[ix] = 0.
212+
# average with neighbor contributions at faces (keeps sign info)
213+
out_north = np.maximum(-f_y, 0.0)
214+
out_west = np.maximum(-f_x, 0.0)
260215

261-
grad_h = np.sqrt(grad_h2)
216+
q_out = out_east + out_west + out_south + out_north
262217

263-
max_grad_h = np.max(grad_h)
218+
inc_west = np.zeros_like(f_x)
219+
# from west neighbor (positive f_x of west cell) with periodic wrap
220+
inc_west[:, 1:] = np.maximum(f_x[:, :-1], 0.0)
221+
inc_west[:, 0] = np.maximum(f_x[:, -1], 0.0)
264222

265-
return max_grad_h, grad_h, grad_h_down
266-
223+
inc_east = np.zeros_like(f_x)
224+
# from east neighbor (negative f_x of east cell) with periodic wrap
225+
inc_east[:, :-1] = np.maximum(-f_x[:, 1:], 0.0)
226+
inc_east[:, -1] = np.maximum(-f_x[:, 0], 0.0)
227+
228+
inc_north = np.zeros_like(f_y)
229+
# from north neighbor (positive f_y of north cell) with periodic wrap
230+
inc_north[1:, :] = np.maximum(f_y[:-1, :], 0.0)
231+
inc_north[0, :] = np.maximum(f_y[-1, :], 0.0)
232+
233+
inc_south = np.zeros_like(f_y)
234+
# from south neighbor (negative f_y of south cell) with periodic wrap
235+
inc_south[:-1, :] = np.maximum(-f_y[1:, :], 0.0)
236+
inc_south[-1, :] = np.maximum(-f_y[0, :], 0.0)
237+
238+
q_in = (inc_west + inc_east + inc_north + inc_south)
239+
240+
# update bed level without non-erodible layer
241+
zb += E * (q_in - q_out)
267242

243+
return zb, grad_h

aeolis/constants.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@
9898
'ustarn', # [m/s] Component of shear velocity in y-direction by wind
9999
'ustar0', # [m/s] Initial shear velocity (without perturbation)
100100
'zsep', # [m] Z level of polynomial that defines the separation bubble
101-
'hsep', # [m] Height of separation bubbel = difference between z-level of zsep and of the bed level zb
102-
'theta_stat', # [degrees] Updated, spatially varying static angle of repose
103-
'theta_dyn', # [degrees] Updated, spatially varying dynamic angle of repose
101+
'hsep', # [m] Height of separation bubble = difference between z-level of zsep and of the bed level zb
102+
'theta_dyn', # [degrees] spatially varying dynamic angle of repose for avalanching
104103
'rhoveg', # [-] Vegetation cover
105104
'drhoveg', # Change in vegetation cover
106105
'hveg', # [m] height of vegetation

aeolis/inout.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ def check_configuration(p):
220220
logger.warning('Warning: the used roughness method (constant) defines the z0 as '
221221
'k (z0 = k), this was implemented to ensure backward compatibility '
222222
'and does not follow the definition of Nikuradse (z0 = k / 30).')
223+
224+
# check if steadystate solver is used with multiple sediment fractions
225+
if p['solver'].lower() in ['steadystate', 'steadystatepieter']:
226+
if len(p['grain_size']) > 1:
227+
logger.log_and_raise('The steadystate solver is not compatible with multiple sediment fractions. '
228+
'Please use a single sediment fraction or switch to a different solver (e.g., trunk or pieter).',
229+
exc=ValueError)
223230

224231

225232
def parse_value(val, parse_files=True, force_list=False):

0 commit comments

Comments
 (0)