ccfx 1.0.9__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ccfx/ccfx.py CHANGED
@@ -22,7 +22,6 @@ import platform
22
22
  import zipfile
23
23
  import pickle
24
24
  import time
25
- from shapely.geometry import box, Point
26
25
  import geopandas, pandas
27
26
  from collections import defaultdict
28
27
  import py7zr
@@ -38,6 +37,12 @@ import yt_dlp
38
37
  from typing import Optional, Any
39
38
  from datetime import datetime, timedelta
40
39
  from PIL import Image
40
+ import scipy as scipy
41
+ from shapely.geometry import LineString, Polygon, MultiPolygon, box, Point
42
+ from shapely.ops import polygonize, unary_union
43
+ import rasterio
44
+ from rasterio import features
45
+ from rasterio.transform import from_bounds
41
46
 
42
47
  # functions
43
48
  def listFiles(path: str, ext: Optional[str] = None) -> list:
@@ -568,6 +573,14 @@ def correctFisheye(inputFile: str, outputFile: str = '',
568
573
  subprocess.run(cmd, check=True)
569
574
  return outputFile
570
575
 
576
+ def correctLens(inputFile: str, outputFile: str = '',
577
+ k1: float = -0.1, k2: float = 0.05,
578
+ cx: float = 0.5, cy: float = 0.5,
579
+ crf: int = 20) -> str:
580
+ """
581
+ Alias for correctFisheye
582
+ """
583
+ return correctFisheye(inputFile, outputFile, k1, k2, cx, cy, crf)
571
584
 
572
585
  def formatStringBlock(input_str: str, max_chars: int = 70) -> str:
573
586
  '''
@@ -680,6 +693,76 @@ def downloadFile(url: str, save_path: str, exists_action: str = 'resume', num_co
680
693
 
681
694
 
682
695
 
696
+
697
+ def createPolygonFromOuterPoints(pointsGdf: geopandas.GeoDataFrame, alpha: float = 1.6, keepHoles: bool = False) -> geopandas.GeoDataFrame:
698
+ """
699
+ Concave hull (alpha-shape) from points.
700
+ alpha: larger -> tighter/more detailed; too large can fragment.
701
+ keepHoles: keep interior holes if True, otherwise drop them.
702
+ """
703
+
704
+ pointsGdf = pointsGdf[pointsGdf.geometry.type.eq("Point") & pointsGdf.geometry.notna()]
705
+ if pointsGdf.empty or pointsGdf.geometry.nunique() < 3:
706
+ raise ValueError("need at least three distinct points")
707
+
708
+ # coordinates (N, 2)
709
+ coords = numpy.array([(g.x, g.y) for g in pointsGdf.geometry])
710
+
711
+ tri = scipy.spatial.Delaunay(coords)
712
+ simplices = tri.simplices # (M, 3) indices into coords
713
+ triPts = coords[simplices] # (M, 3, 2)
714
+
715
+ # side lengths
716
+ a = numpy.linalg.norm(triPts[:, 1] - triPts[:, 2], axis=1)
717
+ b = numpy.linalg.norm(triPts[:, 0] - triPts[:, 2], axis=1)
718
+ c = numpy.linalg.norm(triPts[:, 0] - triPts[:, 1], axis=1)
719
+
720
+ s = (a + b + c) / 2.0
721
+ # heron area, guard against tiny/negative due to fp error
722
+ areaSq = numpy.maximum(s * (s - a) * (s - b) * (s - c), 0.0)
723
+ area = numpy.sqrt(areaSq)
724
+ valid = area > 0.0
725
+ if not numpy.any(valid):
726
+ hull = pointsGdf.unary_union.convex_hull
727
+ return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
728
+
729
+ circumradius = (a * b * c) / (4.0 * area)
730
+ keep = valid & (circumradius < (1.0 / alpha))
731
+ keptSimplices = simplices[keep]
732
+ if keptSimplices.size == 0:
733
+ hull = pointsGdf.unary_union.convex_hull
734
+ return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
735
+
736
+ # count triangle edges; boundary edges appear exactly once
737
+ edgeCounts: dict[tuple[int, int], int] = {}
738
+ for i0, i1, i2 in keptSimplices:
739
+ for e in ((i0, i1), (i1, i2), (i2, i0)):
740
+ key = (e[0], e[1]) if e[0] < e[1] else (e[1], e[0])
741
+ edgeCounts[key] = edgeCounts.get(key, 0) + 1
742
+
743
+ boundaryLines = [
744
+ LineString([coords[i], coords[j]])
745
+ for (i, j), count in edgeCounts.items()
746
+ if count == 1
747
+ ]
748
+ if not boundaryLines:
749
+ hull = pointsGdf.unary_union.convex_hull
750
+ return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
751
+
752
+ polygons = list(polygonize(boundaryLines))
753
+ if not polygons:
754
+ hull = pointsGdf.unary_union.convex_hull
755
+ return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
756
+
757
+ merged = unary_union(polygons)
758
+ if isinstance(merged, MultiPolygon):
759
+ merged = max(merged.geoms, key=lambda g: g.area)
760
+ if not keepHoles and isinstance(merged, Polygon):
761
+ merged = Polygon(merged.exterior)
762
+
763
+ return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[merged], crs=pointsGdf.crs)
764
+
765
+
683
766
  def mergeRasterTiles(tileList:list, outFile:str) -> str:
684
767
  '''
685
768
  Merge raster tiles into one raster file
@@ -1030,6 +1113,116 @@ def extractCompressedFile(inputFile: str, outputDir: str, v: bool = False) -> No
1030
1113
  """
1031
1114
  uncompress(inputFile, outputDir, v)
1032
1115
 
1116
+
1117
+
1118
+ def rasterizeGDF(gdf: geopandas.GeoDataFrame, valueField: str, outRasterFN: str, resolution: float, isCOG: bool = True, allTouched: bool = False, profileOverrides: dict | None = None) -> str:
1119
+ """
1120
+ Rasterize a GeoDataFrame to GeoTIFF/COG.
1121
+
1122
+ Parameters
1123
+ ----------
1124
+ gdf : geopandas.GeoDataFrame
1125
+ valueField : str
1126
+ Column to burn as pixel values.
1127
+ outRasterFN : str
1128
+ resolution : float
1129
+ Pixel size in CRS units (assumes a projected CRS).
1130
+ isCOG : bool, default True
1131
+ If True, writes Cloud-Optimized GeoTIFF. Otherwise plain GeoTIFF.
1132
+ allTouched : bool, default False
1133
+ Pass-through to rasterio.features.rasterize.
1134
+ profileOverrides : dict | None
1135
+ Extra GDAL profile options (e.g., {"compress": "LZW"}). Overrides defaults.
1136
+
1137
+ Returns
1138
+ -------
1139
+ str
1140
+ The path written to (outRasterFN).
1141
+ """
1142
+ # basic checks
1143
+ if gdf is None or len(gdf) == 0 or gdf.geometry.isna().all():
1144
+ raise ValueError("gdf is empty or has no valid geometries.")
1145
+ if gdf.crs is None:
1146
+ raise ValueError("gdf must have a defined CRS.")
1147
+ if resolution <= 0:
1148
+ raise ValueError("resolution must be > 0.")
1149
+ if valueField not in gdf.columns:
1150
+ raise ValueError(f"valueField '{valueField}' not found in gdf.")
1151
+
1152
+ # compute raster shape + transform
1153
+ bounds = gdf.total_bounds # (minx, miny, maxx, maxy)
1154
+ width = int(numpy.ceil((bounds[2] - bounds[0]) / float(resolution)))
1155
+ height = int(numpy.ceil((bounds[3] - bounds[1]) / float(resolution)))
1156
+ if width < 1 or height < 1:
1157
+ raise ValueError("computed raster dimensions are invalid (check resolution and bounds).")
1158
+
1159
+ transform = from_bounds(bounds[0], bounds[1], bounds[2], bounds[3], width, height)
1160
+
1161
+ # infer dtype + nodata
1162
+ pandasDtype = gdf[valueField].dtype
1163
+ if numpy.issubdtype(pandasDtype, numpy.floating):
1164
+ dtype = numpy.float32
1165
+ nodata = numpy.nan
1166
+ fillValue = numpy.nan
1167
+ elif numpy.issubdtype(pandasDtype, numpy.bool_):
1168
+ dtype = numpy.uint8
1169
+ nodata = 255 # sentinel for bool raster
1170
+ fillValue = nodata
1171
+ else:
1172
+ dtype = numpy.int32
1173
+ nodata = -9999
1174
+ fillValue = nodata
1175
+
1176
+ # prefill target array
1177
+ raster = numpy.full((height, width), fillValue, dtype=dtype)
1178
+
1179
+ # build shapes generator (ensure python scalars)
1180
+ shapes = ((geom, (None if numpy.isnan(val) else val) if isinstance(val, float) else int(val) if numpy.issubdtype(type(val), numpy.integer) else float(val))
1181
+ for geom, val in zip(gdf.geometry, gdf[valueField]))
1182
+
1183
+ # burn
1184
+ features.rasterize(
1185
+ shapes=shapes,
1186
+ out_shape=raster.shape,
1187
+ transform=transform,
1188
+ fill=fillValue,
1189
+ out=raster,
1190
+ all_touched=allTouched,
1191
+ dtype=dtype
1192
+ )
1193
+
1194
+ # default profile settings
1195
+ profile = {
1196
+ "driver": "COG" if isCOG else "GTiff",
1197
+ "height": raster.shape[0],
1198
+ "width": raster.shape[1],
1199
+ "count": 1,
1200
+ "dtype": raster.dtype,
1201
+ "crs": gdf.crs,
1202
+ "transform": transform,
1203
+ "nodata": nodata,
1204
+ }
1205
+ # sane compression defaults
1206
+ if isCOG:
1207
+ profile.setdefault("compress", "LZW")
1208
+ profile.setdefault("blocksize", 512) # GDAL COG option via rasterio
1209
+ # overviews are handled by the COG driver
1210
+ else:
1211
+ profile.setdefault("compress", "LZW"),
1212
+ profile.setdefault("tiled", True)
1213
+ profile.setdefault("blockxsize", 512)
1214
+ profile.setdefault("blockysize", 512)
1215
+
1216
+ if profileOverrides:
1217
+ profile.update(profileOverrides)
1218
+
1219
+ # write
1220
+ with rasterio.open(outRasterFN, "w", **profile) as dst:
1221
+ dst.write(raster, 1)
1222
+
1223
+ return outRasterFN
1224
+
1225
+
1033
1226
  def moveDirectory(srcDir:str, destDir:str, v:bool = False) -> bool:
1034
1227
  '''
1035
1228
  this function moves all files from srcDir to destDir
@@ -2076,7 +2269,7 @@ def getTimeseriesStats(data:pandas.DataFrame, observed:Optional[str] = None, sim
2076
2269
 
2077
2270
  return stats
2078
2271
 
2079
- def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optional[int] = None, gis_id: Optional[int] = None, name: Optional[str] = None) -> Optional[pandas.DataFrame]:
2272
+ def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optional[int] = None, gis_id: Optional[int] = None, name: Optional[str] = None, coerceNumeric: bool = True) -> Optional[pandas.DataFrame]:
2080
2273
  '''
2081
2274
  Read SWAT+ output files and return a pandas DataFrame with proper date handling
2082
2275
  and optional filtering capabilities.
@@ -2138,8 +2331,15 @@ def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optio
2138
2331
  # Convert all columns to numeric except 'name' (which is string)
2139
2332
  for col in df.columns:
2140
2333
  if col != 'name':
2141
- df[col] = pandas.to_numeric(df[col], errors='coerce')
2334
+ if coerceNumeric:
2335
+ df[col] = pandas.to_numeric(df[col], errors='coerce')
2142
2336
 
2337
+ if not coerceNumeric:
2338
+ # If not coercing to numeric, ensure date columns are numeric
2339
+ for mandatoryCol in ['yr', 'mon', 'day', 'gis_id']:
2340
+ if mandatoryCol in df.columns:
2341
+ df[mandatoryCol] = pandas.to_numeric(df[mandatoryCol], errors='coerce')
2342
+
2143
2343
  # Create date column from yr, mon, day
2144
2344
  try:
2145
2345
  df['date'] = pandas.to_datetime(pandas.DataFrame({'year': df.yr, 'month': df.mon, 'day': df.day}))
@@ -2179,4 +2379,5 @@ def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optio
2179
2379
 
2180
2380
  return df
2181
2381
 
2382
+
2182
2383
  ignoreWarnings()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccfx
3
- Version: 1.0.9
3
+ Version: 1.1.0
4
4
  Summary: This package simplifies regular common actions for quick prototyping in a user friendly way
5
5
  Author-email: Celray James CHAWANDA <celray@chawanda.com>
6
6
  License-Expression: MIT
@@ -26,6 +26,9 @@ Requires-Dist: mutagen
26
26
  Requires-Dist: requests
27
27
  Requires-Dist: tqdm
28
28
  Requires-Dist: pillow
29
+ Requires-Dist: scipy
30
+ Requires-Dist: rasterio
31
+ Requires-Dist: matplotlib
29
32
  Dynamic: license-file
30
33
 
31
34
  # ccfx
@@ -1,11 +1,11 @@
1
1
  ccfx/__init__.py,sha256=UK62VcGS84SJyGVg1bK4FltZj7OkpdoyhoFWeXcKsX0,144
2
- ccfx/ccfx.py,sha256=doZ9tSJWRBAC3TKd6joThjDpivc6ef3bd1U5FuOVVh0,78988
2
+ ccfx/ccfx.py,sha256=st_zG-KXcqM9K2HjyontOwrR8FpZGbSZyHz0OvPHjoU,86682
3
3
  ccfx/excel.py,sha256=vm_cm4huKKx4_Nstr5neJzhBLmoZjg8qxjzz4hcF5hg,4754
4
4
  ccfx/mssqlConnection.py,sha256=C3HxzgZHmHy_de9EbMaXzR8NrkJxwHc8a00qzxQu_gs,8984
5
5
  ccfx/sqliteConnection.py,sha256=pOT9BBEAcm2kmoS0yBkUi4m9srQVe62J4xG5bnddvis,16207
6
6
  ccfx/word.py,sha256=AGa64jX5Zl5qotZh5L0QmrsjTnktIBhmj_ByRKZ88vw,3061
7
- ccfx-1.0.9.dist-info/licenses/LICENSE,sha256=EuxaawJg_OOCLfikkCGgfXPZmxR-x_5PH7_2e9M-3eA,1099
8
- ccfx-1.0.9.dist-info/METADATA,sha256=asJO3MTzoPwEhrQRcacRz1_GOIlD45X0tFu9tjb9-QE,11282
9
- ccfx-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- ccfx-1.0.9.dist-info/top_level.txt,sha256=_cSvSA1WX2K8TgoV3iBJUdUZZqMKJbOPLNnKLYSLHaw,5
11
- ccfx-1.0.9.dist-info/RECORD,,
7
+ ccfx-1.1.0.dist-info/licenses/LICENSE,sha256=EuxaawJg_OOCLfikkCGgfXPZmxR-x_5PH7_2e9M-3eA,1099
8
+ ccfx-1.1.0.dist-info/METADATA,sha256=hi23r9sqTM-JPXyRrkuGG-0yamVxPCZeJ1qNCPMB5I0,11353
9
+ ccfx-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ ccfx-1.1.0.dist-info/top_level.txt,sha256=_cSvSA1WX2K8TgoV3iBJUdUZZqMKJbOPLNnKLYSLHaw,5
11
+ ccfx-1.1.0.dist-info/RECORD,,
File without changes