Bereiten Sie sich durch Klonen des Übungs-Repository:
git clone https://github.com/Ecohydraulics/Exercise-geco.gitSacramento suckers in the South Yuba River (source: Sebastian Schwindt @hydroinformatics on YouTube).
Was ist Habitat Suitability?¶
Fisch und andere Wasserarten ruhen, orientieren und reproduzieren sich in einer fluvialen Umgebung, die ihren physischen Lebensraum darstellt. Während ihrer verschiedenen Lebensstufen weisen verschiedene Fische spezifische physikalische Lebensraumpräferenzen auf, die beispielsweise in Abhängigkeit von Wassertiefe, Strömungsgeschwindigkeit und Korngröße des Flussbettes definiert sind. Der sogenannte Habitat Suitability Index kann für hydraulische (Wassertiefe oder Strömungsgeschwindigkeit) und morphologische (z.B. Korngröße oder Abdeckung in Form von großem Holz) Parameter individuell berechnet werden, um die Qualität des physikalischen Lebensraums für einen Fisch und in einem bestimmten Lebensstadium zu beschreiben. Die folgende Abbildung zeigt beispielhafte Kurven für die fry, juvenile und erwachsene Lebensstadien der Regenbogenforelle in Abhängigkeit von der Wassertiefe. Die -Kurven sehen in jedem Fluss unterschiedlich aus und sollten von einem aquatischen Ökologen individuell aufgestellt werden.

Figure 1:Habitat Suitability Index Kurven für die fry, juvenile und erwachsene Lebensstadien der Regenbogenforelle in einem Cobble-Bett Fluss. Achten Sie darauf: HSI-Kurven sehen in jedem Fluss unterschiedlich aus und müssen von einem aquatischen Ökologen erstellt werden.
Das -Konzept enthält auch den sogenannten Deckungsraum in Form des cover Habitat Suitability Index . Der Lebensraum ist das Ergebnis lokaler Turbulenzen, die durch Rauhigkeitselemente wie Holz, Boulder oder Brückenpier verursacht werden. Bei dieser Übung werden wir uns jedoch nur mit hydraulischen Lebensraumeigenschaften befassen (nicht mit Lebensraum).
Erwachsene Forellen schwimmen in Deckung Lebensraum durch eine Brücke Pier im oberen Neckar River geschaffen.
Die Kombination mehrerer -Werte (z.B. wassertiefenbezogen , strömungsgeschwindigkeitsbezogen , Korngrößenbezogen und/oder Cover ) ergibt den *kombinierten Habitat Suitability Index. Es gibt verschiedene Berechnungsmethoden für die Kombination verschiedener -Werte in einen -Wert, wobei der geometrische Mittelwert und das Produkt die am weitesten verbreiteten deterministischen Kombinationsmethoden sind:
Sind daher aus einem zweidimensionalen (2d) hydrodynamischen Modell die pixelbasierten -Werte für Wassertiefe und Strömungsgeschwindigkeit bekannt, so kann für jeden Pixel der -Wert entweder als Produkt oder geometrisches Mittel des Einzelparameters -Rasters berechnet werden.
Dieses Konzept für die Lebensraumbewertung wurde von Bovee (1986) und Stalnaker et al. (1995)(direct download) eingeführt. Diese Autoren bauten jedoch ihre nutzbare (physikalische) Lebensraumbewertung auf Basis von eindimensionalen (1d) numerischen Modellen, die im letzten Jahrtausend häufig verwendet wurden. Heutzutage sind 2d numerische Modelle die modernsten, um geospatiell explizit auf Pixelbasis -Werten physikalischen Lebensraum zu bestimmen. Es gibt zwei verschiedene Möglichkeiten zur Berechnung des nutzbaren Lebensraums () basierend auf Pixel-basierten -Werten (und noch mehr Möglichkeiten finden Sie in der wissenschaftlichen Literatur).
Eine Alternative zur deterministischen Berechnung der und -Werte eines Pixels ist ein Fuzzy-Logik-Ansatz Noack et al., 2013. Im Fuzzy-Logik-Ansatz werden Pixel z.B. als low, medium oder high Lebensraumqualität in Abhängigkeit von der zugehörigen Wassertiefe oder Strömungsgeschwindigkeit mit kategorischen (low, medium, oder high), expert assessment-based Kurven klassifiziert. Der -Wert ergibt sich aus dem Schwerpunkt der überlagerten Mitgliedsfunktionen der betrachteten Parameter (z.B. Wassertiefe und Strömungsgeschwindigkeit).
Nachhaltiges Flussmanagement beinhaltet die Herausforderung, einen aquatischen Lebensraum für Zielfischarten in verschiedenen Lebensphasen zu gestalten. Das Konzept des nutzbaren physischen Lebensraums stellt ein leistungsfähiges Werkzeug dar, um die Bewertung der ökologischen Integrität von Flussmanagement- und Engineering-Maßnahmen zu nutzen. So können beispielsweise durch die Berechnung des nutzbaren Lebensraums vor und nach der Durchführung von Maßnahmen wertvolle Rückschlüsse auf die ökologische Integrität der Sanierungsbemühungen gezogen werden.
Diese Übung demonstriert die Verwendung von 2d hydrodynamischen Modellierungsergebnissen zur algorithmischen Bewertung des nutzbaren Lebensraums basierend auf der Berechnung von geospatially expliziten -Werten.
Verfügbare Daten und Codestruktur¶
Das folgende Flussdiagramm verdeutlicht den bereitgestellten Code und die Daten. Funktionen, Methoden und Dateien, die in dieser Übung erstellt werden, werden in fetter, italischer, YELLOW-ish Schriftart hervorgehoben.

Figure 2:Struktur der zur Verfügung gestellten Vorlagendatei Baum und ihre Beziehungen.
Die bereitgestellte QGIS-Projektdatei visualize_with_QGIS.qgz hilft, Eingaberasterdatensätze und -ergebnisse zu überprüfen.
Zweidimensionale (2d) hydrodynamische Modellierung (Folder: BASEMENT)¶
Diese Übung verwendet (hydraulische) Strömungsgeschwindigkeits- und Wassertiefenraster (GeoTIFFs) mit der Software ETH Zürich*s BASEMENT. Lesen Sie mehr über hydrodynamische Modellierung mit BASEMENT im Kapitel ABSCHNITT. Die hydraulischen Raster wurden mit den Beispieldaten des Flaz River in der Schweiz) des BASEMENT-Entwicklers erstellt (weitere Informationen finden Sie auf der Website).
Die Wassertiefe water_depth.tif und Fließgeschwindigkeit flow_velocity.tif rasters sind für diese Übung im Ordner /basement/ vorgesehen.
Habitat Suitability Index HSI Curves (folder: habitat)¶
Der /habitat/-Ordner im Übungsarchiv enthält -Kurven in Form eines xlsx-Arbeitsbuchs (trout.xlsx) und in Form einer JSON-Datei (trout.json). Beide Dateien enthalten die gleichen Daten für Regenbogenforelle eines hypothetischen Cobble-Bett-Flusses und diese Übung verwendet nur die JSON-Datei (das Arbeitsbuch dient nur der visuellen Überprüfung).
Code¶
Im Vortrag wurden ein paar gdal-basierte Funktionen zur Verarbeitung von Rastern und Formdateien vorgestellt. Diese Übung verwendet einige dieser Funktionen wieder, die im Geo-Verarbeitungs-Code-Repository speziell für dieses ebook zur Verfügung stehen. Das Repository enthält einen funktionalen Block aus flusstools.geotools (im geo_utilsordner), der es ermöglicht, das Verhalten von flusstools.
Auch wenn in dieser Übung bereits vorgesehen, stellen Sie sicher, dass das geo utils-Repository gut im Übungsverzeichnis implementiert ist (d.h. geo utils-Skripte werden in einem Ordnerbaum wie dieser gespeichert: Exercise-geco\geo_utils\). Der \geo_utils\-Ordner entspricht dem geo-utils\geo_utils\-Verzeichnis, wenn Sie das Repository klonieren (alternativ verwenden from flusstools import geotools as geo).
Stellen Sie sicher, dass in der \geo_utils\geoconfig.py-Datei die nan_value als 0.0 (nan_value = 0.0) definiert ist.
Der Code in dieser Übung verwendet eine config.py-Datei, in der alle notwendigen Bibliotheken und globalen Variablen zentral geladen werden.
# This is config.py
import os
import logging
import random
import shutil
import string
import json
import numpy as np
import pandas as pd
import geo_utils as geo # alternative: from flusstools import geotools as geo
cache_folder = os.path.abspath("") + "\\__cache__\\"
par_dict = {"velocity": "u",
"depth": "h",
"grain_size": "d"}
nan_value = 0.0An dieser Stelle wird im Kurs angenommen, dass die Studierenden mit Objektorientierung und insbesondere mit Schreibfunktionen vertraut sind. Daher werden bereits viele Grundfunktionen für diese Übung mit dem Skript fun.py (alphabetisch bestellte Liste) bereitgestellt:
cacheist ein Wrapper für Elternfunktionen, um zu durchsetzen, dass in einem temporären cache-Ordner, der nach dem Skript gelöscht wird, intermediäre Geodatensätze (z.B. das Zwischenprodukt einer Summe von Rastern) gespeichert werden.check_cacheprüft, ob der Cache-Ordner, der unterconfig.pydefiniert ist, bereits existiert. Die Funktion wird automatisch vomcacheWrapper aufgerufen.create_random_string(length)generiert einzigartige Dateinamen für temporäre (gespeicherte) Datensätze, wobeilengthein integer-Wert ist, der die Anzahl der Zeichen der zu erstellenden Zufallszeile bestimmt.interpolate_from_list(x_values, y_values, xi_values)linearly interpolates values from two sorted lists containing paired x and y values for a Liste of given values (returns anumpy.arrayof the same length asxi_values). If one of the values is beyond the value range ofx_values, the function appends thenan_valuedefined inconfig.pyto the results array.interpolate_y(x1, x2, y1, y2, xi)is called by theinterpolate_from_listfunction for paired lower and upperx1-y1andx2-y2floats of thex_valuesandy_valuesListes (returns a float number corresponding to the linearly interpolatedyivalue of thexi-yipair betweenx1-y1andx2-y2). Ifxiis not numeric, or if the interpolation results in aZeroDivisionError, the function returns thenan_valuedefined inconfig.py.log_actions(fun)wickelt eine Funktion (fun) ein, in der Aktionen in eine Logdatei geschrieben werden sollen. Die Logging wird mit derstart_logging-Funktion gestartet (siehe unten) und das Logging wird mitlogging.shutdown()angehalten.read_jsonöffnet eine JSON-Datei und gibt sie als Python-Objekt zurück. In dieser Übung wird diese Funktion verwendet, um die/habitat/trout.json-Datei zu öffnen. Die -Werte können dann aus dem JSON-Objekt beurteilt werden, beispielsweise:
trout = read_json("PATH/" + "trout.json")
print(trout["velocity"]["spawning"][0]["u"])
>>> 0.0198remove_directory(directory)entfernt eindirectory(string Argument). Seien Sie vorsichtig, diese Funktion aggressiv entfernt diedirectoryund all ihre Inhalte mit wenig Wahrscheinlichkeit der Datenwiederherstellung.start_logging()beginnt mit der Anmeldung zu einem Logfile (logfile.log) und der Python-Konsole auf derlogging.DEBUG-Ebene.
Die elterliche Raster-Klasse wird im Skript raster.py gespeichert, in dem magische Methoden, ein pseudo privat_make_raster und eine save-Methode in dieser Übung erstellt werden.
Die HSIRaster-Klasse im raster_hsi.py-Skript ist ein Kind der Raster-Klasse. In dieser Übung werden wir uns nur ansehen, wie diese Kinderklasse strukturiert ist und was sie produziert (d.h. keine Modifikationen notwendig sind).
Die beiden Skripte reate_hsi_rasters.py und calculate_habitat_area.py stehen im Mittelpunkt dieser Übung und nutzen die bereitgestellten Daten und Python-Skripte. In diesen beiden Vorlagenskripten existieren daher nur die grundlegenden Rahmenfunktionen und Importe.
HSI Raster erstellen und kombinieren¶
Beenden Sie die __init__ Methode der RasterKlasse (raster.py)¶
The raster.py script imports the functions and libraries loaded in the fun.py script, and therefore, also the config.py script. For this reason, the NumP and Pandas libraries are already available (as np and pd, respectively), and the geo_utils package is already imported as geo (import geo_utils as geo in config.py).
Die Raster-Klasse lädt jeden GeoTIFF-Dateinamen als Geo-Referenzed Array-Objekt, das mit mathematischen Operatoren verwendet werden kann. Zunächst ergänzen wir die __init__ Methode durch eine Raster.name (Auszug aus dem file_name-Argument) sowie Georeferenzen und Array-Datensätze:
# __init__(...) of Raster class in raster.py
self.name = file_name.split("/")[-1].split("\\")[-1].split(".")[0]Wenn die bereitgestellte file_name nicht existiert, erstellt die __init__-Methode einen neuen Raster mit der file_name (dieses Verhalten wird bereits in der if not os.path.exists(file_name)-Anweisung umgesetzt.
Anschließend laden Sie die osgeo.gdal.dataset, die np.array und die geo_transformation des Rasters ein. Zu diesem Zweck nutzen Sie das raster2array function aus diesem eBook, das auch im geo utils (geo)-Paket der Übung implementiert ist:
# __init__(...) of Raster class in raster.py
self.dataset, self.array, self.geo_transformation = geo.raster2array(file_name, band_number=band)Um die EPSG number (Authority code) eines Rasters zu identifizieren, holen Sie das räumliche Referenzsystem (SRS) des Rasters ab. Auch zu diesem Zweck haben wir bereits eine Funktion im Vortrag mit der get_srs von der theory section on reprojection entwickelt. Laden Sie die SRS und die EPSG-Nummer mit der get srs-Funktion mit den folgenden zwei Codezeilen in der __init__ Methode:
# __init__(...) of Raster class in raster.py
self.srs = geo.get_srs(self.dataset)
self.epsg = int(self.srs.GetAuthorityCode(None)Die __init__-Methode der Raster-Klasse ist komplett.
Vollständige magische Methoden der Raster Klasse (raster.py)¶
Um mathematische Operationen zwischen mehreren Instanzen der Raster-Klasse zu ermöglichen, implementieren Sie Überladen und magische Methoden, die der Klasse sagen, was zu tun ist, wenn zwei RasterInstanzen zum Beispiel hinzugefügt werden (+-Zeichen), multipliziert (*-Zeichen), oder subtrahiert (--Zeichen). So können z.B. die magischen Methoden __truediv__ (für die Nutzung des /-Operators), __mul__ (für die Nutzung des *-Operators) und __pow__ (für die Nutzung des **-Operators) die Nutzung von Raster-Instanzen wie folgt ermöglichen:
# example for Raster instances, when operators are defined through magic methods
# load GeoTIFF rasters from file directory
velocity = Raster("/usr/geodata/u.tif")
depth = Raster("/usr/geodata/h.tif")
# calculate the Froude number using operators defined with magic methods
Froude = velocity / (depth * 9.81) ** 0.5
# save the new raster
Froude.save("/usr/geodata/froude.tif")Die Raster-Klassenvorlage enthält bereits eine beispielhafte magische Methode, um Division zu ermöglichen (__truediv__):
# Raster class in raster.py
def __truediv__(self, constant_or_raster):
try:
self.array = np.divide(self.array, constant_or_raster.array)
except AttributeError:
self.array /= constant_or_raster
return self._make_raster("div")Hier ist, was die __truediv__ Methode tut:
Das Eingabeargument
constant_or_rasterkann eine weitereRasterInstanz sein, die einarrayAttribut oder eine numerische Konstante hat (z.B. 9.81).Die Methode versucht, das Array-Attribut von
constant_or_rasteranzurufen.Wenn
constant_or_rasterein Rasterobjekt ist, dann istcontant_or_raster.arrayerfolgreich. In diesem Fall wirdself.arraymit der elementaren Teilung des Arrays voncontant_or_raster.arrayüberschrieben. Die Element-weise Division baut auf NumPs integrierte Funktion np.divide, die eine rechnerisch effiziente Umschlingung von C/C++-Code ist (viel schneller als eine Python-Schleife über Array-Elemente).Wenn
constant_or_rasterein numerischer Wert ist, dann ruftcontant_or_raster.arrayeineAttributeErroran und die__truediv__-Methode fällt in dieexcept AttributeError-Anweisung, woself.arrayeinfach durchconstant_or_rastergeteilt wird.
Die Methode gibt das Ergebnis der Pseudo-Privatmethode
self._make_raster("div")([Recall PEP 8 Code Stil und Konventionen, die einer neuenRasterInstanz der eigentlichenRasterInstanz entspricht, die vonconstant_or_rastergeteilt wird. Die neueRasterInstanz ist eine temporäre GeoTIFF-Datei im cache-Ordner (Recall the cache function). So sieht die pseudo-private Methode_make_raster(self, file_marker)aus:
def make raster(selbst, file marker): f ending = " {0}{1} .tif".format(file marker, create random string(4) geo.create raster(cache folder + self.name + f ending, self.array, epsg=self.epsg, nan val=nan value, geo info=self.geo transformation) zurück Raster(cache folder + selbst.name + f ending)
Diese Funktion:
Verwenden Sie das string-Argument
file_marker, um es dem Dateinamen der Basis GeoTIFF zusammen mit einem zufälligen, vier Zeichen langen string hinzuzufügen (Recall the create_random_string).file_markerist einzigartig für jeden implementierten Betreiber. Für die Methode__truediv__verwenden Siefile_marker="div". So wird der temporäre GeoTIFF-Dateiname alscache_folder + self.name + f_ending(z.B."C:\Excercise-geco\__cache__\velocity__divhjev__.tif") definiert.Setzt die Erstellen eines Rasters (Array to Raster)-Funktion von
geo_utilsum den temporären GeoTIFF an den__cache__-Ordner mit dem räumlichen Referenzsystem des ursprünglichen Rasters zu schreiben.returns eine neueRasterInstanz der temporären GeoTIFF-Datei.
If you find the _make_raster method confusing...
Dann haben Sie einen Punkt. Der oben beschriebene Ansatz implementiert die _make_raster Methode, um die temporären GeoTIFFs später mit Konstanten (float) und Arrays wiederzuverwenden, aber es gibt eine elegantere Möglichkeit, eine neue RasterInstanz zurückzugeben. Die Rückkehr einer neuen Instanz der gleichen Klasse erfordert jedoch, dass das Eingabeargument ein Beispiel der Klasse selbst sein muss (d.h. Raster) und nicht eine numerische Variable. Die alternative Lösung für die Rücksendung einer RasterInstanz beginnt mit einer anderen Umsetzung der magischen Methode (z.B. __truediv__) und erfordert den Import von Python4-styleannotations. Daher muss die erste Zeile des Skripts (nur arbeitet mit Python 3.7 und höher) den folgenden Import enthalten:
from __future__ import annotationsDann neu schreiben Sie die __truediv__ Methode:
def __truediv__(self, other: Raster) -> Raster:
f_ending = "__div%s__.tif" % create_random_string(4)
return Raster(file_name=cache_folder + self.name + f_ending,
raster_array=np.divide(self.array, other.array),
epsg=self.epsg,
geo_info=self.geo_transformation)In diesem Fall ist die Methode _make_raster veraltet. Lesen Sie mehr über die Rückkehr von Instanzen derselben Klasse auf stack overflow.
Bei der Anwendung der _make_raster-Methode fügen Sie die folgenden magischen Methoden an die Raster-Klasse (Funktions-Platzhalter sind bereits in der raster.py-Vorlage vorhanden):
__add__(+Operator):
try:
self.array += constant_or_raster.array
except AttributeError:
self.array += constant_or_raster
return self._make_raster("add")__mul__(*Operator):
try:
self.array = np.multiply(self.array, constant_or_raster.array)
except AttributeError:
self.array *= constant_or_raster
return self._make_raster("mul")__pow__(**Operator):
try:
self.array = np.power(self.array, constant_or_raster.array)
except AttributeError:
self.array **= constant_or_raster
return self._make_raster("pow")__sub__(-Operator):
try:
self.array -= constant_or_raster.array
except AttributeError:
self.array -= constant_or_raster
return self._make_raster("sub")Der letzte Punkt, der in der Raster-Klasse fertig gestellt wird, ist die integrierte save-Methode, die ein file_name(string)-Argument erhält, das das Verzeichnis definiert und den Namen der Raster-Instanz als Speicher bezeichnet:
save_status = geo.create_raster(file_name, self.array, epsg=self.epsg, nan_val=0.0, geo_info=self.geo_transformation)
return save_statusWarum brauchen wir die Variable save_status? Zuerst heißt es, wenn die Speicherung des Rasters erfolgreich war (save_status=0), und zweitens könnten diese Informationen verwendet werden, um den Raster aus dem __cache__-Ordner zu löschen und den Speicher zu spülen (für die Beschleunigung des Codes kostenlos).
Schreiben Sie HSI und cHSI Raster Creation Script¶
Das bereitgestellte create_hsi_rasters.pyScript enthält bereits die erforderlichen Paketimporte, eine if __name__ == '__main__'Stand-alone-Anweisung sowie die Leeremain,get_hsi_curve, get_hsi_raster und combine_hsi_rastersFunktionen:
# create_hsi_rasters.py
from fun import *
from raster_hsi import HSIRaster, Raster
from time import perf_counter
def combine_hsi_rasters(raster_list, method="geometric_mean"):
"""...
"""
pass
def get_hsi_curve(json_file, life_stage, parameters):
"""...
"""
pass
def get_hsi_raster(tif_dir, hsi_curve):
"""...
"""
pass
def main():
pass
if __name__ == '__main__':
# define global variables for the main() function
parameters = ["velocity", "depth"]
life_stage = "juvenile"
fish_file = os.path.abspath("") + "\\habitat\\trout.json"
tifs = {"velocity": os.path.abspath("") + "\\basement\\flow_velocity.tif",
"depth": os.path.abspath("") + "\\basement\\water_depth.tif"}
hsi_output_dir = os.path.abspath("") + "\\habitat\\"
# run code and evaluate performance
t0 = perf_counter()
main()
t1 = perf_counter()
print("Time elapsed: " + str(t1 - t0)Die if __name__ == '__main__'-Anweisung enthält einen Zeitzähler (perf_counter), der anfordert, wie lange das Skript läuft (typisch zwischen 3 bis 6 Sekunden). Stellen Sie sicher, dass
Die
parameters-Liste enthält"velocity"und"depth"(gemäßpar_dictimconfig.pyScript),die Dateipfade korrekt definiert werden, und
Eine Lebensphase ist definiert (d.h. entweder
"fry","juvenile","adult", oder"spawning"gemäß /habitat/fish.xlsx Arbeitsmappe).
Die folgenden Absätze zeigen Schritt für Schritt, wie Sie die Kurven aus der JSON-Datei (get_hsi_curve) laden können, wenden Sie sich an die flow_velocity und water_depth rasters (get_hsi_raster) und kombinieren Sie die resultierenden rasters in rasters (combine_hsi_rasters).
The get_hsi_curve function will load the curve from the JSON file (/habitat/trout.json) in a dictionary for the two parameters "velocity" and "depth". Thus, the goal is to create a curve_data dictionary that contains one Pandas DataFrame object for all parameters (i.e., velocity and depth). For example, curve_data["velocity"]["u"] will be a Pandas Series of velocity entries (in m/s) that corresponds to curve_data["velocity"]["HSI"], which is a Pandas Series of values. Similarly, curve_data["depth"]["h"] is a Pandas Series of depth entries (in meters) that corresponds to curve_data["depth"]["HSI"], which is a Pandas Series of values (corresponds to the curves shown in the HSI graphs above). To extract the desired information from the JSON file, get_hsi_curve takes three arguments (json_file, life_stage, and parameters) in order to:
Erhalten Sie die in der JSON-Datei gespeicherten Informationen mit der
read_json-Funktion (see above).Instantiate a void
curve_dataWörterbuch that will contain the PandasDataFrames for"velocity"and"depth".Führen Sie eine Schleife über die (zwei) Parameter (
"velocity"und"depth") aus, in denen es:Creates a void
par_pairsListe for storing pairs of parameter (par) - values as nested lists.Iterates through the length of provided curve data, where valid data pairs (e.g.,
[u_value, HSI_value]) are appended to thepar_pairsListe. This iteration is what actually creates the nested Liste.Converts the final
par_pairslist to a PandasDataFramethat it adds to thecurve_dataWörterbuch.
returnthecurve_dataWörterbuch with its PandasDataFrames.
# create_hsi_rasters.py
def get_hsi_curve(json_file, life_stage, parameters):
# read the JSON file with fun.read_json
file_info = read_json(json_file)
# instantiate output dictionary
curve_data = {}
# iterate through parameter list (e.g., ["velocity", "depth"])
for par in parameters:
# create a void list to store pairs of parameter-HSI values as nested lists
par_pairs = []
# iterate through the length of parameter-HSI curves in the JSON file
for i in range(0, file_info[par][life_stage].__len__():
# if the parameter is not empty (i.e., __len__ > 0), append the parameter-HSI (e.g., [u_value, HSI_value]) pair as nested list
if str(file_info[par][life_stage][i]["HSI"]).__len__() > 0:
try:
# only append data pairs if both parameter and HSI are numeric (floats)
par_pairs.append([float(file_info[par][life_stage][i][par_dict[par]]),
float(file_info[par][life_stage][i]["HSI"])])
except ValueError:
logging.warning("Invalid HSI curve entry for {0} in parameter {1}.".format(life_stage, par)
# add the nested parameter pair list as pandas DataFrame to the curve_data dictionary
curve_data.update({par: pd.DataFrame(par_pairs, columns=[par_dict[par], "HSI"])})
return curve_dataIn der main-Funktion rufen Sie get_hsi_curves an, um die -Kurven als Wörterbuch zu erhalten. Darüber hinaus setzen Sie die cache und die log_actions-Wrapper (siehe Beschreibungen von provided functions) für die main-Funktion ein:
# create_hsi_rasters.py
...
@log_actions
@cache
def main():
# get HSI curves as pandas DataFrames nested in a dictionary
hsi_curve = get_hsi_curve(fish_file, life_stage=life_stage, parameters=parameters)
...With the provided HSIRaster (raster_hsi.py) class, the rasters can be conveniently created in the get_hsi_raster function. Before using the HSIRaster class, make sure to understand how it works. The HSIRaster class inherits from the Raster class and initiates its parent class in its __init__ method through Raster.__init__(self, file_name=file_name, band=band, raster_array=raster_array, geo_info=geo_info). Then, the class calls its make_hsi method, which takes an curve (nested Liste) of two equal Liste pairs (Liste of parameters and Liste of values) as argument. The make_hsi method:
Extracts parameter values (e.g., depth or velocity) from the first element of the nested
hsi_curvesListe, and values from the second element of the nestedhsi_curvesListe.Uses NumPs built-in
np.nditerfunction, which iterates through NumP arrays with high computational efficiency (read more aboutnditer).The
nditerloop passes thepar_valuesasx_valuesListe argument and thehsi_valuesasy_valuesListe arguments to theinterpolate_from_listfunction (recall the function descriptions above).The array values (i.e., flow velocity or water depth) correspond to the
xi_valuesListe argument of theinterpolate_from_listfunction.The
interpolate_from_listfunction then identifies for each element of thexi_valuesListe the closest elements ( values) in thex_valuesListe and the corresponding positions in they_valuesListe.Die
interpolate_from_list-Funktion übergibt die ermittelten Werte an dieinterpolate_y-Funktion, die dann den entsprechendenyi-Wert linear interpoliert (d.h. einen -Wert).Die Strömungsgeschwindigkeit bzw. die Wassertiefe in
self.arraysind also reihenweise (row-by-row) ersetzt durch -Werte.
returns aRasterInstanz unter Verwendung der pseudo-privaten_make_raster-Methode (recall its contents).
# raster_hsi.py
from raster import *
class HSIRaster(Raster):
def __init__(self, file_name, hsi_curve, band=1, raster_array=None, geo_info=False):
Raster.__init__(self, file_name=file_name, band=band, raster_array=raster_array, geo_info=geo_info)
self.make_hsi(hsi_curve)
def make_hsi(self, hsi_curve):
par_values = hsi_curve[0]
hsi_values = hsi_curve[1]
try:
with np.nditer(self.array, flags=["external_loop"], op_flags=["readwrite"]) as it:
for x in it:
x[...] = interpolate_from_list(par_values, hsi_values, x)
except AttributeError:
print("WARNING: np.array is one-dimensional.")
return self._make_raster("hsi")Ändern Sie die get_hsi_rasters-Funktion, um direkt ein HSIRasterObjekt zurückzugeben:
# create_hsi_rasters.py
...
def get_hsi_raster(tif_dir, hsi_curve):
return HSIRaster(tif_dir, hsi_curve)
...
The get_hsi_raster function requires two arguments, which it must receive from the main function. For this reason, iterate over the parameters Liste in the main function and extract the corresponding raster directories from the tifs Wörterbuch (recall the variable definition in the standalone statement). In addition, save the Raster objects returned by the get_hsi_raster function in another Wörterbuch (eco_rasters) to combine them in the next step into a raster.
# create_hsi_rasters.py
...
@log_actions
@cache
def main():
# get HSI curves as pandas DataFrames nested in a dictionary
hsi_curve = get_hsi_curve(fish_file, life_stage=life_stage, parameters=parameters)
# create HSI rasters for all parameters considered and store the Raster objects in a dictionary
eco_rasters = {}
for par in parameters:
hsi_par_curve = [list(hsi_curve[par][par_dict[par]]),
list(hsi_curve[par]["HSI"])]
eco_rasters.update({par: get_hsi_raster(tif_dir=tifs[par], hsi_curve=hsi_par_curve)})
eco_rasters[par].save(hsi_output_dir + "hsi_%s.tif" % par)
...Selbstverständlich kann man auch die Parameter Liste direkt in der get_hsi_raster-Funktion einschleifen.
Dies ist ein guter Moment, um zu testen, ob der Code funktioniert. Führen Sie create_hsi_rasters.py aus und überprüfen Sie, ob die beiden GeoTIFF-Dateien (/habitat/hsi velocity.tif und /habitat/hsi tiefe.tif) korrekt erstellt werden. QGIS visualisiert die GeoTIFF-Produkte und die aktivierte Identify Features-Taste in QGIS ermöglicht es, zu überprüfen, ob die linear interpolierten -Werte mit den -Kurven im bereitgestellten Arbeitsbuch übereinstimmen (/habitat/trout.xlsx). So laden Sie beide GeoTIFF Paare in QGIS: /habitat/hsi velocity.tif + /basement/flow velocity.tif und /habitat/hsi tiefe.tif + /basement/water tiefe.tif.
Als nächstes kommen wir zu dem Grund, warum wir magische Methoden für die Raster-Klasse definieren mussten: Kombinieren Sie die rasters mit beiden oben vorgestellten Kombinationsformeln (Recall the product and geometric meanFormel), wobei "geometric_mean"Standard verwendet werden sollte. Die combine_hsi_rasters-Funktion akzeptiert zwei Argumente (eine Liste von Raster Objekten entsprechend rasters und die method, um als string zu verwenden).
If the method corresponds to the default value "geometric_mean", then the power to be applied to the product of the Raster Liste is calculated from the nth root, where n corresponds to the number of Raster objects in the raster_list. Otherwise (e.g., method="product"), the power is exactly 1.0.
The combine_hsi_rasters function initially creates an empty Raster in the cache_folder, with each cell having the value 1.0 (filled through np.ones). In a loop over the Raster elements of the raster_list, the function multiplies each raster with the raster.
Schließlich gibt die Funktion das Produkt aller rasters an die Macht des zuvor ermittelten power-Wertes zurück.
# create_hsi_rasters.py
def combine_hsi_rasters(raster_list, method="geometric_mean"):
if method is "geometric_mean":
power = 1.0 / float(raster_list.__len__()
else:
# supposedly method is "product"
power = 1.0
chsi_raster = Raster(cache_folder + "chsi_start.tif",
raster_array=np.ones(raster_list[0].array.shape),
epsg=raster_list[0].epsg,
geo_info=raster_list[0].geo_transformation)
for ras in raster_list:
chsi_raster = chsi_raster * ras
return chsi_raster ** powerUm das create_hsi_rasters.py-Skript zu beenden, setzen Sie den Anruf an die combine_hsi_rasters-Funktion in der main-Funktion um und speichern Sie das Ergebnis als GeoTIFF raster im /habitat/Ordner:
# create_hsi_rasters.py
...
@log_actions
@cache
def main():
...
for par in parameters:
hsi_par_curve = [list(hsi_curve[par][par_dict[par]]),
list(hsi_curve[par]["HSI"])]
eco_rasters.update({par: get_hsi_raster(tif_dir=tifs[par], hsi_curve=hsi_par_curve)})
eco_rasters[par].save(hsi_output_dir + "hsi_%s.tif" % par)
# get and save chsi raster
chsi_raster = combine_hsi_rasters(raster_list=list(eco_rasters.values(),
method="geometric_mean")
chsi_raster.save(hsi_output_dir + "chsi.tif")
...Führen Sie den HSI-Code aus¶
Ein erfolgreicher Lauf des Skripts create_hsi_rasters.py soll so aussehen (in PyCharm):

Figure 3:Eine Windows Python-Konsole mit den oben erstellten Skripten.
Plotted in QGIS, the GeoTIFF raster should look like this:

Figure 4:Der in QGIS aufgetragene cHSI-Raster, bei dem die schlechte physikalische Lebensraumqualität (cHSI in der Nähe von 0,0) in roter und hoher physikalischer Lebensraumqualität (cHSI in der Nähe von 1,0) in grün eingefärbt ist.
Ergebnisse¶
Die Präsentation des raster zeigt, dass bevorzugte Lebensräume für juvenile Forellen nur in der Nähe der Banken existieren. Auch sind numerische Artefakte des dreieckigen Netzes von ABSCHNITT sichtbar. Daher stellt sich die Frage, ob die berechneten Strömungsgeschwindigkeiten und Wassertiefen, und damit auch die -Werte, nahe den Banken, als repräsentativ angesehen werden können.
Berechnen Usable Habitat Area UHA¶
Schreiben Sie den Code¶
Die rasters ermöglichen die Berechnung der verfügbaren nutzbaren Lebensraumfläche. Der vorherige Abschnitt umfasste Beispiele mit der Fischart trout und der juvenile-Lebensstufe, für die wir hier den nutzbaren Lebensraum (in m) unter Verwendung eines -Schwellwerts (anstelle des Pixelflächengewichtungsansatzes) bestimmen werden. So folgen wir dem threshold formula described above, mit einem Schwellenwert von . Jedes Pixel, das einen -Wert von 0,4 oder mehr als nutzbare Lebensraumfläche hat.
Aus technischer Sicht geht es in diesem Teil der Übung darum, einen Raster in eine Polygon-Formdatei umzuwandeln sowie auf die Beitragstabelle der Formdatei zuzugreifen und zu modifizieren.
Ähnlich wie bei der Erstellung des raster gibt es ein Vorlagenskript für diesen Teil der Übung, genannt calculate_habitat_area.py, das Paket- und Modulimporte enthält, eine if __name__ == '__main__'Stand-alone-Anweisung sowie die Leere main und calculate_habitat_area Funktionen. (uha-template)=
Das Template-Skript sieht so aus:
# this is calculate_habitat_area.py (template)
from fun import *
from raster import Raster
def calculate_habitat_area(layer, epsg):
pass
def main():
pass
if __name__ == '__main__':
chsi_raster_name = os.path.abspath("") + "\\habitat\\chsi.tif"
chsi_threshold = 0.4
main()In der if __name__ == '__main__'-Anweisung stellen Sie sicher, dass die globale Variable chsi_raster_name dem Verzeichnis des raster entspricht, das im vorherigen Abschnitt erstellt wurde. Die andere globale Variable (chsi_threshold) entspricht dem -Wert von 0,4, den wir mit der threshold formula verwenden werden.
In der Funktion main starten Sie mit dem Laden des raster (chsi_raster) als Raster object. Dann nutzen Sie das NumP Array des raster und vergleichen Sie es mit chsi_threshold unterNumPs integriertem greater equalfunktion. np.greater_equal nimmt ein Array als erstes Argument und ein zweites Argument an, das die Bedingung ist, die eine numerische Variable oder ein anderes NumPArray sein kann. Dann prüft np.greater_equal, ob die Elemente des ersten Arrays größer oder gleich dem zweiten Argument sind. Für den Fall, dass das zweite Argument ein Array ist, ist dies ein elementarer -Vergleich. Das Ergebnis von np.greater_equal ist ein Boolen-Array (True, bei dem die mehr oder minderwertige Bedingung erfüllt ist und False andernfalls). Um jedoch aus dem Ergebnis von np.greater_equal ein osgeo.gdal.Dataset-Objekt zu erstellen, benötigen wir ein numerisches Array. Aus diesem Grund vervielfältigen Sie das Ergebnis von np.greater_equal by 1.0 und geben Sie es als neue NumP Array of Zeros (False) und eines (True) an eine Variable namens habitat_pixels an (siehe Codeblock unten).
With the habitat_pixels array and the georeference of chsi_raster, create a new integer GeoTIFF raster with the create_raster function (also available in flusstools.geotools); here, use geo.create_raster. In the following code block the new raster is saved in the /habitat/ folder of the exercise as habitat-pixels.tif.
# calculate_habitat_area.py
...
def main():
# open the chsi raster
chsi_raster = Raster(chsi_ras_name)
# extract pixels where the physical habitat quality is higher than the user threshold value
habitat_pixels = np.greater_equal(chsi_raster.array, chsi_threshold_value) * 1
# write the habitat pixels to a binary array (0 -> no habitat, 1 -> usable habitat)
geo.create_raster(os.path.abspath("") + "\\habitat\\habitat-pixels.tif",
raster_array=habitat_pixels,
epsg=chsi_raster.epsg,
geo_info=chsi_raster.geo_transformation)
...Im nächsten Schritt wandeln Sie den Habitat-Pixel-Raster in eine Polygon-Formdatei um und speichern Sie ihn im /habitat/-Ordner als habitat-area.shp. Die Umwandlung eines Rasters in eine Polygon-Formdatei erfordert, dass der Raster nur integer-Werte enthält, was im Habitat-Pixel-Raster der Fall ist (nur Nullen und eins - Rückruf Raster nach Polygon). Verwenden Sie die raster2polygon-Funktion im Ordner geo utils (Paket), um die neue Polygon-Formdatei zu erstellen, habitat-pixels.tif als raster_file_name zu konvertieren, und /habitat/habitat-area.shp als Ausgabedateiname. Die geo.raster2polygon-Funktion gibt ein osgeo.ogr.DataSource-Objekt zurück und wir können ihre Schicht einschließlich der Informationen des EPSG-Behördecodes (vonchsi_raster) direkt an die nicht-yet-writtencalculate_habitat_area()-Funktion weitergeben:
# calculate_habitat_area.py
...
def main():
... (create habitat pixels raster)
# convert the raster with usable pixels to polygon (must be an integer raster!)
tar_shp_file_name = os.path.abspath("") + "\\habitat\\habitat-area.shp"
habitat_polygons = geo.raster2polygon(os.path.abspath("") + "\\habitat\\habitat-pixels.tif",
tar_shp_file_name)
# calculate the habitat area (will be written to the attribute table)
calculate_habitat_area(habitat_polygons.GetLayer(), chsi_raster.epsg)
...For the calculate_habitat_area() function to produce what its name promises, we need to populate this function as well. For this purpose, use the epsg integer argument to identify the unit system of the shapefile.
# calculate_habitat_area.py
...
def calculate_habitat_area(layer, epsg):
# retrieve units
srs = geo.osr.SpatialReference()
srs.ImportFromEPSG(epsg)
area_unit = "square %s" % str(srs.GetLinearUnitsName()
...In der Praxis werden viele Fehler durch die falsche Verwendung von Flächeneinheiten gemacht, die oft nicht offensichtlich ist, weil die Größe der geospatialen Daten (mehrere Gigabyte). Es gibt viele Einheiten von Länge und Fläche (Meter, Füße, acre, ha, km) und ein Unterschied einer Größenordnung wird manchmal nur bemerkt, wenn ein kritischer Reviewer oder ein lokaler Experte misstrauisch wird. In der hier gezeigten Anwendung verwenden wir die Informationen der Längeneinheiten nur, um den gesamten Bereich mit einem korrekten Bezug auf die Flächeneinheiten (m) auf der Konsole auszugeben, aber in der Praxis können diese Informationen eine Karriere speichern.
To determine the habitat area, the area of each polygon must be calculated. For this purpose, add a new field to the layer in the Attribute Table, name it "area", and assign a geo.ogr.OFTReal (numeric) data type (recall how to create a field an data types).
Then, create a void Liste called poly_size, in which we will write the area of all polygons that have a field value of 1. To access the individual polygons (features) of the layer, iterate through all features using a for loop, which:
Extrahiert das Polygon jedes
featureunterpolygon = feature.GetGeometryRef()Appends the polygon’s area size to the
poly_sizeListe if the field"value"of thepolygon(at position 0:feature.GetField(0)) is 1 (True).Schreibt die Flächengröße des Polygons an die Beitragstabelle mit
feature.SetField("area", polygon.GetArea().Speichert die Änderungen (berechnete Fläche) an die formfile
layermitlayer.SetFeature(feature).
Das Schleifen durch eine Attributtabelle ist in Python rechnerisch teuer. Wenn eine Formdatei viele Elemente hat (Punkte, Linien, Polygone), kann diese Schleife für Stunden, Tage oder sogar Wochen dauern. Daher kann es nützlich sein, eine Formdatei in einen Raster umzuwandeln und Berechnungen mit NumPs rechnerisch effizienten Einbaufunktionen (C/C++ Wrapper) durchzuführen, die oft schneller sind. Ein besonderes Problem ist die Verarbeitung großer Lidardatensätze (mehrere Millionen Punkte), wo es notwendig sein kann, andere Software zu verwenden (mehr unter earthdatascience
The last information needed after the for loop is the total area of the "value"=1 polygons, which we get by writing the sum of the poly_size Liste to the console. Therefore, the second and last part of the calculate_habitat_area function looks like this:
# calculate_habitat_area.py
...
def calculate_habitat_area(layer, epsg):
... (extract unit system information)
# add area field
layer.CreateField(geo.ogr.FieldDefn("area", geo.ogr.OFTReal)
# create list to store polygon sizes
poly_size = []
# iterate through geometries (polygon features) of the layer
for feature in layer:
# retrieve polygon geometry
polygon = feature.GetGeometryRef()
# add polygon size if field "value" is one (determined by chsi_treshold)
if int(feature.GetField(0):
poly_size.append(polygon.GetArea()
# write area to area field
feature.SetField("area", polygon.GetArea()
# add the feature modifications to the layer
layer.SetFeature(feature)
# calculate and print habitat area
print("The total habitat area is {0} {1}.".format(str(sum(poly_size), area_unit)
...Zur Berechnung anderer Geometrieattribute als der Polygonbereich (z.B. Umhüllungserstreckungen, Ableitung eines konvexen Rumpfes, oder die Länge der Linien), beziehen sich auf den Geometrische Attribute berechnen-Bereich und verwenden diese Funktionen anstelle von polygon.GetArea().
Führen Sie den nutzbaren Habitat-Bereich Berechnungscode¶
Ein erfolgreicher Lauf des Skripts calculate_habitat_area.py soll so aussehen (in PyCharm):

Figure 5:Erfolgreicher Lauf des Skripts calculate habitat area.py.
Die in QGIS unterteilte habitat-area Formdatei sieht so aus (verwenden Sie Kategorisierte Symbologie):

Figure 6:Die in QGIS mit Kategorisierter Symbologie aufgetragene Lebensraumformdatei, in der der nutzbare Lebensraumbereich ( 0.4) durch die geschlüpften violetten Patches und deren gestrichelte Umrisse abgegrenzt wird.
Ergebnisse¶
Die des analysierten Flussabschnitts stellt einen sehr geringen Anteil der gesamten benetzten Fläche dar, die als ökologisch schlechten Zustand des Flusses interpretiert werden kann. Ein Blick auf eine Karte und die Simulationsdateien des Beispiels Flaz von BASEMENT deutet jedoch darauf hin, dass bei einer Entladung von 50 m/s eine Flutsituation angenommen werden kann. Wie bei Überschwemmungen gibt es in der Regel höhere Strömungsgeschwindigkeiten, die von jugendlichen Fischen abweichen, ist die kleine nutzbare Lebensraumfläche schließlich nicht überraschend.
Denken Sie daran, dass die hier vorgestellte Lebensraumbewertung davon ausgeht, dass Fische Regionen mit hohen -Werten bevorzugen und dass Flüsse mit einem hohen Anteil an Gebieten mit hohen -Werten ökologisch besonders wertvoll sind. Dieser Ansatz stellt eine Bewertung des physischen Lebensraumzustands mit begrenzten Informationen über den funktionalen Lebensraumzustand dar.
HOMEWORK 1: Schreiben Sie die magischen Methoden der RasterKlasse mit def __METHOD__(self, other: Raster) -> Raster: anstelle von def __METHOD__(self, constant_or_raster): und der _make_raster-Methode.
HOMEWORK 2: Abandon (löschen) den Ordner geo utils und ersetzt den import geo_utils as geo durch import flusstools.geotools as geo
- Noack, M., Schneider, M., & Wieprecht, S. (2013). Ecohydraulics: an integrated approach (I. Maddock, A. Harby, P. Kemp, & P. Wood, Eds.; pp. 75–91). Wiley-Blackwell. 10.1002/9781118526576
- Bovee, K. D. (1986). Development and evaluation of Habitat Suitability Criteria for use in the instream flow incremental methodology (Techreport No. 21). National Ecology Center, U.S. Fish. https://pubs.er.usgs.gov/publication/70121265
- Stalnaker, C., Lamb, B. L., Henriksen, J., Bovee, K., & Bartholow, J. (1995). The Instream Flow Incremental Methodology - A Primer for IFIM. National Biological Service, U.S. Department of the Interior, Opler, Paul A. www.dtic.mil/cgi-bin/GetTRDoc?AD=ADA322762
- Yao, W., Bui, M. D., & Rutschmann, P. (2018). Development of eco-hydraulic model for assessing fish habitat and population status in freshwater ecosystems. Ecohydrology, 11(5), 1–17. 10.1002/eco.1961
- Tuhtan, J. A., Noack, M., & Wieprecht, S. (2012). Estimating stranding risk due to hydropeaking for juvenile European grayling considering river morphology. KSCE Journal of Civil Engineering, 16(2), 197–206. 10.1007/s12205-012-0002-5