Skip to content

Commit ad3d553

Browse files
authored
remove pandas < 3 requirement (#550)
* remove pandas<3 requirement * add nan to none conversion to avoid pandas 3.0 errors * coerce string dtypes in test network layers to object for comparison * fix doctest dtype issue
1 parent e0791b7 commit ad3d553

File tree

5 files changed

+37
-20
lines changed

5 files changed

+37
-20
lines changed

documentation/waternetworkmodel.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ The dataset is indexed by pipe name. The first 10 lines of the dataset are show
209209

210210
.. doctest::
211211

212-
>>> print(material.head(10))
212+
>>> material_normalized = material.astype('object')
213+
>>> print(material_normalized.head(10))
213214
20 Steel
214215
40 Cast iron
215216
50 HDPE

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
numpy>=2.2.6
33
scipy
44
networkx
5-
pandas<3.0
5+
pandas>=2.0
66
matplotlib
77
setuptools
88

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
MAINTAINER_EMAIL = 'kaklise@sandia.gov'
7373
LICENSE = 'Revised BSD'
7474
URL = 'https://github.com/USEPA/WNTR'
75-
DEPENDENCIES = ['numpy>=2.2.6 ', 'scipy', 'networkx', 'pandas<3.0', 'matplotlib', 'setuptools']
75+
DEPENDENCIES = ['numpy>=2.2.6 ', 'scipy', 'networkx', 'pandas>=2.0', 'matplotlib', 'setuptools']
7676

7777
# use README file as the long description
7878
file_dir = os.path.abspath(os.path.dirname(__file__))

wntr/network/io.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import json
1010
import networkx as nx
11+
import pandas as pd
1112

1213
import wntr.epanet
1314
from wntr.epanet.util import FlowUnits
@@ -23,6 +24,13 @@
2324
logger = logging.getLogger(__name__)
2425

2526

27+
def _nan_to_none(value):
28+
"""Convert NaN values to None for pandas 3.0 compatibility."""
29+
if pd.isna(value):
30+
return None
31+
return value
32+
33+
2634
def to_dict(wn) -> dict:
2735
"""
2836
Convert a WaterNetworkModel into a dictionary
@@ -120,12 +128,12 @@ def from_dict(d: dict, append=None):
120128
dl = node.setdefault("demand_timeseries_list")
121129
if dl is not None and len(dl) > 0:
122130
base_demand = dl[0].setdefault("base_val", 0.0)
123-
pattern_name = dl[0].setdefault("pattern_name")
124-
demand_category = dl[0].setdefault("category")
131+
pattern_name = _nan_to_none(dl[0].setdefault("pattern_name"))
132+
demand_category = _nan_to_none(dl[0].setdefault("category"))
125133
else:
126134
base_demand = node.setdefault('base_demand',0.0)
127-
pattern_name = node.setdefault('pattern_name')
128-
demand_category = node.setdefault('demand_category')
135+
pattern_name = _nan_to_none(node.setdefault('pattern_name'))
136+
demand_category = _nan_to_none(node.setdefault('demand_category'))
129137
wn.add_junction(
130138
name=name,
131139
base_demand=base_demand,
@@ -140,7 +148,7 @@ def from_dict(d: dict, append=None):
140148
j.minimum_pressure = node.setdefault("minimum_pressure")
141149
j.pressure_exponent = node.setdefault("pressure_exponent")
142150
j.required_pressure = node.setdefault("required_pressure")
143-
j.tag = node.setdefault("tag")
151+
j.tag = _nan_to_none(node.setdefault("tag"))
144152

145153
j._leak = node.setdefault("leak", False)
146154
j._leak_area = node.setdefault("leak_area", 0.0)
@@ -152,8 +160,8 @@ def from_dict(d: dict, append=None):
152160
if dl is not None and len(dl) > 1:
153161
for i in range(1, len(dl)):
154162
base_val = dl[i].setdefault("base_val", 0.0)
155-
pattern_name = dl[i].setdefault("pattern_name")
156-
category = dl[i].setdefault("category")
163+
pattern_name = _nan_to_none(dl[i].setdefault("pattern_name"))
164+
category = _nan_to_none(dl[i].setdefault("category"))
157165
j.add_demand(base_val, pattern_name, category)
158166
elif node["node_type"] == "Tank":
159167
coordinates = node.setdefault("coordinates")
@@ -166,7 +174,7 @@ def from_dict(d: dict, append=None):
166174
max_level=node.setdefault("max_level", node.setdefault("min_level", 0) + 10),
167175
diameter=node.setdefault("diameter", 0),
168176
min_vol=node.setdefault("min_vol", 0),
169-
vol_curve=node.setdefault("vol_curve_name"),
177+
vol_curve=_nan_to_none(node.setdefault("vol_curve_name")),
170178
overflow=node.setdefault("overflow", False),
171179
coordinates=coordinates,
172180
)
@@ -177,20 +185,20 @@ def from_dict(d: dict, append=None):
177185
if node.setdefault("mixing_model"):
178186
t.mixing_model = node.setdefault("mixing_model")
179187
t.bulk_coeff = node.setdefault("bulk_coeff")
180-
t.tag = node.setdefault("tag")
188+
t.tag = _nan_to_none(node.setdefault("tag"))
181189
# custom additional attributes
182190
for attr in list(set(node.keys()) - set(dir(t))):
183191
setattr( t, attr, node[attr] )
184192
elif node["node_type"] == "Reservoir":
185193
wn.add_reservoir(
186194
name,
187195
base_head=node.setdefault("base_head"),
188-
head_pattern=node.setdefault("head_pattern_name"),
196+
head_pattern=_nan_to_none(node.setdefault("head_pattern_name")),
189197
coordinates=node.setdefault("coordinates"),
190198
)
191199
r = wn.get_node(name)
192200
r.initial_quality = node.setdefault("initial_quality", 0.0)
193-
r.tag = node.setdefault("tag")
201+
r.tag = _nan_to_none(node.setdefault("tag"))
194202
# custom additional attributes
195203
for attr in list(set(node.keys()) - set(dir(r))):
196204
setattr( r, attr, node[attr] )
@@ -213,7 +221,7 @@ def from_dict(d: dict, append=None):
213221
)
214222
p = wn.get_link(name)
215223
p.bulk_coeff = link.setdefault("bulk_coeff")
216-
p.tag = link.setdefault("tag")
224+
p.tag = _nan_to_none(link.setdefault("tag"))
217225
p.vertices = link.setdefault("vertices", list())
218226
p.wall_coeff = link.setdefault("wall_coeff")
219227
# custom additional attributes
@@ -230,15 +238,15 @@ def from_dict(d: dict, append=None):
230238
if pump_type.lower() == "power"
231239
else link.setdefault("pump_curve_name"),
232240
speed=link.setdefault("base_speed", 1.0),
233-
pattern=link.setdefault("speed_pattern_name"),
241+
pattern=_nan_to_none(link.setdefault("speed_pattern_name")),
234242
initial_status=link.setdefault("initial_status", "OPEN"),
235243
)
236244
p = wn.get_link(name)
237-
p.efficiency_curve_name = link.setdefault("efficiency_curve_name")
238-
p.energy_pattern = link.setdefault("energy_pattern")
245+
p.efficiency_curve_name = _nan_to_none(link.setdefault("efficiency_curve_name"))
246+
p.energy_pattern = _nan_to_none(link.setdefault("energy_pattern"))
239247
p.energy_price = link.setdefault("energy_price")
240248
p.initial_setting = link.setdefault("initial_setting")
241-
p.tag = link.setdefault("tag")
249+
p.tag = _nan_to_none(link.setdefault("tag"))
242250
p.vertices = link.setdefault("vertices", list())
243251
# custom additional attributes
244252
for attr in list(set(link.keys()) - set(dir(p))):
@@ -257,7 +265,7 @@ def from_dict(d: dict, append=None):
257265
)
258266
v = wn.get_link(name)
259267
if valve_type.lower() == "gpv":
260-
v.headloss_curve_name = link.setdefault("headloss_curve_name")
268+
v.headloss_curve_name = _nan_to_none(link.setdefault("headloss_curve_name"))
261269
v.vertices = link.setdefault("vertices", list())
262270
# custom additional attributes
263271
for attr in list(set(link.keys()) - set(dir(v))):

wntr/tests/test_network_layers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def test_valve_layer_random(self):
3333
join(test_datadir, "valve_layer_random.csv"), index_col=0, dtype="object"
3434
)
3535

36+
# Normalize dtypes to object for comparison (pandas 3.0 uses StringDtype)
37+
valves = valves.astype({'link': 'object', 'node': 'object'})
38+
expected = expected.astype({'link': 'object', 'node': 'object'})
39+
3640
assert_frame_equal(valves, expected)
3741

3842
def test_valve_layer_strategic(self):
@@ -60,6 +64,10 @@ def test_valve_layer_strategic(self):
6064
dtype="object",
6165
)
6266

67+
# Normalize dtypes to object for comparison (pandas 3.0 uses StringDtype)
68+
valves = valves.astype({'link': 'object', 'node': 'object'})
69+
expected_valves = expected_valves.astype({'link': 'object', 'node': 'object'})
70+
6371
self.assertEqual(valves.shape[0], expected_n_valves[N])
6472
assert_frame_equal(valves, expected_valves, check_index_type=False)
6573

0 commit comments

Comments
 (0)