3.1.3. Impact Studies Using SuPy#

3.1.3.1. Aim#

In this tutorial, we aim to perform sensitivity analysis using supy in a parallel mode to investigate the impacts on urban climate of

  1. surface properties: the physical attributes of land covers (e.g., albedo, water holding capacity, etc.)

  2. background climate: longterm meteorological conditions (e.g., air temperature, precipitation, etc.)

3.1.3.1.1. load supy and sample dataset#

[19]:
import supy as sp

import pandas as pd
import numpy as np

from time import time

[20]:
# Load sample datasets
from supy import SUEWSSimulation
import supy as sp

sim = SUEWSSimulation.from_sample_data()

# Extract initial state and forcing for impact studies
df_state_init = sim.state_init
df_forcing = sim.forcing

print(" Sample data loaded using modern SUEWSSimulation.from_sample_data() API")
print(" Ready for impact studies")

# by default, two years of forcing data are included;
# to save running time for demonstration, we only use one year in this demo
df_forcing = df_forcing.loc["2012"].iloc[1:]

# perform an example run to get output samples for later use
# Update simulation with modified data
sim.update_forcing(df_forcing)

# Run simulation
df_output = sim.run()

# Access results
df_output = sim.results
df_state_final = sim.state_final
2025-11-20 00:05:51,192 - SuPy - INFO - Loading config from yaml
 Sample data loaded using modern SUEWSSimulation.from_sample_data() API
 Ready for impact studies
[20]:
SUEWSSimulation(Ready: 1 site(s), 105406 timesteps)

3.1.3.2. Surface properties: surface albedo#

3.1.3.2.1. Examine the default albedo values loaded from the sample dataset#

[21]:
df_state_init.alb
[21]:
ind_dim (0,) (1,) (2,) (3,) (4,) (5,) (6,)
grid
1 0.1 0.12 0.1 0.18 0.21 0.18 0.1

3.1.3.2.2. Copy the initial condition DataFrame to have a clean slate for our study#

Note: DataFrame.copy() defaults to deepcopy

[22]:
df_state_init_test = df_state_init.copy()

3.1.3.2.3. Set the Bldg land cover to 99% and Paved to 1% for this study#

[23]:
df_state_init_test.sfr_surf = 0
df_state_init_test.loc[:, ("sfr_surf", "(1,)")] = 0.99
df_state_init_test.loc[:, ("sfr_surf", "(0,)")] = 0.01
df_state_init_test.sfr_surf

[23]:
ind_dim (0,) (1,) (2,) (3,) (4,) (5,) (6,)
grid
1 0.01 0.99 0 0 0 0 0

3.1.3.2.4. Construct a df_state_init_x dataframe to perform supy simulations with specified albedo#

[24]:
# create a `df_state_init_x` with different surface properties
n_test = 10
list_alb_test = np.linspace(0.1, 0.8, n_test).round(2)
df_state_init_x = (
    pd.concat(
        {alb: df_state_init_test for alb in list_alb_test},
        names=["alb", "grid"],
    )
    .droplevel("grid", axis=0)
    .rename_axis(index="grid")
)

# here we modify surface albedo
df_state_init_x.loc[:, ("alb", "(1,)")] = list_alb_test
df_state_init_x.alb

[24]:
ind_dim (0,) (1,) (2,) (3,) (4,) (5,) (6,)
grid
0.10 0.1 0.10 0.1 0.18 0.21 0.18 0.1
0.18 0.1 0.18 0.1 0.18 0.21 0.18 0.1
0.26 0.1 0.26 0.1 0.18 0.21 0.18 0.1
0.33 0.1 0.33 0.1 0.18 0.21 0.18 0.1
0.41 0.1 0.41 0.1 0.18 0.21 0.18 0.1
0.49 0.1 0.49 0.1 0.18 0.21 0.18 0.1
0.57 0.1 0.57 0.1 0.18 0.21 0.18 0.1
0.64 0.1 0.64 0.1 0.18 0.21 0.18 0.1
0.72 0.1 0.72 0.1 0.18 0.21 0.18 0.1
0.80 0.1 0.80 0.1 0.18 0.21 0.18 0.1

3.1.3.2.5. Conduct simulations with supy#

[25]:
# Conduct simulations with OOP approach
df_forcing_part = df_forcing.loc["2012 01":"2012 07"]

# Create simulation from modified state and forcing
sim_test = SUEWSSimulation.from_state(df_state_init_x).update_forcing(df_forcing_part)

# Run simulation
df_res_alb_test = sim_test.run(logging_level=90)

3.1.3.2.6. Examine the simulation results#

[26]:
# choose results of July 2012 for analysis
df_res_alb_test_july = df_res_alb_test.SUEWS.unstack(0).loc["2012 7"]
df_res_alb_T2_stat = df_res_alb_test_july.T2.describe()
df_res_alb_T2_diff = df_res_alb_T2_stat.transform(
    lambda x: x - df_res_alb_T2_stat.iloc[:, 0]
)
df_res_alb_T2_diff.columns = list_alb_test - list_alb_test[0]
[27]:
# plot the temperature difference
ax_temp_diff = df_res_alb_T2_diff.loc[["max", "mean", "min"]].T.plot()
_ = ax_temp_diff.set_ylabel(r"$\Delta T_2$ ($^\circ$C)")
_ = ax_temp_diff.set_xlabel(r"$\Delta\alpha$")
ax_temp_diff.margins(x=0.2, y=0.2)

../../_images/tutorials_python_impact-studies_19_0.png

3.1.3.3. Background climate: air temperature#

3.1.3.3.1. Examine the monthly climatology of air temperature loaded from the sample dataset#

[28]:
df_plot = df_forcing.Tair.loc["2012"].resample("1m").mean()
ax_temp = df_plot.plot.bar(color="tab:blue")
_ = ax_temp.set_xticklabels(df_plot.index.strftime("%b"))
_ = ax_temp.set_ylabel(r"Mean Air Temperature ($^\circ$C)")
_ = ax_temp.set_xlabel("Month")
../../_images/tutorials_python_impact-studies_22_0.png

3.1.3.3.2. Construct a function to perform parallel supy simulations with specified diff_airtemp_test: the difference in air temperature between the one used in simulation and loaded from sample dataset.#

Note

forcing data df_forcing has different data structure from df_state_init; so we need to modify run_supy_mgrids to implement a run_supy_mclims for different climate scenarios*

Let’s start the implementation of run_supy_mclims with a small problem of four forcing groups (i.e., climate scenarios), where the air temperatures differ from the baseline scenario with a constant bias.

[29]:
# save loaded sample datasets
df_forcing_part_test = df_forcing.loc["2012 1":"2012 7"].copy()
df_state_init_test = df_state_init.copy()
[30]:
from concurrent.futures import ThreadPoolExecutor

# create a dict with four forcing conditions as a test
n_test = 4
list_TairDiff_test = np.linspace(0.0, 2, n_test).round(2)
dict_df_forcing_x = {
    tairdiff: df_forcing_part_test.copy() for tairdiff in list_TairDiff_test
}
for tairdiff in dict_df_forcing_x:
    dict_df_forcing_x[tairdiff].loc[:, "Tair"] += tairdiff


# Helper function for parallel simulation
def run_sim_oop(key, df_forcing, df_state_init, logging_level=90):
    sim = SUEWSSimulation.from_state(df_state_init)
    sim.update_forcing(df_forcing)
    sim.run(logging_level=logging_level)
    return (key, sim.results)


# Run simulations in parallel using Python's built-in ThreadPoolExecutor
# Note: Using threads (not processes) to work in Jupyter notebooks
with ThreadPoolExecutor() as executor:
    futures = [
        executor.submit(run_sim_oop, k, df, df_state_init_test, 90)
        for k, df in dict_df_forcing_x.items()
    ]
    results = {key: result for key, result in [f.result() for f in futures]}

df_res_tairdiff_test0 = pd.concat(
    results,
    keys=list_TairDiff_test,
    names=["tairdiff"],
)
df_res_tairdiff_test = df_res_tairdiff_test0.reset_index("grid", drop=True)
[31]:
# test the performance of a parallel run
t0 = time()
# Execute the parallel simulation (already done in cell above)
t1 = time()
t_par = t1 - t0
print(f"Execution time: {t_par:.2f} s")
Execution time: 0.00 s
[32]:
# function for multi-climate `run_supy` using OOP interface and built-in parallelization
def run_supy_mclims(df_state_init, dict_df_forcing_mclims):
    from concurrent.futures import ThreadPoolExecutor

    # Helper function for parallel simulation
    def run_sim_oop(key, df_forcing, df_state_init, logging_level=90):
        sim = SUEWSSimulation.from_state(df_state_init)
        sim.update_forcing(df_forcing)
        sim.run(logging_level=logging_level)
        return (key, sim.results)

    # Run simulations in parallel using threads
    # Note: Using threads (not processes) to work in Jupyter notebooks
    with ThreadPoolExecutor() as executor:
        futures = [
            executor.submit(run_sim_oop, k, df, df_state_init, 90)
            for k, df in dict_df_forcing_mclims.items()
        ]
        results = {key: result for key, result in [f.result() for f in futures]}

    df_output_mclims0 = pd.concat(
        results,
        keys=list(dict_df_forcing_mclims.keys()),
        names=["clm"],
    )
    df_output_mclims = df_output_mclims0.reset_index("grid", drop=True)

    return df_output_mclims

3.1.3.3.3. Construct dict_df_forcing_x with multiple forcing DataFrames#

[33]:
# save loaded sample datasets
df_forcing_part_test = df_forcing.loc["2012 1":"2012 7"].copy()
df_state_init_test = df_state_init.copy()

# create a dict with a number of forcing conditions
n_test = 12  # can be set with a smaller value to save simulation time
list_TairDiff_test = np.linspace(0.0, 2, n_test).round(2)
dict_df_forcing_x = {
    tairdiff: df_forcing_part_test.copy() for tairdiff in list_TairDiff_test
}
for tairdiff in dict_df_forcing_x:
    dict_df_forcing_x[tairdiff].loc[:, "Tair"] += tairdiff

3.1.3.3.4. Perform simulations#

[34]:
# run parallel simulations using `run_supy_mclims`
t0 = time()
df_airtemp_test_x = run_supy_mclims(df_state_init_test, dict_df_forcing_x)
t1 = time()
t_par = t1 - t0
print(f"Execution time: {t_par:.2f} s")
Execution time: 72.65 s

3.1.3.3.5. Examine the results#

[35]:
df_airtemp_test = df_airtemp_test_x.SUEWS.unstack(0)
df_temp_diff = df_airtemp_test.T2.transform(lambda x: x - df_airtemp_test.T2[0.0])
df_temp_diff_ana = df_temp_diff.loc["2012 7"]
df_temp_diff_stat = df_temp_diff_ana.describe().loc[["max", "mean", "min"]].T
[36]:
ax_temp_diff_stat = df_temp_diff_stat.plot()
_ = ax_temp_diff_stat.set_ylabel(r"$\Delta T_2$ ($^\circ$C)")
_ = ax_temp_diff_stat.set_xlabel(r"$\Delta T_{a}$ ($^\circ$C)")
ax_temp_diff_stat.set_aspect("equal")
../../_images/tutorials_python_impact-studies_36_0.png

The \(T_{2}\) results indicate the increased \(T_{a}\) has different impacts on the \(T_{2}\) metrics (minimum, mean and maximum) but all increase linearly with \(T_{a}.\) The maximum \(T_{2}\) has the stronger response compared to the other metrics.