From d936aa592441bb698849e7975f300d01e6973c68 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 10 Nov 2025 12:49:44 +0930 Subject: [PATCH 1/5] feat: add AlongSection thickness calculator --- map2loop/thickness_calculator.py | 314 ++++++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 2 deletions(-) diff --git a/map2loop/thickness_calculator.py b/map2loop/thickness_calculator.py index 13a1b806..0e48547d 100644 --- a/map2loop/thickness_calculator.py +++ b/map2loop/thickness_calculator.py @@ -6,7 +6,11 @@ multiline_to_line, find_segment_strike_from_pt, set_z_values_from_raster_df, - value_from_raster + value_from_raster, + segment_measure_range, + clean_line_geometry, + nearest_orientation_to_line, + iter_line_segments ) from .interpolators import DipDipDirectionInterpolator @@ -15,11 +19,13 @@ # external imports from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, List +from collections import defaultdict import scipy.interpolate import beartype import numpy import scipy +from scipy.spatial import cKDTree import pandas import geopandas import shapely @@ -756,3 +762,307 @@ def compute( self._check_thickness_percentage_calculations(output_units) return output_units + + +class AlongSection(ThicknessCalculator): + """Thickness calculator that estimates true thicknesses along supplied section lines.""" + + def __init__( + self, + sections: geopandas.GeoDataFrame, + dtm_data: Optional[gdal.Dataset] = None, + bounding_box: Optional[dict] = None, + max_line_length: Optional[float] = None, + is_strike: Optional[bool] = False, + ): + super().__init__(dtm_data, bounding_box, max_line_length, is_strike) # initialise base calculator bits + self.thickness_calculator_label = "AlongSection" # label used externally to identify this strategy + self.sections = ( + sections.copy() # keep a copy so editing outside does not mutate our internal state + if sections is not None + else geopandas.GeoDataFrame({"geometry": []}, geometry="geometry") # ensure predictable structure when no sections supplied + ) + self.section_thickness_records: geopandas.GeoDataFrame | List[dict] = [] # populated as GeoDataFrame during compute + self.section_intersection_points: dict = {} + + def type(self): + """Return the calculator label.""" + return self.thickness_calculator_label + + @beartype.beartype + def compute( + self, + units: pandas.DataFrame, + stratigraphic_order: list, + basal_contacts: geopandas.GeoDataFrame, + structure_data: pandas.DataFrame, + geology_data: geopandas.GeoDataFrame, + sampled_contacts: pandas.DataFrame, + ) -> pandas.DataFrame: + """Estimate unit thicknesses along cross sections. + + Args: + units (pandas.DataFrame): Table of stratigraphic units that will be annotated with + thickness statistics. Must expose a ``name`` column used to map aggregated + thickness values back to each unit. + stratigraphic_order (list): Accepted for API compatibility but not used by this + calculator. Retained so the method signature matches the other calculators. + basal_contacts (geopandas.GeoDataFrame): Unused placeholder maintained for parity + with the other calculators. + structure_data (pandas.DataFrame): Orientation measurements containing X/Y columns + and one dip column (``DIP``, ``dip`` or ``Dip``). A KD-tree is built from these + points so each split section segment can grab the nearest dip when converting its + length to true thickness. + geology_data (geopandas.GeoDataFrame): Mandatory polygon dataset describing map + units. Each section is intersected with these polygons to generate unit-aware + line segments whose lengths underpin the thickness calculation. + sampled_contacts (pandas.DataFrame): Part of the public interface but not used in + this implementation. + + Returns: + pandas.DataFrame: Copy of ``units`` with three new columns – ``ThicknessMean``, + ``ThicknessMedian`` and ``ThicknessStdDev`` – populated from the accumulated segment + thickness samples. Units that never receive a measurement retain the sentinel value + ``-1`` in each column. + + Workflow: + 1. Validate the presence of section geometries, geology polygons, and a recognizable + unit-name column. Reproject sections to match the geology CRS when required. + 2. Pre-build helpers: a spatial index over geology polygons for fast section/polygon + queries, and (if possible) a KD-tree of orientation points so the closest dip to a + split segment can be queried in O(log n). + 3. For every section, reduce complex/multi-part geometries to a single `LineString`, + intersect it with candidate geology polygons, and collect the resulting split line + segments along with their length and measure positions along the parent section. + 4. Walk the ordered segments and keep those bounded by two distinct neighbouring units + (i.e., the segment sits between different units on either side). Fetch the nearest + dip (fallback to 90° once if no structures exist), convert the segment length to + true thickness using ``length * sin(dip)``, and store the sample plus provenance in + ``self.section_thickness_records``. + 5. Aggregate all collected samples per unit (mean/median/std) and return the enriched + ``units`` table. The helper ``self.section_intersection_points`` captures the raw + split segments grouped by section to aid downstream inspection. + + Notes: + - ``stratigraphic_order``, ``basal_contacts`` and ``sampled_contacts`` are retained for + interface compatibility but do not influence this algorithm. + - When no nearby orientation measurements exist, the method emits a single warning and + assumes a vertical dip (90°) for affected segments to avoid silently dropping units. + - ``self.section_thickness_records`` is materialised as a ``GeoDataFrame`` so the split + segments (geometry column) can be visualised directly in notebooks or GIS clients. + """ + + if self.sections is None or self.sections.empty: + logger.warning("AlongSection: No sections provided; skipping thickness calculation.") + return units + + if geology_data is None or geology_data.empty: + logger.warning( + "AlongSection: Geology polygons are required to split sections; skipping thickness calculation." + ) + return units + + unit_column_candidates = [ + "UNITNAME", + "unitname", + "UNIT_NAME", + "UnitName", + "unit", + "Unit", + "UNIT", + "name", + "Name", + ] + unit_column = next((col for col in unit_column_candidates if col in geology_data.columns), None) + if unit_column is None: + logger.warning( + "AlongSection: Unable to identify a unit-name column in geology data; expected one of %s.", + unit_column_candidates, + ) + return units + + sections = self.sections.copy() + geology = geology_data.copy() + + if geology.crs is not None: + if sections.crs is None: + sections = sections.set_crs(geology.crs) + elif sections.crs != geology.crs: + sections = sections.to_crs(geology.crs) + elif sections.crs is not None: + geology = geology.set_crs(sections.crs) + + try: + geology_sindex = geology.sindex + except Exception: + geology_sindex = None + + dip_column = next((col for col in ("DIP", "dip", "Dip") if col in structure_data.columns), None) + orientation_tree = None + orientation_dips = None + orientation_coords = None + if dip_column is not None and {"X", "Y"}.issubset(structure_data.columns): + orient_df = structure_data.dropna(subset=["X", "Y", dip_column]).copy() + if not orient_df.empty: + orient_df["_dip_value"] = pandas.to_numeric(orient_df[dip_column], errors="coerce") + orient_df = orient_df.dropna(subset=["_dip_value"]) + if not orient_df.empty: + try: + orientation_coords = orient_df[["X", "Y"]].astype(float).to_numpy() + except ValueError: + orientation_coords = numpy.empty((0, 2)) + if orientation_coords.size: + orientation_dips = orient_df["_dip_value"].astype(float).to_numpy() + try: + orientation_tree = cKDTree(orientation_coords) + except Exception: + orientation_tree = None + + default_dip_warning_emitted = False + + units_lookup = dict(zip(units["name"], units.index)) + thickness_by_unit = {name: [] for name in units_lookup.keys()} + thickness_records: List[dict] = [] + split_segments_by_section: dict = defaultdict(list) + + for section_idx, section_row in sections.iterrows(): + line = clean_line_geometry(section_row.geometry) + if line is None or line.length == 0: + continue + section_id = section_row.get("ID", section_idx) + + candidate_idx = ( + list(geology_sindex.intersection(line.bounds)) + if geology_sindex is not None + else list(range(len(geology))) + ) + if not candidate_idx: + continue + + split_segments = [] + for _, poly in geology.iloc[candidate_idx].iterrows(): + polygon_geom = poly.geometry + if polygon_geom is None or polygon_geom.is_empty: + continue + intersection = line.intersection(polygon_geom) + if intersection.is_empty: + continue + for segment in iter_line_segments(intersection): + seg_length = segment.length + if seg_length <= 0: + continue + start_measure, end_measure = segment_measure_range(line, segment) + segment_record = { + "section_id": section_id, + "geometry": segment, + "unit": poly[unit_column], + "length": seg_length, + "start_measure": start_measure, + "end_measure": end_measure, + } + split_segments.append(segment_record) + split_segments_by_section[section_id].append(segment_record) + + if len(split_segments) < 1: + continue + + split_segments.sort(key=lambda item: (item["start_measure"], item["end_measure"])) + + for idx_segment, segment in enumerate(split_segments): + prev_unit = split_segments[idx_segment - 1]["unit"] if idx_segment > 0 else None + next_unit = ( + split_segments[idx_segment + 1]["unit"] + if idx_segment + 1 < len(split_segments) + else None + ) + + if prev_unit is None or next_unit is None: + continue + if prev_unit == segment["unit"] or next_unit == segment["unit"]: + continue + if prev_unit == next_unit: + continue + + dip_value, orientation_idx, orientation_distance = nearest_orientation_to_line( + orientation_tree, + orientation_dips, + orientation_coords, + segment["geometry"] + ) + if numpy.isnan(dip_value): + dip_value = 90.0 + orientation_idx = None + orientation_distance = None + if not default_dip_warning_emitted: + logger.warning( + "AlongSection: Missing structure measurements near some sections; assuming vertical dip (90°) for those segments." + ) + default_dip_warning_emitted = True + + thickness = segment["length"] * abs(math.sin(math.radians(dip_value))) + if thickness <= 0: + continue + + unit_name = segment["unit"] + if unit_name not in thickness_by_unit: + thickness_by_unit[unit_name] = [] + thickness_by_unit[unit_name].append(thickness) + + thickness_records.append( + { + "section_id": section_id, + "unit_id": unit_name, + "thickness": thickness, + "segment_length": segment["length"], + "dip_used_deg": dip_value, + "prev_unit": prev_unit, + "next_unit": next_unit, + "orientation_index": orientation_idx, + "orientation_distance": orientation_distance, + "geometry": segment["geometry"], + } + ) + + output_units = units.copy() + output_units["ThicknessMean"] = -1.0 + output_units["ThicknessMedian"] = -1.0 + output_units["ThicknessStdDev"] = -1.0 + + for unit_name, values in thickness_by_unit.items(): + if not values: + continue + idx = units_lookup.get(unit_name) + if idx is None: + continue + arr = numpy.asarray(values, dtype=numpy.float64) + output_units.at[idx, "ThicknessMean"] = float(numpy.nanmean(arr)) + output_units.at[idx, "ThicknessMedian"] = float(numpy.nanmedian(arr)) + output_units.at[idx, "ThicknessStdDev"] = float(numpy.nanstd(arr)) + + if thickness_records: + self.section_thickness_records = geopandas.GeoDataFrame( + thickness_records, + geometry="geometry", + crs=sections.crs, + ) + else: + self.section_thickness_records = geopandas.GeoDataFrame( + columns=[ + "section_id", + "unit_id", + "thickness", + "segment_length", + "dip_used_deg", + "prev_unit", + "next_unit", + "orientation_index", + "orientation_distance", + "geometry", + ], + geometry="geometry", + crs=sections.crs, + ) + self.section_intersection_points = dict(split_segments_by_section) + self._check_thickness_percentage_calculations(output_units) + + return output_units From 3822263b64bf646e07c0808042dbb14ff95e4664 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 10 Nov 2025 12:50:17 +0930 Subject: [PATCH 2/5] feat: add utils for AlongSection class --- map2loop/utils.py | 209 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 1 deletion(-) diff --git a/map2loop/utils.py b/map2loop/utils.py index 55e2e7b2..ce84ee60 100644 --- a/map2loop/utils.py +++ b/map2loop/utils.py @@ -587,4 +587,211 @@ def set_z_values_from_raster_df(dtm_data: gdal.Dataset, df: pandas.DataFrame): axis=1, ) - return df \ No newline at end of file + return df + +def clean_line_geometry(geometry: shapely.geometry.base.BaseGeometry): + """Clean and normalize a Shapely geometry to a single LineString or None. + + Args: + geometry (shapely.geometry.base.BaseGeometry | None): Input geometry to be + cleaned. Accepted inputs include ``LineString``, ``MultiLineString``, + ``GeometryCollection``, and other Shapely geometries. If ``None`` or + an empty geometry is provided, the function returns ``None``. + + Returns: + shapely.geometry.LineString or None: A single ``LineString`` if the + geometry is already a ``LineString`` or can be merged/converted into one. + If the input is ``None``, empty, or cannot be converted into a valid + ``LineString``, the function returns ``None``. + + Raises: + None: Exceptions raised by ``shapely.ops.linemerge`` are caught and result + in a ``None`` return rather than being propagated. + + Notes: + - The function uses ``shapely.ops.linemerge`` to attempt to merge multipart + geometries into a single ``LineString``. + - If ``linemerge`` returns a ``MultiLineString``, the longest constituent + ``LineString`` (by ``length``) is returned. + - Geometries that are already ``LineString`` are returned unchanged. + + Examples: + >>> from shapely.geometry import LineString + >>> clean_line_geometry(None) is None + True + >>> ls = LineString([(0, 0), (1, 1)]) + >>> clean_line_geometry(ls) is ls + True + """ + if geometry is None or geometry.is_empty: + return None + if geometry.geom_type == "LineString": + return geometry + try: + merged = shapely.ops.linemerge(geometry) + except Exception: + return None + if merged.geom_type == "LineString": + return merged + if merged.geom_type == "MultiLineString": + try: + return max(merged.geoms, key=lambda geom: geom.length) + except ValueError: + return None + return None + +def iter_line_segments(geometry): + """Produce all LineString segments contained in a Shapely geometry. + + Args: + geometry (shapely.geometry.base.BaseGeometry | None): Input geometry. Accepted + types include ``LineString``, ``MultiLineString``, ``GeometryCollection`` + and other Shapely geometries that may contain line parts. If ``None`` or + an empty geometry is provided, an empty list is returned. + + Returns: + list[shapely.geometry.LineString]: A list of ``LineString`` objects extracted + from the input geometry. Behavior by input type: + - ``LineString``: returned as a single-element list. + - ``MultiLineString``: returns the non-zero-length constituent parts. + - ``GeometryCollection``: recursively extracts contained line segments. + - Other geometry types: returns an empty list if no line segments found. + + Notes: + Zero-length segments are filtered out. The function is defensive and + will return an empty list for ``None`` or empty geometries rather than + raising an exception. + + Examples: + >>> from shapely.geometry import LineString + >>> iter_line_segments(LineString([(0, 0), (1, 1)])) + [LineString([(0, 0), (1, 1)])] + """ + if geometry is None or geometry.is_empty: + return [] + if isinstance(geometry, shapely.geometry.LineString): + return [geometry] + if isinstance(geometry, shapely.geometry.MultiLineString): + return [geom for geom in geometry.geoms if geom.length > 0] + if isinstance(geometry, shapely.geometry.GeometryCollection): + segments = [] + for geom in geometry.geoms: + segments.extend(iter_line_segments(geom)) + return segments + return [] + +def nearest_orientation_to_line(orientation_tree, orientation_dips, orientation_coords, line_geom: shapely.geometry.LineString): + """Find the nearest orientation measurement to the midpoint of a line. + + This function queries the provided spatial index and orientation arrays and + returns the dip value, the index of the matched orientation, and the + distance from the line to that orientation point. + + Args: + orientation_tree: Spatial index object (e.g., KDTree) with a ``query`` + method accepting an (x, y) tuple and optional ``k``. Typically + provided by the caller so the utility remains stateless. + orientation_dips (Sequence[float]): Sequence of dip values aligned with + ``orientation_coords``. + orientation_coords (Sequence[tuple]): Sequence of (x, y) coordinate + tuples for each orientation measurement. + line_geom (shapely.geometry.LineString): Line geometry for which the + nearest orientation should be found. The midpoint (interpolated at + 50% of the line length) is used as the query point; if the + midpoint is empty, the line centroid is used as a fallback. + + Returns: + tuple: A tuple ``(dip, index, distance)`` where: + - ``dip`` (float or numpy.nan): the dip value from + ``orientation_dips`` at the nearest orientation. Returns + ``numpy.nan`` if no valid orientation can be found or on error. + - ``index`` (int or None): the integer index into the orientation + arrays for the best match, or ``None`` if not found. + - ``distance`` (float or None): the shortest geometric distance + between ``line_geom`` and the matched orientation point, or + ``None`` if not available. + + Notes: + - The function queries up to 5 nearest neighbors (or fewer if fewer + orientations exist) and then computes the actual geometry distance + between the ``line_geom`` and each candidate point to select the + closest match. Any exceptions from the spatial query are caught and + result in ``(numpy.nan, None, None)`` being returned instead of + propagating the exception. + - The function is defensive: invalid or empty geometries return + ``(numpy.nan, None, None)`` rather than raising. + + Examples: + >>> dip, idx, dist = nearest_orientation_to_line(tree, dips, coords, my_line) + >>> if idx is not None: + ... print(f"Nearest dip={dip} at index={idx} (distance={dist})") + """ + if orientation_tree is None or orientation_dips is None or orientation_coords is None: + return numpy.nan, None, None + midpoint = line_geom.interpolate(0.5, normalized=True) + if midpoint.is_empty: + midpoint = line_geom.centroid + if midpoint.is_empty: + return numpy.nan, None, None + query_xy = (float(midpoint.x), float(midpoint.y)) + k = min(len(orientation_dips), 5) + try: + distances, indices = orientation_tree.query(query_xy, k=k) + except Exception: + return numpy.nan, None, None + distances = numpy.atleast_1d(distances) + indices = numpy.atleast_1d(indices) + best_idx = None + best_dist = numpy.inf + for approx_dist, idx in zip(distances, indices): + if idx is None: + continue + candidate_point = shapely.geometry.Point(orientation_coords[int(idx)]) + actual_dist = line_geom.distance(candidate_point) + if actual_dist < best_dist: + best_dist = actual_dist + best_idx = int(idx) + if best_idx is None: + return numpy.nan, None, None + return float(orientation_dips[best_idx]), best_idx, best_dist + +def segment_measure_range(parent_line: shapely.geometry.LineString, segment: shapely.geometry.LineString): + """Compute projected measures of a segment's end points along a parent line. + + This function projects the start and end points of ``segment`` onto + ``parent_line`` using Shapely's ``project`` method and returns the two + measures in ascending order (start <= end). Measures are distances along + the parent line's linear reference (units of the geometry's CRS). + + Args: + parent_line (shapely.geometry.LineString): The reference line onto which + the segment end points will be projected. + segment (shapely.geometry.LineString): The segment whose first and last + vertices will be projected onto ``parent_line``. The function uses + ``segment.coords[0]`` and ``segment.coords[-1]`` as the start and + end points respectively. + + Returns: + tuple(float, float): A tuple ``(start_measure, end_measure)`` containing + the projected distances along ``parent_line`` for the segment's start + and end points. The values are ordered so that ``start_measure <= end_measure``. + + Notes: + - If ``segment`` is degenerate (e.g., a single point), both measures may + be equal. + - The returned measures are in the same linear units as the input + geometries (e.g., metres for projected CRS). + - No validation is performed on the inputs; callers should ensure both + geometries are valid and share a common CRS. + + Examples: + >>> start_m, end_m = segment_measure_range(parent_line, segment) + >>> assert start_m <= end_m + """ + start_point = shapely.geometry.Point(segment.coords[0]) + end_point = shapely.geometry.Point(segment.coords[-1]) + start_measure = parent_line.project(start_point) + end_measure = parent_line.project(end_point) + if end_measure < start_measure: + start_measure, end_measure = end_measure, start_measure + return start_measure, end_measure \ No newline at end of file From 7a2223405833a4c7f398ba92ec52134a21084970 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 10 Nov 2025 12:54:22 +0930 Subject: [PATCH 3/5] fix: renamed to _approx_dist --- map2loop/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/utils.py b/map2loop/utils.py index ce84ee60..927d4c5e 100644 --- a/map2loop/utils.py +++ b/map2loop/utils.py @@ -743,7 +743,7 @@ def nearest_orientation_to_line(orientation_tree, orientation_dips, orientation_ indices = numpy.atleast_1d(indices) best_idx = None best_dist = numpy.inf - for approx_dist, idx in zip(distances, indices): + for _approx_dist, idx in zip(distances, indices): if idx is None: continue candidate_point = shapely.geometry.Point(orientation_coords[int(idx)]) From aa813aa13fc2bbd3b9b4603d52e921f0b4372d4b Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Thu, 13 Nov 2025 12:35:19 +1100 Subject: [PATCH 4/5] Adding exception handling suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- map2loop/thickness_calculator.py | 17 ++++++++++++----- map2loop/utils.py | 6 ++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/map2loop/thickness_calculator.py b/map2loop/thickness_calculator.py index 0e48547d..614344cb 100644 --- a/map2loop/thickness_calculator.py +++ b/map2loop/thickness_calculator.py @@ -782,7 +782,7 @@ def __init__( if sections is not None else geopandas.GeoDataFrame({"geometry": []}, geometry="geometry") # ensure predictable structure when no sections supplied ) - self.section_thickness_records: geopandas.GeoDataFrame | List[dict] = [] # populated as GeoDataFrame during compute + self.section_thickness_records: geopandas.GeoDataFrame = geopandas.GeoDataFrame() # populated as GeoDataFrame during compute self.section_intersection_points: dict = {} def type(self): @@ -894,7 +894,8 @@ def compute( try: geology_sindex = geology.sindex - except Exception: + except Exception as e: + logger.error("Failed to create spatial index for geology data: %s", e, exc_info=True) geology_sindex = None dip_column = next((col for col in ("DIP", "dip", "Dip") if col in structure_data.columns), None) @@ -910,14 +911,19 @@ def compute( try: orientation_coords = orient_df[["X", "Y"]].astype(float).to_numpy() except ValueError: + logger.debug( + "Failed to convert orientation coordinates to float for %d rows. Data quality issue likely. Example rows: %s", + len(orient_df), + orient_df[["X", "Y"]].head(3).to_dict(orient="records") + ) orientation_coords = numpy.empty((0, 2)) if orientation_coords.size: orientation_dips = orient_df["_dip_value"].astype(float).to_numpy() try: orientation_tree = cKDTree(orientation_coords) - except Exception: + except (ValueError, TypeError) as e: + logger.error(f"Failed to construct cKDTree for orientation data: {e}") orientation_tree = None - default_dip_warning_emitted = False units_lookup = dict(zip(units["name"], units.index)) @@ -940,7 +946,8 @@ def compute( continue split_segments = [] - for _, poly in geology.iloc[candidate_idx].iterrows(): + for idx in candidate_idx: + poly = geology.iloc[idx] polygon_geom = poly.geometry if polygon_geom is None or polygon_geom.is_empty: continue diff --git a/map2loop/utils.py b/map2loop/utils.py index 927d4c5e..1434562b 100644 --- a/map2loop/utils.py +++ b/map2loop/utils.py @@ -629,7 +629,8 @@ def clean_line_geometry(geometry: shapely.geometry.base.BaseGeometry): return geometry try: merged = shapely.ops.linemerge(geometry) - except Exception: + except Exception as e: + logger.exception("Exception occurred in shapely.ops.linemerge during clean_line_geometry") return None if merged.geom_type == "LineString": return merged @@ -737,7 +738,8 @@ def nearest_orientation_to_line(orientation_tree, orientation_dips, orientation_ k = min(len(orientation_dips), 5) try: distances, indices = orientation_tree.query(query_xy, k=k) - except Exception: + except Exception as e: + logger.exception("Exception occurred during KDTree query in nearest_orientation_to_line; returning (nan, None, None)") return numpy.nan, None, None distances = numpy.atleast_1d(distances) indices = numpy.atleast_1d(indices) From 6b8e67400b613e557afeb3c2de719f7deaa8515e Mon Sep 17 00:00:00 2001 From: lachlangrose <7371904+lachlangrose@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:35:34 +0000 Subject: [PATCH 5/5] style: style fixes by ruff and autoformatting by black --- map2loop/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/map2loop/utils.py b/map2loop/utils.py index 1434562b..3efe8e76 100644 --- a/map2loop/utils.py +++ b/map2loop/utils.py @@ -629,7 +629,7 @@ def clean_line_geometry(geometry: shapely.geometry.base.BaseGeometry): return geometry try: merged = shapely.ops.linemerge(geometry) - except Exception as e: + except Exception: logger.exception("Exception occurred in shapely.ops.linemerge during clean_line_geometry") return None if merged.geom_type == "LineString": @@ -738,7 +738,7 @@ def nearest_orientation_to_line(orientation_tree, orientation_dips, orientation_ k = min(len(orientation_dips), 5) try: distances, indices = orientation_tree.query(query_xy, k=k) - except Exception as e: + except Exception: logger.exception("Exception occurred during KDTree query in nearest_orientation_to_line; returning (nan, None, None)") return numpy.nan, None, None distances = numpy.atleast_1d(distances)