3333# package modules
3434from aeolis .utils import *
3535
36+ from numba import njit
37+
3638# initialize logger
3739logger = 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
0 commit comments