-
Notifications
You must be signed in to change notification settings - Fork 1
/
calculate_gcs_functions.py
414 lines (346 loc) · 13.2 KB
/
calculate_gcs_functions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
import os
import logging
import arcpy
import file_functions
from file_functions import *
from create_station_lines import create_station_lines_function
import statistics
import pandas as pd
import numpy as np
from typing import List, Union
arcpy.env.overwriteOutput = True
@err_info
def clean_in_table(
table: pd.DataFrame,
w_field: str = 'W',
z_field: str = 'Z',
dist_field: str = 'dist_down',
) -> pd.DataFrame:
"""Renames DataFrame columns corresponding to W, Z, and dist_down, if they are not already columns"""
check_use(table)
df = pd.read_csv(table)
for old_name, replacement_name in [(w_field, 'W'), (z_field, 'Z'), (dist_field, 'dist_down')]:
if replacement_name not in df.columns.tolist():
if old_name in df.columns.tolist():
df.rename(columns={old_name: replacement_name})
logging.info('Renamed %s to %s in %s' %
(old_name, replacement_name, table))
else:
logging.exception('Cannot find column named %s or %s in %s' % (
old_name, replacement_name, table))
df.to_csv(table, index=False)
return df
def standardize(
table: pd.DataFrame,
fields: List[str]
) -> pd.DataFrame:
"""Makes standardized version of field in csv table by subtracting each value by mean and dividing by standard deviation.
Creates a new column Ws_Zs storing C(Ws,Zs) values"""
check_use(table)
df = pd.read_csv(table)
s_fields = []
for f in fields:
new_field = f + 's'
df[new_field] = (df[f] - np.mean(df[f])) * 1.0 / np.std(df[f])
s_fields.append(new_field)
df['%s_%s' % (s_fields[0], s_fields[1])
] = df[s_fields[0]] * df[s_fields[1]]
return df.to_csv(
table,
index=False,
)
def landforms(
table: pd.DataFrame,
zs_field: str = 'Zs',
ws_field: str = 'Ws',
na: int = -9999
) -> pd.DataFrame:
"""Classifies each row by corresponding landform type:
oversized, nozzle, constricted pool, wide bar, normal channel
Adds columns to input table"""
check_use(table)
df = pd.read_csv(table)
df['normal'] = [zs * ws if abs(zs) <= 0.5 or abs(ws) <=
0.5 else na for zs, ws in zip(df[zs_field], df[ws_field])]
df['wide_bar'] = [zs * ws if (zs > 0.5 and ws > 0.5) else na for zs, ws in
zip(df[zs_field], df[ws_field])]
df['const_pool'] = [zs * ws if (zs < -0.5 and ws < -0.5) else na for zs, ws in
zip(df[zs_field], df[ws_field])]
df['nozzle'] = [zs * ws if (zs > 0.5 and ws < -0.5) else na for zs, ws in
zip(df[zs_field], df[ws_field])]
df['oversized'] = [zs * ws if (zs < 0.5 and ws > 0.5) else na for zs, ws in
zip(df[zs_field], df[ws_field])]
df['code'] = [-2 if df['oversized'][i] != na
else -1 if df['const_pool'][i] != na
else 0 if df['normal'][i] != na
else 1 if df['wide_bar'][i] != na
else 2 if df['nozzle'][i] != na
else 0
# Was na, but since for whatever reason normal channel is not mutually exclusive, we are going to hard code this as 0
for i in range(len(df))
]
df["code"].fillna(0, inplace=True)
df.to_csv(table, index=False)
return df
def main_classify_landforms(
tables: List[str],
w_field: str,
z_field: str,
dist_field: str
) -> List[str]:
"""Classifies river segments as normal, wide bar, constricted pool, oversized, or nozzle
Args:
tables: a list of attribute table filenames for each set of wetted polygon rectangular XS's
w_field: name of the attribute table field corresponding to width
z_field: name of the attribute table field corresponding to detrended bed elevation
dist_field: name of the attribute table field corresponding to distance downstream
Returns:
For each input table:
a .csv containing dist_down, W, Z, Ws, Zs, Ws_Zs, and landform classification/code fields
adds these computed values to attribute tables of corresponding wetted polygon rectangular XS's
"""
logging.info('Classifying landforms...')
# TODO: verify this behavior
out_polys = []
fields = [w_field, z_field]
for i in range(len(tables)):
table = tables[i]
clean_in_table(table, w_field=w_field,
z_field=z_field, dist_field=dist_field)
standardize(table, fields=fields)
landforms(table)
logging.info('Finished.')
return out_polys
# Main function that conducts GCS analysis
############################################
@err_info
def extract_gcs(
detrended_dem: str,
zs: Union[str, List[Union[int, float]]],
xs_lengths: Union[str, List[Union[int, float]]],
spacing: Union[str, List[int]],
clip_poly: str = ''
) -> List[str]:
"""This function does a full GCS analysis using key stage heights / Zs (floats).
Results are saved to the gcs_ready_tables, as well as plotted. Results are
aligned to the existing csv to facilitate landform analysis detrend
wetted_folder is the folder containing small increment wetted polygons.
If key_zs parameter is an empty list, a range from 0 to max_stage (deafult is 20)
makes gcs csvs at 1ft increments.
param:wetted_folder (optional) allows for a specific folder containing wetted
polygons to be used instead of the assumed file structure.
"""
arcpy.env.overwriteOutput = True
del_files = [] # stored deletable files
out_csvs = [] # stores output gcs csv files
# Convert stage heights to list format
if type(zs) == str:
zs = string_to_list(zs, format='float')
if type(xs_lengths) == str:
xs_lengths = string_to_list(xs_lengths, format='float')
if type(spacing) == str:
try:
spacing = int(spacing)
except TypeError:
raise TypeError(
'Cross-section spacing parameter must be an integer!')
# set up directories
dem_dir = os.path.dirname(detrended_dem)
if len(dem_dir) == 0:
raise ValueError('Please select valid detrended DEM file')
lines_dir = dem_dir + '\\centerlines'
wetted_dir = dem_dir + '\\wetted_polygons'
temp_files = dem_dir + '\\temp_files'
out_dir = dem_dir + '\\gcs_tables' # Stores output GCS tables
if not os.path.exists(out_dir):
os.makedirs(out_dir)
og_dem = dem_dir + '\\las_dem.tif'
# Get units for string labeling
u, unit, spatial_ref = file_functions.get_label_units(detrended_dem)
for i, z in enumerate(zs):
z_str = float_keyz_format(z)
label = z_str + u
in_list = [
wetted_dir + '\\wetted_poly_%s.shp' % label,
temp_files + '\\%s_centerline_XS.shp' % label,
lines_dir + '\\%s_centerline.shp' % label,
]
xs_length = xs_lengths[i]
# Allows a new/updated clip file to clip all data inputs and outputs, and create new XS for the clipped centerlines
if clip_poly != '' and os.path.exists(clip_poly):
for j, file in enumerate(in_list):
no_clip_name = file.replace('.shp', '_delete.shp')
if os.path.exists(no_clip_name):
arcpy.Delete_management(no_clip_name)
try:
arcpy.Rename_management(file, no_clip_name)
del_files.append(no_clip_name)
except PermissionError:
raise PermissionError(
'Could not rename %s file likely because it does not exist or is open' % file)
if j != 1:
arcpy.Clip_analysis(
no_clip_name,
clip_poly,
out_feature_class=file,
)
create_station_lines_function(
in_list[2],
spacing,
xs_length,
)
# Clip cross-sections by wetted area and create width rectangles
clipped_xs_lines = temp_files + '\\clipped_XS_lines_%s.shp' % label
arcpy.Clip_analysis(
in_list[1],
in_list[0],
out_feature_class=clipped_xs_lines,
)
width_poly_loc = lines_dir + '\\width_rectangles_%s.shp' % label
arcpy.Buffer_analysis(
clipped_xs_lines,
width_poly_loc,
float(spacing / 2),
line_side='FULL',
line_end_type='FLAT',
)
# Extract rectangle width values and create a new attribute table Width field
arcpy.AddField_management(
width_poly_loc,
"Width",
field_type="FLOAT",
)
expression = ("(float(!Shape.area!)) / %d" % spacing)
arcpy.CalculateField_management(
width_poly_loc,
"Width",
expression,
"PYTHON3",
)
logging.info('Width polygons for %sft stage created...' % z)
arcpy.AddField_management(
width_poly_loc,
field_name="loc_id",
field_type="SHORT",
)
field_calc = "(int(!LOCATION!))"
arcpy.CalculateField_management(
width_poly_loc,
field="loc_id",
expression=field_calc,
expression_type="PYTHON3",
)
# Extract the mean detrended DEM raster value from below each width rectangle and join to the shapefile
zonal_table = arcpy.sa.ZonalStatisticsAsTable(
width_poly_loc,
"loc_id",
detrended_dem,
out_table=(temp_files + '\\stats_table_%s.dbf' % label),
statistics_type="ALL",
)
arcpy.JoinField_management(
width_poly_loc,
"loc_id",
join_table=zonal_table,
join_field="loc_id",
fields=["MEAN"],
)
# If las_dem.tif is unmoved and not renamed, we can extract the MEDIAN value and use it to flip tables if necessary
zonal_table = arcpy.sa.ZonalStatisticsAsTable(
width_poly_loc,
"loc_id",
og_dem,
out_table=(temp_files + '\\no_detrend_stats_table_%s.dbf' % label),
statistics_type="ALL",
)
no_sort = arcpy.JoinField_management(
width_poly_loc,
"loc_id",
join_table=zonal_table,
join_field="loc_id",
fields=["MIN"],
)
# Sort width polygon by location identifier
arcpy.Sort_management(
no_sort,
width_poly_loc.replace('.shp', '_s.shp'),
[["LOCATION", "Ascending"]],
)
arcpy.Delete_management(no_sort)
width_poly = arcpy.Rename_management(
width_poly_loc.replace('.shp', '_s.shp'),
width_poly_loc,
)
# Create flipped dist_down index if necessary
cursor = arcpy.SearchCursor(width_poly)
elevs = []
locations = []
for row in cursor:
elevs.append(row.getValue("MIN"))
locations.append(row.getValue("LOCATION"))
arcpy.AddField_management(
width_poly,
'dist_down',
"LONG",
)
max_loc = int(max(locations))
try:
if statistics.mean(elevs[10:20]) < statistics.mean(elevs[-20:-10]):
expression = ("%d - (int(!LOCATION!))" % max_loc)
arcpy.CalculateField_management(
width_poly,
'dist_down',
expression,
"PYTHON3",
)
else:
expression = "(int(!LOCATION!))"
arcpy.CalculateField_management(
width_poly,
'dist_down',
expression,
"PYTHON3",
)
except IndexError:
raise ValueError(
'Error: Cross-section series not longer than 20, cant establish which side is upstream.'
)
# Convert width polygon attribute table to a csv and classify landforms
csv_loc = out_dir + "\\%s_gcs_table.csv" % label
table_to_csv(
width_poly,
csv_filepath=csv_loc,
fld_to_remove_override=[],
)
df = pd.read_csv(csv_loc)
df.rename(
{
'LOCATION': 'location',
'Width': 'W',
'MEAN': 'Z',
'MIN': 'Z_no_detrend',
},
axis=1,
inplace=True,
)
df.sort_values(
by=['dist_down'],
inplace=True,
)
df = df[['location', 'dist_down', 'Z_no_detrend', 'W', 'Z']]
df.to_csv(csv_loc)
# calculate Ws, Zs, Ws_Zs, and landform codes
main_classify_landforms(
tables=[csv_loc],
w_field='W',
z_field='Z',
dist_field='dist_down',
)
gcs_df = pd.read_csv(csv_loc)
gcs_df.to_csv(csv_loc)
logging.info('GCS csv file made for stage %s...' % label)
out_csvs.append(csv_loc)
for file in del_files:
file_functions.delete_gis_files(file)
logging.info('GCS tables completed @ %s' % out_dir)
return out_csvs