rashdf 0.8.3__tar.gz → 0.9.0__tar.gz

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.
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rashdf
3
- Version: 0.8.3
3
+ Version: 0.9.0
4
4
  Summary: Read data from HEC-RAS HDF files.
5
5
  Project-URL: repository, https://github.com/fema-ffrd/rashdf
6
6
  Classifier: Development Status :: 4 - Beta
7
7
  Classifier: Intended Audience :: Developers
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: h5py
@@ -7,12 +7,12 @@ classifiers = [
7
7
  "Intended Audience :: Developers",
8
8
  "License :: OSI Approved :: MIT License",
9
9
  "Programming Language :: Python :: 3",
10
- "Programming Language :: Python :: 3.9",
11
10
  "Programming Language :: Python :: 3.10",
12
11
  "Programming Language :: Python :: 3.11",
13
12
  "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
14
  ]
15
- version = "0.8.3"
15
+ version = "0.9.0"
16
16
  dependencies = ["h5py", "geopandas>=1.0,<2.0", "pyarrow", "xarray<=2025.4.0"]
17
17
 
18
18
  [project.optional-dependencies]
@@ -26,6 +26,7 @@ COMMANDS = [
26
26
  "reference_lines",
27
27
  "reference_points",
28
28
  "structures",
29
+ "generate_bridge_xs_lines",
29
30
  ]
30
31
 
31
32
 
@@ -27,6 +27,8 @@ from .utils import (
27
27
  convert_ras_hdf_value,
28
28
  get_first_hdf_group,
29
29
  hdf5_attrs_to_dict,
30
+ copy_lines_parallel,
31
+ experimental,
30
32
  )
31
33
 
32
34
 
@@ -474,6 +476,67 @@ class RasGeomHdf(RasHdf):
474
476
  def connections(self) -> GeoDataFrame: # noqa D102
475
477
  raise NotImplementedError
476
478
 
479
+ @experimental
480
+ def generate_bridge_xs_lines(self, datetime_to_str: bool = False) -> GeoDataFrame:
481
+ """[EXPERIMENTAL] Attempt to return the 2D bridge cross-section lines.
482
+
483
+ This method attempts to generate the cross-section lines for bridges modeled
484
+ within 2D mesh areas. It should be noted that these lines are not explicitly
485
+ stored within the HEC-RAS Geometry HDF file, and are instead generated based
486
+ on the bridge attributes and centerline geometry. As such, the accuracy of
487
+ these lines may vary depending on the complexity of the bridge geometry and
488
+ output from this method should be reviewed for accuracy.
489
+
490
+ Parameters
491
+ ----------
492
+ datetime_to_str : bool, optional
493
+ If True, convert datetime values to string format (default: False).
494
+
495
+ Returns
496
+ -------
497
+ GeoDataFrame
498
+ A GeoDataFrame containing the 2D bridge cross-section lines if they exist.
499
+ """
500
+ profile_info = self.get(self.GEOM_STRUCTURES_PATH + "/Table Info")
501
+ structs = self.structures().merge(
502
+ pd.DataFrame(profile_info[()] if profile_info is not None else None),
503
+ left_index=True,
504
+ right_index=True,
505
+ )
506
+ if structs.empty:
507
+ return GeoDataFrame()
508
+
509
+ bridges = structs[structs["Mode"] == "Bridge Opening"].copy()
510
+ if bridges.empty:
511
+ return GeoDataFrame()
512
+
513
+ inside_buffer_widths = bridges["Weir Width"] / 2
514
+ inside_bridge_xs = copy_lines_parallel(
515
+ bridges, inside_buffer_widths.values, "struct_id"
516
+ )
517
+ inside_bridge_xs["level"] = "inside"
518
+
519
+ outside_buffer_widths = inside_buffer_widths + bridges["Upstream Distance"]
520
+ outside_bridge_xs = copy_lines_parallel(
521
+ bridges, outside_buffer_widths.values, "struct_id"
522
+ )
523
+ outside_bridge_xs["level"] = "outside"
524
+
525
+ br_xs = GeoDataFrame(
526
+ pd.concat([inside_bridge_xs, outside_bridge_xs], ignore_index=True),
527
+ geometry="geometry",
528
+ )
529
+ br_xs.side = br_xs.side.apply(
530
+ lambda x: "upstream" if x == "right" else "downstream"
531
+ )
532
+
533
+ if datetime_to_str:
534
+ br_xs[self.LAST_EDITED_COLUMN] = br_xs[self.LAST_EDITED_COLUMN].apply(
535
+ lambda x: pd.Timestamp.isoformat(x)
536
+ )
537
+
538
+ return br_xs
539
+
477
540
  def ic_points(self) -> GeoDataFrame: # noqa D102
478
541
  """Return initial conditions points.
479
542
 
@@ -1178,7 +1178,7 @@ class RasPlanHdf(RasGeomHdf):
1178
1178
  xr.Dataset
1179
1179
  An xarray Dataset with timeseries output data for boundary conditions lines.
1180
1180
  """
1181
- df_bc_lines = self.bc_lines()
1181
+ df_bc_lines = super().bc_lines()
1182
1182
  bc_lines_names = df_bc_lines["name"]
1183
1183
  datasets = []
1184
1184
  for bc_line_name in bc_lines_names:
@@ -1197,6 +1197,57 @@ class RasPlanHdf(RasGeomHdf):
1197
1197
  )
1198
1198
  return ds
1199
1199
 
1200
+ def bc_lines(
1201
+ self, include_output: bool = True, datetime_to_str: bool = False
1202
+ ) -> GeoDataFrame:
1203
+ """Return the boundary condition lines from a HEC-RAS HDF plan file.
1204
+
1205
+ Optionally include summary output data for each boundary condition line.
1206
+
1207
+ Parameters
1208
+ ----------
1209
+ include_output : bool, optional
1210
+ If True, include summary output data in the GeoDataFrame. (default: True)
1211
+ datetime_to_str : bool, optional
1212
+ If True, convert datetime columns to strings. (default: False)
1213
+
1214
+ Returns
1215
+ -------
1216
+ GeoDataFrame
1217
+ A GeoDataFrame with boundary condition line geometry and summary output data.
1218
+ """
1219
+ gdf = super().bc_lines()
1220
+ if include_output is False:
1221
+ return gdf
1222
+
1223
+ ds = self.bc_lines_timeseries_output()
1224
+ summary = {
1225
+ "bc_line_id": ds.coords["bc_line_id"].values,
1226
+ "name": ds.coords["bc_line_name"].values,
1227
+ "mesh_name": ds.coords["mesh_name"].values,
1228
+ "type": ds.coords["bc_line_type"].values,
1229
+ }
1230
+
1231
+ for var in ds.data_vars:
1232
+ abbrev = "q" if var.lower() == "flow" else "ws"
1233
+ summary[f"max_{abbrev}"] = ds[var].max(dim="time").values
1234
+ summary[f"max_{abbrev}_time"] = (
1235
+ ds[var].time[ds[var].argmax(dim="time")].values
1236
+ )
1237
+ summary[f"min_{abbrev}"] = ds[var].min(dim="time").values
1238
+ summary[f"min_{abbrev}_time"] = (
1239
+ ds[var].time[ds[var].argmin(dim="time")].values
1240
+ )
1241
+
1242
+ gdf_with_output = gdf.merge(
1243
+ pd.DataFrame(summary),
1244
+ on=["bc_line_id", "name", "mesh_name", "type"],
1245
+ how="left",
1246
+ )
1247
+ return (
1248
+ df_datetimes_to_str(gdf_with_output) if datetime_to_str else gdf_with_output
1249
+ )
1250
+
1200
1251
  def observed_timeseries_input(self, vartype: str = "Flow") -> xr.DataArray:
1201
1252
  """Return observed timeseries input data for reference lines and points from a HEC-RAS HDF plan file.
1202
1253
 
@@ -8,6 +8,68 @@ from datetime import datetime, timedelta
8
8
  import re
9
9
  from typing import Any, Callable, List, Tuple, Union, Optional
10
10
  import warnings
11
+ from shapely import LineString, MultiLineString
12
+ import geopandas as gpd
13
+
14
+
15
+ def experimental(func) -> Callable:
16
+ """
17
+ Declare a function to be experimental.
18
+
19
+ This is a decorator which can be used to mark functions as experimental.
20
+ It will result in a warning being emitted when the function is used.
21
+
22
+ Parameters
23
+ ----------
24
+ func: The function to be declared experimental.
25
+
26
+ Returns
27
+ -------
28
+ The decorated function.
29
+ """
30
+
31
+ def new_func(*args, **kwargs):
32
+ warnings.warn(
33
+ f"{func.__name__} is experimental and could change in the future. Please review output carefully.",
34
+ category=UserWarning,
35
+ stacklevel=2,
36
+ )
37
+ return func(*args, **kwargs)
38
+
39
+ new_func.__name__ = func.__name__
40
+ new_func.__doc__ = func.__doc__
41
+ new_func.__dict__.update(func.__dict__)
42
+ return new_func
43
+
44
+
45
+ def deprecated(func) -> Callable:
46
+ """
47
+ Deprecate a function.
48
+
49
+ This is a decorator which can be used to mark functions as deprecated.
50
+ It will result in a warning being emitted when the function is used.
51
+
52
+ Parameters
53
+ ----------
54
+ func: The function to be deprecated.
55
+
56
+ Returns
57
+ -------
58
+ The decorated function.
59
+ """
60
+
61
+ def new_func(*args, **kwargs):
62
+ warnings.warn(
63
+ f"{func.__name__} is deprecated and will be removed in a future version.",
64
+ category=DeprecationWarning,
65
+ stacklevel=2,
66
+ )
67
+ return func(*args, **kwargs)
68
+
69
+ new_func.__name__ = func.__name__
70
+ new_func.__doc__ = func.__doc__
71
+ new_func.__dict__.update(func.__dict__)
72
+ return new_func
11
73
 
12
74
 
13
75
  def parse_ras_datetime_ms(datetime_str: str) -> datetime:
@@ -36,24 +98,37 @@ def parse_ras_datetime(datetime_str: str) -> datetime:
36
98
 
37
99
  Parameters
38
100
  ----------
39
- datetime_str (str): The datetime string to be parsed. The string should be in the format "ddMMMyyyy HH:mm:ss".
101
+ datetime_str (str): The datetime string to be parsed.
40
102
 
41
103
  Returns
42
104
  -------
43
105
  datetime: A datetime object representing the parsed datetime.
44
106
  """
45
- datetime_format = "%d%b%Y %H:%M:%S"
107
+ date_formats = ["%d%b%Y", "%m/%d/%Y", "%m-%d-%Y", "%Y/%m/%d", "%Y-%m-%d"]
108
+ time_formats = ["%H:%M:%S", "%H%M"]
109
+ datetime_formats = [
110
+ f"{date} {time}" for date in date_formats for time in time_formats
111
+ ]
46
112
 
47
- if datetime_str.endswith("24:00:00"):
48
- datetime_str = datetime_str.replace("24:00:00", "00:00:00")
49
- parsed_dt = datetime.strptime(datetime_str, datetime_format)
50
- parsed_dt += timedelta(days=1)
51
- else:
52
- parsed_dt = datetime.strptime(datetime_str, datetime_format)
113
+ is_2400 = datetime_str.endswith((" 24:00:00", " 2400", " 24:00"))
114
+ if is_2400:
115
+ datetime_str = datetime_str.split()[0] + " 00:00:00"
53
116
 
54
- return parsed_dt
117
+ last_exception = None
118
+ for fmt in datetime_formats:
119
+ try:
120
+ parsed_dt = datetime.strptime(datetime_str, fmt)
121
+ if is_2400:
122
+ parsed_dt += timedelta(days=1)
123
+ return parsed_dt
124
+ except ValueError as e:
125
+ last_exception = e
126
+ continue
127
+
128
+ raise ValueError(f"Invalid date format: {datetime_str}") from last_exception
55
129
 
56
130
 
131
+ @deprecated
57
132
  def parse_ras_simulation_window_datetime(datetime_str) -> datetime:
58
133
  """
59
134
  Parse a datetime string from a RAS simulation window into a datetime object.
@@ -139,32 +214,37 @@ def convert_ras_hdf_string(
139
214
  a list of datetime strings, a timedelta objects, or the original string
140
215
  if no other conditions are met.
141
216
  """
142
- ras_datetime_format1_re = r"\d{2}\w{3}\d{4} \d{2}:\d{2}:\d{2}"
143
- ras_datetime_format2_re = r"\d{2}\w{3}\d{4} \d{2}\d{2}"
144
217
  ras_duration_format_re = r"\d{2}:\d{2}:\d{2}"
218
+ date_patterns_re = [
219
+ r"\d{2}\w{3}\d{4}",
220
+ r"\d{2}/\d{2}/\d{4}",
221
+ r"\d{2}-\d{2}-\d{4}",
222
+ r"\d{4}/\d{2}/\d{2}",
223
+ r"\d{4}-\d{2}-\d{2}",
224
+ ]
225
+ time_patterns_re = [
226
+ r"\d{2}:\d{2}:\d{2}",
227
+ r"\d{4}",
228
+ ]
229
+ datetime_patterns_re = [
230
+ f"{date} {time}" for date in date_patterns_re for time in time_patterns_re
231
+ ]
145
232
  s = value.decode("utf-8")
146
233
  if s == "True":
147
234
  return True
148
235
  elif s == "False":
149
236
  return False
150
- elif re.match(rf"^{ras_datetime_format1_re}", s):
151
- if re.match(rf"^{ras_datetime_format1_re} to {ras_datetime_format1_re}$", s):
152
- split = s.split(" to ")
153
- return [
154
- parse_ras_datetime(split[0]),
155
- parse_ras_datetime(split[1]),
156
- ]
157
- return parse_ras_datetime(s)
158
- elif re.match(rf"^{ras_datetime_format2_re}", s):
159
- if re.match(rf"^{ras_datetime_format2_re} to {ras_datetime_format2_re}$", s):
160
- split = s.split(" to ")
161
- return [
162
- parse_ras_simulation_window_datetime(split[0]),
163
- parse_ras_simulation_window_datetime(split[1]),
164
- ]
165
- return parse_ras_simulation_window_datetime(s)
166
237
  elif re.match(rf"^{ras_duration_format_re}$", s):
167
238
  return parse_duration(s)
239
+ for dt_re in datetime_patterns_re:
240
+ if re.match(rf"^{dt_re}", s):
241
+ if re.match(rf"^{dt_re} to {dt_re}$", s):
242
+ start, end = s.split(" to ")
243
+ return [
244
+ parse_ras_datetime(start),
245
+ parse_ras_datetime(end),
246
+ ]
247
+ return parse_ras_datetime(s)
168
248
  return s
169
249
 
170
250
 
@@ -308,31 +388,129 @@ def ras_timesteps_to_datetimes(
308
388
  ]
309
389
 
310
390
 
311
- def deprecated(func) -> Callable:
391
+ def remove_line_ends(
392
+ geom: Union[LineString, MultiLineString],
393
+ ) -> Union[LineString, MultiLineString]:
312
394
  """
313
- Deprecate a function.
395
+ Remove endpoints from a LineString or each LineString in a MultiLineString if longer than 3 points.
314
396
 
315
- This is a decorator which can be used to mark functions as deprecated.
316
- It will result in a warning being emitted when the function is used.
397
+ Parameters
398
+ ----------
399
+ geom : LineString or MultiLineString
400
+ The geometry to trim.
401
+
402
+ Returns
403
+ -------
404
+ LineString or MultiLineString
405
+ The trimmed geometry, or original if not enough points to trim.
406
+ """
407
+ if isinstance(geom, LineString):
408
+ coords = list(geom.coords)
409
+ if len(coords) > 3:
410
+ return LineString(coords[1:-1])
411
+ return geom
412
+ elif isinstance(geom, MultiLineString):
413
+ trimmed = []
414
+ for line in geom.geoms:
415
+ coords = list(line.coords)
416
+ if len(coords) > 3:
417
+ trimmed.append(LineString(coords[1:-1]))
418
+ else:
419
+ trimmed.append(line)
420
+ return MultiLineString(trimmed)
421
+ return geom
422
+
423
+
424
+ def reverse_line(
425
+ line: Union[LineString, MultiLineString],
426
+ ) -> Union[LineString, MultiLineString]:
427
+ """
428
+ Reverse the order of coordinates in a LineString or each LineString in a MultiLineString.
317
429
 
318
430
  Parameters
319
431
  ----------
320
- func: The function to be deprecated.
432
+ line : LineString or MultiLineString
433
+ The geometry to reverse.
321
434
 
322
435
  Returns
323
436
  -------
324
- The decorated function.
437
+ LineString or MultiLineString
438
+ The reversed geometry.
325
439
  """
440
+ return (
441
+ MultiLineString([LineString(list(line.coords)[::-1]) for line in line.geoms])
442
+ if isinstance(line, MultiLineString)
443
+ else LineString(list(line.coords)[::-1])
444
+ )
445
+
446
+
447
+ def copy_lines_parallel(
448
+ lines: gpd.GeoDataFrame,
449
+ offset_ft: Union[np.ndarray, float],
450
+ id_col: str = "id",
451
+ ) -> gpd.GeoDataFrame:
452
+ """
453
+ Create parallel copies of line geometries offset to the left and right, then trim and erase overlaps.
326
454
 
327
- def new_func(*args, **kwargs):
328
- warnings.warn(
329
- f"{func.__name__} is deprecated and will be removed in a future version.",
330
- category=DeprecationWarning,
331
- stacklevel=2,
455
+ Parameters
456
+ ----------
457
+ lines : gpd.GeoDataFrame
458
+ GeoDataFrame containing line geometries.
459
+ offset_ft : float or np.ndarray
460
+ Offset distance (in feet) for parallel lines.
461
+ id_col : str
462
+ Name of the column containing unique structure IDs. Default is "id".
463
+
464
+ Returns
465
+ -------
466
+ gpd.GeoDataFrame
467
+ GeoDataFrame with trimmed, parallel left and right offset lines.
468
+ """
469
+ # Offset lines to the left
470
+ left = lines.copy()
471
+ offset_ft = offset_ft.astype(float)
472
+ left.geometry = lines.buffer(
473
+ offset_ft, cap_style="flat", single_sided=True, resolution=3
474
+ ).boundary
475
+ left["side"] = "left"
476
+
477
+ # Offset lines to the right (reverse direction first)
478
+ reversed_lines = lines.copy()
479
+ reversed_lines.geometry = reversed_lines.geometry.apply(reverse_line)
480
+ right = lines.copy()
481
+ right.geometry = reversed_lines.buffer(
482
+ offset_ft, cap_style="flat", single_sided=True, resolution=3
483
+ ).boundary.apply(reverse_line)
484
+ right["side"] = "right"
485
+
486
+ # Combine left and right boundaries
487
+ boundaries = pd.concat([left, right], ignore_index=True)
488
+ boundaries_gdf = gpd.GeoDataFrame(boundaries, crs=lines.crs, geometry="geometry")
489
+
490
+ # Erase buffer caps
491
+ erase_buffer = 0.1
492
+ cleaned_list = []
493
+ eraser = gpd.GeoDataFrame(
494
+ {
495
+ id_col: lines[id_col],
496
+ "geometry": lines.buffer(
497
+ offset_ft - erase_buffer, cap_style="square", resolution=3
498
+ ),
499
+ },
500
+ crs=lines.crs,
501
+ )
502
+ for id in lines[id_col].unique():
503
+ cleaned_list.append(
504
+ gpd.overlay(
505
+ boundaries_gdf[boundaries_gdf[id_col] == id],
506
+ eraser[eraser[id_col] == id],
507
+ how="difference",
508
+ )
332
509
  )
333
- return func(*args, **kwargs)
510
+ cleaned = gpd.GeoDataFrame(
511
+ pd.concat(cleaned_list, ignore_index=True), crs=lines.crs, geometry="geometry"
512
+ )
334
513
 
335
- new_func.__name__ = func.__name__
336
- new_func.__doc__ = func.__doc__
337
- new_func.__dict__.update(func.__dict__)
338
- return new_func
514
+ # trim ends
515
+ cleaned["geometry"] = cleaned["geometry"].apply(remove_line_ends)
516
+ return cleaned
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rashdf
3
- Version: 0.8.3
3
+ Version: 0.9.0
4
4
  Summary: Read data from HEC-RAS HDF files.
5
5
  Project-URL: repository, https://github.com/fema-ffrd/rashdf
6
6
  Classifier: Development Status :: 4 - Beta
7
7
  Classifier: Intended Audience :: Developers
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: h5py
@@ -17,6 +17,7 @@ TEST_JSON = TEST_DATA / "json"
17
17
  BALD_EAGLE_P18_REF = TEST_DATA / "ras/BaldEagleDamBrk.reflines-refpts.p18.hdf"
18
18
  LOWER_KANAWHA_P01_IC_POINTS = TEST_DATA / "ras/LowerKanawha.p01.icpoints.hdf"
19
19
  LOWER_KANAWHA_P01_IC_POINTS_JSON = TEST_JSON / "LowerKanawha.p01.icpoints.geojson"
20
+ ROSEBERRY_G01 = TEST_DATA / "ras/Roseberry_Creek.g01.hdf"
20
21
 
21
22
  TEST_ATTRS = {"test_attribute1": "test_str1", "test_attribute2": 500}
22
23
 
@@ -274,3 +275,16 @@ def test_ic_points():
274
275
  valid_gdf,
275
276
  check_dtype=False,
276
277
  )
278
+
279
+
280
+ def test_generate_bridge_xs_lines():
281
+ bridge_xs_json = TEST_JSON / "bridge_xs_lines.json"
282
+ with RasGeomHdf(ROSEBERRY_G01) as ghdf:
283
+ assert _gdf_matches_json(
284
+ ghdf.generate_bridge_xs_lines(datetime_to_str=True), bridge_xs_json
285
+ )
286
+
287
+
288
+ def test_generate_bridge_xs_lines_not_found():
289
+ with RasGeomHdf(MUNCIE_G05) as ghdf:
290
+ assert ghdf.generate_bridge_xs_lines().empty
@@ -733,3 +733,18 @@ def test_invalid_group_reference_summary_output():
733
733
  with RasPlanHdf(BALD_EAGLE_P18) as phdf:
734
734
  with pytest.raises(ValueError):
735
735
  phdf.reference_summary_output(reftype="Not supported type")
736
+
737
+
738
+ def test_bc_lines_include_output_false():
739
+ bc_lines_json = TEST_JSON / "bc_lines.json"
740
+ with RasPlanHdf(MUNCIE_G05) as plan_hdf:
741
+ assert _gdf_matches_json(plan_hdf.bc_lines(include_output=False), bc_lines_json)
742
+
743
+
744
+ def test_bc_lines_include_output_true():
745
+ bc_lines_with_output_json = TEST_JSON / "bc_lines_with_output.json"
746
+ with RasPlanHdf(LOWER_KANAWHA_P01_BC_LINES) as plan_hdf:
747
+ assert _gdf_matches_json(
748
+ plan_hdf.bc_lines(include_output=True, datetime_to_str=True),
749
+ bc_lines_with_output_json,
750
+ )
@@ -0,0 +1,231 @@
1
+ from src.rashdf import utils
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import pytest
6
+
7
+ from datetime import datetime, timedelta
8
+ from shapely.geometry import LineString, MultiLineString
9
+ import geopandas as gpd
10
+ from pathlib import Path
11
+
12
+ from . import _assert_geodataframes_close
13
+
14
+ TEST_DATA = Path("./tests/data")
15
+ TEST_JSON = TEST_DATA / "json"
16
+
17
+
18
+ def test_convert_ras_hdf_value():
19
+ assert utils.convert_ras_hdf_value(b"True") is True
20
+ assert utils.convert_ras_hdf_value(b"False") is False
21
+ assert utils.convert_ras_hdf_value(np.float32(1.23)) == pytest.approx(1.23)
22
+ assert utils.convert_ras_hdf_value(np.int32(123)) == 123
23
+ assert utils.convert_ras_hdf_value(b"15Mar2024 16:39:01") == datetime(
24
+ 2024, 3, 15, 16, 39, 1
25
+ )
26
+ assert utils.convert_ras_hdf_value(b"15Mar2024 24:00:00") == datetime(
27
+ 2024, 3, 16, 0, 0, 0
28
+ )
29
+ assert utils.convert_ras_hdf_value(b"15Mar2024 16:39:01 to 16Mar2024 16:39:01") == [
30
+ datetime(2024, 3, 15, 16, 39, 1),
31
+ datetime(2024, 3, 16, 16, 39, 1),
32
+ ]
33
+ assert utils.convert_ras_hdf_value(b"18Mar2024 24:00:00 to 19Mar2024 24:00:00") == [
34
+ datetime(2024, 3, 19, 0, 0, 0),
35
+ datetime(2024, 3, 20, 0, 0, 0),
36
+ ]
37
+ assert utils.convert_ras_hdf_value(b"01:23:45") == timedelta(
38
+ hours=1, minutes=23, seconds=45
39
+ )
40
+ assert utils.convert_ras_hdf_value(b"15Mar2024 2400") == datetime(
41
+ 2024, 3, 16, 0, 0, 0
42
+ )
43
+ assert utils.convert_ras_hdf_value(b"15Mar2024 2315") == datetime(
44
+ 2024, 3, 15, 23, 15, 0
45
+ )
46
+ assert utils.convert_ras_hdf_value(b"15Mar2024 2400") == datetime(
47
+ 2024, 3, 16, 0, 0, 0
48
+ )
49
+ assert utils.convert_ras_hdf_value(b"03/15/2024 2400") == datetime(
50
+ 2024, 3, 16, 0, 0, 0
51
+ )
52
+ assert utils.convert_ras_hdf_value(b"03-15-2024 2400") == datetime(
53
+ 2024, 3, 16, 0, 0, 0
54
+ )
55
+ assert utils.convert_ras_hdf_value(b"2024/03/15 2400") == datetime(
56
+ 2024, 3, 16, 0, 0, 0
57
+ )
58
+ assert utils.convert_ras_hdf_value(b"2024-03-15 2400") == datetime(
59
+ 2024, 3, 16, 0, 0, 0
60
+ )
61
+ assert utils.convert_ras_hdf_value(b"15Mar2024 0000") == datetime(
62
+ 2024, 3, 15, 0, 0, 0
63
+ )
64
+ assert utils.convert_ras_hdf_value(b"03/15/2024 0000") == datetime(
65
+ 2024, 3, 15, 0, 0, 0
66
+ )
67
+ assert utils.convert_ras_hdf_value(b"03-15-2024 0000") == datetime(
68
+ 2024, 3, 15, 0, 0, 0
69
+ )
70
+ assert utils.convert_ras_hdf_value(b"2024/03/15 0000") == datetime(
71
+ 2024, 3, 15, 0, 0, 0
72
+ )
73
+ assert utils.convert_ras_hdf_value(b"2024-03-15 0000") == datetime(
74
+ 2024, 3, 15, 0, 0, 0
75
+ )
76
+ assert utils.convert_ras_hdf_value(b"15Mar2024 23:59:59") == datetime(
77
+ 2024, 3, 15, 23, 59, 59
78
+ )
79
+ assert utils.convert_ras_hdf_value(b"03/15/2024 23:59:59") == datetime(
80
+ 2024, 3, 15, 23, 59, 59
81
+ )
82
+ assert utils.convert_ras_hdf_value(b"03-15-2024 23:59:59") == datetime(
83
+ 2024, 3, 15, 23, 59, 59
84
+ )
85
+ assert utils.convert_ras_hdf_value(b"2024/03/15 23:59:59") == datetime(
86
+ 2024, 3, 15, 23, 59, 59
87
+ )
88
+ assert utils.convert_ras_hdf_value(b"2024-03-15 23:59:59") == datetime(
89
+ 2024, 3, 15, 23, 59, 59
90
+ )
91
+ assert utils.convert_ras_hdf_value(b"15Mar2024 1639 to 16Mar2024 1639") == [
92
+ datetime(2024, 3, 15, 16, 39, 0),
93
+ datetime(2024, 3, 16, 16, 39, 0),
94
+ ]
95
+ assert utils.convert_ras_hdf_value(b"18Mar2024 2400 to 19Mar2024 2400") == [
96
+ datetime(2024, 3, 19, 0, 0, 0),
97
+ datetime(2024, 3, 20, 0, 0, 0),
98
+ ]
99
+
100
+
101
+ def test_df_datetimes_to_str():
102
+ df = pd.DataFrame(
103
+ {
104
+ "datetime": [
105
+ datetime(2024, 3, 15, 16, 39, 1),
106
+ datetime(2024, 3, 16, 16, 39, 1),
107
+ ],
108
+ "asdf": [
109
+ 0.123,
110
+ 0.456,
111
+ ],
112
+ }
113
+ )
114
+ assert df["datetime"].dtype.name == "datetime64[ns]"
115
+ df = utils.df_datetimes_to_str(df)
116
+ assert df["datetime"].dtype.name == "object"
117
+ assert df["datetime"].tolist() == ["2024-03-15T16:39:01", "2024-03-16T16:39:01"]
118
+ assert df["asdf"].tolist() == [0.123, 0.456]
119
+
120
+
121
+ def test_parse_ras_datetime_ms():
122
+ assert utils.parse_ras_datetime_ms("15Mar2024 16:39:01.123") == datetime(
123
+ 2024, 3, 15, 16, 39, 1, 123000
124
+ )
125
+ assert utils.parse_ras_datetime_ms("15Mar2024 24:00:00.000") == datetime(
126
+ 2024, 3, 16, 0, 0, 0, 0
127
+ )
128
+
129
+
130
+ def test_trim_line():
131
+ gdf = gpd.GeoDataFrame(
132
+ {
133
+ "id": [1, 2, 3],
134
+ "geometry": [
135
+ LineString([(0, 0), (5, 5), (10, 10)]),
136
+ LineString([(0, 0), (5, 5), (10, 10), (15, 15)]),
137
+ MultiLineString(
138
+ [
139
+ [(0, 0), (3, 3)],
140
+ [(3, 3), (6, 6), (9, 9)],
141
+ [(3, 3), (6, 6), (9, 9), (12, 12), (15, 15)],
142
+ ]
143
+ ),
144
+ ],
145
+ },
146
+ )
147
+ assert gdf.geometry.apply(utils.remove_line_ends).equals(
148
+ gpd.GeoSeries(
149
+ [
150
+ LineString([(0, 0), (5, 5), (10, 10)]),
151
+ LineString(
152
+ [
153
+ (5, 5),
154
+ (10, 10),
155
+ ]
156
+ ),
157
+ MultiLineString(
158
+ [
159
+ [(0, 0), (3, 3)],
160
+ [(3, 3), (6, 6), (9, 9)],
161
+ [(6, 6), (9, 9), (12, 12)],
162
+ ]
163
+ ),
164
+ ]
165
+ )
166
+ )
167
+
168
+
169
+ def test_reverse_line():
170
+ gdf = gpd.GeoDataFrame(
171
+ {
172
+ "id": [1, 2, 3],
173
+ "geometry": [
174
+ LineString([(0, 0), (5, 5), (10, 10)]),
175
+ LineString([(0, 0), (5, 5), (10, 10), (15, 15)]),
176
+ MultiLineString(
177
+ [
178
+ [(0, 0), (3, 3)],
179
+ [(3, 3), (6, 6), (9, 9)],
180
+ [(3, 3), (6, 6), (9, 9), (12, 12), (15, 15)],
181
+ ]
182
+ ),
183
+ ],
184
+ },
185
+ )
186
+ assert gdf.geometry.apply(utils.reverse_line).equals(
187
+ gpd.GeoSeries(
188
+ [
189
+ LineString([(10, 10), (5, 5), (0, 0)]),
190
+ LineString([(15, 15), (10, 10), (5, 5), (0, 0)]),
191
+ MultiLineString(
192
+ [
193
+ [(3, 3), (0, 0)],
194
+ [(9, 9), (6, 6), (3, 3)],
195
+ [(15, 15), (12, 12), (9, 9), (6, 6), (3, 3)],
196
+ ]
197
+ ),
198
+ ]
199
+ )
200
+ )
201
+
202
+
203
+ def test_copy_lines_parallel():
204
+ gdf = gpd.GeoDataFrame(
205
+ {
206
+ "id": [1, 2, 3],
207
+ "geometry": [
208
+ LineString([(0, 0), (5, 5), (10, 10)]),
209
+ LineString([(20, 20), (30, 30), (40, 40), (50, 50)]),
210
+ MultiLineString(
211
+ [
212
+ [(100.0, 100.0), (103.0, 103.0)],
213
+ [(103.0, 103.0), (106.0, 106.0), (109.0, 109.0)],
214
+ [
215
+ (103.0, 103.0),
216
+ (106.0, 106.0),
217
+ (109.0, 109.0),
218
+ (112.0, 112.0),
219
+ (115.0, 115.0),
220
+ ],
221
+ ]
222
+ ),
223
+ ],
224
+ },
225
+ )
226
+ offsets = np.array([1, 2, 3])
227
+ copied = utils.copy_lines_parallel(gdf, offsets)
228
+ expected = gpd.read_file(TEST_JSON / "copy_lines_parallel.json").set_crs(
229
+ None, allow_override=True
230
+ )
231
+ _assert_geodataframes_close(copied, expected, tol=1e-3)
@@ -1,75 +0,0 @@
1
- from src.rashdf import utils
2
-
3
- import numpy as np
4
- import pandas as pd
5
- import pytest
6
-
7
- from datetime import datetime, timedelta
8
-
9
-
10
- def test_convert_ras_hdf_value():
11
- assert utils.convert_ras_hdf_value(b"True") is True
12
- assert utils.convert_ras_hdf_value(b"False") is False
13
- assert utils.convert_ras_hdf_value(np.float32(1.23)) == pytest.approx(1.23)
14
- assert utils.convert_ras_hdf_value(np.int32(123)) == 123
15
- assert utils.convert_ras_hdf_value(b"15Mar2024 16:39:01") == datetime(
16
- 2024, 3, 15, 16, 39, 1
17
- )
18
- assert utils.convert_ras_hdf_value(b"15Mar2024 24:00:00") == datetime(
19
- 2024, 3, 16, 0, 0, 0
20
- )
21
- assert utils.convert_ras_hdf_value(b"15Mar2024 16:39:01 to 16Mar2024 16:39:01") == [
22
- datetime(2024, 3, 15, 16, 39, 1),
23
- datetime(2024, 3, 16, 16, 39, 1),
24
- ]
25
- assert utils.convert_ras_hdf_value(b"18Mar2024 24:00:00 to 19Mar2024 24:00:00") == [
26
- datetime(2024, 3, 19, 0, 0, 0),
27
- datetime(2024, 3, 20, 0, 0, 0),
28
- ]
29
- assert utils.convert_ras_hdf_value(b"01:23:45") == timedelta(
30
- hours=1, minutes=23, seconds=45
31
- )
32
- assert utils.convert_ras_hdf_value(b"15Mar2024 2400") == datetime(
33
- 2024, 3, 16, 0, 0, 0
34
- )
35
- assert utils.convert_ras_hdf_value(b"15Mar2024 2315") == datetime(
36
- 2024, 3, 15, 23, 15, 0
37
- )
38
-
39
- assert utils.convert_ras_hdf_value(b"15Mar2024 1639 to 16Mar2024 1639") == [
40
- datetime(2024, 3, 15, 16, 39, 0),
41
- datetime(2024, 3, 16, 16, 39, 0),
42
- ]
43
- assert utils.convert_ras_hdf_value(b"18Mar2024 2400 to 19Mar2024 2400") == [
44
- datetime(2024, 3, 19, 0, 0, 0),
45
- datetime(2024, 3, 20, 0, 0, 0),
46
- ]
47
-
48
-
49
- def test_df_datetimes_to_str():
50
- df = pd.DataFrame(
51
- {
52
- "datetime": [
53
- datetime(2024, 3, 15, 16, 39, 1),
54
- datetime(2024, 3, 16, 16, 39, 1),
55
- ],
56
- "asdf": [
57
- 0.123,
58
- 0.456,
59
- ],
60
- }
61
- )
62
- assert df["datetime"].dtype.name == "datetime64[ns]"
63
- df = utils.df_datetimes_to_str(df)
64
- assert df["datetime"].dtype.name == "object"
65
- assert df["datetime"].tolist() == ["2024-03-15T16:39:01", "2024-03-16T16:39:01"]
66
- assert df["asdf"].tolist() == [0.123, 0.456]
67
-
68
-
69
- def test_parse_ras_datetime_ms():
70
- assert utils.parse_ras_datetime_ms("15Mar2024 16:39:01.123") == datetime(
71
- 2024, 3, 15, 16, 39, 1, 123000
72
- )
73
- assert utils.parse_ras_datetime_ms("15Mar2024 24:00:00.000") == datetime(
74
- 2024, 3, 16, 0, 0, 0, 0
75
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes