Skip to content

Commit a7d70e8

Browse files
dwolfschfso42
authored andcommitted
Updates the ellipsoid function of the scarp script; changed license field in pyproject.toml
chore(pyproject): update license format and add `black` as a dependency - Changed license field to `EUPL-1.2` for consistency. - Added `black` tool to the dev dependencies for code formatting. docs(com6RockAvalanche): remove `ScarpReadMe.txt` and move to module documentation - Deleted `ScarpReadMe.txt` as part of documentation cleanup. - Enhanced and detailed the attribute descriptions in `moduleCom6RockAvalanche.rst`.
1 parent 0e67110 commit a7d70e8

File tree

4 files changed

+131
-46
lines changed

4 files changed

+131
-46
lines changed

avaframe/com6RockAvalanche/scarp.py

Lines changed: 91 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -112,28 +112,30 @@ def scarpAnalysisMain(cfg, baseDir):
112112
ellipsoidsMaxDepth = list(map(float, SHPdata['maxdepth']))
113113
ellipsoidsSemiMajor = list(map(float, SHPdata['semimajor']))
114114
ellipsoidsSemiMinor = list(map(float, SHPdata['semiminor']))
115+
ellipsoidsTilt = list(map(float, SHPdata['tilt']))
116+
ellipsoidsDir = list(map(float, SHPdata['direc']))
117+
ellipsoidsOffset = list(map(float, SHPdata['offset']))
118+
ellipsoidDip = list(map(float, SHPdata['dip']))
115119
except KeyError as e:
116-
raise ValueError(f"Required attribute '{e.args[0]}' not found in shapefile. Ensure the fields 'maxdepth', 'semimajor', and 'semiminor' exist.")
117-
118-
if not (
119-
len(ellipsoidsMaxDepth)
120-
== len(ellipsoidsSemiMajor)
121-
== len(ellipsoidsSemiMinor)
122-
== SHPdata["nFeatures"]
123-
):
124-
raise ValueError(
125-
"Mismatch between number of shapefile features and ellipsoid parameters in the .ini file."
126-
)
127-
120+
raise ValueError(f"Required attribute '{e.args[0]}' not found in shapefile. Ensure the fields 'maxdepth', 'semimajor', 'semiminor', 'tilt', 'dir', 'dip', and 'offset' exist.")
121+
122+
if not all(len(lst) == SHPdata["nFeatures"] for lst in [ellipsoidsMaxDepth, ellipsoidsSemiMajor, ellipsoidsSemiMinor, ellipsoidsTilt, ellipsoidsDir, ellipsoidsOffset, ellipsoidDip]):
123+
raise ValueError("Mismatch between number of shapefile features and ellipsoid parameters.")
124+
128125
for i in range(SHPdata["nFeatures"]):
129126
xCenter = SHPdata["x"][int(SHPdata["Start"][i])]
130127
yCenter = SHPdata["y"][int(SHPdata["Start"][i])]
131128
maxDepth = ellipsoidsMaxDepth[i]
132129
semiMajor = ellipsoidsSemiMajor[i]
133130
semiMinor = ellipsoidsSemiMinor[i]
134-
ellipsoidFeatures.extend([xCenter, yCenter, maxDepth, semiMajor, semiMinor])
135-
131+
tilt = ellipsoidsTilt[i]
132+
direction = ellipsoidsDir[i]
133+
offset = ellipsoidsOffset[i]
134+
dip = ellipsoidDip[i]
135+
ellipsoidFeatures.extend([xCenter, yCenter, maxDepth, semiMajor, semiMinor, tilt, direction, offset, dip])
136+
136137
features = ",".join(map(str, ellipsoidFeatures))
138+
137139
log.debug("Ellipsoid features extracted and combined: %s", features)
138140

139141
if method == 'plane':
@@ -262,55 +264,104 @@ def calculateScarpWithPlanes(elevData, periData, elevTransform, planes):
262264
return scarpData
263265

264266
def calculateScarpWithEllipsoids(elevData, periData, elevTransform, ellipsoids):
265-
"""Calculate the scarp using sliding circles (rotational ellipsoids).
267+
"""Calculate the scarp using tilted and offset ellipsoids.
266268
267269
Parameters
268270
----------
269271
elevData : np.ndarray
270272
The elevation data as a 2D array.
271273
periData : np.ndarray
272-
The perimeter data as a 2D array, which defines the extent of the scarp.
274+
The perimeter data as a 2D array.
273275
elevTransform : Affine
274-
The affine transformation matrix of the raster (used to convert pixel to geographic coordinates).
276+
The affine transformation matrix of the raster.
275277
ellipsoids : str
276-
Comma-separated string defining ellipsoids (x_center, y_center, max_depth, semi_major_axis, semi_minor_axis).
278+
Comma-separated string defining ellipsoids with parameters:
279+
(x_center, y_center, max_depth, semi_major, semi_minor, tilt, dir, offset)
277280
278281
Returns
279282
-------
280283
np.ndarray
281284
A 2D array with the calculated scarp values.
282285
"""
286+
283287
n, m = elevData.shape
284288
scarpData = np.zeros_like(elevData, dtype=np.float32)
285289

290+
# Parse ellipsoid definitions
286291
ellipsoids = list(map(float, ellipsoids.split(',')))
287-
nEllipsoids = int(len(ellipsoids) / 5)
288-
289-
xCenter = [ellipsoids[0]]
290-
yCenter = [ellipsoids[1]]
291-
maxDepth = [ellipsoids[2]]
292-
semiMajor = [ellipsoids[3]]
293-
semiMinor = [ellipsoids[4]]
294-
295-
for i in range(1, nEllipsoids):
296-
xCenter.append(ellipsoids[5 * i])
297-
yCenter.append(ellipsoids[5 * i + 1])
298-
maxDepth.append(ellipsoids[5 * i + 2])
299-
semiMajor.append(ellipsoids[5 * i + 3])
300-
semiMinor.append(ellipsoids[5 * i + 4])
292+
nEllipsoids = int(len(ellipsoids) / 9)
293+
294+
xCenter, yCenter, maxDepth = [], [], []
295+
semiMajor, semiMinor = [], []
296+
tilt, tiltDir, offset, dip = [], [], [], []
297+
298+
for i in range(nEllipsoids):
299+
xCenter.append(ellipsoids[9 * i])
300+
yCenter.append(ellipsoids[9 * i + 1])
301+
maxDepth.append(ellipsoids[9 * i + 2])
302+
semiMajor.append(ellipsoids[9 * i + 3])
303+
semiMinor.append(ellipsoids[9 * i + 4])
304+
tilt.append(ellipsoids[9 * i + 5])
305+
tiltDir.append(np.radians(ellipsoids[9 * i + 6])) # tilt direction in radians
306+
offset.append(ellipsoids[9 * i + 7])
307+
dip.append(np.radians(ellipsoids[9 * i + 8])) # rotation of base ellipse in radians
308+
309+
# Compute slope direction and magnitude from DEM gradients
310+
grad_y, grad_x = np.gradient(elevData, abs(elevTransform[4]), elevTransform[0])
311+
slopeDir = np.arctan2(-grad_y, grad_x) # Azimuth of steepest descent
312+
slopeMagnitude = np.sqrt(grad_x**2 + grad_y**2)
301313

302314
for row in range(n):
303315
for col in range(m):
304316
west, north = rasterio.transform.xy(elevTransform, row, col, offset='center')
305317
scarpVal = elevData[row, col]
318+
306319
for k in range(nEllipsoids):
307-
x2 = ((west - xCenter[k]) / semiMajor[k]) ** 2
308-
y2 = ((north - yCenter[k]) / semiMinor[k]) ** 2
309-
if x2 + y2 < 1:
310-
scarpVal = min(scarpVal, elevData[row, col] - maxDepth[k] * (1 - (x2 + y2)))
311-
if periData[row, col] > 0:
312-
scarpData[row, col] = scarpVal
313-
else:
314-
scarpData[row, col] = elevData[row, col]
320+
center_col, center_row = ~elevTransform * (xCenter[k], yCenter[k])
321+
center_row = int(round(center_row))
322+
center_col = int(round(center_col))
323+
324+
if 0 <= center_row < n and 0 <= center_col < m:
325+
slopeAzimuth = slopeDir[center_row, center_col]
326+
slopeAzimuth = (slopeAzimuth + 2 * np.pi) % (2 * np.pi)
327+
slopeMag = slopeMagnitude[center_row, center_col]
328+
329+
if not np.isnan(slopeAzimuth) and slopeMag > 0:
330+
normal_dx = np.cos(slopeAzimuth)
331+
normal_dy = np.sin(slopeAzimuth)
332+
slopeAngle = math.atan(slopeMag)
333+
dxOffset = -offset[k] * normal_dx * math.sin(slopeAngle)
334+
dyOffset = -offset[k] * normal_dy * math.sin(slopeAngle)
335+
dzOffset = -offset[k] * math.cos(slopeAngle)
336+
else:
337+
dxOffset = dyOffset = dzOffset = 0
338+
else:
339+
dxOffset = dyOffset = dzOffset = 0
340+
341+
x0 = xCenter[k] + dxOffset
342+
y0 = yCenter[k] + dyOffset
343+
z0 = dzOffset
344+
345+
dxPos = west - x0
346+
dyPos = north - y0
347+
348+
# Rotate the position by dip angle
349+
dxRot = dxPos * np.cos(dip[k]) + dyPos * np.sin(dip[k])
350+
dyRot = -dxPos * np.sin(dip[k]) + dyPos * np.cos(dip[k])
351+
352+
# Normalize to ellipsoid axes
353+
xNorm = dxRot / semiMajor[k]
354+
yNorm = dyRot / semiMinor[k]
355+
distance = xNorm**2 + yNorm**2
356+
357+
if distance <= 1:
358+
baseDepth = maxDepth[k] * (1 - distance)
359+
tiltEffect = math.tan(math.radians(tilt[k])) * (
360+
dxRot * np.cos(tiltDir[k]) + dyRot * np.sin(tiltDir[k])
361+
)
362+
totalDepth = baseDepth + tiltEffect + z0
363+
scarpVal = min(scarpVal, elevData[row, col] - totalDepth)
364+
365+
scarpData[row, col] = scarpVal if periData[row, col] > 0 else elevData[row, col]
315366

316367
return scarpData

avaframe/in2Trans/shpConversion.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ def SHP2Array(infile, defname=None):
8888
semiminor_value = None
8989
maxdepth_value = None
9090
semimajor_value = None
91+
tilt_value = None
92+
direc_value = None
93+
offset_value = None
9194

9295

9396
# get coordinate system
@@ -111,6 +114,10 @@ def SHP2Array(infile, defname=None):
111114
semiminorList = []
112115
semimajorList = []
113116
maxdepthList = []
117+
tiltList = []
118+
direcList = []
119+
offsetList = []
120+
114121
Length = np.empty((0))
115122
Start = np.empty((0))
116123
Coordx = np.empty((0))
@@ -163,6 +170,12 @@ def SHP2Array(infile, defname=None):
163170
maxdepth_value = value
164171
if name == "semimajor":
165172
semimajor_value = value
173+
if name == "tilt":
174+
tilt_value = value
175+
if name == "direc":
176+
direc_value = value
177+
if name == "offset":
178+
offset_value = value
166179
if name == "rho":
167180
rho = value
168181
if name == "sks":
@@ -194,6 +207,9 @@ def SHP2Array(infile, defname=None):
194207
semiminorList.append(semiminor_value)
195208
semimajorList.append(semimajor_value)
196209
maxdepthList.append(maxdepth_value)
210+
tiltList.append(tilt_value)
211+
direcList.append(direc_value)
212+
offsetList.append(offset_value)
197213

198214
Start = np.append(Start, start)
199215
length = len(pts)
@@ -224,6 +240,9 @@ def SHP2Array(infile, defname=None):
224240
SHPdata["maxdepth"] = maxdepthList
225241
SHPdata["semimajor"] = semimajorList
226242
SHPdata["semiminor"] = semiminorList
243+
SHPdata["tilt"] = tiltList
244+
SHPdata["direc"] = direcList
245+
SHPdata["offset"] = offsetList
227246

228247
sf.close()
229248

docs/moduleCom6RockAvalanche.rst

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,27 @@ Input
4646
* geometries: a shapefile containing point geometries. These points represent the centers of the ellipsoids or planes.
4747
The coordinates (x,y) of these points are used. If the plane method is used, the shape file must contain the
4848
Attributes "zseed", "dip" and "slopeangle" as float values. If the ellipsoid method is used, the shape file must
49-
contain the Attributes "maxdepth", "semimajor" and "semiminor". The file must be located in avalancheDir/Inputs/
50-
POINTS and file name must end with “_coordinates”. If you are using the QGis Connector, the naming and location of the
51-
file is not relevant.
49+
contain the attributes "maxdepth", "semimajor", "semiminor", "tilt", "direc", "dip", "offset" (see below).
50+
The file must be located in avalancheDir/Inputs/POINTS and file name must end with “_coordinates”.
51+
If you are using the QGis Connector, the naming and location of the file is not relevant.
52+
5253
* perimeter: A shapefile that specifies a boundary area. Must be located in avalancheDir/Inputs/POLYGONS and file name
5354
must end with “_perimeter”. If you are using the QGis Connector, the naming and location of the file is not relevant.
5455

56+
**Attribute meanings:**
57+
58+
* zseed: defines z coordinate of plane Center (m)
59+
* dip: direction in which the plane/slope is facing (degree)
60+
* slopeangle: steepness/angle of the slope (degree)
61+
62+
* maxdepth: maximum depth of the ellipsoid (m)
63+
* semimajor: length of the major axis (m)
64+
* semiminor: length of the minor axis (m)
65+
* tilt: steepness/angle of the slope (degree)
66+
* direc: direction in which the slope is facing (degree)
67+
* dip: direction in which the ellipsoid is facing (degree)
68+
* offset: offset, normal to the DEM slope (m)
69+
5570
Output
5671
~~~~~~
5772

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ name = "avaframe"
99
description = "The Open Avalanche Framework"
1010
readme = "README.md"
1111
dynamic = ["version"]
12-
license = { text = "EUPL" }
12+
license = "EUPL-1.2"
1313
authors = [
1414
{ name = "AvaFrame Contributors", email = "[email protected]" }
1515
]
1616
urls = { Homepage = "http://avaframe.org" }
1717
classifiers = [
1818
"Development Status :: 5 - Production/Stable",
1919
"Intended Audience :: Science/Research",
20-
"License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)",
2120
"Programming Language :: Python :: 3.9",
2221
"Programming Language :: Python :: 3.10",
2322
"Programming Language :: Python :: 3.11",
@@ -99,7 +98,8 @@ avaframe = { path = "./", editable = true }
9998
pixi-pycharm = "*"
10099
pytest = "*"
101100
pytest-cov = "*"
102-
pytest-mock ="*"
101+
pytest-mock = "*"
102+
black = "*"
103103

104104
#Feature doc
105105
[tool.pixi.feature.doc.dependencies]

0 commit comments

Comments
 (0)