From cd5d8db7a88fa85826e667ad8282ae45ebe4b840 Mon Sep 17 00:00:00 2001 From: Dobson Date: Wed, 13 Mar 2024 12:16:00 +0000 Subject: [PATCH 01/15] Create demo_config.yml --- tests/test_data/demo_config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/test_data/demo_config.yml diff --git a/tests/test_data/demo_config.yml b/tests/test_data/demo_config.yml new file mode 100644 index 00000000..f4572339 --- /dev/null +++ b/tests/test_data/demo_config.yml @@ -0,0 +1 @@ +# placeholder \ No newline at end of file From 8f9b4815c90d1892096b49870fb4e02d7748749b Mon Sep 17 00:00:00 2001 From: Dobson Date: Wed, 13 Mar 2024 15:08:55 +0000 Subject: [PATCH 02/15] Update swmmanywhere and various file addresses/tests --- swmmanywhere/geospatial_utilities.py | 12 +-- swmmanywhere/graph_utilities.py | 38 ++++++++-- swmmanywhere/metric_utilities.py | 32 ++++++++ swmmanywhere/parameters.py | 25 +++---- swmmanywhere/post_processing.py | 21 +++--- swmmanywhere/swmmanywhere.py | 108 +++++++++++++++++++++++++++ tests/test_data/demo_config.yml | 61 ++++++++++++++- tests/test_geospatial_utilities.py | 5 +- tests/test_graph_utilities.py | 8 +- tests/test_post_processing.py | 21 ++++-- tests/test_preprocessing.py | 4 +- 11 files changed, 281 insertions(+), 54 deletions(-) diff --git a/swmmanywhere/geospatial_utilities.py b/swmmanywhere/geospatial_utilities.py index c0b0021e..18d21ea9 100644 --- a/swmmanywhere/geospatial_utilities.py +++ b/swmmanywhere/geospatial_utilities.py @@ -751,20 +751,23 @@ def edges_to_features(G: nx.Graph): return features def graph_to_geojson(graph: nx.Graph, - fid: Path, + fid_nodes: Path, + fid_edges: Path, crs: str): """Write a graph to a GeoJSON file. Args: graph (nx.Graph): The input graph. - fid (Path): The filepath to save the GeoJSON file. + fid_nodes (Path): The filepath to save the nodes GeoJSON file. + fid_edges (Path): The filepath to save the edges GeoJSON file. crs (str): The CRS of the graph. """ graph = graph.copy() nodes = nodes_to_features(graph) edges = edges_to_features(graph) - for iterable, label in zip([nodes, edges], ['nodes', 'edges']): + for iterable, fid in zip([nodes, edges], + [fid_nodes, fid_edges]): geojson = { 'type': 'FeatureCollection', 'features' : iterable, @@ -775,7 +778,6 @@ def graph_to_geojson(graph: nx.Graph, } } } - fid_ = fid.with_stem(fid.stem + f'_{label}').with_suffix('.geojson') - with fid_.open('w') as output_file: + with fid.open('w') as output_file: json.dump(geojson, output_file, indent=2) \ No newline at end of file diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index 72abbdf0..30e8fd95 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -157,6 +157,28 @@ def get_osmid_id(data: dict) -> Hashable: id_ = id_[0] return id_ +def iterate_graphfcns(G: nx.Graph, + graphfcn_list: list[str], + params: dict, + addresses: parameters.FilePaths) -> nx.Graph: + """Iterate a list of graph functions over a graph. + + Args: + G (nx.Graph): The graph to iterate over. + graphfcn_list (list[str]): A list of graph functions to iterate. + params (dict): A dictionary of parameters to pass to the graph + functions. + addresses (parameters.FilePaths): A FilePaths parameter object + + Returns: + nx.Graph: The graph after the graph functions have been applied. + """ + for function in graphfcn_list: + assert function in graphfcns.keys(), \ + logger.error(f"Function {function} not registered in graphfcns.") + G = graphfcns[function](G, addresses = addresses, **params) + return G + @register_graphfcn class assign_id(BaseGraphFunction, required_edge_attributes = ['osmid'], @@ -451,7 +473,7 @@ def __call__(self, G: nx.Graph, @register_graphfcn class set_elevation(BaseGraphFunction, required_node_attributes = ['x', 'y'], - adds_node_attributes = ['elevation']): + adds_node_attributes = ['surface_elevation']): """set_elevation class.""" def __call__(self, G: nx.Graph, @@ -477,12 +499,12 @@ def __call__(self, G: nx.Graph, y, addresses.elevation) elevations_dict = {id_: elev for id_, elev in zip(G.nodes, elevations)} - nx.set_node_attributes(G, elevations_dict, 'elevation') + nx.set_node_attributes(G, elevations_dict, 'surface_elevation') return G @register_graphfcn class set_surface_slope(BaseGraphFunction, - required_node_attributes = ['elevation'], + required_node_attributes = ['surface_elevation'], adds_edge_attributes = ['surface_slope']): """set_surface_slope class.""" @@ -502,7 +524,8 @@ def __call__(self, G: nx.Graph, """ G = G.copy() # Compute the slope for each edge - slope_dict = {(u, v, k): (G.nodes[u]['elevation'] - G.nodes[v]['elevation']) + slope_dict = {(u, v, k): (G.nodes[u]['surface_elevation'] - \ + G.nodes[v]['surface_elevation']) / d['length'] for u, v, k, d in G.edges(data=True, keys=True)} @@ -977,8 +1000,9 @@ def process_successors(G: nx.Graph, @register_graphfcn class pipe_by_pipe(BaseGraphFunction, - required_edge_attributes = ['length', 'elevation'], - required_node_attributes = ['contributing_area', 'elevation'], + required_edge_attributes = ['length'], + required_node_attributes = ['contributing_area', + 'surface_elevation'], adds_edge_attributes = ['diameter'], adds_node_attributes = ['chamber_floor_elevation']): """pipe_by_pipe class.""" @@ -1015,7 +1039,7 @@ def __call__(self, G (nx.Graph): A graph """ G = G.copy() - surface_elevations = {n : d['elevation'] for n, d in G.nodes(data=True)} + surface_elevations = {n : d['surface_elevation'] for n, d in G.nodes(data=True)} topological_order = list(nx.topological_sort(G)) chamber_floor = {} edge_diams: dict[tuple[Hashable,Hashable,int],float] = {} diff --git a/swmmanywhere/metric_utilities.py b/swmmanywhere/metric_utilities.py index 77b76eae..5c2d850d 100644 --- a/swmmanywhere/metric_utilities.py +++ b/swmmanywhere/metric_utilities.py @@ -55,6 +55,38 @@ def __getattr__(self, name): metrics = MetricRegistry() +def iterate_metrics(synthetic_results: pd.DataFrame, + synthetic_subs: gpd.GeoDataFrame, + synthetic_G: nx.Graph, + real_results: pd.DataFrame, + real_subs: gpd.GeoDataFrame, + real_G: nx.Graph, + metric_list: list[str]) -> list[float]: + """Iterate a list of metrics over a graph. + + Args: + synthetic_results (pd.DataFrame): The synthetic results. + synthetic_subs (gpd.GeoDataFrame): The synthetic subcatchments. + synthetic_G (nx.Graph): The synthetic graph. + real_results (pd.DataFrame): The real results. + real_subs (gpd.GeoDataFrame): The real subcatchments. + real_G (nx.Graph): The real graph. + metric_list (list[str]): A list of metrics to iterate. + + Returns: + list[float]: The results of the metrics. + """ + results = [] + for metric in metric_list: + assert metric in metrics.keys(), f"Metric {metric} not registered in metrics." + results.append(metrics[metric](synthetic_results = synthetic_results, + synthetic_subs = synthetic_subs, + synthetic_G = synthetic_G, + real_results = real_results, + real_subs = real_subs, + real_G = real_G)) + return pd.DataFrame(results) + def extract_var(df: pd.DataFrame, var: str) -> pd.DataFrame: """Extract var from a dataframe.""" diff --git a/swmmanywhere/parameters.py b/swmmanywhere/parameters.py index 0ebad60d..3f9615c3 100644 --- a/swmmanywhere/parameters.py +++ b/swmmanywhere/parameters.py @@ -129,18 +129,6 @@ def check_weights(cls, values): raise ValueError(f"Missing {weight}_exponent") return values -# TODO move this to tests and run it if we're happy with this way of doing things -class NewTopo(TopologyDerivation): - """Demo for changing weights that should break the validator.""" - weights: list = Field(default = ['chahinian_slope', - 'chahinian_angle', - 'length', - 'contributing_area', - 'test'], - min_items = 1, - unit = "-", - description = "Weights for topo derivation") - class HydraulicDesign(BaseModel): """Parameters for hydraulic design.""" diameters: list = Field(default = np.linspace(0.15,3,int((3-0.15)/0.075) + 1), @@ -249,8 +237,17 @@ def _generate_bbox(self): def _generate_model(self): return self._generate_property(f'model_{self.model_number}', 'bbox') + def _generate_inp(self): + return self._generate_property(f'model_{self.model_number}.inp', + 'model') def _generate_subcatchments(self): - return self._generate_property(f'subcatchments.{self.extension}', + return self._generate_property(f'subcatchments.geo{self.extension}', + 'model') + def _generate_nodes(self): + return self._generate_property(f'nodes.geo{self.extension}', + 'model') + def _generate_edges(self): + return self._generate_property(f'edges.geo{self.extension}', 'model') def _generate_download(self): return self._generate_property('download', @@ -264,7 +261,7 @@ def _generate_street(self): def _generate_elevation(self): return self._generate_property('elevation.tif', 'download') def _generate_building(self): - return self._generate_property(f'building.{self.extension}', + return self._generate_property(f'building.geo{self.extension}', 'download') def _generate_precipitation(self): return self._generate_property(f'precipitation.{self.extension}', diff --git a/swmmanywhere/post_processing.py b/swmmanywhere/post_processing.py index 45c3f905..ca701d81 100644 --- a/swmmanywhere/post_processing.py +++ b/swmmanywhere/post_processing.py @@ -16,12 +16,14 @@ import pandas as pd import yaml +from swmmanywhere.parameters import FilePaths -def synthetic_write(model_dir: Path): + +def synthetic_write(addresses: FilePaths): """Load synthetic data and write to SWMM input file. Loads nodes, edges and subcatchments from synthetic data, assumes that - these are all located in `model_dir`. Fills in appropriate default values + these are all located in `addresses`. Fills in appropriate default values for many SWMM parameters. More parameters are available to edit (see defs/swmm_conversion.yml). Identifies outfalls and automatically ensures that they have only one link to them (as is required by SWMM). Formats @@ -29,14 +31,13 @@ def synthetic_write(model_dir: Path): a SWMM input (.inp) file. Args: - model_dir (str): model directory address. Assumes a format along the - lines of 'model_2', where the number is the model number. + addresses (FilePaths): A dictionary of file paths. """ # TODO these node/edge names are probably not good or extendible defulats # revisit once overall software architecture is more clear. - nodes = gpd.read_file(model_dir / 'pipe_by_pipe_nodes.geojson') - edges = gpd.read_file(model_dir / 'pipe_by_pipe_edges.geojson') - subs = gpd.read_file(model_dir / 'subcatchments.geojson') + nodes = gpd.read_file(addresses.nodes) + edges = gpd.read_file(addresses.edges) + subs = gpd.read_file(addresses.subcatchments) # Extract SWMM relevant data edges = edges[['u','v','diameter','length']] @@ -111,10 +112,6 @@ def synthetic_write(model_dir: Path): existing_input_file = Path(__file__).parent / 'defs' /\ 'basic_drainage_all_bits.inp' - # New input file - model_number = model_dir.name.split('_')[-1] - new_input_file = model_dir / f'model_{model_number}.inp' - # Format to dict data_dict = format_to_swmm_dict(nodes, outfalls, @@ -124,7 +121,7 @@ def synthetic_write(model_dir: Path): symbol) # Write new input file - data_dict_to_inp(data_dict, existing_input_file, new_input_file) + data_dict_to_inp(data_dict, existing_input_file, addresses.inp) def overwrite_section(data: np.ndarray, diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index a8afb1db..5cf72f5e 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -5,9 +5,117 @@ """ from pathlib import Path +import geopandas as gpd import pandas as pd import pyswmm +import yaml +import swmmanywhere.geospatial_utilities as go +from swmmanywhere import preprocessing +from swmmanywhere.graph_utilities import iterate_graphfcns, load_graph +from swmmanywhere.logging import logger +from swmmanywhere.metric_utilities import iterate_metrics +from swmmanywhere.parameters import get_full_parameters +from swmmanywhere.post_processing import synthetic_write + + +def swmmanywhere(config_: Path | dict): + """Run a SWMM model and store the results. + + Args: + config_ (Path | dict): The path to the configuration file, or the loaded + file as a dict. + + Returns: + pd.DataFrame: A DataFrame containing the results. + """ + # Load the configuration + if isinstance(config_, Path): + config = load_config(config_) + else: + assert isinstance(config_, dict), \ + logger.error("config must be a Path or a dict.") + config = config_ + # Create the project structure + addresses = preprocessing.create_project_structure(config['bbox'], + config['project'], + Path(config['base_dir']) + ) + + for address_override in config['address_overrides']: + addresses[address_override] = config['address_overrides'][address_override] + + # Run downloads + preprocessing.run_downloads(config['bbox'], + addresses, + config['api_keys'] + ) + + # Identify the starting graph + if config['starting_graph']: + G = load_graph(config['starting_graph']) + else: + G = preprocessing.create_starting_graph(addresses) + + # Load the parameters and perform any manual overrides + parameters = get_full_parameters() + for category, overrides in config['parameter_overrides'].items(): + for key, val in overrides.items(): + setattr(parameters[category], key, val) + + # Iterate the graph functions + G = iterate_graphfcns(G, + config['graphfcns'], + parameters, + addresses) + + # Save the final graph + go.graph_to_geojson(G, + addresses.nodes, + addresses.edges, + G.graph['crs'] + ) + + # Write to .inp + synthetic_write(addresses.model) + + # Run the model + synthetic_results = run(addresses.inp, + **config['run_settings']) + + # Get the real results + if config['real']['results']: + # TODO.. bit messy + real_results = pd.read_parquet(config['real']['results']) + elif config['real']['inp']: + real_results = run(config['real']['inp'], + **config['run_settings']) + else: + logger.info("No real network provided, returning SWMM .inp file.") + return addresses.inp + + # Iterate the metrics + metrics = iterate_metrics(synthetic_results, + gpd.read_file(addresses.subcatchments), + G, + real_results, + gpd.read_file(config['real']['subcatchments']), + load_graph(config['real']['graph']), + config['metric_list']) + + return metrics + +def load_config(config: Path): + """Load a configuration file. + + Args: + config (Path): The path to the configuration file. + + Returns: + dict: The configuration. + """ + with config.open('r') as f: + return yaml.safe_load(f) def run(model: Path, reporting_iters: int = 50, diff --git a/tests/test_data/demo_config.yml b/tests/test_data/demo_config.yml index f4572339..d877aadd 100644 --- a/tests/test_data/demo_config.yml +++ b/tests/test_data/demo_config.yml @@ -1 +1,60 @@ -# placeholder \ No newline at end of file +base_dir: /path/to/base/directory +project: demo +bbox: [0.04020, 51.55759, 0.09826, 51.62050] +api_keys: /path/to/api/keys.yml +run_settings: + reporting_iters: 100 + duration: 86400 + storevars: [flooding, flow] +real: + inp: /path/to/real/model.inp + graph: /path/to/real/graph.json + subcatchments: /path/to/real/subcatchments.geojson + results: null +starting_graph: null +graphfcn_list: + - assign_id + - format_osmnx_lanes + - double_directed + - split_long_edges + - calculate_contributing_area + - set_elevation + - set_surface_slope + - set_chahinian_slope + - set_chahinan_angle + - calculate_weights + - identify_outlets + - derive_topology + - pipe_by_pipe +metric_list: + - nc_deltacon0 + - nc_laplacian_dist + - nc_laplacian_norm_dist + - nc_adjacency_dist + - nc_vertex_edge_distance + - nc_resistance_distance + - bias_flood_depth + - kstest_edge_betweenness + - kstest_betweenness + - outlet_nse_flow + - outlet_nse_flooding +parameter_overrides: + hydraulic_design: + diameters: + - 0.15 + - 0.225 + - 0.3 + - 0.375 + - 0.45 + - 0.525 + - 0.6 + - 0.675 + - 0.75 + - 0.825 + - 0.9 + - 1.125 + - 1.35 + - 1.5 + - 1.95 + - 3.0 +address_overrides: null \ No newline at end of file diff --git a/tests/test_geospatial_utilities.py b/tests/test_geospatial_utilities.py index 5c8a1017..6e6c048b 100644 --- a/tests/test_geospatial_utilities.py +++ b/tests/test_geospatial_utilities.py @@ -378,7 +378,10 @@ def test_graph_to_geojson(): crs = G.graph['crs'] with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - go.graph_to_geojson(G, temp_path / 'graph.geojson', crs) + go.graph_to_geojson(G, + temp_path / 'graph_nodes.geojson', + temp_path / 'graph_edges.geojson', + crs) gdf = gpd.read_file(temp_path / 'graph_nodes.geojson') assert gdf.crs == crs assert gdf.shape[0] == len(G.nodes) diff --git a/tests/test_graph_utilities.py b/tests/test_graph_utilities.py index 21925fac..097afcf0 100644 --- a/tests/test_graph_utilities.py +++ b/tests/test_graph_utilities.py @@ -116,9 +116,9 @@ def test_set_elevation_and_slope(): addresses.elevation = Path(__file__).parent / 'test_data' / 'elevation.tif' G = gu.set_elevation(G, addresses) for id_, data in G.nodes(data=True): - assert 'elevation' in data.keys() - assert math.isfinite(data['elevation']) - assert data['elevation'] > 0 + assert 'surface_elevation' in data.keys() + assert math.isfinite(data['surface_elevation']) + assert data['surface_elevation'] > 0 G = gu.set_surface_slope(G) for u, v, data in G.edges(data=True): @@ -228,7 +228,7 @@ def test_pipe_by_pipe(): """Test the pipe_by_pipe function.""" G = load_graph(Path(__file__).parent / 'test_data' / 'graph_topo_derived.json') for ix, (u,d) in enumerate(G.nodes(data=True)): - d['elevation'] = ix + d['surface_elevation'] = ix d['contributing_area'] = ix params = parameters.HydraulicDesign() diff --git a/tests/test_post_processing.py b/tests/test_post_processing.py index ff3f4020..57bf5c51 100644 --- a/tests/test_post_processing.py +++ b/tests/test_post_processing.py @@ -9,6 +9,7 @@ import pyswmm from shapely import geometry as sgeom +from swmmanywhere import parameters from swmmanywhere import post_processing as stt fid = Path(__file__).parent.parent / 'swmmanywhere' / 'defs' /\ @@ -131,25 +132,29 @@ def test_synthetic_write(): data_dict = generate_data_dict() with tempfile.TemporaryDirectory() as model_dir: model_dir = Path(model_dir) - model_dir = model_dir / 'model_1' - model_dir.mkdir() + addresses = parameters.FilePaths(base_dir = model_dir, + project_name = 'test', + bbox_number = 1, + extension = 'json', + model_number = 0) + addresses.model.mkdir(parents=True, exist_ok=True) # Write the model with synthetic_write nodes = gpd.GeoDataFrame(data_dict['nodes']) nodes.geometry = gpd.points_from_xy(nodes.x, nodes.y) - nodes.to_file(model_dir / 'pipe_by_pipe_nodes.geojson') + nodes.to_file(addresses.nodes) nodes = nodes.set_index('id') edges = gpd.GeoDataFrame(pd.DataFrame(data_dict['conduits']).iloc[[0]]) edges.geometry = [sgeom.LineString([nodes.loc[u,'geometry'], nodes.loc[v,'geometry']]) for u,v in zip(edges.u, edges.v)] - edges.to_file(model_dir / 'pipe_by_pipe_edges.geojson') + edges.to_file(addresses.edges) subs = data_dict['subs'].copy() subs['subcatchment'] = ['node1'] - subs.to_file(model_dir / 'subcatchments.geojson') - stt.synthetic_write(model_dir) + subs.to_file(addresses.subcatchments) + stt.synthetic_write(addresses) # Write the model with data_dict_to_inp - comparison_file = model_dir / "model_base.inp" + comparison_file = addresses.model / "model_base.inp" template_fid = Path(__file__).parent.parent / 'swmmanywhere' / 'defs' /\ 'basic_drainage_all_bits.inp' stt.data_dict_to_inp(stt.format_to_swmm_dict(**data_dict), @@ -157,7 +162,7 @@ def test_synthetic_write(): comparison_file) # Compare - new_input_file = model_dir / "model_1.inp" + new_input_file = addresses.inp are_files_identical = filecmp.cmp(new_input_file, comparison_file, shallow=False) diff --git a/tests/test_preprocessing.py b/tests/test_preprocessing.py index 4b6708c4..fdaba97b 100644 --- a/tests/test_preprocessing.py +++ b/tests/test_preprocessing.py @@ -18,9 +18,9 @@ def test_getattr(): assert addresses.bbox == addresses.project / 'bbox_1' assert addresses.download == addresses.bbox / 'download' assert addresses.elevation == addresses.download / 'elevation.tif' - assert addresses.building == addresses.download / 'building.parquet' + assert addresses.building == addresses.download / 'building.geoparquet' assert addresses.model == addresses.bbox / 'model_1' - assert addresses.subcatchments == addresses.model / 'subcatchments.parquet' + assert addresses.subcatchments == addresses.model / 'subcatchments.geoparquet' assert addresses.precipitation == addresses.download / 'precipitation.parquet' addresses.elevation = filepath From 2deadd436e27f3e298da7689bb63498e4dcfc0f7 Mon Sep 17 00:00:00 2001 From: Dobson Date: Wed, 13 Mar 2024 15:24:02 +0000 Subject: [PATCH 03/15] Test iterate functions --- swmmanywhere/metric_utilities.py | 12 +++++------ tests/test_graph_utilities.py | 20 +++++++++++++++++-- tests/test_metric_utilities.py | 34 ++++++++++++++++++++++---------- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/swmmanywhere/metric_utilities.py b/swmmanywhere/metric_utilities.py index 5c2d850d..8ddda368 100644 --- a/swmmanywhere/metric_utilities.py +++ b/swmmanywhere/metric_utilities.py @@ -61,7 +61,7 @@ def iterate_metrics(synthetic_results: pd.DataFrame, real_results: pd.DataFrame, real_subs: gpd.GeoDataFrame, real_G: nx.Graph, - metric_list: list[str]) -> list[float]: + metric_list: list[str]) -> dict[str, float]: """Iterate a list of metrics over a graph. Args: @@ -74,18 +74,18 @@ def iterate_metrics(synthetic_results: pd.DataFrame, metric_list (list[str]): A list of metrics to iterate. Returns: - list[float]: The results of the metrics. + dict[str, float]: The results of the metrics. """ - results = [] + results = {} for metric in metric_list: assert metric in metrics.keys(), f"Metric {metric} not registered in metrics." - results.append(metrics[metric](synthetic_results = synthetic_results, + results[metric] = metrics[metric](synthetic_results = synthetic_results, synthetic_subs = synthetic_subs, synthetic_G = synthetic_G, real_results = real_results, real_subs = real_subs, - real_G = real_G)) - return pd.DataFrame(results) + real_G = real_G) + return results def extract_var(df: pd.DataFrame, var: str) -> pd.DataFrame: diff --git a/tests/test_graph_utilities.py b/tests/test_graph_utilities.py index 097afcf0..9d97c86b 100644 --- a/tests/test_graph_utilities.py +++ b/tests/test_graph_utilities.py @@ -13,7 +13,7 @@ from swmmanywhere import parameters from swmmanywhere.graph_utilities import graphfcns as gu -from swmmanywhere.graph_utilities import load_graph, save_graph +from swmmanywhere.graph_utilities import iterate_graphfcns, load_graph, save_graph def load_street_network(): @@ -241,4 +241,20 @@ def test_pipe_by_pipe(): for u, d in G.nodes(data=True): assert 'chamber_floor_elevation' in d.keys() assert math.isfinite(d['chamber_floor_elevation']) - \ No newline at end of file + +def test_iterate_graphfcns(): + """Test the iterate_graphfcns function.""" + G = load_graph(Path(__file__).parent / 'test_data' / 'graph_topo_derived.json') + params = parameters.get_full_parameters() + addresses = parameters.FilePaths(base_dir = None, + project_name = None, + bbox_number = None, + model_number = None) + G = iterate_graphfcns(G, + ['assign_id', + 'format_osmnx_lanes'], + params, + addresses) + for u, v, d in G.edges(data=True): + assert 'id' in d.keys() + assert 'width' in d.keys() diff --git a/tests/test_metric_utilities.py b/tests/test_metric_utilities.py index 5918f06c..e2457412 100644 --- a/tests/test_metric_utilities.py +++ b/tests/test_metric_utilities.py @@ -279,20 +279,34 @@ def test_outlet_nse_flooding(): real_subs = subs) assert val == 0.0 -def test_netcomp(): - """Test the netcomp metrics.""" +def test_netcomp_iterate(): + """Test the netcomp metrics and iterate_metrics.""" netcomp_results = {'nc_deltacon0' : 0.00129408, 'nc_laplacian_dist' : 36.334773, 'nc_laplacian_norm_dist' : 1.932007, 'nc_adjacency_dist' : 3.542749, 'nc_resistance_distance' : 8.098548, 'nc_vertex_edge_distance' : 0.132075} + G = load_graph(Path(__file__).parent / 'test_data' / 'graph_topo_derived.json') + metrics = mu.iterate_metrics(synthetic_G = G, + synthetic_subs = None, + synthetic_results = None, + real_G = G, + real_subs = None, + real_results = None, + metric_list = netcomp_results.keys()) + for metric, val in metrics.items(): + assert metric in netcomp_results + assert np.isclose(val, 0) - for func, val in netcomp_results.items(): - G = load_graph(Path(__file__).parent / 'test_data' / 'graph_topo_derived.json') - val_ = getattr(mu.metrics, func)(synthetic_G = G, real_G = G) - assert val_ == 0.0, func - - G_ = load_graph(Path(__file__).parent / 'test_data' / 'street_graph.json') - val_ = getattr(mu.metrics, func)(synthetic_G = G_, real_G = G) - assert np.isclose(val, val_), func \ No newline at end of file + G_ = load_graph(Path(__file__).parent / 'test_data' / 'street_graph.json') + metrics = mu.iterate_metrics(synthetic_G = G_, + synthetic_subs = None, + synthetic_results = None, + real_G = G, + real_subs = None, + real_results = None, + metric_list = netcomp_results.keys()) + for metric, val in metrics.items(): + assert metric in netcomp_results + assert np.isclose(val, netcomp_results[metric]) \ No newline at end of file From 75609cfb958b8c79f04489277e63c6095b21b876 Mon Sep 17 00:00:00 2001 From: Dobson Date: Wed, 13 Mar 2024 15:30:26 +0000 Subject: [PATCH 04/15] Update preprocessing.py fix typo --- swmmanywhere/preprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swmmanywhere/preprocessing.py b/swmmanywhere/preprocessing.py index 527413bc..5dba0b44 100644 --- a/swmmanywhere/preprocessing.py +++ b/swmmanywhere/preprocessing.py @@ -169,7 +169,7 @@ def prepare_precipitation(bbox: tuple[float, float, float, float], precip = go.reproject_df(precip, source_crs, target_crs) write_df(precip, addresses.precipitation) -def prepare_elvation(bbox: tuple[float, float, float, float], +def prepare_elevation(bbox: tuple[float, float, float, float], addresses: parameters.FilePaths, api_keys: dict[str, str], target_crs: str): @@ -256,7 +256,7 @@ def run_downloads(bbox: tuple[float, float, float, float], prepare_precipitation(bbox, addresses, api_keys, target_crs) # Download elevation data - prepare_elvation(bbox, addresses, api_keys, target_crs) + prepare_elevation(bbox, addresses, api_keys, target_crs) # Download building data prepare_building(bbox, addresses, target_crs) From 5b9c0037762a90d563cb436c5cb6099367e10380 Mon Sep 17 00:00:00 2001 From: Dobson Date: Wed, 13 Mar 2024 16:48:10 +0000 Subject: [PATCH 05/15] test full config - almost --- swmmanywhere/geospatial_utilities.py | 2 +- swmmanywhere/graph_utilities.py | 1 + swmmanywhere/metric_utilities.py | 15 +++++++------ swmmanywhere/parameters.py | 3 +++ swmmanywhere/post_processing.py | 5 +++-- swmmanywhere/swmmanywhere.py | 24 +++++++++++++-------- tests/test_data/building.geojson | 10 +++++++++ tests/test_data/demo_config.yml | 2 +- tests/test_metric_utilities.py | 32 ++++++++++++++-------------- tests/test_swmmanywhere.py | 30 +++++++++++++++++++++++++- 10 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 tests/test_data/building.geojson diff --git a/swmmanywhere/geospatial_utilities.py b/swmmanywhere/geospatial_utilities.py index 18d21ea9..f5df9859 100644 --- a/swmmanywhere/geospatial_utilities.py +++ b/swmmanywhere/geospatial_utilities.py @@ -258,7 +258,7 @@ def reproject_graph(G: nx.Graph, G_new.nodes[u]['y']], [G_new.nodes[v]['x'], G_new.nodes[v]['y']]]) - + G_new.graph['crs'] = target_crs return G_new diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index 30e8fd95..5b651edd 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -177,6 +177,7 @@ def iterate_graphfcns(G: nx.Graph, assert function in graphfcns.keys(), \ logger.error(f"Function {function} not registered in graphfcns.") G = graphfcns[function](G, addresses = addresses, **params) + logger.info(f"graphfcn: {function} completed.") return G @register_graphfcn diff --git a/swmmanywhere/metric_utilities.py b/swmmanywhere/metric_utilities.py index 8ddda368..d825abbf 100644 --- a/swmmanywhere/metric_utilities.py +++ b/swmmanywhere/metric_utilities.py @@ -16,6 +16,8 @@ import pandas as pd from scipy import stats +from swmmanywhere.logging import logger + class MetricRegistry(dict): """Registry object.""" @@ -85,6 +87,7 @@ def iterate_metrics(synthetic_results: pd.DataFrame, real_results = real_results, real_subs = real_subs, real_G = real_G) + logger.info(f"metric: {metric} completed.") return results def extract_var(df: pd.DataFrame, @@ -112,11 +115,11 @@ def align_calc_nse(synthetic_results: pd.DataFrame, # Extract data syn_data = extract_var(synthetic_results, variable) - syn_data = syn_data.loc[syn_data.object.isin(syn_ids)] + syn_data = syn_data.loc[syn_data.id.isin(syn_ids)] syn_data = syn_data.groupby('date').value.sum() real_data = extract_var(real_results, variable) - real_data = real_data.loc[real_data.object.isin(real_ids)] + real_data = real_data.loc[real_data.id.isin(real_ids)] real_data = real_data.groupby('date').value.sum() # Align data @@ -230,8 +233,8 @@ def dominant_outlet(G: nx.DiGraph, # Identify the outlet with the highest flow outlet_flows = results.loc[(results.variable == 'flow') & - (results.object.isin(outlet_arcs))] - max_outlet_arc = outlet_flows.groupby('object').value.mean().idxmax() + (results.id.isin(outlet_arcs))] + max_outlet_arc = outlet_flows.groupby('id').value.mean().idxmax() max_outlet = [v for u,v,d in G.edges(data=True) if d['id'] == max_outlet_arc][0] @@ -346,12 +349,12 @@ def _f(x): return np.trapz(x.value,x.duration) syn_flooding = extract_var(synthetic_results, - 'flooding').groupby('object').apply(_f) + 'flooding').groupby('id').apply(_f) syn_area = synthetic_subs.impervious_area.sum() syn_tot = syn_flooding.sum() / syn_area real_flooding = extract_var(real_results, - 'flooding').groupby('object').apply(_f) + 'flooding').groupby('id').apply(_f) real_area = real_subs.impervious_area.sum() real_tot = real_flooding.sum() / real_area diff --git a/swmmanywhere/parameters.py b/swmmanywhere/parameters.py index 3f9615c3..b0b90f57 100644 --- a/swmmanywhere/parameters.py +++ b/swmmanywhere/parameters.py @@ -243,6 +243,9 @@ def _generate_inp(self): def _generate_subcatchments(self): return self._generate_property(f'subcatchments.geo{self.extension}', 'model') + def _generate_graph(self): + return self._generate_property(f'graph.{self.extension}', + 'model') def _generate_nodes(self): return self._generate_property(f'nodes.geo{self.extension}', 'model') diff --git a/swmmanywhere/post_processing.py b/swmmanywhere/post_processing.py index ca701d81..12bfc7e0 100644 --- a/swmmanywhere/post_processing.py +++ b/swmmanywhere/post_processing.py @@ -38,7 +38,8 @@ def synthetic_write(addresses: FilePaths): nodes = gpd.read_file(addresses.nodes) edges = gpd.read_file(addresses.edges) subs = gpd.read_file(addresses.subcatchments) - + subs = subs.loc[subs.id.isin(nodes.id)] + # Extract SWMM relevant data edges = edges[['u','v','diameter','length']] nodes = nodes[['id', @@ -99,7 +100,7 @@ def synthetic_write(addresses: FilePaths): event = {'name' : '1', 'unit' : 'mm', 'interval' : '01:00', - 'fid' : 'storm.dat' # overwritten at runtime + 'fid' : str(addresses.precipitation) } # Locate raingage(s) on the map diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index 5cf72f5e..9e6918f2 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -12,7 +12,7 @@ import swmmanywhere.geospatial_utilities as go from swmmanywhere import preprocessing -from swmmanywhere.graph_utilities import iterate_graphfcns, load_graph +from swmmanywhere.graph_utilities import iterate_graphfcns, load_graph, save_graph from swmmanywhere.logging import logger from swmmanywhere.metric_utilities import iterate_metrics from swmmanywhere.parameters import get_full_parameters @@ -20,7 +20,11 @@ def swmmanywhere(config_: Path | dict): - """Run a SWMM model and store the results. + """Run SWMManywhere processes. + + This function runs the SWMManywhere processes, including downloading data, + preprocessing the graphfcns, running the model, and comparing the results + to real data using metrics. Args: config_ (Path | dict): The path to the configuration file, or the loaded @@ -36,19 +40,21 @@ def swmmanywhere(config_: Path | dict): assert isinstance(config_, dict), \ logger.error("config must be a Path or a dict.") config = config_ + # Create the project structure addresses = preprocessing.create_project_structure(config['bbox'], config['project'], Path(config['base_dir']) ) - for address_override in config['address_overrides']: - addresses[address_override] = config['address_overrides'][address_override] + for key, val in config['address_overrides'].items(): + setattr(addresses,key,val) # Run downloads + api_keys = yaml.safe_load(config['api_keys'].open('r')) preprocessing.run_downloads(config['bbox'], addresses, - config['api_keys'] + api_keys ) # Identify the starting graph @@ -65,7 +71,7 @@ def swmmanywhere(config_: Path | dict): # Iterate the graph functions G = iterate_graphfcns(G, - config['graphfcns'], + config['graphfcn_list'], parameters, addresses) @@ -75,13 +81,13 @@ def swmmanywhere(config_: Path | dict): addresses.edges, G.graph['crs'] ) - + save_graph(G, addresses.graph) # Write to .inp - synthetic_write(addresses.model) + synthetic_write(addresses) # Run the model synthetic_results = run(addresses.inp, - **config['run_settings']) + **config['run_settings']) # Get the real results if config['real']['results']: diff --git a/tests/test_data/building.geojson b/tests/test_data/building.geojson new file mode 100644 index 00000000..82cc8c17 --- /dev/null +++ b/tests/test_data/building.geojson @@ -0,0 +1,10 @@ +{ +"type": "FeatureCollection", +"name": "building", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::32631" } }, +"features": [ +{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 296706.168717324966565, 5716618.06729694083333 ], [ 296685.100549778027926, 5716609.276876767165959 ], [ 296669.952219038328622, 5716601.54295277968049 ], [ 296682.250571176817175, 5716589.005810517817736 ], [ 296703.019824567134492, 5716593.431084375828505 ], [ 296706.428113158093765, 5716600.50705910474062 ], [ 296706.168717324966565, 5716618.06729694083333 ] ] ] } }, +{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 296730.287840720382519, 5716543.322053890675306 ], [ 296713.126153496676125, 5716548.882848112843931 ], [ 296712.229420348943677, 5716535.787398532964289 ], [ 296727.33823764603585, 5716521.595944404602051 ], [ 296731.543616473325528, 5716540.312317433767021 ], [ 296724.866189784486778, 5716549.540801198221743 ], [ 296730.287840720382519, 5716543.322053890675306 ] ] ] } }, +{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 296737.564884341089055, 5716436.10706106480211 ], [ 296717.194186427397653, 5716437.501960881985724 ], [ 296713.148613883298822, 5716399.770296772941947 ], [ 296732.960980701842345, 5716411.570486682467163 ], [ 296750.261788603849709, 5716429.390141798183322 ], [ 296737.564884341089055, 5716436.10706106480211 ] ] ] } } +] +} diff --git a/tests/test_data/demo_config.yml b/tests/test_data/demo_config.yml index d877aadd..a9433266 100644 --- a/tests/test_data/demo_config.yml +++ b/tests/test_data/demo_config.yml @@ -21,7 +21,7 @@ graphfcn_list: - set_elevation - set_surface_slope - set_chahinian_slope - - set_chahinan_angle + - set_chahinian_angle - calculate_weights - identify_outlets - derive_topology diff --git a/tests/test_metric_utilities.py b/tests/test_metric_utilities.py index e2457412..563320b5 100644 --- a/tests/test_metric_utilities.py +++ b/tests/test_metric_utilities.py @@ -54,14 +54,14 @@ def test_bias_flood_depth(): """Test the bias_flood_depth metric.""" # Create synthetic and real data synthetic_results = pd.DataFrame({ - 'object': ['obj1', 'obj1','obj2','obj2'], + 'id': ['obj1', 'obj1','obj2','obj2'], 'value': [10, 20, 5, 2], 'variable': 'flooding', 'date' : pd.to_datetime(['2021-01-01 00:00:00','2021-01-01 00:05:00', '2021-01-01 00:00:00','2021-01-01 00:05:00']) }) real_results = pd.DataFrame({ - 'object': ['obj1', 'obj1','obj2','obj2'], + 'id': ['obj1', 'obj1','obj2','obj2'], 'value': [15, 25, 10, 20], 'variable': 'flooding', 'date' : pd.to_datetime(['2021-01-01 00:00:00','2021-01-01 00:05:00', @@ -131,19 +131,19 @@ def test_outlet_nse_flow(): subs = get_subs() # Mock results - results = pd.DataFrame([{'object' : 4253560, + results = pd.DataFrame([{'id' : 4253560, 'variable' : 'flow', 'value' : 10, 'date' : pd.to_datetime('2021-01-01').date()}, - {'object' : '', + {'id' : '', 'variable' : 'flow', 'value' : 5, 'date' : pd.to_datetime('2021-01-01').date()}, - {'object' : 4253560, + {'id' : 4253560, 'variable' : 'flow', 'value' : 5, 'date' : pd.to_datetime('2021-01-01 00:00:05')}, - {'object' : '', + {'id' : '', 'variable' : 'flow', 'value' : 2, 'date' : pd.to_datetime('2021-01-01 00:00:05')}]) @@ -173,7 +173,7 @@ def test_outlet_nse_flow(): new_outlet, 'outlet') G_.remove_node(12354833) - results_.loc[results_.object == 4253560, 'object'] = 725226531 + results_.loc[results_.id == 4253560, 'id'] = 725226531 # Calculate NSE (mean results) val = mu.metrics.outlet_nse_flow(synthetic_G = G_, @@ -200,35 +200,35 @@ def test_outlet_nse_flooding(): subs = get_subs() # Mock results - results = pd.DataFrame([{'object' : 4253560, + results = pd.DataFrame([{'id' : 4253560, 'variable' : 'flow', 'value' : 10, 'date' : pd.to_datetime('2021-01-01 00:00:00')}, - {'object' : 4253560, + {'id' : 4253560, 'variable' : 'flow', 'value' : 5, 'date' : pd.to_datetime('2021-01-01 00:00:05')}, - {'object' : 25472468, + {'id' : 25472468, 'variable' : 'flooding', 'value' : 4.5, 'date' : pd.to_datetime('2021-01-01 00:00:00')}, - {'object' : 770549936, + {'id' : 770549936, 'variable' : 'flooding', 'value' : 5, 'date' : pd.to_datetime('2021-01-01 00:00:00')}, - {'object' : 109753, + {'id' : 109753, 'variable' : 'flooding', 'value' : 10, 'date' : pd.to_datetime('2021-01-01 00:00:00')}, - {'object' : 25472468, + {'id' : 25472468, 'variable' : 'flooding', 'value' : 0, 'date' : pd.to_datetime('2021-01-01 00:00:05')}, - {'object' : 770549936, + {'id' : 770549936, 'variable' : 'flooding', 'value' : 5, 'date' : pd.to_datetime('2021-01-01 00:00:05')}, - {'object' : 109753, + {'id' : 109753, 'variable' : 'flooding', 'value' : 15, 'date' : pd.to_datetime('2021-01-01 00:00:05')}]) @@ -243,7 +243,7 @@ def test_outlet_nse_flooding(): # Calculate NSE (mean results) results_ = results.copy() - results_.loc[results_.object.isin([770549936, 25472468]),'value'] = [14.5 / 4] * 4 + results_.loc[results_.id.isin([770549936, 25472468]),'value'] = [14.5 / 4] * 4 val = mu.metrics.outlet_nse_flooding(synthetic_G = G, synthetic_results = results_, real_G = G, diff --git a/tests/test_swmmanywhere.py b/tests/test_swmmanywhere.py index b854c230..fcc8847f 100644 --- a/tests/test_swmmanywhere.py +++ b/tests/test_swmmanywhere.py @@ -1,6 +1,9 @@ """Tests for the main module.""" +import tempfile from pathlib import Path +import yaml + from swmmanywhere import __version__, swmmanywhere @@ -32,4 +35,29 @@ def test_run(): assert results_.shape[0] < results.shape[0] model.with_suffix('.out').unlink() - model.with_suffix('.rpt').unlink() \ No newline at end of file + model.with_suffix('.rpt').unlink() + +def test_swmmanywhere(): + """Test the swmmanywhere function.""" + with tempfile.TemporaryDirectory() as base_dir: + test_data_dir = Path(__file__).parent / 'test_data' + defs_dir = Path(__file__).parent.parent / 'swmmanywhere' / 'defs' + config = swmmanywhere.load_config(test_data_dir / 'demo_config.yml') + base_dir = Path(base_dir) + config['base_dir'] = str(base_dir) + config['bbox'] = (0.05428,51.55847,0.07193,51.56726) + config['address_overrides'] = { + 'building': test_data_dir / 'building.geojson', + 'precipitation': defs_dir / 'storm.dat' + } + model_dir = base_dir / 'demo' / 'bbox_1' / 'model_1' + config['real']['subcatchments'] = model_dir / 'subcatchments.geoparquet' + config['real']['inp'] = model_dir / 'model_1.inp' + config['real']['graph'] = model_dir / 'graph.parquet' + config['run_settings']['duration'] = 1000 + api_keys = {'nasadem_key' : 'b206e65629ac0e53d599e43438560d28'} + with open(base_dir / 'api_keys.yml', 'w') as f: + yaml.dump(api_keys, f) + config['api_keys'] = base_dir / 'api_keys.yml' + swmmanywhere.swmmanywhere(config) + \ No newline at end of file From befede715d159162b495fe55030cdeb73c04631b Mon Sep 17 00:00:00 2001 From: Dobson Date: Thu, 14 Mar 2024 13:20:12 +0000 Subject: [PATCH 06/15] Add test and debug swmmanywhere --- swmmanywhere/graph_utilities.py | 9 ++++++--- swmmanywhere/metric_utilities.py | 5 ++--- swmmanywhere/post_processing.py | 6 ++---- swmmanywhere/preprocessing.py | 2 +- tests/test_data/building.geojson | 10 ---------- tests/test_data/building.geoparquet | Bin 0 -> 138224 bytes tests/test_data/demo_config.yml | 1 + tests/test_graph_utilities.py | 2 +- tests/test_post_processing.py | 1 + tests/test_swmmanywhere.py | 6 +++--- 10 files changed, 17 insertions(+), 25 deletions(-) delete mode 100644 tests/test_data/building.geojson create mode 100644 tests/test_data/building.geoparquet diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index 5b651edd..7f7ba097 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -182,7 +182,6 @@ def iterate_graphfcns(G: nx.Graph, @register_graphfcn class assign_id(BaseGraphFunction, - required_edge_attributes = ['osmid'], adds_edge_attributes = ['id'] ): """assign_id class.""" @@ -203,8 +202,12 @@ def __call__(self, Returns: G (nx.Graph): The same graph with an ID assigned to each edge """ + edge_ids: set[str] = set() for u, v, data in G.edges(data=True): - data['id'] = get_osmid_id(data) + data['id'] = f'{u}-{v}' + edge_ids.add(data['id']) + if data['id'] in edge_ids: + logger.warning(f"Duplicate edge ID: {data['id']}") return G @register_graphfcn @@ -446,7 +449,7 @@ def __call__(self, G: nx.Graph, subs_gdf = go.derive_subcatchments(G,temp_fid) # Calculate runoff coefficient (RC) - if addresses.building.suffix == '.parquet': + if addresses.building.suffix in ('.geoparquet','.parquet'): buildings = gpd.read_parquet(addresses.building) else: buildings = gpd.read_file(addresses.building) diff --git a/swmmanywhere/metric_utilities.py b/swmmanywhere/metric_utilities.py index d825abbf..2844a6b5 100644 --- a/swmmanywhere/metric_utilities.py +++ b/swmmanywhere/metric_utilities.py @@ -217,7 +217,7 @@ def dominant_outlet(G: nx.DiGraph, subgraph of the graph of nodes that drain to that outlet. Args: - G (nx.Graph): The graph. + G (nx.DiGraph): The graph. results (pd.DataFrame): The results, which include a 'flow' and 'id' column. @@ -234,7 +234,7 @@ def dominant_outlet(G: nx.DiGraph, # Identify the outlet with the highest flow outlet_flows = results.loc[(results.variable == 'flow') & (results.id.isin(outlet_arcs))] - max_outlet_arc = outlet_flows.groupby('id').value.mean().idxmax() + max_outlet_arc = outlet_flows.groupby('id').value.median().idxmax() max_outlet = [v for u,v,d in G.edges(data=True) if d['id'] == max_outlet_arc][0] @@ -415,7 +415,6 @@ def outlet_nse_flow(synthetic_G: nx.Graph, syn_arc, real_arc) - @metrics.register def outlet_nse_flooding(synthetic_G: nx.Graph, synthetic_results: pd.DataFrame, diff --git a/swmmanywhere/post_processing.py b/swmmanywhere/post_processing.py index 12bfc7e0..bf75b89a 100644 --- a/swmmanywhere/post_processing.py +++ b/swmmanywhere/post_processing.py @@ -41,7 +41,7 @@ def synthetic_write(addresses: FilePaths): subs = subs.loc[subs.id.isin(nodes.id)] # Extract SWMM relevant data - edges = edges[['u','v','diameter','length']] + edges = edges[['id','u','v','diameter','length']] nodes = nodes[['id', 'x', 'y', @@ -85,15 +85,13 @@ def synthetic_write(addresses: FilePaths): new_edges = edges.iloc[0:outfalls.shape[0]].copy() new_edges['u'] = outfalls['id'].str.replace('_outfall','').values new_edges['v'] = outfalls['id'].values + new_edges['id'] = [f'{u}-{v}' for u,v in zip(new_edges['u'], new_edges['v'])] new_edges['diameter'] = 15 # TODO .. big pipe to enable all outfall... new_edges['length'] = 1 # Append new edges edges = pd.concat([edges, new_edges], ignore_index = True) - # Name all edges - edges['id'] = edges.u.astype(str) + '-' + edges.v.astype(str) - # Create event # TODO will need some updating if multiple rain gages # TODO automatically match units to storm.csv? diff --git a/swmmanywhere/preprocessing.py b/swmmanywhere/preprocessing.py index 5dba0b44..995653dd 100644 --- a/swmmanywhere/preprocessing.py +++ b/swmmanywhere/preprocessing.py @@ -145,7 +145,7 @@ def write_df(df: pd.DataFrame | gpd.GeoDataFrame, df (DataFrame): DataFrame to write to a file. fid (Path): Path to the file. """ - if fid.suffix == '.parquet': + if fid.suffix in ('.geoparquet','.parquet'): df.to_parquet(fid) elif fid.suffix == '.json': if isinstance(df, gpd.GeoDataFrame): diff --git a/tests/test_data/building.geojson b/tests/test_data/building.geojson deleted file mode 100644 index 82cc8c17..00000000 --- a/tests/test_data/building.geojson +++ /dev/null @@ -1,10 +0,0 @@ -{ -"type": "FeatureCollection", -"name": "building", -"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::32631" } }, -"features": [ -{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 296706.168717324966565, 5716618.06729694083333 ], [ 296685.100549778027926, 5716609.276876767165959 ], [ 296669.952219038328622, 5716601.54295277968049 ], [ 296682.250571176817175, 5716589.005810517817736 ], [ 296703.019824567134492, 5716593.431084375828505 ], [ 296706.428113158093765, 5716600.50705910474062 ], [ 296706.168717324966565, 5716618.06729694083333 ] ] ] } }, -{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 296730.287840720382519, 5716543.322053890675306 ], [ 296713.126153496676125, 5716548.882848112843931 ], [ 296712.229420348943677, 5716535.787398532964289 ], [ 296727.33823764603585, 5716521.595944404602051 ], [ 296731.543616473325528, 5716540.312317433767021 ], [ 296724.866189784486778, 5716549.540801198221743 ], [ 296730.287840720382519, 5716543.322053890675306 ] ] ] } }, -{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 296737.564884341089055, 5716436.10706106480211 ], [ 296717.194186427397653, 5716437.501960881985724 ], [ 296713.148613883298822, 5716399.770296772941947 ], [ 296732.960980701842345, 5716411.570486682467163 ], [ 296750.261788603849709, 5716429.390141798183322 ], [ 296737.564884341089055, 5716436.10706106480211 ] ] ] } } -] -} diff --git a/tests/test_data/building.geoparquet b/tests/test_data/building.geoparquet new file mode 100644 index 0000000000000000000000000000000000000000..a758595f36fdf13ab1179161a8bd9311bbae5d2d GIT binary patch literal 138224 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∾@PtfRpiqa-JuX5&0X^FN%J@m@nN32reZc*R=!e^ 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 Date: Thu, 14 Mar 2024 13:24:17 +0000 Subject: [PATCH 07/15] Update graph_utilities.py typo --- swmmanywhere/graph_utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index 7f7ba097..3e5e60b5 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -205,9 +205,9 @@ def __call__(self, edge_ids: set[str] = set() for u, v, data in G.edges(data=True): data['id'] = f'{u}-{v}' - edge_ids.add(data['id']) if data['id'] in edge_ids: logger.warning(f"Duplicate edge ID: {data['id']}") + edge_ids.add(data['id']) return G @register_graphfcn From 158807d955a74ff5a9dca07de944177e5840cf3a Mon Sep 17 00:00:00 2001 From: Dobson Date: Fri, 15 Mar 2024 09:24:01 +0000 Subject: [PATCH 08/15] raise errors properly --- swmmanywhere/graph_utilities.py | 4 ++-- swmmanywhere/metric_utilities.py | 3 ++- swmmanywhere/swmmanywhere.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index 3e5e60b5..ccad72a4 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -174,8 +174,8 @@ def iterate_graphfcns(G: nx.Graph, nx.Graph: The graph after the graph functions have been applied. """ for function in graphfcn_list: - assert function in graphfcns.keys(), \ - logger.error(f"Function {function} not registered in graphfcns.") + if function not in graphfcns: + raise ValueError(f"Function {function} not registered in graphfcns.") G = graphfcns[function](G, addresses = addresses, **params) logger.info(f"graphfcn: {function} completed.") return G diff --git a/swmmanywhere/metric_utilities.py b/swmmanywhere/metric_utilities.py index 2844a6b5..9bd91b8c 100644 --- a/swmmanywhere/metric_utilities.py +++ b/swmmanywhere/metric_utilities.py @@ -80,7 +80,8 @@ def iterate_metrics(synthetic_results: pd.DataFrame, """ results = {} for metric in metric_list: - assert metric in metrics.keys(), f"Metric {metric} not registered in metrics." + if metric not in metrics: + raise ValueError(f"{metric} not registered in metrics.") results[metric] = metrics[metric](synthetic_results = synthetic_results, synthetic_subs = synthetic_subs, synthetic_G = synthetic_G, diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index 9e6918f2..58eaea39 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -37,8 +37,8 @@ def swmmanywhere(config_: Path | dict): if isinstance(config_, Path): config = load_config(config_) else: - assert isinstance(config_, dict), \ - logger.error("config must be a Path or a dict.") + if not isinstance(config_, dict): + raise TypeError("config must be a Path or a dict.") config = config_ # Create the project structure From 332fc18403f8853ee1f268022e0fef2b84d462dd Mon Sep 17 00:00:00 2001 From: barneydobson Date: Fri, 15 Mar 2024 09:29:54 +0000 Subject: [PATCH 09/15] Update swmmanywhere/swmmanywhere.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Diego Alonso Álvarez <6095790+dalonsoa@users.noreply.github.com> --- swmmanywhere/swmmanywhere.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index 58eaea39..0fb46cb3 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -47,8 +47,8 @@ def swmmanywhere(config_: Path | dict): Path(config['base_dir']) ) - for key, val in config['address_overrides'].items(): - setattr(addresses,key,val) + for key, val in config.get('address_overrides', {}).items(): + setattr(addresses, key, val) # Run downloads api_keys = yaml.safe_load(config['api_keys'].open('r')) From 31dbdd6ff1a520ca9619fe9a82b990b5a818bf4a Mon Sep 17 00:00:00 2001 From: barneydobson Date: Fri, 15 Mar 2024 09:30:05 +0000 Subject: [PATCH 10/15] Update swmmanywhere/swmmanywhere.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Diego Alonso Álvarez <6095790+dalonsoa@users.noreply.github.com> --- swmmanywhere/swmmanywhere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index 0fb46cb3..38b2e326 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -65,7 +65,7 @@ def swmmanywhere(config_: Path | dict): # Load the parameters and perform any manual overrides parameters = get_full_parameters() - for category, overrides in config['parameter_overrides'].items(): + for category, overrides in config.get('parameter_overrides', {}).items(): for key, val in overrides.items(): setattr(parameters[category], key, val) From 520e0b06ccbf2a14dcd3ca6bf5f7fcbb296d77ac Mon Sep 17 00:00:00 2001 From: Dobson Date: Fri, 15 Mar 2024 10:30:18 +0000 Subject: [PATCH 11/15] Update swmmanywhere.py remove config load from swmmanywhere.swmmanywhere --- swmmanywhere/swmmanywhere.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index 58eaea39..018283d0 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -19,7 +19,7 @@ from swmmanywhere.post_processing import synthetic_write -def swmmanywhere(config_: Path | dict): +def swmmanywhere(config: dict): """Run SWMManywhere processes. This function runs the SWMManywhere processes, including downloading data, @@ -27,20 +27,11 @@ def swmmanywhere(config_: Path | dict): to real data using metrics. Args: - config_ (Path | dict): The path to the configuration file, or the loaded - file as a dict. + config (dict): The loaded config as a dict. Returns: pd.DataFrame: A DataFrame containing the results. """ - # Load the configuration - if isinstance(config_, Path): - config = load_config(config_) - else: - if not isinstance(config_, dict): - raise TypeError("config must be a Path or a dict.") - config = config_ - # Create the project structure addresses = preprocessing.create_project_structure(config['bbox'], config['project'], From 1ac5562eb121f31bc43d8aed369833004ce389e2 Mon Sep 17 00:00:00 2001 From: Dobson Date: Fri, 15 Mar 2024 11:21:58 +0000 Subject: [PATCH 12/15] Validate config and filepaths --- dev-requirements.txt | 32 ++++++++++++++++--------- pyproject.toml | 6 ++++- requirements.txt | 22 +++++++++++++++++- swmmanywhere/defs/schema.yml | 33 ++++++++++++++++++++++++++ swmmanywhere/swmmanywhere.py | 45 +++++++++++++++++++++++++++++++----- tests/test_swmmanywhere.py | 39 +++++++++++++++++++++++-------- 6 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 swmmanywhere/defs/schema.yml diff --git a/dev-requirements.txt b/dev-requirements.txt index 006102ed..852b7e5a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=dev --output-file=dev-requirements.txt @@ -17,8 +17,10 @@ annotated-types==0.6.0 attrs==23.2.0 # via # fiona + # jsonschema # pytest-mypy # rasterio + # referencing build==1.0.3 # via pip-tools cdsapi==0.6.1 @@ -74,8 +76,6 @@ dill==0.3.7 # via multiprocess distlib==0.3.8 # via virtualenv -exceptiongroup==1.2.0 - # via pytest fastparquet==2023.10.1 # via swmmanywhere (pyproject.toml) filelock==3.13.1 @@ -100,6 +100,10 @@ geopandas==0.14.2 # swmmanywhere (pyproject.toml) geopy==2.4.1 # via swmmanywhere (pyproject.toml) +gitdb==4.0.11 + # via gitpython +gitpython==3.1.42 + # via swmmanywhere (pyproject.toml) identify==2.5.33 # via pre-commit idna==3.6 @@ -110,6 +114,10 @@ iniconfig==2.0.0 # via pytest joblib==1.3.2 # via swmmanywhere (pyproject.toml) +jsonschema==4.21.1 + # via swmmanywhere (pyproject.toml) +jsonschema-specifications==2023.12.1 + # via jsonschema julian==0.14 # via pyswmm kiwisolver==1.4.5 @@ -253,12 +261,20 @@ rasterio==1.3.9 # pysheds # rioxarray # swmmanywhere (pyproject.toml) +referencing==0.33.0 + # via + # jsonschema + # jsonschema-specifications requests==2.31.0 # via # cdsapi # osmnx rioxarray==0.15.1 # via swmmanywhere (pyproject.toml) +rpds-py==0.18.0 + # via + # jsonschema + # referencing ruff==0.1.11 # via swmmanywhere (pyproject.toml) salib==1.4.7 @@ -281,20 +297,14 @@ six==1.16.0 # via # fiona # python-dateutil +smmap==5.0.1 + # via gitdb snuggs==1.4.7 # via rasterio swmm-toolkit==0.15.3 # via pyswmm tifffile==2024.1.30 # via scikit-image -tomli==2.0.1 - # via - # build - # coverage - # mypy - # pip-tools - # pyproject-hooks - # pytest toolz==0.12.1 # via cytoolz tqdm==4.66.2 diff --git a/pyproject.toml b/pyproject.toml index a3c4e1a3..be65c610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "geopy", "GitPython", "joblib", + "jsonschema", "loguru", "matplotlib", "netcdf4", @@ -94,4 +95,7 @@ skip = "swmmanywhere/defs/iso_converter.yml,*.inp" ignore-words-list = "gage,gages" [tool.refurb] -ignore = [184] +ignore = [ + 184, # Because some frankly bizarre suggestions + 109 # Because pyyaml doesn't support tuples + ] diff --git a/requirements.txt b/requirements.txt index 533f956a..32ffe117 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile @@ -17,7 +17,9 @@ annotated-types==0.6.0 attrs==23.2.0 # via # fiona + # jsonschema # rasterio + # referencing cdsapi==0.6.1 # via swmmanywhere (pyproject.toml) certifi==2023.11.17 @@ -80,12 +82,20 @@ geopandas==0.14.2 # swmmanywhere (pyproject.toml) geopy==2.4.1 # via swmmanywhere (pyproject.toml) +gitdb==4.0.11 + # via gitpython +gitpython==3.1.42 + # via swmmanywhere (pyproject.toml) idna==3.6 # via requests imageio==2.33.1 # via scikit-image joblib==1.3.2 # via swmmanywhere (pyproject.toml) +jsonschema==4.21.1 + # via swmmanywhere (pyproject.toml) +jsonschema-specifications==2023.12.1 + # via jsonschema julian==0.14 # via pyswmm kiwisolver==1.4.5 @@ -195,12 +205,20 @@ rasterio==1.3.9 # pysheds # rioxarray # swmmanywhere (pyproject.toml) +referencing==0.33.0 + # via + # jsonschema + # jsonschema-specifications requests==2.31.0 # via # cdsapi # osmnx rioxarray==0.15.1 # via swmmanywhere (pyproject.toml) +rpds-py==0.18.0 + # via + # jsonschema + # referencing salib==1.4.7 # via swmmanywhere (pyproject.toml) scikit-image==0.22.0 @@ -221,6 +239,8 @@ six==1.16.0 # via # fiona # python-dateutil +smmap==5.0.1 + # via gitdb snuggs==1.4.7 # via rasterio swmm-toolkit==0.15.3 diff --git a/swmmanywhere/defs/schema.yml b/swmmanywhere/defs/schema.yml new file mode 100644 index 00000000..c9a7d108 --- /dev/null +++ b/swmmanywhere/defs/schema.yml @@ -0,0 +1,33 @@ +type: object +properties: + base_dir: {type: string} + project: {type: string} + bbox: {type: array, items: {type: number}, minItems: 4, maxItems: 4} + api_keys: {type: string} + run_settings: + type: object + properties: + reporting_iters: {type: integer, minimum: 1} + duration: {type: number} + storevars: + type: array + items: + type: string + enum: [flooding, flow, depth, runoff] + real: + type: object + properties: + inp: {type: string} + graph: {type: string} + subcatchments: {type: string} + results: {type: ['string', 'null']} + required: [graph, subcatchments] + anyOf: + - required: [inp] + - required: [results] + starting_graph: {type: ['string', 'null']} + graphfcn_list: {type: array, items: {type: string}} + metric_list: {type: array, items: {type: string}} + address_overrides: {type: ['object', 'null']} + parameter_overrides: {type: ['object', 'null']} +required: [base_dir, project, bbox, api_keys, run_settings, real, graphfcn_list, metric_list, parameter_overrides] \ No newline at end of file diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index 0a15294e..ecdda381 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -6,6 +6,7 @@ from pathlib import Path import geopandas as gpd +import jsonschema import pandas as pd import pyswmm import yaml @@ -35,7 +36,7 @@ def swmmanywhere(config: dict): # Create the project structure addresses = preprocessing.create_project_structure(config['bbox'], config['project'], - Path(config['base_dir']) + config['base_dir'] ) for key, val in config.get('address_overrides', {}).items(): @@ -102,17 +103,49 @@ def swmmanywhere(config: dict): return metrics -def load_config(config: Path): - """Load a configuration file. +def load_config(config_path: Path): + """Load, validate, and convert Paths in a configuration file. Args: - config (Path): The path to the configuration file. + config_path (Path): The path to the configuration file. Returns: dict: The configuration. """ - with config.open('r') as f: - return yaml.safe_load(f) + # Load the schema + schema_fid = Path(__file__).parent / 'defs' / 'schema.yml' + with schema_fid.open('r') as file: + schema = yaml.safe_load(file) + + with config_path.open('r') as f: + # Load the config + config = yaml.safe_load(f) + + # Validate the config + jsonschema.validate(instance = config, schema = schema) + + # Check top level paths + for key in ['base_dir', 'api_keys']: + if not Path(config[key]).exists(): + raise FileNotFoundError(f"{key} not found at {config[key]}") + config[key] = Path(config[key]) + + # Check real network paths + for key, path in config['real'].items(): + if not isinstance(path, str): + continue + if not Path(path).exists(): + raise FileNotFoundError(f"{key} not found at {path}") + config['real'][key] = Path(path) + + # Check address overrides + for key, path in config.get('address_overrides', {}).items(): + if not Path(path).exists(): + raise FileNotFoundError(f"{key} not found at {path}") + config['address_overrides'][key] = Path(path) + + return config + def run(model: Path, reporting_iters: int = 50, diff --git a/tests/test_swmmanywhere.py b/tests/test_swmmanywhere.py index bb948f47..e939be31 100644 --- a/tests/test_swmmanywhere.py +++ b/tests/test_swmmanywhere.py @@ -40,24 +40,45 @@ def test_run(): def test_swmmanywhere(): """Test the swmmanywhere function.""" with tempfile.TemporaryDirectory() as temp_dir: + # Load the config test_data_dir = Path(__file__).parent / 'test_data' defs_dir = Path(__file__).parent.parent / 'swmmanywhere' / 'defs' - config = swmmanywhere.load_config(test_data_dir / 'demo_config.yml') + with (test_data_dir / 'demo_config.yml').open('r') as f: + config = yaml.safe_load(f) + + # Set some test values base_dir = Path(temp_dir) config['base_dir'] = str(base_dir) - config['bbox'] = (0.05428,51.55847,0.07193,51.56726) + config['bbox'] = [0.05428,51.55847,0.07193,51.56726] config['address_overrides'] = { - 'building': test_data_dir / 'building.geoparquet', - 'precipitation': defs_dir / 'storm.dat' + 'building': str(test_data_dir / 'building.geoparquet'), + 'precipitation': str(defs_dir / 'storm.dat') } - model_dir = base_dir / 'demo' / 'bbox_1' / 'model_1' - config['real']['subcatchments'] = model_dir / 'subcatchments.geoparquet' - config['real']['inp'] = model_dir / 'model_1.inp' - config['real']['graph'] = model_dir / 'graph.parquet' + config['run_settings']['duration'] = 1000 api_keys = {'nasadem_key' : 'b206e65629ac0e53d599e43438560d28'} with open(base_dir / 'api_keys.yml', 'w') as f: yaml.dump(api_keys, f) - config['api_keys'] = base_dir / 'api_keys.yml' + config['api_keys'] = str(base_dir / 'api_keys.yml') + + # Fill the real dict with unused paths to avoid filevalidation errors + config['real']['subcatchments'] = str(defs_dir / 'storm.dat') + config['real']['inp'] = str(defs_dir / 'storm.dat') + config['real']['graph'] = str(defs_dir / 'storm.dat') + + # Write the config + with open(base_dir / 'test_config.yml', 'w') as f: + yaml.dump(config, f) + + # Load and test validation of the config + config = swmmanywhere.load_config(base_dir / 'test_config.yml') + + # Set the test config to just use the generated data + model_dir = base_dir / 'demo' / 'bbox_1' / 'model_1' + config['real']['subcatchments'] = model_dir / 'subcatchments.geoparquet' + config['real']['inp'] = model_dir / 'model_1.inp' + config['real']['graph'] = model_dir / 'graph.parquet' + + # Run swmmanywhere swmmanywhere.swmmanywhere(config) \ No newline at end of file From beff05606213bbe47bd13ddf1d6078f9a7bf8c86 Mon Sep 17 00:00:00 2001 From: Dobson Date: Fri, 15 Mar 2024 16:36:11 +0000 Subject: [PATCH 13/15] Test validation separately --- swmmanywhere/defs/schema.yml | 4 +- swmmanywhere/swmmanywhere.py | 79 ++++++++++++++++++++++++++++-------- tests/test_swmmanywhere.py | 53 +++++++++++++++++++++++- 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/swmmanywhere/defs/schema.yml b/swmmanywhere/defs/schema.yml index c9a7d108..3653df92 100644 --- a/swmmanywhere/defs/schema.yml +++ b/swmmanywhere/defs/schema.yml @@ -15,7 +15,7 @@ properties: type: string enum: [flooding, flow, depth, runoff] real: - type: object + type: ['object', 'null'] properties: inp: {type: string} graph: {type: string} @@ -30,4 +30,4 @@ properties: metric_list: {type: array, items: {type: string}} address_overrides: {type: ['object', 'null']} parameter_overrides: {type: ['object', 'null']} -required: [base_dir, project, bbox, api_keys, run_settings, real, graphfcn_list, metric_list, parameter_overrides] \ No newline at end of file +required: [base_dir, project, bbox, api_keys, graphfcn_list] \ No newline at end of file diff --git a/swmmanywhere/swmmanywhere.py b/swmmanywhere/swmmanywhere.py index ecdda381..89923163 100644 --- a/swmmanywhere/swmmanywhere.py +++ b/swmmanywhere/swmmanywhere.py @@ -103,6 +103,64 @@ def swmmanywhere(config: dict): return metrics +def check_top_level_paths(config: dict): + """Check the top level paths in the config. + + Args: + config (dict): The configuration. + + Raises: + FileNotFoundError: If a top level path does not exist. + """ + for key in ['base_dir', 'api_keys']: + if not Path(config[key]).exists(): + raise FileNotFoundError(f"{key} not found at {config[key]}") + config[key] = Path(config[key]) + return config + +def check_address_overrides(config: dict): + """Check the address overrides in the config. + + Args: + config (dict): The configuration. + + Raises: + FileNotFoundError: If an address override path does not exist. + """ + overrides = config.get('address_overrides', None) + + if not overrides: + return config + + for key, path in overrides.items(): + if not Path(path).exists(): + raise FileNotFoundError(f"{key} not found at {path}") + config['address_overrides'][key] = Path(path) + return config + +def check_real_network_paths(config: dict): + """Check the paths to the real network in the config. + + Args: + config (dict): The configuration. + + Raises: + FileNotFoundError: If a real network path does not exist. + """ + real = config.get('real', None) + + if not real: + return config + + for key, path in real.items(): + if not isinstance(path, str): + continue + if not Path(path).exists(): + raise FileNotFoundError(f"{key} not found at {path}") + config['real'][key] = Path(path) + + return config + def load_config(config_path: Path): """Load, validate, and convert Paths in a configuration file. @@ -125,25 +183,14 @@ def load_config(config_path: Path): jsonschema.validate(instance = config, schema = schema) # Check top level paths - for key in ['base_dir', 'api_keys']: - if not Path(config[key]).exists(): - raise FileNotFoundError(f"{key} not found at {config[key]}") - config[key] = Path(config[key]) - - # Check real network paths - for key, path in config['real'].items(): - if not isinstance(path, str): - continue - if not Path(path).exists(): - raise FileNotFoundError(f"{key} not found at {path}") - config['real'][key] = Path(path) + config = check_top_level_paths(config) # Check address overrides - for key, path in config.get('address_overrides', {}).items(): - if not Path(path).exists(): - raise FileNotFoundError(f"{key} not found at {path}") - config['address_overrides'][key] = Path(path) + config = check_address_overrides(config) + # Check real network paths + config = check_real_network_paths(config) + return config diff --git a/tests/test_swmmanywhere.py b/tests/test_swmmanywhere.py index e939be31..aab18b5b 100644 --- a/tests/test_swmmanywhere.py +++ b/tests/test_swmmanywhere.py @@ -2,6 +2,8 @@ import tempfile from pathlib import Path +import jsonschema +import pytest import yaml from swmmanywhere import __version__, swmmanywhere @@ -81,4 +83,53 @@ def test_swmmanywhere(): # Run swmmanywhere swmmanywhere.swmmanywhere(config) - \ No newline at end of file + +def test_load_config_file_validation(): + """Test the file validation of the config.""" + with tempfile.TemporaryDirectory() as temp_dir: + test_data_dir = Path(__file__).parent / 'test_data' + defs_dir = Path(__file__).parent.parent / 'swmmanywhere' / 'defs' + base_dir = Path(temp_dir) + + # Test file not found + with pytest.raises(FileNotFoundError) as exc_info: + swmmanywhere.load_config(base_dir / 'test_config.yml') + assert "test_config.yml" in str(exc_info.value) + + with (test_data_dir / 'demo_config.yml').open('r') as f: + config = yaml.safe_load(f) + + # Correct and avoid filevalidation errors + config['real'] = None + + # Fill with unused paths to avoid filevalidation errors + config['base_dir'] = str(defs_dir / 'storm.dat') + config['api_keys'] = str(defs_dir / 'storm.dat') + + with open(base_dir / 'test_config.yml', 'w') as f: + yaml.dump(config, f) + + config = swmmanywhere.load_config(base_dir / 'test_config.yml') + assert isinstance(config, dict) + +def test_load_config_schema_validation(): + """Test the schema validation of the config.""" + with tempfile.TemporaryDirectory() as temp_dir: + test_data_dir = Path(__file__).parent / 'test_data' + base_dir = Path(temp_dir) + + # Load the config + with (test_data_dir / 'demo_config.yml').open('r') as f: + config = yaml.safe_load(f) + + # Make an edit not to schema + config['base_dir'] = 1 + + with open(base_dir / 'test_config.yml', 'w') as f: + yaml.dump(config, f) + + # Test schema validation + with pytest.raises(jsonschema.exceptions.ValidationError) as exc_info: + swmmanywhere.load_config(base_dir / 'test_config.yml') + assert "null" in str(exc_info.value) + From 708b40967043008d13a793496f27b6d6105b78af Mon Sep 17 00:00:00 2001 From: Dobson Date: Fri, 15 Mar 2024 16:41:08 +0000 Subject: [PATCH 14/15] Update graph_utilities.py --- swmmanywhere/graph_utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index ccad72a4..3c1cef45 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -1043,7 +1043,7 @@ def __call__(self, G (nx.Graph): A graph """ G = G.copy() - surface_elevations = {n : d['surface_elevation'] for n, d in G.nodes(data=True)} + surface_elevations = nx.get_node_attributes(G, 'surface_elevation') topological_order = list(nx.topological_sort(G)) chamber_floor = {} edge_diams: dict[tuple[Hashable,Hashable,int],float] = {} From c5250ff97a7270760563f77e7b5eb0684172a615 Mon Sep 17 00:00:00 2001 From: Dobson Date: Fri, 15 Mar 2024 16:50:07 +0000 Subject: [PATCH 15/15] Comprehension check iterate validity --- swmmanywhere/graph_utilities.py | 5 +++-- swmmanywhere/metric_utilities.py | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/swmmanywhere/graph_utilities.py b/swmmanywhere/graph_utilities.py index 3c1cef45..aa755a0d 100644 --- a/swmmanywhere/graph_utilities.py +++ b/swmmanywhere/graph_utilities.py @@ -173,9 +173,10 @@ def iterate_graphfcns(G: nx.Graph, Returns: nx.Graph: The graph after the graph functions have been applied. """ + not_exists = [g for g in graphfcn_list if g not in graphfcns] + if not_exists: + raise ValueError(f"Graphfcns are not registered:\n{', '.join(not_exists)}") for function in graphfcn_list: - if function not in graphfcns: - raise ValueError(f"Function {function} not registered in graphfcns.") G = graphfcns[function](G, addresses = addresses, **params) logger.info(f"graphfcn: {function} completed.") return G diff --git a/swmmanywhere/metric_utilities.py b/swmmanywhere/metric_utilities.py index 9bd91b8c..5620833e 100644 --- a/swmmanywhere/metric_utilities.py +++ b/swmmanywhere/metric_utilities.py @@ -16,8 +16,6 @@ import pandas as pd from scipy import stats -from swmmanywhere.logging import logger - class MetricRegistry(dict): """Registry object.""" @@ -78,18 +76,20 @@ def iterate_metrics(synthetic_results: pd.DataFrame, Returns: dict[str, float]: The results of the metrics. """ - results = {} - for metric in metric_list: - if metric not in metrics: - raise ValueError(f"{metric} not registered in metrics.") - results[metric] = metrics[metric](synthetic_results = synthetic_results, - synthetic_subs = synthetic_subs, - synthetic_G = synthetic_G, - real_results = real_results, - real_subs = real_subs, - real_G = real_G) - logger.info(f"metric: {metric} completed.") - return results + not_exists = [m for m in metric_list if m not in metrics] + if not_exists: + raise ValueError(f"Metrics are not registered:\n{', '.join(not_exists)}") + + kwargs = { + "synthetic_results": synthetic_results, + "synthetic_subs": synthetic_subs, + "synthetic_G": synthetic_G, + "real_results": real_results, + "real_subs": real_subs, + "real_G": real_G, + } + + return {m : metrics[m](**kwargs) for m in metric_list} def extract_var(df: pd.DataFrame, var: str) -> pd.DataFrame: