Skip to content

Commit cdf8625

Browse files
authored
Merge branch 'main' into pre-commit-ci-update-config
2 parents ac7b1e7 + e8a9cf5 commit cdf8625

11 files changed

+274
-44
lines changed

.github/workflows/publish.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
path: dist
6262

6363
- name: Generate artifact attestation for sdist and wheel
64-
uses: actions/attest-build-provenance@v1
64+
uses: actions/attest-build-provenance@v2
6565
with:
6666
subject-path: dist
6767

@@ -84,7 +84,7 @@ jobs:
8484
path: dist
8585

8686
- name: Generate artifact attestation for sdist and wheel
87-
uses: actions/attest-build-provenance@v1
87+
uses: actions/attest-build-provenance@v2
8888
with:
8989
subject-path: dist
9090

docs/config_guide.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ parameter_overrides:
4949
max_street_length: 40
5050
```
5151
52-
Note that we must provide the parameter category for the parameter that we are
52+
Note that we must provide the parameter group for the parameter that we are
5353
changing (`subcatchment_derivation` above).
5454

55+
If you want to understand how parameters are implemented in more detail, and in particular if you are creating new behaviours and need to add your own parameters, see our [parameter guide](parameters_guide.md).
56+
5557
As our SWMManywhere paper [link preprint](https://doi.org/10.1016/j.envsoft.2025.106358) demonstrates, you can capture an enormously wide range of UDM behaviours through changing parameters. However, if your system is particularly unusual, or you are testing out new behaviours then you may need to adopt a more elaborate approach.
5658

5759
### Customise `graphfcns`

docs/index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ derive a synthetic urban drainage network anywhere in the world.
66
## Table of contents
77
<!-- markdownlint-disable MD007 -->
88
- [Home](index.md)
9-
- [About](./paper/paper.pdf)
9+
- [About](./paper/paper.md)
1010
- [Quickstart](quickstart.md)
1111
- Guides:
1212
- [Configuration file](config_guide.md)
1313
- [Extended demo](./notebooks/extended_demo.py)
14+
- [Parameters guide](parameters_guide.md)
1415
- [Graph functions](graphfcns_guide.md)
1516
- [Metrics guide](metrics_guide.md)
1617
- [Contributing](CONTRIBUTING.md)

docs/parameters_guide.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Parameters guide
2+
3+
SWMManywhere is a deliberately highly parameterised workflow, with the goal of enabling users to create a diverse range of UDMs. This guide is to explain the logic of the implemented parameters and how to customise them, as what each parameter does is highly specific to the [`graphfcn`](graphfcns_guide.md) that uses it. Instead, to understand specific parameter purposes, you can view all available parameters at the [API](reference-parameters.md).
4+
5+
## Using parameters
6+
7+
Let's look at a [parameter group](reference-parameters.md#swmmanywhere.parameters.OutfallDerivation), which is a group of parameters related to identifying outfall locations.
8+
9+
:::swmmanywhere.parameters.OutfallDerivation
10+
handler: python
11+
options:
12+
members: no
13+
show_root_heading: false
14+
show_bases: false
15+
show_source: true
16+
show_root_toc_entry: false
17+
show_docstring_attributes: false
18+
show_docstring_description: false
19+
show_docstring_examples: false
20+
show_docstring_parameters: false
21+
show_docstring_returns: false
22+
show_docstring_raises: false
23+
24+
We can see here three related parameters and relevant metadata, grouped together in a [`pydantic.BaseModel`](https://docs.pydantic.dev/latest/api/base_model/) object. Parameters in SWMManywhere are grouped together because `graphfcns` that need one of them tend to need the others. Let's look at [`identify_outfalls`](reference-graph-utilities.md#swmmanywhere.graphfcns.outfall_graphfcns.identify_outfalls), which needs these parameters.
25+
26+
:::swmmanywhere.graphfcns.outfall_graphfcns.identify_outfalls
27+
handler: python
28+
options:
29+
members: no
30+
show_root_heading: false
31+
show_bases: false
32+
show_source: true
33+
show_root_toc_entry: false
34+
show_docstring_attributes: false
35+
show_docstring_description: false
36+
show_docstring_examples: false
37+
show_docstring_parameters: false
38+
show_docstring_returns: false
39+
show_docstring_raises: false
40+
41+
When calling [`iterate_graphfcns`](reference-graph-utilities.md#swmmanywhere.graph_utilities.iterate_graphfcns), for more information see [here](graphfcns_guide.md#lists-of-graph-functions), SWMManywhere will automatically provide any parameters that have been registered to any graphfcn.
42+
43+
## Registering parameters
44+
45+
When you create a new parameter, it will need to belong to an existing or new parameter group.
46+
47+
### Creating a new parameter group(s)
48+
49+
You create a new module(s) that can contain multiple parameter groups. See below as a template of such amodule.
50+
51+
```python
52+
{%
53+
include-markdown "../tests/test_data/custom_parameters.py"
54+
comments=false
55+
%}
56+
```
57+
58+
### Adjust config file
59+
60+
We will add the required lines to the
61+
[minimum viable config](config_guide.md#minimum-viable-configuration) template.
62+
63+
```yml
64+
{%
65+
include-markdown "snippets/minimum_viable_template.yml"
66+
comments=false
67+
%}
68+
custom_parameter_modules:
69+
- /path/to/custom_parameters.py
70+
```
71+
72+
Now when we run our `config` file, these parameters will be registered and any [custom graphfcns](graphfcns_guide.md#add-a-new-graph-function) will have access to them.
73+
74+
### Changing existing parameter groups
75+
76+
There may be cases where you want to change existing parameter groups, such as introducing new weights to the [`calculate_weights`](reference-graph-utilities.md#swmmanywhere.graphfcns.topology_graphfcns.calculate_weights) step so that they are minimized during the shortest path optimization. In this example, we want the [`TopologyDerivation`](reference-parameters.md#swmmanywhere.parameters.TopologyDerviation) group to include some new parameters. We can do this in a similar way to [above](#creating-a-new-parameter-groups), but being mindful to inherit from `TopologyDerivation` rather than `BaseModel`:
77+
78+
```python
79+
from swmmanywhere.parameters import register_parameter_group, TopologyDerivation, Field
80+
81+
@register_parameter_group("topology_derivation")
82+
class NewTopologyDerivation(TopologyDerivation):
83+
new_weight_scaling: float = Field(
84+
default=1,
85+
le=1,
86+
ge=0,
87+
)
88+
new_weight_exponent: float = Field(
89+
default=1,
90+
le=2,
91+
ge=0,
92+
)
93+
```
94+
95+
Now the `calculate_weights` function will have access to these new weighting parameters, as well as existing ones.
96+
97+
Note, in this specific example of adding custom weights, you will also have to:
98+
99+
- Update the `weights` parameter in your `config` file, for example:
100+
101+
```yaml
102+
parameter_overrides:
103+
topology_derviation:
104+
weights:
105+
- new_weight
106+
- length
107+
```
108+
109+
- [Create and register a `graphfcn`](graphfcns_guide.md#add-a-new-graph-function) that adds the `new_weight` parameter to the graph.

mkdocs.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ extra_javascript:
4141

4242
nav:
4343
- Home: index.md
44-
- About: ./paper/paper.pdf
44+
- About: ./paper/paper.md
4545
- Quickstart: quickstart.md
4646
- Guides:
4747
- Configuration guide: config_guide.md
4848
- Extended demo: ./notebooks/extended_demo.py
49+
- Parameters guide: parameters_guide.md
4950
- Graph functions: graphfcns_guide.md
5051
- Metrics guide: metrics_guide.md
5152
- Contributing: CONTRIBUTING.md

src/swmmanywhere/defs/schema.yml

+1
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ properties:
3333
parameter_overrides: {type: ['object', 'null']}
3434
custom_metric_modules: {type: array, items: {type: string}}
3535
custom_graphfcn_modules: {type: array, items: {type: string}}
36+
custom_parameters_modules: {type: array, items: {type: string}}
3637
required: [base_dir, project, bbox]

src/swmmanywhere/parameters.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,36 @@
22

33
from __future__ import annotations
44

5+
from typing import Callable
6+
57
import numpy as np
68
from pydantic import BaseModel, Field, model_validator
79

10+
from swmmanywhere.logging import logger
11+
12+
parameter_register = {}
13+
14+
15+
def register_parameter_group(name: str) -> Callable:
16+
"""Register a parameter group.
17+
18+
Args:
19+
name (str): Name of the parameter group that it will be keyed to in
20+
parameter_register.
21+
"""
22+
23+
def wrapper(cls: BaseModel) -> BaseModel:
24+
if name in parameter_register:
25+
logger.warning(f"{name} already in parameter register, overwriting.")
26+
parameter_register[name] = cls()
27+
return cls
28+
29+
return wrapper
30+
831

932
def get_full_parameters():
1033
"""Get the full set of parameters."""
11-
return {
12-
"subcatchment_derivation": SubcatchmentDerivation(),
13-
"outfall_derivation": OutfallDerivation(),
14-
"topology_derivation": TopologyDerivation(),
15-
"hydraulic_design": HydraulicDesign(),
16-
"metric_evaluation": MetricEvaluation(),
17-
}
34+
return parameter_register
1835

1936

2037
def get_full_parameters_flat():
@@ -32,6 +49,7 @@ def get_full_parameters_flat():
3249
return parameters_flat
3350

3451

52+
@register_parameter_group(name="subcatchment_derivation")
3553
class SubcatchmentDerivation(BaseModel):
3654
"""Parameters for subcatchment derivation."""
3755

@@ -86,6 +104,7 @@ class SubcatchmentDerivation(BaseModel):
86104
)
87105

88106

107+
@register_parameter_group(name="outfall_derivation")
89108
class OutfallDerivation(BaseModel):
90109
"""Parameters for outfall derivation."""
91110

@@ -113,6 +132,7 @@ class OutfallDerivation(BaseModel):
113132
)
114133

115134

135+
@register_parameter_group(name="topology_derivation")
116136
class TopologyDerivation(BaseModel):
117137
"""Parameters for topology derivation."""
118138

@@ -212,6 +232,7 @@ def check_weights(cls, values):
212232
return values
213233

214234

235+
@register_parameter_group("hydraulic_design")
215236
class HydraulicDesign(BaseModel):
216237
"""Parameters for hydraulic design."""
217238

@@ -273,6 +294,7 @@ class HydraulicDesign(BaseModel):
273294
)
274295

275296

297+
@register_parameter_group(name="metric_evaluation")
276298
class MetricEvaluation(BaseModel):
277299
"""Parameters for metric evaluation."""
278300

src/swmmanywhere/swmmanywhere.py

+48-31
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,38 @@ def check_parameter_overrides(config: dict):
314314
return config
315315

316316

317+
def import_module(module: Path):
318+
"""Import module with importlib.
319+
320+
Args:
321+
module (Path): path to module.
322+
"""
323+
# Import the module
324+
spec = importlib.util.spec_from_file_location( # type: ignore[attr-defined]
325+
module.stem, module
326+
)
327+
module = importlib.util.module_from_spec(spec) # type: ignore[attr-defined]
328+
spec.loader.exec_module(module)
329+
330+
331+
def import_modules(modules: list[str | Path]):
332+
"""Import modules specified in list of files.
333+
334+
Args:
335+
modules (list): List of files
336+
337+
Raises:
338+
ValueError: If a module does not exist.
339+
"""
340+
for module in modules:
341+
module = Path(module)
342+
343+
# Check that module exists
344+
if not module.exists():
345+
raise FileNotFoundError(f"Module not found at {module}")
346+
import_module(module)
347+
348+
317349
def check_and_register_custom_graphfcns(config: dict):
318350
"""Check, register and validate custom graphfcns in the config.
319351
@@ -324,21 +356,7 @@ def check_and_register_custom_graphfcns(config: dict):
324356
ValueError: If a graphfcn module does not exist.
325357
ValueError: If a custom graphfcn is not successfully registered.
326358
"""
327-
for custom_graphfcn_module in config.get("custom_graphfcn_modules", []):
328-
custom_graphfcn_module = Path(custom_graphfcn_module)
329-
330-
# Check that the custom graphfcn exists
331-
if not custom_graphfcn_module.exists():
332-
raise FileNotFoundError(
333-
f"Custom graphfcn not found at {custom_graphfcn_module}"
334-
)
335-
336-
# Import the custom graphfcn module
337-
spec = importlib.util.spec_from_file_location( # type: ignore[attr-defined]
338-
custom_graphfcn_module.stem, custom_graphfcn_module
339-
)
340-
custom_graphfcn_module = importlib.util.module_from_spec(spec) # type: ignore[attr-defined]
341-
spec.loader.exec_module(custom_graphfcn_module)
359+
import_modules(config.get("custom_graphfcn_modules", []))
342360

343361
# Validate the import
344362
validate_graphfcn_list(config.get("graphfcn_list", []))
@@ -355,28 +373,24 @@ def check_and_register_custom_metrics(config: dict):
355373
Raises:
356374
ValueError: If the custom metrics module does not exist.
357375
"""
358-
for custom_metric_module in config.get("custom_metric_modules", []):
359-
custom_metric_module = Path(custom_metric_module)
360-
361-
# Check that the custom graphfcn exists
362-
if not custom_metric_module.exists():
363-
raise FileNotFoundError(
364-
f"Custom graphfcn not found at {custom_metric_module}"
365-
)
366-
367-
# Import the custom graphfcn module
368-
spec = importlib.util.spec_from_file_location( # type: ignore[attr-defined]
369-
custom_metric_module.stem, custom_metric_module
370-
)
371-
custom_metric_module = importlib.util.module_from_spec(spec) # type: ignore[attr-defined]
372-
spec.loader.exec_module(custom_metric_module)
376+
import_modules(config.get("custom_metric_modules", []))
373377

374378
# Validate metric list
375379
validate_metric_list(config.get("metric_list", []))
376380

377381
return config
378382

379383

384+
def register_custom_parameters(config: dict):
385+
"""Register custom parameter modules.
386+
387+
Args:
388+
config (dict): The configuration.
389+
"""
390+
import_modules(config.get("custom_parameter_modules", []))
391+
return config
392+
393+
380394
def save_config(config: dict, config_path: Path):
381395
"""Save the configuration to a file.
382396
@@ -441,9 +455,12 @@ def load_config(
441455
# Check and register custom metrics
442456
config = check_and_register_custom_metrics(config)
443457

444-
# Check custom graphfcns
458+
# Check and register custom graphfcns
445459
config = check_and_register_custom_graphfcns(config)
446460

461+
# Register custom parameters
462+
config = register_custom_parameters(config)
463+
447464
return config
448465

449466

tests/test_data/custom_parameters.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
from swmmanywhere import parameters
4+
5+
6+
@parameters.register_parameter_group(name="new_params")
7+
class new_params(parameters.BaseModel):
8+
"""New parameters."""
9+
10+
new_param: int = parameters.Field(
11+
default=1,
12+
ge=0,
13+
le=10,
14+
unit="-",
15+
description="A new parameter.",
16+
)

0 commit comments

Comments
 (0)