From cc0ca45d8338248535c8d2c1004ee75df428ad35 Mon Sep 17 00:00:00 2001 From: gbene Date: Mon, 22 Apr 2024 20:40:19 +0200 Subject: [PATCH] Better comments --- development/Basics/export_data.py | 26 ++++++ development/Basics/import_and_plot.py | 29 ++++++ docs/API/Entities/Nodes.rst | 10 ++ fracability/AbstractClasses.py | 107 ++++++++++++++++------ fracability/Entities.py | 127 +++++++++++++++----------- fracability/tests/test_entities.py | 4 +- fracability/tests/test_geometry.py | 2 +- fracability/tests/test_plotters.py | 2 +- fracability/tests/test_topology.py | 4 +- 9 files changed, 223 insertions(+), 88 deletions(-) create mode 100644 development/Basics/export_data.py create mode 100644 development/Basics/import_and_plot.py create mode 100644 docs/API/Entities/Nodes.rst diff --git a/development/Basics/export_data.py b/development/Basics/export_data.py new file mode 100644 index 0000000..1323715 --- /dev/null +++ b/development/Basics/export_data.py @@ -0,0 +1,26 @@ +""" +This example introduces the different ways that a FracAbility entity can be exported. +""" + +import os +import sys + +cwd = os.path.dirname(os.getcwd()) +sys.path.append(cwd) + +from fracability import DATADIR # import the path of the sample data +from fracability import Entities # import the Entities class + + +# Import a fracture entity set +fracture_set1 = Entities.Fractures(shp=f'{DATADIR}/cava_pontrelli/Set_a.shp', set_n=1) + + +# Export such set as a shapefile + +fracture_set1.save_shp(path='Basics') + +# Export such set as a csv + +fracture_set1.save_csv(path='Basics', sep=';') + diff --git a/development/Basics/import_and_plot.py b/development/Basics/import_and_plot.py new file mode 100644 index 0000000..a266554 --- /dev/null +++ b/development/Basics/import_and_plot.py @@ -0,0 +1,29 @@ +""" +This example introduces the different ways that a fracture network can be imported. We then visualize such +network using vtk and matplotlib +""" + +import os +import sys + +cwd = os.path.dirname(os.getcwd()) +sys.path.append(cwd) + +from fracability import DATADIR # import the path of the sample data +from fracability import Entities # import the Entities class + + +# Import a fracture entity set +fracture_set1 = Entities.Fractures(shp=f'{DATADIR}/cava_pontrelli/Set_a.shp', set_n=1) + +# Plot such fracture entity using vtk + +fracture_set1.vtk_plot() + +# Plot such fracture entity using matplotlib + +fracture_set1.mat_plot() + + + + diff --git a/docs/API/Entities/Nodes.rst b/docs/API/Entities/Nodes.rst new file mode 100644 index 0000000..71e7eee --- /dev/null +++ b/docs/API/Entities/Nodes.rst @@ -0,0 +1,10 @@ +.. image:: ../../images/logo.png + +------------------------------------- + +Nodes +------------------ +.. autoclass:: fracability.Entities.Nodes + :members: + :inherited-members: + :undoc-members: \ No newline at end of file diff --git a/fracability/AbstractClasses.py b/fracability/AbstractClasses.py index fb67742..7d25c76 100644 --- a/fracability/AbstractClasses.py +++ b/fracability/AbstractClasses.py @@ -1,3 +1,4 @@ +import os.path from abc import ABC, abstractmethod, abstractproperty from geopandas import GeoDataFrame @@ -9,6 +10,7 @@ from copy import deepcopy from shapely import remove_repeated_points + class BaseEntity(ABC): """ Abstract class for Fracture network entities: @@ -24,21 +26,38 @@ def __init__(self, gdf: GeoDataFrame = None, csv: str = None, shp: str = None): Init the entity. If a geopandas dataframe is specified then it is set as the source entity df. - :param gdf: Geopandas dataframe - :param csv: Path of a csv + Parameters + ----------- + gdf: GeoDataFrame + Use as input a geopandas dataframe + csv: str + Use as input a csv indicated by the path + shp: str + Use as input a shapefile indicated by the path + + Notes + -------- + The csv needs to have a "geometry" column. If missing the import will fail. """ + + self._df: GeoDataFrame = GeoDataFrame() if gdf is not None: self.entity_df = gdf elif csv is not None: - self.entity_df = read_file(csv, GEOM_POSSIBLE_NAMES="geometry", KEEP_GEOM_COLUMNS="NO") + gdf = read_file(csv, GEOM_POSSIBLE_NAMES="geometry", KEEP_GEOM_COLUMNS="NO") + if 'geometry' not in self.entity_df.columns: + exit('Missing geometry column, terminating') + else: + self.entity_df = gdf elif shp is not None: self.entity_df = read_file(shp) - - @property - def name(self): + def name(self) -> str: + """ + Property used to return the name of the current class as a string. + """ return self.__class__.__name__ @property @@ -104,28 +123,19 @@ def network_object(self) -> Graph: def mat_plot(self): """ Plot entity using matplotlib backend - :return: """ @abstractmethod def vtk_plot(self): """ Plot entity using vtk backend - :return: - """ - - @property - def name(self) -> str: """ - Property used to return the name of the class (i.e. Fractures) - :return: Name of the class as string - """ - return self.__class__.__name__ @property def crs(self) -> str: """ Property used to return the crs of the entity + :return: Name of the coordinate system as a string """ return self.entity_df.crs @@ -134,13 +144,16 @@ def crs(self) -> str: def crs(self, crs): """ Property used to return the crs of the entity + :return: Name of the coordinate system as a string """ self.entity_df.crs = crs + @property def centroid(self) -> np.ndarray: """ Property used to return the centroid of the entity. Dissolve is used to aggregate each shape in a single entity. + :return: 1D numpy array of the centroid """ trans_center = np.array(self.entity_df.dissolve().centroid[0].coords).flatten() @@ -150,6 +163,7 @@ def centroid(self) -> np.ndarray: def get_copy(self): """ Property used to return a deep copy of the entity + :return: """ return deepcopy(self) @@ -209,28 +223,69 @@ def center_object(self, trans_center: np.array = None, return_center: bool = Fal def save_csv(self, path: str, sep: str = ',', index: bool = False): """ Save the entity df as csv - :param index: - :type sep: object - :param path: - :return: + + Parameters + ------------- + index: Bool + Indicate whether to include or not the index column + sep: String + Indicate the separator string used to save the csv + path: String + Indicate the path in where to save the csv. **DO NOT** include the extension (.csv). + + Notes + --------- + + The csv will be saved in the output/csv directory in the indicated path in the working directory. If this path does not exist, then it will be created. """ - self.entity_df.to_csv(f'{self.name}_{path}', sep=sep, index=index) + if not self.entity_df.empty: + cwd = os.getcwd() + output_path = os.path.join(cwd, path, 'output/csv') + + if not os.path.isdir(output_path): + os.makedirs(output_path) + + final_path = os.path.join(output_path, f'{self.name}.csv') + self.entity_df.to_csv(final_path, sep=sep, index=index) + else: + print('Cannot save an empty entity') def save_shp(self, path: str): """ - Save the entity df as shp - :param path: - :return: + Save the entity df as shapefile + + Parameters + ------------- + path: String. + Indicate the path in where to save the csv. **DO NOT** include the extension (.csv). + + Notes + --------- + + The shapefile will be saved in output/shp directory in the indicated path in the working directory. If this path does not exist, then it will be created. """ if not self.entity_df.empty: - self.entity_df.to_file(path, crs=self.crs) + cwd = os.getcwd() + output_path = os.path.join(cwd, path, 'output', 'shp') + + if not os.path.isdir(output_path): + os.makedirs(output_path) + + final_path = os.path.join(output_path, f'{self.name}.shp') + self.entity_df.to_file(final_path, crs=self.crs) + else: + print('Cannot save an empty entity') def remove_double_points(self): + """ + Utility used to clean geometries with double points + """ for line, geom in enumerate(self.entity_df.geometry): - self.entity_df.loc[line, 'geometry'] = remove_repeated_points(geom, tolerance=0.000001) + + class BaseOperator(ABC): """ Abstract class for Operators such as: diff --git a/fracability/Entities.py b/fracability/Entities.py index e72e469..dc50d34 100644 --- a/fracability/Entities.py +++ b/fracability/Entities.py @@ -25,18 +25,24 @@ class Nodes(BaseEntity): """ Node base entity, represents all the nodes in the network. + + Parameters + ----------- + gdf: GeoDataFrame + Use as input a geopandas dataframe + csv: str + Use as input a csv indicated by the path + shp: str + Use as input a shapefile indicated by the path + node_type: int + Node type. I:1, Y:3, X:4, U:5 + + Notes + -------- + The csv needs to have a "geometry" column. If missing the import will fail. """ def __init__(self, gdf: GeoDataFrame = None, csv: str = None, shp: str = None, node_type: int = -9999): - """ - Init for node entity. Different inputs can be used. Geopandas dataframe, csv or shapefile. The csv needs to be - structured in such a way to be compatible with the Nodes entity. - - :param gdf: Geopandas dataframe - :param csv: Path of a csv - :param shp: Path of the shapefile - :param node_type: Node type (I,Y,X,U) - """ self.node_type = node_type if gdf is not None: @@ -53,7 +59,7 @@ def entity_df(self) -> GeoDataFrame: @entity_df.setter def entity_df(self, gdf: GeoDataFrame): """ - Each entity process the input dataframe in different ways + Each entity process the input dataframe in different ways. Nodes modify the entity_df only to add the type column (if not already present) """ @@ -75,7 +81,6 @@ def vtk_object(self) -> PolyData: @vtk_object.setter def vtk_object(self, obj: DataSet): - for index, point in enumerate(obj.points): self.entity_df.loc[self.entity_df['id'] == index, 'geometry'] = Point(point) @@ -85,7 +90,11 @@ def network_object(self) -> Graph: @property def node_count(self) -> tuple[float, float, float, float, float]: + """ + Calculate the node proportions and precise connectivity value following Manzocchi 2002 + :return: A tuple of values PI, PY, PX, PU, precise_n + """ nodes = self.vtk_object['n_type'] unique, count = np.unique(nodes, return_counts=True) count_dict = dict(zip(unique, count)) @@ -144,6 +153,7 @@ def node_count(self) -> tuple[float, float, float, float, float]: @property def n_censored(self) -> int: """ Return the number of censored nodes""" + nodes = self.vtk_object['n_type'] unique, count = np.unique(nodes, return_counts=True) count_dict = dict(zip(unique, count)) @@ -155,7 +165,7 @@ def n_censored(self) -> int: @property def n_complete(self) -> int: - """ Return the number of I nodes (complete fractures)""" + """ Return the number of I nodes""" nodes = self.vtk_object['n_type'] unique, count = np.unique(nodes, return_counts=True) @@ -183,24 +193,31 @@ def ternary_plot(self): class Fractures(BaseEntity): """ - Base entity for fractures - - + Add method to plot rose diagram + Base entity for fractures, represents all the fractures in the network. + + Parameters + ----------- + gdf: GeoDataFrame + Use as input a geopandas dataframe + csv: str + Use as input a csv indicated by the path + shp: str + Use as input a shapefile indicated by the path + set_n: int + Fracture set number. + check_geometry: Bool + Perform geometry check. Default is True + + + Notes + -------- + + The csv needs to have a "geometry" column. If missing the import will fail. + + If the input csv or shapefile has an f_set column then the set_n will be ignored """ def __init__(self, gdf: GeoDataFrame = None, csv: str = None, shp: str = None, set_n: int = None, check_geometry: bool = True): - """ - Init for Fractures entity. Different inputs can be used. Geopandas dataframe, csv or shapefile. The csv needs to be - structured in such a way to be compatible with the Nodes entity. - - :param gdf: Geopandas dataframe - :param csv: Path of a csv - :param shp: Path of the shapefile - :param set_n: Fracture set number - :param check_geometry: Perform geometry check. Default is True - """ self.check_geometries_flag = check_geometry self._set_n = set_n @@ -214,7 +231,10 @@ def __init__(self, gdf: GeoDataFrame = None, csv: str = None, super().__init__() @property - def set_n(self): + def set_n(self) -> int: + """ + Get the set number + """ return self._set_n @property @@ -223,6 +243,14 @@ def entity_df(self): @entity_df.setter def entity_df(self, gdf: GeoDataFrame): + """ + Each entity process the input dataframe in different ways. + Fractures modify the entity_df in a couple of ways: + + No Multilines can be used. + + If not f_set column is present, it will be created following the set_n value + + If no length column is present, it will be created (with length rounded to the 4th decimal point) + + If no censoring column is present, it will be created setting all values to 0 + """ multiline_list = [] for index, geom in zip(gdf.index, gdf['geometry']): # For each geometry in the df @@ -233,6 +261,7 @@ def entity_df(self, gdf: GeoDataFrame): print(f'Multilines found, removing from database. If necessary correct them: {np.array(multiline_list)+1}') gdf.drop(multiline_list, inplace=True) + self._df = gdf.copy() self._df.reset_index(inplace=True, drop=True) if 'original_line_id' not in self._df.columns: @@ -306,34 +335,8 @@ def check_geometries(self, remove_dup=True, save_shp=False): if len(overlaps_list) > 0: print(f'Detected overlaps for set {self._set_n}: {overlaps_list}. Check geometries in gis and fix.') - # def simplify_lines(self, tolerance: float, max_points: int = None, preserve_topology: bool = True): - # """ - # Method used to simplify lines in a fracture entity. The coordinates of the new simplified geometry will be - # no more than the tolerance distance from the original. If max_points is None then all lines will be simplified. - # If max points is specified then only lines that have more points that max_points wil be simplified. - # - # :param tolerance: Maximum distance of a point between the new and old geometry - # :param max_points: Maximum tolerated points in a line over which the simplification is run - # :param preserve_topology: Bool flag to ensure that the topology is preserved. - # If set to False, self intersecting geometries may occur - # """ - # - # df = self.entity_df.copy() - # if max_points is None: - # df['geometry'] = df['geometry'].simplify(tolerance=tolerance, preserve_topology=preserve_topology) - # - # else: - # new_geom = [] - # for i, row in df.iterrows(): - # line = row['geometry'] - # if len(line.coords) > tolerance: - # new_geom.append(line.simplify(tolerance=tolerance, - # preserve_topology=preserve_topology)) - # df['geometry'] = new_geom - # - # self.entity_df = df - def mat_plot(self): + plts.matplot_fractures(self) def vtk_plot(self, linewidth=1, color='white', color_set=False, return_plot=False, display_property: str = None): @@ -344,7 +347,23 @@ def vtk_plot(self, linewidth=1, color='white', color_set=False, return_plot=Fals class Boundary(BaseEntity): """ - Base entity for boundaries + Base entity for boundaries, represents all the boundaries in the network. + + Parameters + ----------- + gdf: GeoDataFrame + Use as input a geopandas dataframe + csv: str + Use as input a csv indicated by the path + shp: str + Use as input a shapefile indicated by the path + group_n: int + Boundary number. + + + Notes + -------- + + The csv needs to have a "geometry" column. If missing the import will fail. """ def __init__(self, gdf: GeoDataFrame = None, csv: str = None, shp: str = None, group_n: int = 1): """ diff --git a/fracability/tests/test_entities.py b/fracability/tests/test_entities.py index 32cb133..b87250d 100644 --- a/fracability/tests/test_entities.py +++ b/fracability/tests/test_entities.py @@ -4,11 +4,9 @@ import os import glob -import numpy as np from fracability.Entities import Nodes, Fractures, Boundary, FractureNetwork -from fracability.examples import example_fracture_network -from fracability.utils.general_use import centers_to_lines +from examples import example_fracture_network class TestNodes: diff --git a/fracability/tests/test_geometry.py b/fracability/tests/test_geometry.py index edd15c1..0dc34e2 100644 --- a/fracability/tests/test_geometry.py +++ b/fracability/tests/test_geometry.py @@ -1,4 +1,4 @@ -from fracability.examples import example_fracture_network +from examples import example_fracture_network import pytest from fracability import Entities from fracability.operations.Geometry import tidy_intersections, calculate_seg_length diff --git a/fracability/tests/test_plotters.py b/fracability/tests/test_plotters.py index 38548e9..d30f4e5 100644 --- a/fracability/tests/test_plotters.py +++ b/fracability/tests/test_plotters.py @@ -1,4 +1,4 @@ -from fracability.examples import example_fracture_network +from examples import example_fracture_network import pytest from fracability.Entities import FractureNetwork from fracability.Plotters import * diff --git a/fracability/tests/test_topology.py b/fracability/tests/test_topology.py index 7bfc13f..49596ec 100644 --- a/fracability/tests/test_topology.py +++ b/fracability/tests/test_topology.py @@ -1,8 +1,6 @@ -from fracability.examples import example_fracture_network +from examples import example_fracture_network import pytest from fracability import Entities -from fracability.operations.Geometry import tidy_intersections -from fracability.operations.Topology import nodes_conn @pytest.fixture(scope="session", autouse=True)