Chris Leaman http://www.leaman.io/ Coastal scientist and structural engineer based in Brisbane, Australia Tue, 13 Feb 2024 10:00:32 +0000 en-US hourly 1 https://wordpress.org/?v=6.4.3 https://www.leaman.io/wp-content/uploads/2020/04/cropped-favicon-32x32.jpg Chris Leaman http://www.leaman.io/ 32 32 A new Storm Hazard Matrix for both coastal flooding and erosion hazards https://www.leaman.io/vicce2020/ Tue, 06 Oct 2020 04:39:22 +0000 https://www.leaman.io/?p=322 This was my presentation for the Virtual International Conference on Coastal Engineering (vICCE). Unfortuanely due to COVID19, ICCE 2020 which was originally scheduled to be held in Sydney has been postponed to 2022, and vICCE took its place.

Here, I describe some of the work I’m undertaking looking at how we can categorize coastal storm hazards using a new Storm Hazard Matrix. Because in the differences in coastal settings and storm hydrodyanmics, both flooding and erosion hazard should be considered distinctly. I apply the new matrix approach to two extreme storm events: 1) the 2016 East Coast Low impacting south east Australian and 2) Hurricane Ivan which impacted the gulf coast of Florida in 2004. This work is currently being prepared, to be submitted for publication soon.

]]>
3 Minute Thesis: A crystal ball for coastal change https://www.leaman.io/3-minute-thesis/ Fri, 03 Jul 2020 00:33:47 +0000 https://www.leaman.io/?p=299 The 3 Minute Thesis is an academic competition that challenges researcher to explain their thesis and its significance in just 3 minutes. This year, UNSW is hosting their 3 Minute Thesis online. In my video submission I explain the importance of being able to gaze into the crystal ball and see what the future has in store for our coastlines.

Comments

]]>
Modelling the sun’s reflection into our apartment with Python https://www.leaman.io/suns-reflection/ Thu, 07 May 2020 16:00:00 +0000 https://www.leaman.io/?p=205 The final plot and full code are stored as a Github gist.

We moved apartments right before the COVID lockdowns began. Being stuck inside all day, I’ve started noticing things I probably would have missed in my regular routine. One of these things is that our east facing apartment gets lit up in the afternoon from the sun’s reflection from a particularly reflective building. The first time it happened was quite a surprise and it quickly passed. However, over the coming days, the glowing light started lasting longer.

Our living room lit up by the sun at around 3:30 pm in the afternoon… but our windows face east, not west?!

It turns out the culprit was a particularly reflective building located a couple of blocks away. As the sun sets in the afternoon, our apartment is in the right position to collect the light which is bounced off the building.

The questions

The path of the sun through the sky changes throughout the year and so only a certain position would reflect light into our apartment. This then begs the following questions:

  • What time of year can I expect to have the sun reflecting into our apartment?
  • Would the time of day the reflection occurred change throughout the year?
  • Would the duration of the reflection change throughout the year?

The approach

I’m no astrophysicist, but I thought I could tackle the problem in a couple of steps:

  1. Measure the positions of the building acting as a mirror, and the position of our apartment.
  2. Calculate the range of sun azimuths and elevations that will reflect light into the apartment.
  3. Calculate the position of the sun over a year and check if the sun lies within the ranges calculated in step 2.
When the sun is at a certain azimuth (in plan view) and altitude (in elevation view), the light reflects off the building and into our apartment.

This is going to be a very simplified approach, so there are a couple of assumptions:

  • Reflection of the light from the building is specular (i.e. the angle of the incident ray equals the angle of the reflected ray). This may not be completely true if the glass is slightly diffuse, resulting in a different reflected angle.
  • If it’s a cloudy day, we probably aren’t going to see a reflection even if the sun is in the correct location.
  • Since our apartment isn’t very wide compared to the width of the reflective building, I’m going to model our apartment as a point.
  • The reflective building is made up of many smaller panels, so the brightness of the reflection is going to vary as sun sets.

I’m sure there are many other things an architect or designer would be able to take into account, but this is going to be a decent first attempt.

Step 1) Measure positions

The first step is to figure out where our mirror building is in relation to our apartment. We don’t need any fancy tools for this, latitudes and longitudes can be read straight from Google Maps. We’re going to need two points to define the plane of the reflective building face (mirror_pt1 and mirror_pt2) and one point to define the location of our apartment (focus_pt). Each point is defined by a latitude, a longitude and an elevation. Elevation can be estimated by measuring the vertical angle from the apartment to each point. Note that area of the building that can act as a mirror can only be the top half of the reflected building about the apartment altitude.

Next, let’s load these into Python and store them in a Point class. Here, I’m using the dataclass feature introduced in Python 3.7 to create a minimal class to store our points. The utm package converts lat/lons to x/y coordinates in the correct UTM zone.

from dataclasses import dataclass
import utm

@dataclass
class Point:
    """
    Point class which helps us convert between lat,lon and x,y easily.
    """

    lat: float
    lon: float
    z: float

    @property
    def x(self):
        x, _, _, _ = utm.from_latlon(self.lat, self.lon)
        return x

    @property
    def y(self):
        _, y, _, _ = utm.from_latlon(self.lat, self.lon)
        return y

# Coordinates redacted here, but feel free to substitue your own.

# Mirror points define the plane of the building which is reflecting the sun
mirror_pt1 = Point(lat=-33.8, lon=151.2, z=200)
mirror_pt2 = Point(lat=-33.8, lon=151.2, z=100)

# Focus point is the apartment
focus_pt = Point(lat=-33.8, lon=151.2, z=0)

Step 2) Calculate sun elevations & azimuths which result in a reflection

I’m defining the position of the sun in the sky by two angles, azimuth (clockwise from north) and it’s elevation (the angle between the horizon to the sun’s center). We want to back-calculate the azimuths and elevations which result in a reflection by using our law of reflections.

Let’s store the calculations in a ReflectionCalculator class. The brunt of the work is done by the find_sun_positions() method. We check both points defined by the mirror as they determine what the limits of the sun azimuth and elevations are going to be.

import math
import numpy as np

class ReflectionCalculator:
    def __init__(self, mirror_pt1, mirror_pt2, focus_pt):

        self.mirror_pt1 = mirror_pt1
        self.mirror_pt2 = mirror_pt2
        self.focus_pt = focus_pt

        self.valid_sun_azimuths, self.valid_sun_elevations = self.find_sun_positions()

    @property
    def mirror_azimuth(self):
        """
        Returns the azimuth of the mirror in degrees. Needed to calculate the range
        of sun positions which result in a reflection.
        """

        return 90 - math.degrees(
            math.atan(
                (self.mirror_pt2.y - self.mirror_pt1.y)
                / (self.mirror_pt2.x - self.mirror_pt1.x)
            )
        )

    def find_sun_positions(self):
        """
        Find the minimum and maximum azimuth and elevations that the sun must be
        positioned at to reflect off the building mirror and into the focus point
        """

        # Check both mirror points
        mirror_pts = [self.mirror_pt1, self.mirror_pt2]

        # Initialize lists to store the calculated azimuths and elevations
        sun_azimuths = []
        sun_elevations = []

        for mirror_pt in mirror_pts:

            elevation, azimuth = self.angles_between_points(mirror_pt, self.focus_pt)

            # Calculate the position of where the sun needs to be to result in a
            # reflection
            sun_azimuths.append(90 + azimuth + 2 * self.mirror_azimuth)
            sun_elevations.append(-elevation)

        valid_sun_azimuths = (min(sun_azimuths), max(sun_azimuths))
        valid_sun_elevations = (min(sun_elevations), max(sun_elevations))
        return valid_sun_azimuths, valid_sun_elevations

    @staticmethod
    def angles_between_points(pt1, pt2):
        """
        Returns the elevation and azimuth in degrees at pt1, looking at pt2
        """

        dx = pt2.x - pt1.x
        dy = pt2.y - pt1.y
        dz = pt2.z - pt1.z

        azimuth = np.degrees(math.atan2(dy, dx))

        if azimuth < 0:
            azimuth += 360

        elevation = math.degrees(math.atan(dz / np.sqrt(dx ** 2 + dy ** 2)))

        return elevation, azimuth

Step 3) Calculate the position of the sun over the year

There are several packages that can return the azimuth and elevation of the sun in the sky at a particular location. I’ve gone with the pysolar package which has fast (but less accurate methods) of doing these calculations. For our cases, the reduced accuracy is going to be fine. Inside our ReflectionCalculator class, we add the following function which generates a timeseries, and checks whether the sun falls within the bounds of reflecting of the building and into our apartment.

import pandas as pd

    def calculate_reflective_times(
        self, start, end, freq, tz, hour_start=14, hour_end=18
    ):
        """
        Creates a pandas dataframe containing whether the sun will be reflecting into
        the apartment at a each time or not.

        :param start: datetime of the start time to analyse
        :param end: datetime of the end time to analyse
        :param freq: str of the frequency of items between start and end
        :param tz: pytz.timezone of the timezone of the location
        :param hour_start: int of the minimum hour of day to check
        :param hour_end: int of the maxmimum hour of day to check
        """

        # Use position at the center of the mirror
        mirror_lat = statistics.mean([self.mirror_pt1.lat, self.mirror_pt2.lat])
        mirror_lon = statistics.mean([self.mirror_pt1.lon, self.mirror_pt2.lon])

        # Build timeseries
        index = pd.date_range(start, end, freq=freq, tz=tz)

        # Make dataframe, but only interested in certain times during the day
        df = pd.DataFrame(index=index)

        df = df.loc[
            (df.index.to_series().dt.hour >= hour_start)
            & (df.index.to_series().dt.hour < hour_end)
        ]

        # Calculate position of sun at each time step
        df["sun_altitudes"] = df.index.to_series().apply(
            lambda x: get_altitude_fast(mirror_lat, mirror_lon, x)
        )
        df["sun_azimuths"] = df.index.to_series().apply(
            lambda x: get_azimuth_fast(mirror_lat, mirror_lon, x)
        )

        # Determine whether sun is in correct position to reflect off mirror
        df['in_elevation'] = False
        df['in_azimuth'] = False
        df['will_reflect'] = False

        df.loc[df.sun_altitudes.between(*self.valid_sun_elevations),
               'in_elevation'] =True
        df.loc[df.sun_azimuths.between(*self.valid_sun_azimuths),
               'in_azimuth'] =True
        df.loc[(df.in_elevation) & (df.in_azimuth), 'will_reflect'] = True

        return df

Now we can initalize our reflection class with our points and calculate a dataframe of times and showing whether the sun will reflect

# Initalize our reflection calculator class
reflector = ReflectionCalculator(mirror_pt1, mirror_pt2, focus_pt)

# Create a dataframe for the period we're interested in which calculates whether
# the sun will reflect into the apartment
df = reflector.calculate_reflective_times(
    start=datetime.datetime(2020, 3, 1),
    end=datetime.datetime(2020, 10, 31),
    freq="1min",
    tz=pytz.timezone("Australia/Brisbane"),
)

This gives us a dataframe where each row shows whether the sun is at the correct azimuth and elevatation to reflect into our building. We can also get the values of the sun azimuths and elevations which result in a reflection

>>> df.head()
                           sun_altitudes  sun_azimuths  in_elevation  in_azimuth  will_reflect
2020-03-01 14:00:00+10:00      53.432027    308.714212         False       False         False
2020-03-01 14:01:00+10:00      53.269642    308.401035         False       False         False
2020-03-01 14:02:00+10:00      53.106555    308.090071         False       False         False
2020-03-01 14:03:00+10:00      52.942775    307.781299         False       False         False
2020-03-01 14:04:00+10:00      52.778311    307.474697         False       False         False

>>> reflector.valid_sun_elevations
(10.9, 20.2)

>>> reflector.valid_sun_azimuths
(296.7, 303.4)

This is telling us the sun needs to be between 10.9 and 20.2 degrees above the horizon at an azimuth between 296.7 and 303.4 degrees clockwise from true north.

This is not bad, but still difficult to visualize. Let’s turn to matplotlib to better make sense of what we have. The following function generates a heatmap and shows us exactly what’s happening with the reflection times.

def plot_time_heatmap(df, output_file, use_tex=False):
    """
    Plot a time heat map of the times where the sun is reflecting
    """

    # Let's pivot the table so it's a bit more useful
    # Add day to dataframe
    df["day"] = df.index.to_series().dt.floor("d")
    df["time"] = df.index.to_series().dt.time

    # Create another column with the reason if it will reflect or not
    df["reflect_reason"] = 3
    df.loc[(df.in_azimuth) & (~df.in_elevation), "reflect_reason"] = 2
    df.loc[(~df.in_azimuth) & (df.in_elevation), "reflect_reason"] = 1
    df.loc[(df.in_azimuth) & (df.in_elevation), "reflect_reason"] = 0

    # Pivot the table, se we have time as the index and each day as a column with if
    # it will reflect as the values
    df = pd.pivot_table(df, columns="day", index="time", values="reflect_reason")

    # Data to plot.
    plot_data = df.values

    # Use custom color map
    cmap = colors.ListedColormap(["#9e0142", "#fee08b", "#abdda4", "#5e4fa2"])
    bounds = [-0.5, 0.5, 1.5, 2.5, 3.5]
    norm = colors.BoundaryNorm(bounds, cmap.N)

    # Create y_lims
    y_vals = [datetime.datetime.combine(datetime.date(2000, 1, 1), x) for x in df.index]
    y_vals = mdates.date2num(y_vals)

    # Create x-lims
    x_vals = mdates.date2num(df.columns)

    # Add settings to customize look of plot
    plt.rc("grid", linestyle="--", color="black", alpha=0.3)
    plt.rc("font", family="sans-serif")

    # Note that LaTeX needs to be install on the machine for this to run properly.
    # It's not necessary to use LaTeX, but you get nicer fonts
    if use_tex:
        plt.rc("text", usetex=True)
        plt.rc(
            "text.latex",
            preamble=[
                r"\usepackage{siunitx}",
                r"\sisetup{detect-all}",
                r"\usepackage[default]{sourcesanspro}",
                r"\usepackage{amsmath}",
                r"\usepackage{sansmath}",
                r"\sansmath",
            ],
        )

    # Create figure
    fig, ax = plt.subplots(figsize=(5, 4))

    _ = ax.imshow(
        plot_data,
        cmap=cmap,
        norm=norm,
        extent=[x_vals[0], x_vals[-1], y_vals[-1], y_vals[0]],
        origin="upper",
        aspect="auto",
    )

    ax.xaxis_date()
    ax.yaxis_date()

    month_year_format = mdates.DateFormatter("%b %Y")
    hour_min_format = mdates.DateFormatter("%H:%M %p")

    ax.xaxis.set_major_formatter(month_year_format)
    ax.yaxis.set_major_formatter(hour_min_format)

    # Rotate ticks
    plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")

    # Make custom legend
    legend_elements = [
        Patch(
            facecolor="#9e0142",
            edgecolor="#9e0142",
            label="Correct elevation\n& azimuth",
        ),
        Patch(facecolor="#fee08b", edgecolor="#fee08b", label="Correct elevation"),
        Patch(facecolor="#abdda4", edgecolor="#abdda4", label="Correct azimuth"),
        Patch(
            facecolor="#5e4fa2", edgecolor="#5e4fa2", label="Wrong elevation\n& azimuth"
        ),
    ]
    ax.legend(handles=legend_elements, prop={"size": 6})

    plt.grid(True)
    ax.set_title("When will sun reflect from building to home?")
    plt.tight_layout()

    fig.savefig(output_file, dpi=300)

The conclusion

Calling plot_time_heatmap(df, output_file='plot.png) gives us this overview of the position of the sun relative to our window of correct elevations and azimuths.

I’m really fond of this plot because it shows exactly when we’re going to get that reflection. The sun’s azimuth and elevation are changing throughout the year, and only when we get that perfect overlap do we see the right position for a reflection. We get reflections for about a month between mid-April to mid-May and again between mid-July and mid-August. At it’s longest duration, the reflection lasts for nearly 45 minutes.

The sketch below is another way we could have plotted these results. The sun follows slightly different paths throughout the year. Depending when they cross the red window of correct azimuth and elevation do we get that reflection.

I think this is a great example of how a little bit of programming knowledge can go a long way and uncover some interesting things about your daily, confined life. Domain-specific packages that are freely available make it easy to implement something that might have been too difficult in the past. If pysolar or another package weren’t available, would I have implemented my own solar calculation routine? Probably not. But now, I can make sure not to miss the small things that brighten up the day.

]]>
Forecasting coastal erosion during the 2016 SE Australian East Coast Low https://www.leaman.io/coasts-ports-2019/ Sun, 01 Dec 2019 11:25:00 +0000 http:/?p=1

This was the first presentation I gave about my PhD work as part of the 2019 Coasts & Ports conference in Hobart, Tasmania. The slides I used are below as well as an outline of my talk. If you found this interesting or have any questions or ideas, please reach out!


  • This drone footage shows damage caused at Sydney’s Collaroy beach in 2016 due to a large East Coast Low.
  • Houses were put at risk due to ongoing erosion from a combination of large waves and high water levels.
  • If we can predict where damage is going to occur, we can give Early Warning and start taking actions to minimize damage.
  • We can use morphodynamic models (e.g. XBeach) to predict how severe erosion and flooding hazards may be.
  • However, XBeach needs a lot of computational power to run and a lot of data to properly calibrate.
  • Storms can affect 100-1000 km’s worth of coastline (regional-scale), so we need another approach to estimate where damage might occur.
  • We’re developing a two tier trial storm impact Early Warning System to see if we can forecast damage at the regional scale.
  • The first tier is a fast, simple, preliminary assessment of which areas may be affected by large hazards.
  • The second tier uses more sophesiticated morphodynamic models at chronic hotspots to get a more detailed understanding of hazards.
  • There are many ways of doing a fast, preliminary assessment of storm impacts.
  • One option is the Storm Impact Scale which compares the Total Water Level to the dune toe and crest elevation.
  • This approach is used by the USGS Coastal Change Hazard Portal for the U.S. East coast.
  • However, the U.S. East coast and the S.E. Australian coastlines are quite different.
  • The U.S. East coast can experience large storm surges, but wave energy is usually dissipated across the continental shelf causing flooding.
  • On the S.E. Australian coast however, the storm surges are relatively small, but more wave energy is allowed to reach the coastline causing erosion.
  • We need to see if the Storm Impact Scale works well on the SE Australian coast where hazards are more to do with erosion than flooding.
  • Let’s use the Storm Impact Scale and hindcast severity of erosion and flooding hazards from the 2016 storm and see if it produces a useful forecast.
  • The 2016 East Coast Low caused significant erosion on the South-East Australian coastline.
  • The severe erosion was mostly caused by the anomalously easterly wave direction combined with the fact that the peak of the storm coincided with Highest Astronomical Tides.
  • Immediately prior and after the event, UNSW flew airborne LIDAR over 177 km of shorelines between Sydney and Nambucca Heads.
  • Cross shore beach profiles were extracted at 100 m alongshore spacings for this analysis.
  • Several wave buoys and tide gauges were also operational during the storm within the surveyed region.
  • To establish the observed Storm Impact Regime (either swash, collision, overwash or inundation), the pre and post storm surveys were compared.
  • It was assumed the point where the pre and post-storm surveys started to differ was the highest point reached by the Total Water Level.
  • We then hindcast the Storm Impact at each beach transect.
  • Wave runup was estimated using the equation provided by Stockdon et al. (2006) with modelled waves at the 10m depth contour.
  • Dune toe and crest elevations were manually extracted from the survey data.
  • Comparing the hindcast regime to the observed regime, we see that the correct regime was selected around 65% of the time.
  • But, 97% of the beach profiles either fell into the swash or collision regime.
  • If most of the profiles fall into only one of two categories, it is difficult to find which areas are most at risk.
  • The figure on the left shows Collaroy beach and the observed storm impact at each transect.
  • The yellow dots indicate dune collision occurs at the majority of the beach.
  • At the northern end, dune collision is relatively minor, but at the southern end, houses are at risk of damage.
  • We need a better way to describe the severity of erosion at each location.
  • For surge-dominated coastlines where there can be a large range of water levels, such as on the U.S. East coast, all four Storm Impact Regimes can be commonly observed.
  • However, for wave-dominated coasts, such as those in SE Australia, water levels don’t vary as much.
  • This means, most of the time we are either in the swash or collision regime.
  • There can be two different ways of thinking about storm impacts.
  • Flooding hazards apply to the vertical axis, i.e. the height of the water level is very important.
  • Erosion hazards apply to the horizontal axis, i.e. the width of the beach which is lost due to erosion is of more concern.
  • Therefore, a new scale to better describe erosion hazards is being developed.
  • In the figure, each coloured line represents a post-storm profile.
  • We can classify erosion hazards into four levels based on how eroded our beach and dune are.

  • Applying the new scale to Collaroy becah, the southern end of the beach experiences a more severe hazard.
  • This is an improvement compared to the Storm Impact Scale which just assigned collision at both locations.
  • Work is still ongoing to see how the Erosion Hazard Scale would perform over the entire NSW coastline.
  • In summary, we can do a reasonable job of predicting storm impacts at a regional-scale.
  • If our impacts are primarily erosion, it might be better to use a different framework to describe those impacts.
  • This framework could be used by an EWS to alert the community and authorities before a storm so they can take actions to minimize potential damage.

]]>