
.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "auto_examples/tutorial_06_external_coupling.py"
.. LINE NUMBERS ARE GIVEN BELOW.

.. only:: html

    .. note::
        :class: sphx-glr-download-link-note

        :ref:`Go to the end <sphx_glr_download_auto_examples_tutorial_06_external_coupling.py>`
        to download the full example code.

.. rst-class:: sphx-glr-example-title

.. _sphx_glr_auto_examples_tutorial_06_external_coupling.py:


External Model Coupling
=======================

Coupling SUEWS with external models for anthropogenic heat flux.

This example demonstrates how SUEWS can be coupled to external models that
provide forcing data. We use a simple anthropogenic heat flux (:math:`Q_F`) model
driven by outdoor air temperature to show the coupling approach.

**Key concepts:**

- Anthropogenic heat flux depends on outdoor temperature
- SUEWS can receive external :math:`Q_F` as forcing input
- One-way coupling: external model provides forcing to SUEWS
- Temperature feedback affects urban energy balance

**API approach**: This tutorial uses the :class:`~supy.SUEWSSimulation` OOP interface but
extracts DataFrames for forcing modification. This hybrid pattern is required
for external model coupling where forcing variables must be modified at runtime.

.. GENERATED FROM PYTHON SOURCE LINES 22-29

.. code-block:: Python


    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd

    from supy import SUEWSSimulation








.. GENERATED FROM PYTHON SOURCE LINES 30-41

Simple Anthropogenic Heat Model
-------------------------------

We define a simple :math:`Q_F` model based on building heating and cooling:

- Heating activates when T < 10 :math:`^{\circ}C`
- Cooling activates when T > 20 :math:`^{\circ}C`
- Between 10-20 :math:`^{\circ}C`: baseline :math:`Q_F` = 0

This simplified model captures the temperature-dependent nature of
building energy use without requiring detailed building simulation.

.. GENERATED FROM PYTHON SOURCE LINES 41-72

.. code-block:: Python



    def QF_simple(T2):
        """Calculate anthropogenic heat flux from air temperature.

        Parameters
        ----------
        T2 : float
            2-metre air temperature (:math:`^{\circ}C`)

        Returns
        -------
        float
            Anthropogenic heat flux (W m\ :sup:`-2`)
        """
        T_cool = 20  # Cooling threshold (degC)
        T_heat = 10  # Heating threshold (degC)
        C_B = 5  # Cooling rate (W m^-2 K^-1)
        H_B = 10  # Heating rate (W m^-2 K^-1)
        scale = 0.3  # Scaling factor

        if T2 > T_cool:  # noqa: SIM300
            qf = (T2 - T_cool) * C_B
        elif T2 < T_heat:  # noqa: SIM300
            qf = (T_heat - T2) * H_B
        else:
            qf = 0

        return qf * scale






.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    /private/tmp/claude/-Users-tingsun-conductor-workspaces-suews-havana-v1/41d4ccd2-265e-4416-b3db-fa0bcb209699/scratchpad/docs-worktree/docs/source/tutorials/tutorial_06_external_coupling.py:44: SyntaxWarning: invalid escape sequence '\c'
      """Calculate anthropogenic heat flux from air temperature.




.. GENERATED FROM PYTHON SOURCE LINES 73-77

Visualise the :math:`Q_F` Model
-------------------------------

Plot the :math:`Q_F` response to temperature to understand the model behaviour.

.. GENERATED FROM PYTHON SOURCE LINES 77-114

.. code-block:: Python


    temp_range = np.arange(-5, 45, 0.5)
    qf_values = [QF_simple(t) for t in temp_range]

    # Create DataFrames for plotting by region
    df_qf = pd.DataFrame({"Temperature": temp_range, "QF": qf_values})

    fig, ax = plt.subplots(figsize=(8, 5))

    # Heating region (T < 10 degC)
    mask_heat = temp_range < 10
    ax.plot(temp_range[mask_heat], np.array(qf_values)[mask_heat], "C1-", label="Heating", linewidth=2)

    # Neutral region (10 <= T <= 20 degC)
    mask_neutral = (temp_range >= 10) & (temp_range <= 20)
    ax.plot(
        temp_range[mask_neutral], np.array(qf_values)[mask_neutral], "C2-", label="Baseline", linewidth=2
    )

    # Cooling region (T > 20 degC)
    mask_cool = temp_range > 20
    ax.plot(temp_range[mask_cool], np.array(qf_values)[mask_cool], "C0-", label="Cooling", linewidth=2)

    # Annotations
    ax.axvline(10, color="grey", linestyle="--", alpha=0.5)
    ax.axvline(20, color="grey", linestyle="--", alpha=0.5)
    ax.annotate("$T_H$ = 10$^{\\circ}$C", xy=(10, 5), fontsize=10)
    ax.annotate("$T_C$ = 20$^{\\circ}$C", xy=(20, 5), fontsize=10)

    ax.set_xlabel("Air Temperature ($^\\circ$C)")
    ax.set_ylabel("$Q_F$ (W m$^{-2}$)")
    ax.set_title("Simple Anthropogenic Heat Flux Model")
    ax.legend()
    ax.grid(True, alpha=0.3)
    plt.tight_layout()





.. image-sg:: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_001.png
   :alt: Simple Anthropogenic Heat Flux Model
   :srcset: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_001.png
   :class: sphx-glr-single-img





.. GENERATED FROM PYTHON SOURCE LINES 116-120

Load Sample Data
----------------

Load the sample dataset and prepare for coupled simulation.

.. GENERATED FROM PYTHON SOURCE LINES 120-131

.. code-block:: Python


    sim = SUEWSSimulation.from_sample_data()
    df_state_init = sim.state_init.copy()

    # Disable snow module (not needed for this site)
    df_state_init.loc[:, "snowuse"] = 0

    # Slice forcing by time (returns SUEWSForcing object)
    # Use shorter period for faster tutorial execution
    forcing_sliced = sim.forcing["2012-01":"2012-02"].iloc[1:]








.. GENERATED FROM PYTHON SOURCE LINES 132-138

.. important::

   **External coupling requires DataFrame extraction**: When injecting
   variables from external models (here: :math:`Q_F`), you must extract the
   DataFrame with ``.df``, add the new column, then pass it to the
   simulation. This is the expected pattern for model coupling.

.. GENERATED FROM PYTHON SOURCE LINES 138-145

.. code-block:: Python


    # Get DataFrame for modification (adding external :math:`Q_F`)
    df_forcing = forcing_sliced.df

    print(f"Simulation period: {df_forcing.index[0]} to {df_forcing.index[-1]}")
    print(f"Time steps: {len(df_forcing)}")





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    Simulation period: 2012-01-01 00:10:00 to 2012-02-29 23:55:00
    Time steps: 17278




.. GENERATED FROM PYTHON SOURCE LINES 146-151

Calculate External :math:`Q_F` from Forcing Data
------------------------------------------------

Apply the simple :math:`Q_F` model to the air temperature from the forcing data.
This is a one-way coupling: the external model uses prescribed temperatures.

.. GENERATED FROM PYTHON SOURCE LINES 151-168

.. code-block:: Python


    # Calculate :math:`Q_F` for each timestep
    qf_external = df_forcing["Tair"].apply(QF_simple)

    # Create modified forcing with external :math:`Q_F`
    df_forcing_with_qf = df_forcing.copy()
    df_forcing_with_qf["qf"] = qf_external

    # Configure SUEWS to use external :math:`Q_F`
    df_state_qf = df_state_init.copy()
    df_state_qf.loc[:, "emissionsmethod"] = 0  # Use prescribed Q_F

    print("Q_F statistics:")
    print(f"  Mean: {qf_external.mean():.2f} W/m^2")
    print(f"  Max:  {qf_external.max():.2f} W/m^2")
    print(f"  Min:  {qf_external.min():.2f} W/m^2")





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    Q_F statistics:
      Mean: 15.49 W/m^2
      Max:  47.10 W/m^2
      Min:  0.00 W/m^2




.. GENERATED FROM PYTHON SOURCE LINES 169-173

Run Baseline Simulation (:math:`Q_F` = 0)
-----------------------------------------

First, run a simulation without anthropogenic heat for comparison.

.. GENERATED FROM PYTHON SOURCE LINES 173-188

.. code-block:: Python


    df_forcing_baseline = df_forcing.copy()
    df_forcing_baseline["qf"] = 0

    df_state_baseline = df_state_init.copy()
    df_state_baseline.loc[:, "emissionsmethod"] = 0

    sim_baseline = SUEWSSimulation.from_state(df_state_baseline).update_forcing(df_forcing_baseline)
    output_baseline = sim_baseline.run(logging_level=90)

    grid = df_state_init.index[0]
    df_suews_baseline = output_baseline.SUEWS.loc[grid]

    print("Baseline simulation complete (Q_F = 0)")





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    Baseline simulation complete (Q_F = 0)




.. GENERATED FROM PYTHON SOURCE LINES 189-193

Run Simulation with External :math:`Q_F`
----------------------------------------

Now run with the temperature-dependent :math:`Q_F` from our simple model.

.. GENERATED FROM PYTHON SOURCE LINES 193-201

.. code-block:: Python


    sim_with_qf = SUEWSSimulation.from_state(df_state_qf).update_forcing(df_forcing_with_qf)
    output_qf = sim_with_qf.run(logging_level=90)

    df_suews_qf = output_qf.SUEWS.loc[grid]

    print("Coupled simulation complete (with external Q_F)")





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    Coupled simulation complete (with external Q_F)




.. GENERATED FROM PYTHON SOURCE LINES 202-206

Compare :math:`Q_F` Values
--------------------------

Compare the prescribed :math:`Q_F` with the baseline (zero) case.

.. GENERATED FROM PYTHON SOURCE LINES 206-226

.. code-block:: Python


    fig, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)

    # Plot Q_F time series
    df_suews_qf["QF"].resample("1h").mean().plot(ax=axes[0], label="With external $Q_F$")
    df_suews_baseline["QF"].resample("1h").mean().plot(ax=axes[0], label="Baseline ($Q_F$=0)")
    axes[0].set_ylabel("$Q_F$ (W m$^{-2}$)")
    axes[0].set_title("Anthropogenic Heat Flux")
    axes[0].legend()

    # Plot difference
    diff_qf = (df_suews_qf["QF"] - df_suews_baseline["QF"]).resample("1h").mean()
    diff_qf.plot(ax=axes[1], color="C2")
    axes[1].set_ylabel("$\\Delta Q_F$ (W m$^{-2}$)")
    axes[1].set_xlabel("Date")
    axes[1].set_title("$Q_F$ Difference (Coupled - Baseline)")
    axes[1].axhline(0, color="grey", linestyle="--", alpha=0.5)

    plt.tight_layout()




.. image-sg:: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_002.png
   :alt: Anthropogenic Heat Flux, $Q_F$ Difference (Coupled - Baseline)
   :srcset: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_002.png
   :class: sphx-glr-single-img





.. GENERATED FROM PYTHON SOURCE LINES 227-231

Temperature Feedback
--------------------

Examine how the external :math:`Q_F` affects the simulated 2-metre temperature.

.. GENERATED FROM PYTHON SOURCE LINES 231-251

.. code-block:: Python


    fig, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)

    # Plot T2 time series
    df_suews_qf["T2"].resample("1h").mean().plot(ax=axes[0], label="With external $Q_F$")
    df_suews_baseline["T2"].resample("1h").mean().plot(ax=axes[0], label="Baseline")
    axes[0].set_ylabel("$T_2$ ($^\\circ$C)")
    axes[0].set_title("2-Metre Air Temperature")
    axes[0].legend()

    # Plot temperature difference
    diff_t2 = (df_suews_qf["T2"] - df_suews_baseline["T2"]).resample("1h").mean()
    diff_t2.plot(ax=axes[1], color="C3")
    axes[1].set_ylabel("$\\Delta T_2$ ($^\\circ$C)")
    axes[1].set_xlabel("Date")
    axes[1].set_title("Temperature Difference (Coupled - Baseline)")
    axes[1].axhline(0, color="grey", linestyle="--", alpha=0.5)

    plt.tight_layout()




.. image-sg:: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_003.png
   :alt: 2-Metre Air Temperature, Temperature Difference (Coupled - Baseline)
   :srcset: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_003.png
   :class: sphx-glr-single-img





.. GENERATED FROM PYTHON SOURCE LINES 252-256

:math:`Q_F` vs Temperature Response
-----------------------------------

Analyse the relationship between :math:`Q_F` change and temperature change.

.. GENERATED FROM PYTHON SOURCE LINES 256-281

.. code-block:: Python


    # Daily statistics
    df_daily_qf = df_suews_qf.resample("1D").mean()
    df_daily_baseline = df_suews_baseline.resample("1D").mean()

    diff_qf_daily = df_daily_qf["QF"] - df_daily_baseline["QF"]
    diff_t2_daily = df_daily_qf["T2"] - df_daily_baseline["T2"]

    fig, ax = plt.subplots(figsize=(6, 5))
    ax.scatter(diff_qf_daily, diff_t2_daily, alpha=0.7, s=30)
    ax.set_xlabel("$\\Delta Q_F$ (W m$^{-2}$)")
    ax.set_ylabel("$\\Delta T_2$ ($^\\circ$C)")
    ax.set_title("$Q_F$ - Temperature Feedback (Daily Means)")

    # Add trend line
    if len(diff_qf_daily.dropna()) > 2:
        z = np.polyfit(diff_qf_daily.dropna(), diff_t2_daily.dropna(), 1)
        p = np.poly1d(z)
        x_line = np.linspace(diff_qf_daily.min(), diff_qf_daily.max(), 100)
        ax.plot(x_line, p(x_line), "r--", alpha=0.7, label=f"Slope: {z[0]:.3f} $^{{\\circ}}$C/(W/m$^2$)")
        ax.legend()

    ax.grid(True, alpha=0.3)
    plt.tight_layout()




.. image-sg:: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_004.png
   :alt: $Q_F$ - Temperature Feedback (Daily Means)
   :srcset: /auto_examples/images/sphx_glr_tutorial_06_external_coupling_004.png
   :class: sphx-glr-single-img





.. GENERATED FROM PYTHON SOURCE LINES 282-309

Summary
-------

This example demonstrated one-way coupling between SUEWS and an external
anthropogenic heat flux model:

1. Simple :math:`Q_F` model: Temperature-dependent heating/cooling emissions
2. One-way coupling: External :math:`Q_F` prescribed from air temperature
3. Temperature feedback: :math:`Q_F` affects simulated air temperature

**Key findings:**

- Anthropogenic heat adds energy to the urban surface
- Higher :math:`Q_F` leads to elevated air temperatures
- The feedback magnitude depends on atmospheric conditions

**Extensions:**

- Two-way coupling: SUEWS T2 drives external model, which provides :math:`Q_F`
- Building energy models for detailed :math:`Q_F` estimation
- Traffic and metabolic heat contributions

.. note::

   For tight two-way coupling (timestep-by-timestep interaction),
   low-level SUEWS functions are required. See the source notebook
   for implementation details.


.. rst-class:: sphx-glr-timing

   **Total running time of the script:** (0 minutes 7.260 seconds)


.. _sphx_glr_download_auto_examples_tutorial_06_external_coupling.py:

.. only:: html

  .. container:: sphx-glr-footer sphx-glr-footer-example

    .. container:: sphx-glr-download sphx-glr-download-jupyter

      :download:`Download Jupyter notebook: tutorial_06_external_coupling.ipynb <tutorial_06_external_coupling.ipynb>`

    .. container:: sphx-glr-download sphx-glr-download-python

      :download:`Download Python source code: tutorial_06_external_coupling.py <tutorial_06_external_coupling.py>`

    .. container:: sphx-glr-download sphx-glr-download-zip

      :download:`Download zipped: tutorial_06_external_coupling.zip <tutorial_06_external_coupling.zip>`


.. only:: html

 .. rst-class:: sphx-glr-signature

    `Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io>`_
