diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..5036759 --- /dev/null +++ b/404.html @@ -0,0 +1,797 @@ + + + + + + + + + + + + + + + + + + + + + + + eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/api/index.html b/api/index.html new file mode 100644 index 0000000..0ff9b54 --- /dev/null +++ b/api/index.html @@ -0,0 +1,8930 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + API reference - eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

API reference

+ +
+ + + +

+ eo_tides.model + + +

+ +
+ + + + + + + + + +

Functions:

+ + + + + + + + + + + + + + + + + + + + + +
NameDescription
ensemble_tides +
+

Combine multiple tide models into a single locally optimised

+
+
model_phases +
+

Model tide phases (low-flow, high-flow, high-ebb, low-ebb)

+
+
model_tides +
+

Model tide heights at multiple coordinates and/or timesteps

+
+
+ + + + + +
+ + + + + + + + + +
+ + +

+ ensemble_tides + + +

+
ensemble_tides(
+    tide_df,
+    crs,
+    ensemble_models,
+    ensemble_func=None,
+    ensemble_top_n=3,
+    ranking_points="https://dea-public-data-dev.s3-ap-southeast-2.amazonaws.com/derivative/dea_intertidal/supplementary/rankings_ensemble_2017-2019.fgb",
+    ranking_valid_perc=0.02,
+    **idw_kwargs
+)
+
+ +
+ +

Combine multiple tide models into a single locally optimised +ensemble tide model using external model ranking data (e.g. +satellite altimetry or NDWI-tide correlations along the coastline) +to inform the selection of the best local models.

+

This function performs the following steps: +1. Takes a dataframe of tide heights from multiple tide models, as + produced by eo_tides.model.model_tides +2. Loads model ranking points from an external file, filters them + based on the valid data percentage, and retains relevant columns +3. Interpolates the model rankings into the coordinates of the + original dataframe using Inverse Weighted Interpolation (IDW) +4. Uses rankings to combine multiple tide models into a single + optimised ensemble model (by default, by taking the mean of the + top 3 ranked models) +5. Returns a new dataFrame with the combined ensemble model predictions

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

tide_df +

+ DataFrame + +
+

DataFrame produced by eo_tides.model.model_tides, containing +tide model predictions in long format with columns: +["time", "x", "y", "tide_height", "tide_model"].

+
+
+ required +
+

crs +

+ string + +
+

Coordinate reference system for the "x" and "y" coordinates in +tide_df. Used to ensure that interpolations are performed +in the correct CRS.

+
+
+ required +
+

ensemble_models +

+ list + +
+

A list of models to include in the ensemble modelling process. +All values must exist as columns with the prefix "rank_" in +ranking_points.

+
+
+ required +
+

ensemble_func +

+ dict + +
+

By default, a simple ensemble model will be calculated by taking +the mean of the ensemble_top_n tide models at each location. +However, a dictionary containing more complex ensemble +calculations can also be provided. Dictionary keys are used +to name output ensemble models; functions should take a column +named "rank" and convert it to a weighting, e.g.: +ensemble_func = {"ensemble-custom": lambda x: x["rank"] <= 3}

+
+
+ None +
+

ensemble_top_n +

+ int + +
+

If ensemble_func is None, this sets the number of top models +to include in the mean ensemble calculation. Defaults to 3.

+
+
+ 3 +
+

ranking_points +

+ str + +
+

Path to the file containing model ranking points. This dataset +should include columns containing rankings for each tide +model, named with the prefix "rank_". e.g. "rank_EOT20". +Low values should represent high rankings (e.g. 1 = top ranked). +The default value points to an example file covering Australia.

+
+
+ 'https://dea-public-data-dev.s3-ap-southeast-2.amazonaws.com/derivative/dea_intertidal/supplementary/rankings_ensemble_2017-2019.fgb' +
+

ranking_valid_perc +

+ float + +
+

Minimum percentage of valid data required to include a model +rank point in the analysis, as defined in a column named +"valid_perc". Defaults to 0.02.

+
+
+ 0.02 +
+

**idw_kwargs +

+ +
+

Optional keyword arguments to pass to the idw function used +for interpolation. Useful values include k (number of nearest +neighbours to use in interpolation), max_dist (maximum +distance to nearest neighbours), and k_min (minimum number of +neighbours required after max_dist is applied).

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ DataFrame + +
+

DataFrame containing the ensemble model predictions, matching +the format of the input tide_df (e.g. columns ["time", "x", +"y", "tide_height", "tide_model"]. By default the 'tide_model' +column will be labeled "ensemble" for the combined model +predictions (but if a custom dictionary of ensemble functions is +provided via ensemble_func, each ensemble will be named using +the provided dictionary keys).

+
+
+ +
+ Source code in eo_tides/model.py +
def ensemble_tides(
+    tide_df,
+    crs,
+    ensemble_models,
+    ensemble_func=None,
+    ensemble_top_n=3,
+    ranking_points="https://dea-public-data-dev.s3-ap-southeast-2.amazonaws.com/derivative/dea_intertidal/supplementary/rankings_ensemble_2017-2019.fgb",
+    ranking_valid_perc=0.02,
+    **idw_kwargs,
+):
+    """Combine multiple tide models into a single locally optimised
+    ensemble tide model using external model ranking data (e.g.
+    satellite altimetry or NDWI-tide correlations along the coastline)
+    to inform the selection of the best local models.
+
+    This function performs the following steps:
+    1. Takes a dataframe of tide heights from multiple tide models, as
+       produced by `eo_tides.model.model_tides`
+    2. Loads model ranking points from an external file, filters them
+       based on the valid data percentage, and retains relevant columns
+    3. Interpolates the model rankings into the coordinates of the
+       original dataframe using Inverse Weighted Interpolation (IDW)
+    4. Uses rankings to combine multiple tide models into a single
+       optimised ensemble model (by default, by taking the mean of the
+       top 3 ranked models)
+    5. Returns a new dataFrame with the combined ensemble model predictions
+
+    Parameters
+    ----------
+    tide_df : pandas.DataFrame
+        DataFrame produced by `eo_tides.model.model_tides`, containing
+        tide model predictions in long format with columns:
+        `["time", "x", "y", "tide_height", "tide_model"]`.
+    crs : string
+        Coordinate reference system for the "x" and "y" coordinates in
+        `tide_df`. Used to ensure that interpolations are performed
+        in the correct CRS.
+    ensemble_models : list
+        A list of models to include in the ensemble modelling process.
+        All values must exist as columns with the prefix "rank_" in
+        `ranking_points`.
+    ensemble_func : dict, optional
+        By default, a simple ensemble model will be calculated by taking
+        the mean of the `ensemble_top_n` tide models at each location.
+        However, a dictionary containing more complex ensemble
+        calculations can also be provided. Dictionary keys are used
+        to name output ensemble models; functions should take a column
+        named "rank" and convert it to a weighting, e.g.:
+        `ensemble_func = {"ensemble-custom": lambda x: x["rank"] <= 3}`
+    ensemble_top_n : int, optional
+        If `ensemble_func` is None, this sets the number of top models
+        to include in the mean ensemble calculation. Defaults to 3.
+    ranking_points : str, optional
+        Path to the file containing model ranking points. This dataset
+        should include columns containing rankings for each tide
+        model, named with the prefix "rank_". e.g. "rank_EOT20".
+        Low values should represent high rankings (e.g. 1 = top ranked).
+        The default value points to an example file covering Australia.
+    ranking_valid_perc : float, optional
+        Minimum percentage of valid data required to include a model
+        rank point in the analysis, as defined in a column named
+        "valid_perc". Defaults to 0.02.
+    **idw_kwargs
+        Optional keyword arguments to pass to the `idw` function used
+        for interpolation. Useful values include `k` (number of nearest
+        neighbours to use in interpolation), `max_dist` (maximum
+        distance to nearest neighbours), and `k_min` (minimum number of
+        neighbours required after `max_dist` is applied).
+
+    Returns
+    -------
+    pandas.DataFrame
+        DataFrame containing the ensemble model predictions, matching
+        the format of the input `tide_df` (e.g. columns `["time", "x",
+        "y", "tide_height", "tide_model"]`. By default the 'tide_model'
+        column will be labeled "ensemble" for the combined model
+        predictions (but if a custom dictionary of ensemble functions is
+        provided via `ensemble_func`, each ensemble will be named using
+        the provided dictionary keys).
+
+    """
+    # Raise data if `tide_df` provided in wide format
+    if "tide_model" not in tide_df:
+        raise Exception(
+            "`tide_df` does not contain the expected 'tide_model' and "
+            "'tide_height' columns. Ensure that tides were modelled in "
+            "long format (i.e. `output_format='long'` in `model_tides`)."
+        )
+
+    # Extract x and y coords from dataframe
+    x = tide_df.index.get_level_values(level="x")
+    y = tide_df.index.get_level_values(level="y")
+
+    # Load model ranks points and reproject to same CRS as x and y
+    model_ranking_cols = [f"rank_{m}" for m in ensemble_models]
+    try:
+        model_ranks_gdf = (
+            gpd.read_file(ranking_points, engine="pyogrio")
+            .to_crs(crs)
+            .query(f"valid_perc > {ranking_valid_perc}")
+            .dropna(how="all")[model_ranking_cols + ["geometry"]]
+        )
+    except KeyError:
+        error_msg = f"""
+        Not all of the expected "rank_" columns {model_ranking_cols} were
+        found in the columns of the ranking points file ({ranking_points}).
+        Consider passing a custom list of models using `ensemble_models`.
+        """
+        raise Exception(textwrap.dedent(error_msg).strip()) from None
+
+    # Use points to interpolate model rankings into requested x and y
+    id_kwargs_str = "" if idw_kwargs == {} else idw_kwargs
+    print(f"Interpolating model rankings using IDW interpolation {id_kwargs_str}")
+    ensemble_ranks_df = (
+        # Run IDW interpolation on subset of ranking columns
+        pd.DataFrame(
+            idw(
+                input_z=model_ranks_gdf[model_ranking_cols],
+                input_x=model_ranks_gdf.geometry.x,
+                input_y=model_ranks_gdf.geometry.y,
+                output_x=x,
+                output_y=y,
+                **idw_kwargs,
+            ),
+            columns=model_ranking_cols,
+        )
+        .assign(x=x, y=y)
+        # Drop any duplicates then melt columns into long format
+        .drop_duplicates()
+        .melt(id_vars=["x", "y"], var_name="tide_model", value_name="rank")
+        # Remore "rank_" prefix to get plain model names
+        .replace({"^rank_": ""}, regex=True)
+        # Set index columns and rank across groups
+        .set_index(["tide_model", "x", "y"])
+        .groupby(["x", "y"])
+        .rank()
+    )
+
+    # If no custom ensemble funcs are provided, use a default ensemble
+    # calculation that takes the mean of the top N tide models
+    if ensemble_func is None:
+        ensemble_func = {"ensemble": lambda x: x["rank"] <= ensemble_top_n}
+
+    # Create output list to hold computed ensemble model outputs
+    ensemble_list = []
+
+    # Loop through all provided ensemble generation functions
+    for ensemble_n, ensemble_f in ensemble_func.items():
+        print(f"Combining models into single {ensemble_n} model")
+
+        # Join ranks to input tide data, compute weightings and group
+        grouped = (
+            # Add tide model as an index so we can join with model ranks
+            tide_df.set_index("tide_model", append=True)
+            .join(ensemble_ranks_df)
+            # Add temp columns containing weightings and weighted values
+            .assign(
+                weights=ensemble_f,  # use custom func to compute weights
+                weighted=lambda i: i.tide_height * i.weights,
+            )
+            # Groupby is specified in a weird order here as this seems
+            # to be the easiest way to preserve correct index sorting
+            .groupby(["x", "y", "time"])
+        )
+
+        # Use weightings to combine multiple models into single ensemble
+        ensemble_df = (
+            # Calculate weighted mean and convert back to dataframe
+            grouped.weighted.sum()
+            .div(grouped.weights.sum())
+            .to_frame("tide_height")
+            # Label ensemble model and ensure indexes are in expected order
+            .assign(tide_model=ensemble_n)
+            .reorder_levels(["time", "x", "y"], axis=0)
+        )
+
+        ensemble_list.append(ensemble_df)
+
+    # Combine all ensemble models and return as a single dataframe
+    return pd.concat(ensemble_list)
+
+
+
+ +
+ +
+ + +

+ model_phases + + +

+
model_phases(
+    x,
+    y,
+    time,
+    model="EOT20",
+    directory=None,
+    time_offset="15 min",
+    return_tides=False,
+    **model_tides_kwargs
+)
+
+ +
+ +

Model tide phases (low-flow, high-flow, high-ebb, low-ebb) +at multiple coordinates and/or timesteps using using one +or more ocean tide models.

+

Ebb and low phases are calculated by running the +eo_tides.model.model_tides function twice, once for +the requested timesteps, and again after subtracting a +small time offset (by default, 15 minutes). If tides +increased over this period, they are assigned as "flow"; +if they decreased, they are assigned as "ebb". +Tides are considered "high" if equal or greater than 0 +metres tide height, otherwise "low".

+

This function supports all parameters that are supported +by model_tides.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

x +

+ float or list of float + +
+

One or more x and y coordinates used to define +the location at which to model tide phases. By default +these coordinates should be lat/lon; use "crs" if they +are in a custom coordinate reference system.

+
+
+ required +
+

y +

+ float or list of float + +
+

One or more x and y coordinates used to define +the location at which to model tide phases. By default +these coordinates should be lat/lon; use "crs" if they +are in a custom coordinate reference system.

+
+
+ required +
+

time +

+ DatetimeLike + +
+

Times at which to model tide phases (in UTC). Accepts +any format that can be converted by pandas.to_datetime(); +e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp, +datetime.datetime and strings (e.g. "2020-01-01 23:00"). +For example: time=pd.date_range(start="2000", end="2001", freq="5h")

+
+
+ required +
+

model +

+ str or list of str + +
+

The tide model (or list of models) to use to model tides. +Defaults to "EOT20"; specify "all" to use all models available +in directory. For a full list of available and supported models, +run eo_tides.model.list_models.

+
+
+ 'EOT20' +
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

time_offset +

+ str + +
+

The time offset/delta used to generate a time series of +offset tide heights required for phase calculation. Defeaults +to "15 min"; can be any string passed to pandas.Timedelta.

+
+
+ '15 min' +
+

return_tides +

+ bool + +
+

Whether to return intermediate modelled tide heights as a +"tide_height" column in the output dataframe. Defaults to False.

+
+
+ False +
+

**model_tides_kwargs +

+ +
+

Optional parameters passed to the eo_tides.model.model_tides +function. Important parameters include output_format (e.g. +whether to return results in wide or long format), crop +(whether to crop tide model constituent files on-the-fly to +improve performance) etc.

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ DataFrame + +
+

A dataframe containing modelled tide phases.

+
+
+ +
+ Source code in eo_tides/model.py +
def model_phases(
+    x: float | list[float] | xr.DataArray,
+    y: float | list[float] | xr.DataArray,
+    time: DatetimeLike,
+    model: str | list[str] = "EOT20",
+    directory: str | os.PathLike | None = None,
+    time_offset: str = "15 min",
+    return_tides: bool = False,
+    **model_tides_kwargs,
+) -> pd.DataFrame:
+    """
+    Model tide phases (low-flow, high-flow, high-ebb, low-ebb)
+    at multiple coordinates and/or timesteps using using one
+    or more ocean tide models.
+
+    Ebb and low phases are calculated by running the
+    `eo_tides.model.model_tides` function twice, once for
+    the requested timesteps, and again after subtracting a
+    small time offset (by default, 15 minutes). If tides
+    increased over this period, they are assigned as "flow";
+    if they decreased, they are assigned as "ebb".
+    Tides are considered "high" if equal or greater than 0
+    metres tide height, otherwise "low".
+
+    This function supports all parameters that are supported
+    by `model_tides`.
+
+    Parameters
+    ----------
+    x, y : float or list of float
+        One or more x and y coordinates used to define
+        the location at which to model tide phases. By default
+        these coordinates should be lat/lon; use "crs" if they
+        are in a custom coordinate reference system.
+    time : DatetimeLike
+        Times at which to model tide phases (in UTC). Accepts
+        any format that can be converted by `pandas.to_datetime()`;
+        e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp,
+        datetime.datetime and strings (e.g. "2020-01-01 23:00").
+        For example: `time=pd.date_range(start="2000", end="2001", freq="5h")`
+    model : str or list of str, optional
+        The tide model (or list of models) to use to model tides.
+        Defaults to "EOT20"; specify "all" to use all models available
+        in `directory`. For a full list of available and supported models,
+        run `eo_tides.model.list_models`.
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    time_offset: str, optional
+        The time offset/delta used to generate a time series of
+        offset tide heights required for phase calculation. Defeaults
+        to "15 min"; can be any string passed to `pandas.Timedelta`.
+    return_tides: bool, optional
+        Whether to return intermediate modelled tide heights as a
+        "tide_height" column in the output dataframe. Defaults to False.
+    **model_tides_kwargs :
+        Optional parameters passed to the `eo_tides.model.model_tides`
+        function. Important parameters include `output_format` (e.g.
+        whether to return results in wide or long format), `crop`
+        (whether to crop tide model constituent files on-the-fly to
+        improve performance) etc.
+
+    Returns
+    -------
+    pandas.DataFrame
+        A dataframe containing modelled tide phases.
+
+    """
+
+    # Pop output format and mode for special handling
+    output_format = model_tides_kwargs.pop("output_format", "long")
+    mode = model_tides_kwargs.pop("mode", "one-to-many")
+
+    # Model tides
+    tide_df = model_tides(
+        x=x,
+        y=y,
+        time=time,
+        model=model,
+        directory=directory,
+        **model_tides_kwargs,
+    )
+
+    # Model tides for a time 15 minutes prior to each previously
+    # modelled satellite acquisition time. This allows us to compare
+    # tide heights to see if they are rising or falling.
+    pre_df = model_tides(
+        x=x,
+        y=y,
+        time=time - pd.Timedelta(time_offset),
+        model=model,
+        directory=directory,
+        **model_tides_kwargs,
+    )
+
+    # Compare tides computed for each timestep. If the previous tide
+    # was higher than the current tide, the tide is 'ebbing'. If the
+    # previous tide was lower, the tide is 'flowing'
+    ebb_flow = (tide_df.tide_height < pre_df.tide_height.values).replace({True: "ebb", False: "flow"})
+
+    # If tides are greater than 0, then "high", otherwise "low"
+    high_low = (tide_df.tide_height >= 0).replace({True: "high", False: "low"})
+
+    # Combine into one string and add to data
+    tide_df["tide_phase"] = high_low.astype(str) + "-" + ebb_flow.astype(str)
+
+    # Optionally convert to a wide format dataframe with a tide model in
+    # each dataframe column
+    if output_format == "wide":
+        # Pivot into wide format with each time model as a column
+        print("Converting to a wide format dataframe")
+        tide_df = tide_df.pivot(columns="tide_model")
+
+        # If in 'one-to-one' mode, reindex using our input time/x/y
+        # values to ensure the output is sorted the same as our inputs
+        if mode == "one-to-one":
+            output_indices = pd.MultiIndex.from_arrays([time, x, y], names=["time", "x", "y"])
+            tide_df = tide_df.reindex(output_indices)
+
+        # Optionally drop tides
+        if not return_tides:
+            return tide_df.drop("tide_height", axis=1)["tide_phase"]
+
+    # Optionally drop tide heights
+    if not return_tides:
+        return tide_df.drop("tide_height", axis=1)
+
+    return tide_df
+
+
+
+ +
+ +
+ + +

+ model_tides + + +

+
model_tides(
+    x,
+    y,
+    time,
+    model="EOT20",
+    directory=None,
+    crs="EPSG:4326",
+    mode="one-to-many",
+    output_format="long",
+    output_units="m",
+    method="linear",
+    extrapolate=True,
+    cutoff=None,
+    crop=True,
+    crop_buffer=5,
+    parallel=True,
+    parallel_splits="auto",
+    parallel_max=None,
+    ensemble_models=None,
+    **ensemble_kwargs
+)
+
+ +
+ +

Model tide heights at multiple coordinates and/or timesteps +using using one or more ocean tide models.

+

This function is parallelised to improve performance, and +supports all tidal models supported by pyTMD, including:

+
    +
  • Empirical Ocean Tide model (EOT20)
  • +
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • +
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • +
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • +
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
  • +
+

This function requires access to tide model data files. +These should be placed in a folder with subfolders matching +the structure required by pyTMD. For more details: +https://geoscienceaustralia.github.io/eo-tides/setup/ +https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories

+

This function is a modification of the pyTMD package's +pyTMD.compute.tide_elevations function. For more info: +https://pytmd.readthedocs.io/en/latest/api_reference/compute.html#pyTMD.compute.tide_elevations

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

x +

+ float or list of float + +
+

One or more x and y coordinates used to define +the location at which to model tides. By default these +coordinates should be lat/lon; use "crs" if they +are in a custom coordinate reference system.

+
+
+ required +
+

y +

+ float or list of float + +
+

One or more x and y coordinates used to define +the location at which to model tides. By default these +coordinates should be lat/lon; use "crs" if they +are in a custom coordinate reference system.

+
+
+ required +
+

time +

+ DatetimeLike + +
+

Times at which to model tide heights (in UTC). Accepts +any format that can be converted by pandas.to_datetime(); +e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp, +datetime.datetime and strings (e.g. "2020-01-01 23:00"). +For example: time=pd.date_range(start="2000", end="2001", freq="5h")

+
+
+ required +
+

model +

+ str or list of str + +
+

The tide model (or list of models) to use to model tides. +Defaults to "EOT20"; specify "all" to use all models available +in directory. For a full list of available and supported models, +run eo_tides.model.list_models.

+
+
+ 'EOT20' +
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

crs +

+ str + +
+

Input coordinate reference system for x and y coordinates. +Defaults to "EPSG:4326" (WGS84; degrees latitude, longitude).

+
+
+ 'EPSG:4326' +
+

mode +

+ str + +
+

The analysis mode to use for tide modelling. Supports two options:

+
    +
  • "one-to-many": Models tides for every timestep in "time" at +every input x and y coordinate point. This is useful if you +want to model tides for a specific list of timesteps across +multiple spatial points (e.g. for the same set of satellite +acquisition times at various locations across your study area).
  • +
  • "one-to-one": Model tides using a unique timestep for each +set of x and y coordinates. In this mode, the number of x and +y points must equal the number of timesteps provided in "time".
  • +
+
+
+ 'one-to-many' +
+

output_format +

+ str + +
+

Whether to return the output dataframe in long format (with +results stacked vertically along "tide_model" and "tide_height" +columns), or wide format (with a column for each tide model). +Defaults to "long".

+
+
+ 'long' +
+

output_units +

+ str + +
+

Whether to return modelled tides in floating point metre units, +or integer centimetre units (i.e. scaled by 100) or integer +millimetre units (i.e. scaled by 1000. Returning outputs in +integer units can be useful for reducing memory usage. +Defaults to "m" for metres; set to "cm" for centimetres or "mm" +for millimetres.

+
+
+ 'm' +
+

method +

+ str + +
+

Method used to interpolate tidal constituents +from model files. Defaults to "linear"; options include:

+
    +
  • "linear", "nearest": scipy regular grid interpolations
  • +
  • "spline": scipy bivariate spline interpolation
  • +
  • "bilinear": quick bilinear interpolation
  • +
+
+
+ 'linear' +
+

extrapolate +

+ bool + +
+

Whether to extrapolate tides for x and y coordinates outside of +the valid tide modelling domain using nearest-neighbor.

+
+
+ True +
+

cutoff +

+ float + +
+

Extrapolation cutoff in kilometers. The default is None, which +will extrapolate for all points regardless of distance from the +valid tide modelling domain.

+
+
+ None +
+

crop +

+ bool + +
+

Whether to crop tide model constituent files on-the-fly to +improve performance. Defaults to True; use crop_buffer +to customise the buffer distance used to crop the files.

+
+
+ True +
+

crop_buffer +

+ int or float + +
+

The buffer distance in degrees used to crop tide model +constituent files around the modelling area. Defaults to 5, +which will crop constituents using a five degree buffer on either +side of the analysis extent.

+
+
+ 5 +
+

parallel +

+ bool + +
+

Whether to parallelise tide modelling. If multiple tide models are +requested, these will be run in parallel using concurrent.futures. +If enough workers are available, the analysis will also be split +into spatial chunks for additional parallelisation (see "parallel_splits" +below). Default is True.

+
+
+ True +
+

parallel_splits +

+ str or int + +
+

Whether to split the input x and y coordinates into smaller, +evenly-sized chunks that are processed in parallel. This can +provide a large performance boost when processing large numbers +of coordinates. The default is "auto", which will automatically +attempt to determine optimal splits based on available CPUs, +the number of input points, and the number of models.

+
+
+ 'auto' +
+

parallel_max +

+ int + +
+

Maximum number of processes to run in parallel. The default of +None will automatically determine this from your available CPUs.

+
+
+ None +
+

ensemble_models +

+ list of str + +
+

An optional list of models used to generate the ensemble tide +model if "ensemble" tide modelling is requested. Defaults to +["EOT20", "FES2012", "FES2014_extrapolated", "FES2022_extrapolated", +"GOT4.10", "GOT5.5_extrapolated", "GOT5.6_extrapolated", +"TPXO10-atlas-v2-nc", "TPXO8-atlas-nc", "TPXO9-atlas-v5-nc"].

+
+
+ None +
+

**ensemble_kwargs +

+ +
+

Keyword arguments used to customise the generation of optional +ensemble tide models if "ensemble" modelling are requested. +These are passed to the underlying _ensemble_model function. +Useful parameters include ranking_points (path to model +rankings data), k (for controlling how model rankings are +interpolated), and ensemble_top_n (how many top models to use +in the ensemble calculation).

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ DataFrame + +
+

A dataframe containing modelled tide heights.

+
+
+ +
+ Source code in eo_tides/model.py +
385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
+673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
def model_tides(
+    x: float | list[float] | xr.DataArray,
+    y: float | list[float] | xr.DataArray,
+    time: DatetimeLike,
+    model: str | list[str] = "EOT20",
+    directory: str | os.PathLike | None = None,
+    crs: str = "EPSG:4326",
+    mode: str = "one-to-many",
+    output_format: str = "long",
+    output_units: str = "m",
+    method: str = "linear",
+    extrapolate: bool = True,
+    cutoff: float | None = None,
+    crop: bool = True,
+    crop_buffer: float | None = 5,
+    parallel: bool = True,
+    parallel_splits: int | str = "auto",
+    parallel_max: int | None = None,
+    ensemble_models: list[str] | None = None,
+    **ensemble_kwargs,
+) -> pd.DataFrame:
+    """
+    Model tide heights at multiple coordinates and/or timesteps
+    using using one or more ocean tide models.
+
+    This function is parallelised to improve performance, and
+    supports all tidal models supported by `pyTMD`, including:
+
+    - Empirical Ocean Tide model (EOT20)
+    - Finite Element Solution tide models (FES2022, FES2014, FES2012)
+    - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
+    - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
+    - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
+
+    This function requires access to tide model data files.
+    These should be placed in a folder with subfolders matching
+    the structure required by `pyTMD`. For more details:
+    <https://geoscienceaustralia.github.io/eo-tides/setup/>
+    <https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>
+
+    This function is a modification of the `pyTMD` package's
+    `pyTMD.compute.tide_elevations` function. For more info:
+    <https://pytmd.readthedocs.io/en/latest/api_reference/compute.html#pyTMD.compute.tide_elevations>
+
+    Parameters
+    ----------
+    x, y : float or list of float
+        One or more x and y coordinates used to define
+        the location at which to model tides. By default these
+        coordinates should be lat/lon; use "crs" if they
+        are in a custom coordinate reference system.
+    time : DatetimeLike
+        Times at which to model tide heights (in UTC). Accepts
+        any format that can be converted by `pandas.to_datetime()`;
+        e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp,
+        datetime.datetime and strings (e.g. "2020-01-01 23:00").
+        For example: `time=pd.date_range(start="2000", end="2001", freq="5h")`
+    model : str or list of str, optional
+        The tide model (or list of models) to use to model tides.
+        Defaults to "EOT20"; specify "all" to use all models available
+        in `directory`. For a full list of available and supported models,
+        run `eo_tides.model.list_models`.
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    crs : str, optional
+        Input coordinate reference system for x and y coordinates.
+        Defaults to "EPSG:4326" (WGS84; degrees latitude, longitude).
+    mode : str, optional
+        The analysis mode to use for tide modelling. Supports two options:
+
+        - "one-to-many": Models tides for every timestep in "time" at
+        every input x and y coordinate point. This is useful if you
+        want to model tides for a specific list of timesteps across
+        multiple spatial points (e.g. for the same set of satellite
+        acquisition times at various locations across your study area).
+        - "one-to-one": Model tides using a unique timestep for each
+        set of x and y coordinates. In this mode, the number of x and
+        y points must equal the number of timesteps provided in "time".
+    output_format : str, optional
+        Whether to return the output dataframe in long format (with
+        results stacked vertically along "tide_model" and "tide_height"
+        columns), or wide format (with a column for each tide model).
+        Defaults to "long".
+    output_units : str, optional
+        Whether to return modelled tides in floating point metre units,
+        or integer centimetre units (i.e. scaled by 100) or integer
+        millimetre units (i.e. scaled by 1000. Returning outputs in
+        integer units can be useful for reducing memory usage.
+        Defaults to "m" for metres; set to "cm" for centimetres or "mm"
+        for millimetres.
+    method : str, optional
+        Method used to interpolate tidal constituents
+        from model files. Defaults to "linear"; options include:
+
+        - "linear", "nearest": scipy regular grid interpolations
+        - "spline": scipy bivariate spline interpolation
+        - "bilinear": quick bilinear interpolation
+    extrapolate : bool, optional
+        Whether to extrapolate tides for x and y coordinates outside of
+        the valid tide modelling domain using nearest-neighbor.
+    cutoff : float, optional
+        Extrapolation cutoff in kilometers. The default is None, which
+        will extrapolate for all points regardless of distance from the
+        valid tide modelling domain.
+    crop : bool, optional
+        Whether to crop tide model constituent files on-the-fly to
+        improve performance. Defaults to True; use `crop_buffer`
+        to customise the buffer distance used to crop the files.
+    crop_buffer : int or float, optional
+        The buffer distance in degrees used to crop tide model
+        constituent files around the modelling area. Defaults to 5,
+        which will crop constituents using a five degree buffer on either
+        side of the analysis extent.
+    parallel : bool, optional
+        Whether to parallelise tide modelling. If multiple tide models are
+        requested, these will be run in parallel using `concurrent.futures`.
+        If enough workers are available, the analysis will also be split
+        into spatial chunks for additional parallelisation (see "parallel_splits"
+        below). Default is True.
+    parallel_splits : str or int, optional
+        Whether to split the input x and y coordinates into smaller,
+        evenly-sized chunks that are processed in parallel. This can
+        provide a large performance boost when processing large numbers
+        of coordinates. The default is "auto", which will automatically
+        attempt to determine optimal splits based on available CPUs,
+        the number of input points, and the number of models.
+    parallel_max : int, optional
+        Maximum number of processes to run in parallel. The default of
+        None will automatically determine this from your available CPUs.
+    ensemble_models : list of str, optional
+        An optional list of models used to generate the ensemble tide
+        model if "ensemble" tide modelling is requested. Defaults to
+        `["EOT20", "FES2012", "FES2014_extrapolated", "FES2022_extrapolated",
+        "GOT4.10", "GOT5.5_extrapolated", "GOT5.6_extrapolated",
+        "TPXO10-atlas-v2-nc", "TPXO8-atlas-nc", "TPXO9-atlas-v5-nc"]`.
+    **ensemble_kwargs :
+        Keyword arguments used to customise the generation of optional
+        ensemble tide models if "ensemble" modelling are requested.
+        These are passed to the underlying `_ensemble_model` function.
+        Useful parameters include `ranking_points` (path to model
+        rankings data), `k` (for controlling how model rankings are
+        interpolated), and `ensemble_top_n` (how many top models to use
+        in the ensemble calculation).
+
+    Returns
+    -------
+    pandas.DataFrame
+        A dataframe containing modelled tide heights.
+
+    """
+    # Turn inputs into arrays for consistent handling
+    x = np.atleast_1d(x)
+    y = np.atleast_1d(y)
+    time = _standardise_time(time)
+
+    # Validate input arguments
+    assert time is not None, "Times for modelling tides muyst be provided via `time`."
+    assert method in ("bilinear", "spline", "linear", "nearest")
+    assert output_units in (
+        "m",
+        "cm",
+        "mm",
+    ), "Output units must be either 'm', 'cm', or 'mm'."
+    assert output_format in (
+        "long",
+        "wide",
+    ), "Output format must be either 'long' or 'wide'."
+    assert len(x) == len(y), "x and y must be the same length."
+    if mode == "one-to-one":
+        assert len(x) == len(time), (
+            "The number of supplied x and y points and times must be "
+            "identical in 'one-to-one' mode. Use 'one-to-many' mode if "
+            "you intended to model multiple timesteps at each point."
+        )
+
+    # Set tide modelling files directory. If no custom path is
+    # provided, try global environment variable.
+    directory = _set_directory(directory)
+
+    # Standardise model list, handling "all" and "ensemble" functionality
+    models_to_process, models_requested, ensemble_models = _standardise_models(
+        model=model,
+        directory=directory,
+        ensemble_models=ensemble_models,
+    )
+
+    # Update tide modelling func to add default keyword arguments that
+    # are used for every iteration during parallel processing
+    iter_func = partial(
+        _model_tides,
+        directory=directory,
+        crs=crs,
+        mode=mode,
+        output_units=output_units,
+        method=method,
+        extrapolate=extrapolate,
+        cutoff=np.inf if cutoff is None else cutoff,
+        crop=crop,
+        crop_buffer=crop_buffer,
+    )
+
+    # If automatic parallel splits, calculate optimal value
+    # based on available parallelisation, number of points
+    # and number of models
+    if parallel_splits == "auto":
+        parallel_splits = _parallel_splits(
+            total_points=len(x),
+            model_count=len(models_to_process),
+            parallel_max=parallel_max,
+        )
+
+    # Verify that parallel splits are not larger than number of points
+    assert isinstance(parallel_splits, int)
+    if parallel_splits > len(x):
+        raise ValueError(f"Parallel splits ({parallel_splits}) cannot be larger than the number of points ({len(x)}).")
+
+    # Parallelise if either multiple models or multiple splits requested
+    if parallel & ((len(models_to_process) > 1) | (parallel_splits > 1)):
+        with ProcessPoolExecutor(max_workers=parallel_max) as executor:
+            print(
+                f"Modelling tides with {', '.join(models_to_process)} in parallel (models: {len(models_to_process)}, splits: {parallel_splits})"
+            )
+
+            # Optionally split lon/lat points into `splits_n` chunks
+            # that will be applied in parallel
+            x_split = np.array_split(x, parallel_splits)
+            y_split = np.array_split(y, parallel_splits)
+
+            # Get every combination of models and lat/lon points, and
+            # extract as iterables that can be passed to `executor.map()`
+            # In "one-to-many" mode, pass entire set of timesteps to each
+            # parallel iteration by repeating timesteps by number of total
+            # parallel iterations. In "one-to-one" mode, split up
+            # timesteps into smaller parallel chunks too.
+            if mode == "one-to-many":
+                model_iters, x_iters, y_iters = zip(
+                    *[(m, x_split[i], y_split[i]) for m in models_to_process for i in range(parallel_splits)],
+                )
+                time_iters = [time] * len(model_iters)
+            elif mode == "one-to-one":
+                time_split = np.array_split(time, parallel_splits)
+                model_iters, x_iters, y_iters, time_iters = zip(
+                    *[
+                        (m, x_split[i], y_split[i], time_split[i])
+                        for m in models_to_process
+                        for i in range(parallel_splits)
+                    ],
+                )
+
+            # Apply func in parallel, iterating through each input param
+            try:
+                model_outputs = list(
+                    tqdm(
+                        executor.map(iter_func, model_iters, x_iters, y_iters, time_iters),
+                        total=len(model_iters),
+                    ),
+                )
+            except BrokenProcessPool:
+                error_msg = (
+                    "Parallelised tide modelling failed, likely to to an out-of-memory error. "
+                    "Try reducing the size of your analysis, or set `parallel=False`."
+                )
+                raise RuntimeError(error_msg)
+
+    # Model tides in series if parallelisation is off
+    else:
+        model_outputs = []
+
+        for model_i in models_to_process:
+            print(f"Modelling tides with {model_i}")
+            tide_df = iter_func(model_i, x, y, time)
+            model_outputs.append(tide_df)
+
+    # Combine outputs into a single dataframe
+    tide_df = pd.concat(model_outputs, axis=0)
+
+    # Optionally compute ensemble model and add to dataframe
+    if "ensemble" in models_requested:
+        ensemble_df = ensemble_tides(tide_df, crs, ensemble_models, **ensemble_kwargs)
+
+        # Update requested models with any custom ensemble models, then
+        # filter the dataframe to keep only models originally requested
+        models_requested = list(np.union1d(models_requested, ensemble_df.tide_model.unique()))
+        tide_df = pd.concat([tide_df, ensemble_df]).query("tide_model in @models_requested")
+
+    # Optionally convert to a wide format dataframe with a tide model in
+    # each dataframe column
+    if output_format == "wide":
+        # Pivot into wide format with each time model as a column
+        print("Converting to a wide format dataframe")
+        tide_df = tide_df.pivot(columns="tide_model", values="tide_height")
+
+        # If in 'one-to-one' mode, reindex using our input time/x/y
+        # values to ensure the output is sorted the same as our inputs
+        if mode == "one-to-one":
+            output_indices = pd.MultiIndex.from_arrays([time, x, y], names=["time", "x", "y"])
+            tide_df = tide_df.reindex(output_indices)
+
+    return tide_df
+
+
+
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ eo_tides.eo + + +

+ +
+ + + + + + + + + +

Functions:

+ + + + + + + + + + + + + + + + + +
NameDescription
pixel_tides +
+

Model tide heights for every pixel in a multi-dimensional

+
+
tag_tides +
+

Model tide heights for every timestep in a multi-dimensional

+
+
+ + + + + +
+ + + + + + + + + +
+ + +

+ pixel_tides + + +

+
pixel_tides(
+    data,
+    time=None,
+    model="EOT20",
+    directory=None,
+    resample=True,
+    calculate_quantiles=None,
+    resolution=None,
+    buffer=None,
+    resample_method="bilinear",
+    dask_chunks=None,
+    dask_compute=True,
+    **model_tides_kwargs
+)
+
+ +
+ +

Model tide heights for every pixel in a multi-dimensional +dataset, using one or more ocean tide models.

+

This function models tides into a low-resolution tide +modelling grid covering the spatial extent of the input +data (buffered to reduce potential edge effects). These +modelled tides can then be resampled back into the original +higher resolution dataset's extent and resolution to +produce a modelled tide height for every pixel through time.

+

This function uses the parallelised model_tides function +under the hood. It supports all tidal models supported by +pyTMD, including:

+
    +
  • Empirical Ocean Tide model (EOT20)
  • +
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • +
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • +
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • +
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
  • +
+

This function requires access to tide model data files. +These should be placed in a folder with subfolders matching +the structure required by pyTMD. For more details: +https://geoscienceaustralia.github.io/eo-tides/setup/ +https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

data +

+ Dataset or DataArray or GeoBox + +
+

A multi-dimensional dataset or GeoBox pixel grid that will +be used to define the spatial tide modelling grid. If data +is an xarray object, it should include a "time" dimension. +If no "time" dimension exists or if data is a GeoBox, +then times must be passed using the time parameter.

+
+
+ required +
+

time +

+ DatetimeLike + +
+

By default, tides will be modelled using times from the +"time" dimension of data. Alternatively, this param can +be used to provide a custom set of times. Accepts any format +that can be converted by pandas.to_datetime(). For example: +time=pd.date_range(start="2000", end="2001", freq="5h")

+
+
+ None +
+

model +

+ str or list of str + +
+

The tide model (or list of models) to use to model tides. +If a list is provided, a new "tide_model" dimension will be +added to the xarray.DataArray outputs. Defaults to "EOT20"; +specify "all" to use all models available in directory. +For a full list of available and supported models, run +eo_tides.model.list_models.

+
+
+ 'EOT20' +
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

resample +

+ bool + +
+

Whether to resample low resolution tides back into data's original +higher resolution grid. Set this to False if you do not want +low resolution tides to be re-projected back to higher resolution.

+
+
+ True +
+

calculate_quantiles +

+ tuple of float or numpy.ndarray + +
+

Rather than returning all individual tides, low-resolution tides +can be first aggregated using a quantile calculation by passing in +a tuple or array of quantiles to compute. For example, this could +be used to calculate the min/max tide across all times: +calculate_quantiles=(0.0, 1.0).

+
+
+ None +
+

resolution +

+ float + +
+

The desired resolution of the low-resolution grid used for tide +modelling. The default None will create a 5000 m resolution grid +if data has a projected CRS (i.e. metre units), or a 0.05 degree +resolution grid if data has a geographic CRS (e.g. degree units). +Note: higher resolutions do not necessarily provide better +tide modelling performance, as results will be limited by the +resolution of the underlying global tide model (e.g. 1/16th +degree / ~5 km resolution grid for FES2014).

+
+
+ None +
+

buffer +

+ float + +
+

The amount by which to buffer the higher resolution grid extent +when creating the new low resolution grid. This buffering +ensures that modelled tides are seamless across analysis +boundaries. This buffer is eventually be clipped away when +the low-resolution modelled tides are re-projected back to the +original resolution and extent of data. To ensure that at least +two low-resolution grid pixels occur outside of the dataset +bounds, the default None applies a 12000 m buffer if data has a +projected CRS (i.e. metre units), or a 0.12 degree buffer if +data has a geographic CRS (e.g. degree units).

+
+
+ None +
+

resample_method +

+ str + +
+

If resampling is requested (see resample above), use this +resampling method when converting from low resolution to high +resolution pixels. Defaults to "bilinear"; valid options include +"nearest", "cubic", "min", "max", "average" etc.

+
+
+ 'bilinear' +
+

dask_chunks +

+ tuple of float + +
+

Can be used to configure custom Dask chunking for the final +resampling step. By default, chunks will be automatically set +to match y/x chunks from data if they exist; otherwise chunks +will be chosen to cover the entire y/x extent of the dataset. +For custom chunks, provide a tuple in the form (y, x), e.g. +(2048, 2048).

+
+
+ None +
+

dask_compute +

+ bool + +
+

Whether to compute results of the resampling step using Dask. +If False, tides_highres will be returned as a Dask array.

+
+
+ True +
+

**model_tides_kwargs +

+ +
+

Optional parameters passed to the eo_tides.model.model_tides +function. Important parameters include cutoff (used to +extrapolate modelled tides away from the coast; defaults to +np.inf), crop (whether to crop tide model constituent files +on-the-fly to improve performance) etc.

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
tides_da + DataArray + +
+

A three-dimensional tide height array. +If resample=True (default), a high-resolution array of tide +heights will be returned that matches the exact spatial resolution +and extents of data. This will contain either tide heights for +every timestep in data (or in times if provided), or tide height +quantiles for every quantile provided by calculate_quantiles. +If resample=False, results for the intermediate low-resolution +tide modelling grid will be returned instead.

+
+
+ +
+ Source code in eo_tides/eo.py +
def pixel_tides(
+    data: xr.Dataset | xr.DataArray | GeoBox,
+    time: DatetimeLike | None = None,
+    model: str | list[str] = "EOT20",
+    directory: str | os.PathLike | None = None,
+    resample: bool = True,
+    calculate_quantiles: np.ndarray | tuple[float, float] | None = None,
+    resolution: float | None = None,
+    buffer: float | None = None,
+    resample_method: str = "bilinear",
+    dask_chunks: tuple[float, float] | None = None,
+    dask_compute: bool = True,
+    **model_tides_kwargs,
+) -> xr.DataArray:
+    """
+    Model tide heights for every pixel in a multi-dimensional
+    dataset, using one or more ocean tide models.
+
+    This function models tides into a low-resolution tide
+    modelling grid covering the spatial extent of the input
+    data (buffered to reduce potential edge effects). These
+    modelled tides can then be resampled back into the original
+    higher resolution dataset's extent and resolution to
+    produce a modelled tide height for every pixel through time.
+
+    This function uses the parallelised `model_tides` function
+    under the hood. It supports all tidal models supported by
+    `pyTMD`, including:
+
+    - Empirical Ocean Tide model (EOT20)
+    - Finite Element Solution tide models (FES2022, FES2014, FES2012)
+    - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
+    - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
+    - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
+
+    This function requires access to tide model data files.
+    These should be placed in a folder with subfolders matching
+    the structure required by `pyTMD`. For more details:
+    <https://geoscienceaustralia.github.io/eo-tides/setup/>
+    <https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>
+
+    Parameters
+    ----------
+    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox
+        A multi-dimensional dataset or GeoBox pixel grid that will
+        be used to define the spatial tide modelling grid. If `data`
+        is an xarray object, it should include a "time" dimension.
+        If no "time" dimension exists or if `data` is a GeoBox,
+        then times must be passed using the `time` parameter.
+    time : DatetimeLike, optional
+        By default, tides will be modelled using times from the
+        "time" dimension of `data`. Alternatively, this param can
+        be used to provide a custom set of times. Accepts any format
+        that can be converted by `pandas.to_datetime()`. For example:
+        `time=pd.date_range(start="2000", end="2001", freq="5h")`
+    model : str or list of str, optional
+        The tide model (or list of models) to use to model tides.
+        If a list is provided, a new "tide_model" dimension will be
+        added to the `xarray.DataArray` outputs. Defaults to "EOT20";
+        specify "all" to use all models available in `directory`.
+        For a full list of available and supported models, run
+        `eo_tides.model.list_models`.
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    resample : bool, optional
+        Whether to resample low resolution tides back into `data`'s original
+        higher resolution grid. Set this to `False` if you do not want
+        low resolution tides to be re-projected back to higher resolution.
+    calculate_quantiles : tuple of float or numpy.ndarray, optional
+        Rather than returning all individual tides, low-resolution tides
+        can be first aggregated using a quantile calculation by passing in
+        a tuple or array of quantiles to compute. For example, this could
+        be used to calculate the min/max tide across all times:
+        `calculate_quantiles=(0.0, 1.0)`.
+    resolution : float, optional
+        The desired resolution of the low-resolution grid used for tide
+        modelling. The default None will create a 5000 m resolution grid
+        if `data` has a projected CRS (i.e. metre units), or a 0.05 degree
+        resolution grid if `data` has a geographic CRS (e.g. degree units).
+        Note: higher resolutions do not necessarily provide better
+        tide modelling performance, as results will be limited by the
+        resolution of the underlying global tide model (e.g. 1/16th
+        degree / ~5 km resolution grid for FES2014).
+    buffer : float, optional
+        The amount by which to buffer the higher resolution grid extent
+        when creating the new low resolution grid. This buffering
+        ensures that modelled tides are seamless across analysis
+        boundaries. This buffer is eventually be clipped away when
+        the low-resolution modelled tides are re-projected back to the
+        original resolution and extent of `data`. To ensure that at least
+        two low-resolution grid pixels occur outside of the dataset
+        bounds, the default None applies a 12000 m buffer if `data` has a
+        projected CRS (i.e. metre units), or a 0.12 degree buffer if
+        `data` has a geographic CRS (e.g. degree units).
+    resample_method : str, optional
+        If resampling is requested (see `resample` above), use this
+        resampling method when converting from low resolution to high
+        resolution pixels. Defaults to "bilinear"; valid options include
+        "nearest", "cubic", "min", "max", "average" etc.
+    dask_chunks : tuple of float, optional
+        Can be used to configure custom Dask chunking for the final
+        resampling step. By default, chunks will be automatically set
+        to match y/x chunks from `data` if they exist; otherwise chunks
+        will be chosen to cover the entire y/x extent of the dataset.
+        For custom chunks, provide a tuple in the form `(y, x)`, e.g.
+        `(2048, 2048)`.
+    dask_compute : bool, optional
+        Whether to compute results of the resampling step using Dask.
+        If False, `tides_highres` will be returned as a Dask array.
+    **model_tides_kwargs :
+        Optional parameters passed to the `eo_tides.model.model_tides`
+        function. Important parameters include `cutoff` (used to
+        extrapolate modelled tides away from the coast; defaults to
+        `np.inf`), `crop` (whether to crop tide model constituent files
+        on-the-fly to improve performance) etc.
+    Returns
+    -------
+    tides_da : xr.DataArray
+        A three-dimensional tide height array.
+        If `resample=True` (default), a high-resolution array of tide
+        heights will be returned that matches the exact spatial resolution
+        and extents of `data`. This will contain either tide heights for
+        every timestep in `data` (or in `times` if provided), or tide height
+        quantiles for every quantile provided by `calculate_quantiles`.
+        If `resample=False`, results for the intermediate low-resolution
+        tide modelling grid will be returned instead.
+    """
+    # Standardise data inputs, time and models
+    gbox, time_coords = _standardise_inputs(data, time)
+    dask_chunks = _resample_chunks(data, dask_chunks)
+    model = [model] if isinstance(model, str) else model
+
+    # Determine spatial dimensions
+    y_dim, x_dim = gbox.dimensions
+
+    # Determine resolution and buffer, using different defaults for
+    # geographic (i.e. degrees) and projected (i.e. metres) CRSs:
+    assert gbox.crs is not None
+    crs_units = gbox.crs.units[0][0:6]
+    if gbox.crs.geographic:
+        if resolution is None:
+            resolution = 0.05
+        elif resolution > 360:
+            raise ValueError(
+                f"A resolution of greater than 360 was "
+                f"provided, but `data` has a geographic CRS "
+                f"in {crs_units} units. Did you accidently "
+                f"provide a resolution in projected "
+                f"(i.e. metre) units?",
+            )
+        if buffer is None:
+            buffer = 0.12
+    else:
+        if resolution is None:
+            resolution = 5000
+        elif resolution < 1:
+            raise ValueError(
+                f"A resolution of less than 1 was provided, "
+                f"but `data` has a projected CRS in "
+                f"{crs_units} units. Did you accidently "
+                f"provide a resolution in geographic "
+                f"(degree) units?",
+            )
+        if buffer is None:
+            buffer = 12000
+
+    # Raise error if resolution is less than dataset resolution
+    dataset_res = gbox.resolution.x
+    if resolution < dataset_res:
+        raise ValueError(
+            f"The resolution of the low-resolution tide "
+            f"modelling grid ({resolution:.2f}) is less "
+            f"than `data`'s pixel resolution ({dataset_res:.2f}). "
+            f"This can cause extremely slow tide modelling "
+            f"performance. Please select provide a resolution "
+            f"greater than {dataset_res:.2f} using "
+            f"`pixel_tides`'s 'resolution' parameter.",
+        )
+
+    # Create a new reduced resolution tide modelling grid after
+    # first buffering the grid
+    print(f"Creating reduced resolution {resolution} x {resolution} {crs_units} tide modelling array")
+    buffered_geobox = gbox.buffered(buffer)
+    rescaled_geobox = GeoBox.from_bbox(bbox=buffered_geobox.boundingbox, resolution=resolution)
+    rescaled_ds = odc.geo.xr.xr_zeros(rescaled_geobox)
+
+    # Flatten grid to 1D, then add time dimension
+    flattened_ds = rescaled_ds.stack(z=(x_dim, y_dim))
+    flattened_ds = flattened_ds.expand_dims(dim={"time": time_coords})
+
+    # Model tides in parallel, returning a pandas.DataFrame
+    tide_df = model_tides(
+        x=flattened_ds[x_dim],
+        y=flattened_ds[y_dim],
+        time=flattened_ds.time,
+        crs=f"EPSG:{gbox.crs.epsg}",
+        model=model,
+        directory=directory,
+        **model_tides_kwargs,
+    )
+
+    # Convert our pandas.DataFrame tide modelling outputs to xarray
+    tides_lowres = (
+        # Rename x and y dataframe indexes to match x and y xarray dims
+        tide_df.rename_axis(["time", x_dim, y_dim])
+        # Add tide model column to dataframe indexes so we can convert
+        # our dataframe to a multidimensional xarray
+        .set_index("tide_model", append=True)
+        # Convert to xarray and select our tide modelling xr.DataArray
+        .to_xarray()
+        .tide_height
+        # Re-index and transpose into our input coordinates and dim order
+        .reindex_like(rescaled_ds)
+        .transpose("tide_model", "time", y_dim, x_dim)
+    )
+
+    # Optionally calculate and return quantiles rather than raw data.
+    # Set dtype to dtype of the input data as quantile always returns
+    # float64 (memory intensive)
+    if calculate_quantiles is not None:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            print("Computing tide quantiles")
+            tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim="time").astype(tides_lowres.dtype)
+
+    # If only one tidal model exists, squeeze out "tide_model" dim
+    if len(tides_lowres.tide_model) == 1:
+        tides_lowres = tides_lowres.squeeze("tide_model")
+
+    # Ensure CRS is present before we apply any resampling
+    tides_lowres = tides_lowres.odc.assign_crs(gbox.crs)
+
+    # Reproject into original high resolution grid
+    if resample:
+        print("Reprojecting tides into original resolution")
+        tides_highres = _pixel_tides_resample(
+            tides_lowres,
+            gbox,
+            resample_method,
+            dask_chunks,
+            dask_compute,
+        )
+        return tides_highres
+
+    print("Returning low resolution tide array")
+    return tides_lowres
+
+
+
+ +
+ +
+ + +

+ tag_tides + + +

+
tag_tides(
+    data,
+    time=None,
+    model="EOT20",
+    directory=None,
+    tidepost_lat=None,
+    tidepost_lon=None,
+    **model_tides_kwargs
+)
+
+ +
+ +

Model tide heights for every timestep in a multi-dimensional +dataset, and return a new tide_height array that can +be used to "tag" each observation with tide heights.

+

The function models tides at the centroid of the dataset +by default, but a custom tidal modelling location can +be specified using tidepost_lat and tidepost_lon.

+

This function uses the parallelised model_tides function +under the hood. It supports all tidal models supported by +pyTMD, including:

+
    +
  • Empirical Ocean Tide model (EOT20)
  • +
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • +
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • +
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • +
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
  • +
+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

data +

+ Dataset or DataArray or GeoBox + +
+

A multi-dimensional dataset or GeoBox pixel grid that will +be used to define the tide modelling location. If data +is an xarray object, it should include a "time" dimension. +If no "time" dimension exists or if data is a GeoBox, +then times must be passed using the time parameter.

+
+
+ required +
+

time +

+ DatetimeLike + +
+

By default, tides will be modelled using times from the +"time" dimension of data. Alternatively, this param can +be used to provide a custom set of times. Accepts any format +that can be converted by pandas.to_datetime(). For example: +time=pd.date_range(start="2000", end="2001", freq="5h")

+
+
+ None +
+

model +

+ str or list of str + +
+

The tide model (or list of models) to use to model tides. +If a list is provided, a new "tide_model" dimension will be +added to the xarray.DataArray outputs. Defaults to "EOT20"; +specify "all" to use all models available in directory. +For a full list of available and supported models, run +eo_tides.model.list_models.

+
+
+ 'EOT20' +
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

tidepost_lat +

+ float + +
+

Optional coordinates used to model tides. The default is None, +which uses the centroid of the dataset as the tide modelling +location.

+
+
+ None +
+

tidepost_lon +

+ float + +
+

Optional coordinates used to model tides. The default is None, +which uses the centroid of the dataset as the tide modelling +location.

+
+
+ None +
+

**model_tides_kwargs +

+ +
+

Optional parameters passed to the eo_tides.model.model_tides +function. Important parameters include cutoff (used to +extrapolate modelled tides away from the coast; defaults to +np.inf), crop (whether to crop tide model constituent files +on-the-fly to improve performance) etc.

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
tides_da + DataArray + +
+

A one-dimensional tide height array. This will contain either +tide heights for every timestep in data, or for every time in +times if provided.

+
+
+ +
+ Source code in eo_tides/eo.py +
def tag_tides(
+    data: xr.Dataset | xr.DataArray | GeoBox,
+    time: DatetimeLike | None = None,
+    model: str | list[str] = "EOT20",
+    directory: str | os.PathLike | None = None,
+    tidepost_lat: float | None = None,
+    tidepost_lon: float | None = None,
+    **model_tides_kwargs,
+) -> xr.DataArray:
+    """
+    Model tide heights for every timestep in a multi-dimensional
+    dataset, and return a new `tide_height` array that can
+    be used to "tag" each observation with tide heights.
+
+    The function models tides at the centroid of the dataset
+    by default, but a custom tidal modelling location can
+    be specified using `tidepost_lat` and `tidepost_lon`.
+
+    This function uses the parallelised `model_tides` function
+    under the hood. It supports all tidal models supported by
+    `pyTMD`, including:
+
+    - Empirical Ocean Tide model (EOT20)
+    - Finite Element Solution tide models (FES2022, FES2014, FES2012)
+    - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
+    - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
+    - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
+
+    Parameters
+    ----------
+    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox
+        A multi-dimensional dataset or GeoBox pixel grid that will
+        be used to define the tide modelling location. If `data`
+        is an xarray object, it should include a "time" dimension.
+        If no "time" dimension exists or if `data` is a GeoBox,
+        then times must be passed using the `time` parameter.
+    time : DatetimeLike, optional
+        By default, tides will be modelled using times from the
+        "time" dimension of `data`. Alternatively, this param can
+        be used to provide a custom set of times. Accepts any format
+        that can be converted by `pandas.to_datetime()`. For example:
+        `time=pd.date_range(start="2000", end="2001", freq="5h")`
+    model : str or list of str, optional
+        The tide model (or list of models) to use to model tides.
+        If a list is provided, a new "tide_model" dimension will be
+        added to the `xarray.DataArray` outputs. Defaults to "EOT20";
+        specify "all" to use all models available in `directory`.
+        For a full list of available and supported models, run
+        `eo_tides.model.list_models`.
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    tidepost_lat, tidepost_lon : float, optional
+        Optional coordinates used to model tides. The default is None,
+        which uses the centroid of the dataset as the tide modelling
+        location.
+    **model_tides_kwargs :
+        Optional parameters passed to the `eo_tides.model.model_tides`
+        function. Important parameters include `cutoff` (used to
+        extrapolate modelled tides away from the coast; defaults to
+        `np.inf`), `crop` (whether to crop tide model constituent files
+        on-the-fly to improve performance) etc.
+
+    Returns
+    -------
+    tides_da : xr.DataArray
+        A one-dimensional tide height array. This will contain either
+        tide heights for every timestep in `data`, or for every time in
+        `times` if provided.
+    """
+    # Standardise data inputs, time and models
+    gbox, time_coords = _standardise_inputs(data, time)
+    model = [model] if isinstance(model, str) else model
+
+    # If custom tide posts are not provided, use dataset centroid
+    if tidepost_lat is None or tidepost_lon is None:
+        lon, lat = gbox.geographic_extent.centroid.coords[0]
+        print(f"Setting tide modelling location from dataset centroid: {lon:.2f}, {lat:.2f}")
+    else:
+        lon, lat = tidepost_lon, tidepost_lat
+        print(f"Using tide modelling location: {lon:.2f}, {lat:.2f}")
+
+    # Model tide heights for each observation:
+    tide_df = model_tides(
+        x=lon,  # type: ignore
+        y=lat,  # type: ignore
+        time=time_coords,
+        model=model,
+        directory=directory,
+        crs="EPSG:4326",
+        **model_tides_kwargs,
+    )
+
+    # If tides cannot be successfully modeled (e.g. if the centre of the
+    # xarray dataset is located is over land), raise an exception
+    if tide_df.tide_height.isnull().all():
+        raise ValueError(
+            f"Tides could not be modelled for dataset centroid located "
+            f"at {tidepost_lon:.2f}, {tidepost_lat:.2f}. This can occur if "
+            f"this coordinate occurs over land. Please manually specify "
+            f"a tide modelling location located over water using the "
+            f"`tidepost_lat` and `tidepost_lon` parameters."
+        )
+
+    # Convert to xarray format
+    tides_da = tide_df.reset_index().set_index(["time", "tide_model"]).drop(["x", "y"], axis=1).tide_height.to_xarray()
+
+    # If only one tidal model exists, squeeze out "tide_model" dim
+    if len(tides_da.tide_model) == 1:
+        tides_da = tides_da.squeeze("tide_model")
+
+    return tides_da
+
+
+
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ eo_tides.stats + + +

+ +
+ + + + + + + + + +

Functions:

+ + + + + + + + + + + + + + + + + +
NameDescription
pixel_stats +
+

Takes a multi-dimensional dataset and generate two-dimensional

+
+
tide_stats +
+

Takes a multi-dimensional dataset and generate tide statistics

+
+
+ + + + + +
+ + + + + + + + + +
+ + +

+ pixel_stats + + +

+
pixel_stats(
+    data,
+    time=None,
+    model="EOT20",
+    directory=None,
+    resample=False,
+    modelled_freq="3h",
+    min_max_q=(0.0, 1.0),
+    extrapolate=True,
+    cutoff=10,
+    **pixel_tides_kwargs
+)
+
+ +
+ +

Takes a multi-dimensional dataset and generate two-dimensional +tide statistics and satellite-observed tide bias metrics, +calculated based on every timestep in the satellte data and +modelled into the spatial extent of the imagery.

+

By comparing the subset of tides observed by satellites +against the full astronomical tidal range, we can evaluate +whether the tides observed by satellites are biased +(e.g. fail to observe either the highest or lowest tides).

+

Compared to tide_stats, this function models tide metrics +spatially to produce a two-dimensional output.

+

For more information about the tidal statistics computed by this +function, refer to Figure 8 in Bishop-Taylor et al. 2018: +https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

data +

+ Dataset or DataArray or GeoBox + +
+

A multi-dimensional dataset or GeoBox pixel grid that will +be used to calculate 2D tide statistics. If data +is an xarray object, it should include a "time" dimension. +If no "time" dimension exists or if data is a GeoBox, +then times must be passed using the time parameter.

+
+
+ required +
+

time +

+ DatetimeLike + +
+

By default, tides will be modelled using times from the +"time" dimension of data. Alternatively, this param can +be used to provide a custom set of times. Accepts any format +that can be converted by pandas.to_datetime(). For example: +time=pd.date_range(start="2000", end="2001", freq="5h")

+
+
+ None +
+

model +

+ str or list of str + +
+

The tide model (or list of models) to use to model tides. +If a list is provided, a new "tide_model" dimension will be +added to the xarray.DataArray outputs. Defaults to "EOT20"; +specify "all" to use all models available in directory. +For a full list of available and supported models, run +eo_tides.model.list_models.

+
+
+ 'EOT20' +
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

resample +

+ bool + +
+

Whether to resample tide statistics back into data's original +higher resolution grid. Defaults to False, which will return +lower-resolution statistics that are typically sufficient for +most purposes.

+
+
+ False +
+

modelled_freq +

+ str + +
+

An optional string giving the frequency at which to model tides +when computing the full modelled tidal range. Defaults to '3h', +which computes a tide height for every three hours across the +temporal extent of data.

+
+
+ '3h' +
+

min_max_q +

+ tuple + +
+

Quantiles used to calculate max and min observed and modelled +astronomical tides. By default (0.0, 1.0) which is equivalent +to minimum and maximum; to use a softer threshold that is more +robust to outliers, use e.g. (0.1, 0.9).

+
+
+ (0.0, 1.0) +
+

extrapolate +

+ bool + +
+

Whether to extrapolate tides for x and y coordinates outside of +the valid tide modelling domain using nearest-neighbor. Defaults +to True.

+
+
+ True +
+

cutoff +

+ float + +
+

Extrapolation cutoff in kilometers. To avoid producing tide +statistics too far inland, the default is 10 km.

+
+
+ 10 +
+

**pixel_tides_kwargs +

+ +
+

Optional parameters passed to the eo_tides.eo.pixel_tides +function.

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
stats_ds + Dataset + +
+

An xarray.Dataset containing the following statistics as two-dimensional data variables:

+
    +
  • lot: minimum tide height observed by the satellite (metres)
  • +
  • lat: minimum tide height from modelled astronomical tidal range (metres)
  • +
  • hot: maximum tide height observed by the satellite (metres)
  • +
  • hat: maximum tide height from modelled astronomical tidal range (metres)
  • +
  • otr: tidal range observed by the satellite (metres)
  • +
  • tr: modelled astronomical tide range (metres)
  • +
  • spread: proportion of the full modelled tidal range observed by the satellite
  • +
  • offset_low: proportion of the lowest tides never observed by the satellite
  • +
  • offset_high: proportion of the highest tides never observed by the satellite
  • +
+
+
+ +
+ Source code in eo_tides/stats.py +
def pixel_stats(
+    data: xr.Dataset | xr.DataArray | GeoBox,
+    time: DatetimeLike | None = None,
+    model: str | list[str] = "EOT20",
+    directory: str | os.PathLike | None = None,
+    resample: bool = False,
+    modelled_freq: str = "3h",
+    min_max_q: tuple[float, float] = (0.0, 1.0),
+    extrapolate: bool = True,
+    cutoff: float = 10,
+    **pixel_tides_kwargs,
+) -> xr.Dataset:
+    """
+    Takes a multi-dimensional dataset and generate two-dimensional
+    tide statistics and satellite-observed tide bias metrics,
+    calculated based on every timestep in the satellte data and
+    modelled into the spatial extent of the imagery.
+
+    By comparing the subset of tides observed by satellites
+    against the full astronomical tidal range, we can evaluate
+    whether the tides observed by satellites are biased
+    (e.g. fail to observe either the highest or lowest tides).
+
+    Compared to `tide_stats`, this function models tide metrics
+    spatially to produce a two-dimensional output.
+
+    For more information about the tidal statistics computed by this
+    function, refer to Figure 8 in Bishop-Taylor et al. 2018:
+    <https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8>
+
+    Parameters
+    ----------
+    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox
+        A multi-dimensional dataset or GeoBox pixel grid that will
+        be used to calculate 2D tide statistics. If `data`
+        is an xarray object, it should include a "time" dimension.
+        If no "time" dimension exists or if `data` is a GeoBox,
+        then times must be passed using the `time` parameter.
+    time : DatetimeLike, optional
+        By default, tides will be modelled using times from the
+        "time" dimension of `data`. Alternatively, this param can
+        be used to provide a custom set of times. Accepts any format
+        that can be converted by `pandas.to_datetime()`. For example:
+        `time=pd.date_range(start="2000", end="2001", freq="5h")`
+    model : str or list of str, optional
+        The tide model (or list of models) to use to model tides.
+        If a list is provided, a new "tide_model" dimension will be
+        added to the `xarray.DataArray` outputs. Defaults to "EOT20";
+        specify "all" to use all models available in `directory`.
+        For a full list of available and supported models, run
+        `eo_tides.model.list_models`.
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    resample : bool, optional
+        Whether to resample tide statistics back into `data`'s original
+        higher resolution grid. Defaults to False, which will return
+        lower-resolution statistics that are typically sufficient for
+        most purposes.
+    modelled_freq : str, optional
+        An optional string giving the frequency at which to model tides
+        when computing the full modelled tidal range. Defaults to '3h',
+        which computes a tide height for every three hours across the
+        temporal extent of `data`.
+    min_max_q : tuple, optional
+        Quantiles used to calculate max and min observed and modelled
+        astronomical tides. By default `(0.0, 1.0)` which is equivalent
+        to minimum and maximum; to use a softer threshold that is more
+        robust to outliers, use e.g. `(0.1, 0.9)`.
+    extrapolate : bool, optional
+        Whether to extrapolate tides for x and y coordinates outside of
+        the valid tide modelling domain using nearest-neighbor. Defaults
+        to True.
+    cutoff : float, optional
+        Extrapolation cutoff in kilometers. To avoid producing tide
+        statistics too far inland, the default is 10 km.
+    **pixel_tides_kwargs :
+        Optional parameters passed to the `eo_tides.eo.pixel_tides`
+        function.
+
+    Returns
+    -------
+    stats_ds : xarray.Dataset
+        An `xarray.Dataset` containing the following statistics as two-dimensional data variables:
+
+        - `lot`: minimum tide height observed by the satellite (metres)
+        - `lat`: minimum tide height from modelled astronomical tidal range (metres)
+        - `hot`: maximum tide height observed by the satellite (metres)
+        - `hat`: maximum tide height from modelled astronomical tidal range (metres)
+        - `otr`: tidal range observed by the satellite (metres)
+        - `tr`: modelled astronomical tide range (metres)
+        - `spread`: proportion of the full modelled tidal range observed by the satellite
+        - `offset_low`: proportion of the lowest tides never observed by the satellite
+        - `offset_high`: proportion of the highest tides never observed by the satellite
+
+    """
+    # Standardise data inputs, time and models
+    gbox, time_coords = _standardise_inputs(data, time)
+    model = [model] if isinstance(model, str) else model
+
+    # Model observed tides
+    assert time_coords is not None
+    obs_tides = pixel_tides(
+        gbox,
+        time=time_coords,
+        resample=False,
+        model=model,
+        directory=directory,
+        calculate_quantiles=min_max_q,
+        extrapolate=extrapolate,
+        cutoff=cutoff,
+        **pixel_tides_kwargs,
+    )
+
+    # Generate times covering entire period of satellite record
+    all_timerange = pd.date_range(
+        start=time_coords.min().item(),
+        end=time_coords.max().item(),
+        freq=modelled_freq,
+    )
+
+    # Model all tides
+    all_tides = pixel_tides(
+        gbox,
+        time=all_timerange,
+        model=model,
+        directory=directory,
+        calculate_quantiles=min_max_q,
+        resample=False,
+        extrapolate=extrapolate,
+        cutoff=cutoff,
+        **pixel_tides_kwargs,
+    )
+
+    # # Calculate means
+    # TODO: Find way to make this work with `calculate_quantiles`
+    # mot = obs_tides.mean(dim="time")
+    # mat = all_tides.mean(dim="time")
+
+    # Calculate min and max tides
+    lot = obs_tides.isel(quantile=0)
+    hot = obs_tides.isel(quantile=-1)
+    lat = all_tides.isel(quantile=0)
+    hat = all_tides.isel(quantile=-1)
+
+    # Calculate tidal range
+    otr = hot - lot
+    tr = hat - lat
+
+    # Calculate Bishop-Taylor et al. 2018 tidal metrics
+    spread = otr / tr
+    offset_low_m = abs(lat - lot)
+    offset_high_m = abs(hat - hot)
+    offset_low = offset_low_m / tr
+    offset_high = offset_high_m / tr
+
+    # Combine into a single dataset
+    stats_ds = (
+        xr.merge(
+            [
+                # mot.rename("mot"),
+                # mat.rename("mat"),
+                hot.rename("hot"),
+                hat.rename("hat"),
+                lot.rename("lot"),
+                lat.rename("lat"),
+                otr.rename("otr"),
+                tr.rename("tr"),
+                spread.rename("spread"),
+                offset_low.rename("offset_low"),
+                offset_high.rename("offset_high"),
+            ],
+            compat="override",
+        )
+        .drop_vars("quantile")
+        .odc.assign_crs(crs=gbox.crs)
+    )
+
+    # Optionally resample into the original pixel grid of `data`
+    if resample:
+        stats_ds = stats_ds.odc.reproject(how=gbox, resample_method="bilinear")
+
+    return stats_ds
+
+
+
+ +
+ +
+ + +

+ tide_stats + + +

+
tide_stats(
+    data,
+    time=None,
+    model="EOT20",
+    directory=None,
+    tidepost_lat=None,
+    tidepost_lon=None,
+    plain_english=True,
+    plot=True,
+    plot_col=None,
+    modelled_freq="3h",
+    linear_reg=False,
+    min_max_q=(0.0, 1.0),
+    round_stats=3,
+    **model_tides_kwargs
+)
+
+ +
+ +

Takes a multi-dimensional dataset and generate tide statistics +and satellite-observed tide bias metrics, calculated based on +every timestep in the satellte data and the geographic centroid +of the imagery.

+

By comparing the subset of tides observed by satellites +against the full astronomical tidal range, we can evaluate +whether the tides observed by satellites are biased +(e.g. fail to observe either the highest or lowest tides).

+

For more information about the tidal statistics computed by this +function, refer to Figure 8 in Bishop-Taylor et al. 2018: +https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

data +

+ Dataset or DataArray or GeoBox + +
+

A multi-dimensional dataset or GeoBox pixel grid that will +be used to calculate tide statistics. If data is an +xarray object, it should include a "time" dimension. +If no "time" dimension exists or if data is a GeoBox, +then times must be passed using the time parameter.

+
+
+ required +
+

time +

+ DatetimeLike + +
+

By default, tides will be modelled using times from the +"time" dimension of data. Alternatively, this param can +be used to provide a custom set of times. Accepts any format +that can be converted by pandas.to_datetime(). For example: +time=pd.date_range(start="2000", end="2001", freq="5h")

+
+
+ None +
+

model +

+ str + +
+

The tide model to use to model tides. Defaults to "EOT20"; +for a full list of available/supported models, run +eo_tides.model.list_models.

+
+
+ 'EOT20' +
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

tidepost_lat +

+ float or int + +
+

Optional coordinates used to model tides. The default is None, +which uses the centroid of the dataset as the tide modelling +location.

+
+
+ None +
+

tidepost_lon +

+ float or int + +
+

Optional coordinates used to model tides. The default is None, +which uses the centroid of the dataset as the tide modelling +location.

+
+
+ None +
+

plain_english +

+ bool + +
+

An optional boolean indicating whether to print a plain english +version of the tidal statistics to the screen. Defaults to True.

+
+
+ True +
+

plot +

+ bool + +
+

An optional boolean indicating whether to plot how satellite- +observed tide heights compare against the full tidal range. +Defaults to True.

+
+
+ True +
+

plot_col +

+ str + +
+

Optional name of a coordinate, dimension or variable in the array +that will be used to plot observations with unique symbols. +Defaults to None, which will plot all observations as circles.

+
+
+ None +
+

modelled_freq +

+ str + +
+

An optional string giving the frequency at which to model tides +when computing the full modelled tidal range. Defaults to '3h', +which computes a tide height for every three hours across the +temporal extent of data.

+
+
+ '3h' +
+

linear_reg +

+ bool + +
+

Whether to return linear regression statistics that assess +whether satellite-observed tides show any decreasing or +increasing trends over time. This may indicate whether your +satellite data may produce misleading trends based on uneven +sampling of the local tide regime.

+
+
+ False +
+

min_max_q +

+ tuple + +
+

Quantiles used to calculate max and min observed and modelled +astronomical tides. By default (0.0, 1.0) which is equivalent +to minimum and maximum; to use a softer threshold that is more +robust to outliers, use e.g. (0.1, 0.9).

+
+
+ (0.0, 1.0) +
+

round_stats +

+ int + +
+

The number of decimal places used to round the output statistics. +Defaults to 3.

+
+
+ 3 +
+

**model_tides_kwargs +

+ +
+

Optional parameters passed to the eo_tides.model.model_tides +function. Important parameters include cutoff (used to +extrapolate modelled tides away from the coast; defaults to +np.inf), crop (whether to crop tide model constituent files +on-the-fly to improve performance) etc.

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
stats_df + Series + +
+

A pandas.Series containing the following statistics:

+
    +
  • y: latitude used for modelling tide heights
  • +
  • x: longitude used for modelling tide heights
  • +
  • mot: mean tide height observed by the satellite (metres)
  • +
  • mat: mean modelled astronomical tide height (metres)
  • +
  • lot: minimum tide height observed by the satellite (metres)
  • +
  • lat: minimum tide height from modelled astronomical tidal range (metres)
  • +
  • hot: maximum tide height observed by the satellite (metres)
  • +
  • hat: maximum tide height from modelled astronomical tidal range (metres)
  • +
  • otr: tidal range observed by the satellite (metres)
  • +
  • tr: modelled astronomical tide range (metres)
  • +
  • spread: proportion of the full modelled tidal range observed by the satellite
  • +
  • offset_low: proportion of the lowest tides never observed by the satellite
  • +
  • offset_high: proportion of the highest tides never observed by the satellite
  • +
+

If linear_reg = True, the output will also contain:

+
    +
  • observed_slope: slope of any relationship between observed tide heights and time
  • +
  • observed_pval: significance/p-value of any relationship between observed tide heights and time
  • +
+
+
+ +
+ Source code in eo_tides/stats.py +
def tide_stats(
+    data: xr.Dataset | xr.DataArray | GeoBox,
+    time: DatetimeLike | None = None,
+    model: str = "EOT20",
+    directory: str | os.PathLike | None = None,
+    tidepost_lat: float | None = None,
+    tidepost_lon: float | None = None,
+    plain_english: bool = True,
+    plot: bool = True,
+    plot_col: str | None = None,
+    modelled_freq: str = "3h",
+    linear_reg: bool = False,
+    min_max_q: tuple = (0.0, 1.0),
+    round_stats: int = 3,
+    **model_tides_kwargs,
+) -> pd.Series:
+    """
+    Takes a multi-dimensional dataset and generate tide statistics
+    and satellite-observed tide bias metrics, calculated based on
+    every timestep in the satellte data and the geographic centroid
+    of the imagery.
+
+    By comparing the subset of tides observed by satellites
+    against the full astronomical tidal range, we can evaluate
+    whether the tides observed by satellites are biased
+    (e.g. fail to observe either the highest or lowest tides).
+
+    For more information about the tidal statistics computed by this
+    function, refer to Figure 8 in Bishop-Taylor et al. 2018:
+    <https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8>
+
+    Parameters
+    ----------
+    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox
+        A multi-dimensional dataset or GeoBox pixel grid that will
+        be used to calculate tide statistics. If `data` is an
+        xarray object, it should include a "time" dimension.
+        If no "time" dimension exists or if `data` is a GeoBox,
+        then times must be passed using the `time` parameter.
+    time : DatetimeLike, optional
+        By default, tides will be modelled using times from the
+        "time" dimension of `data`. Alternatively, this param can
+        be used to provide a custom set of times. Accepts any format
+        that can be converted by `pandas.to_datetime()`. For example:
+        `time=pd.date_range(start="2000", end="2001", freq="5h")`
+    model : str, optional
+        The tide model to use to model tides. Defaults to "EOT20";
+        for a full list of available/supported models, run
+        `eo_tides.model.list_models`.
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    tidepost_lat, tidepost_lon : float or int, optional
+        Optional coordinates used to model tides. The default is None,
+        which uses the centroid of the dataset as the tide modelling
+        location.
+    plain_english : bool, optional
+        An optional boolean indicating whether to print a plain english
+        version of the tidal statistics to the screen. Defaults to True.
+    plot : bool, optional
+        An optional boolean indicating whether to plot how satellite-
+        observed tide heights compare against the full tidal range.
+        Defaults to True.
+    plot_col : str, optional
+        Optional name of a coordinate, dimension or variable in the array
+        that will be used to plot observations with unique symbols.
+        Defaults to None, which will plot all observations as circles.
+    modelled_freq : str, optional
+        An optional string giving the frequency at which to model tides
+        when computing the full modelled tidal range. Defaults to '3h',
+        which computes a tide height for every three hours across the
+        temporal extent of `data`.
+    linear_reg: bool, optional
+        Whether to return linear regression statistics that assess
+        whether satellite-observed tides show any decreasing  or
+        increasing trends over time. This may indicate whether your
+        satellite data may produce misleading trends based on uneven
+        sampling of the local tide regime.
+    min_max_q : tuple, optional
+        Quantiles used to calculate max and min observed and modelled
+        astronomical tides. By default `(0.0, 1.0)` which is equivalent
+        to minimum and maximum; to use a softer threshold that is more
+        robust to outliers, use e.g. `(0.1, 0.9)`.
+    round_stats : int, optional
+        The number of decimal places used to round the output statistics.
+        Defaults to 3.
+    **model_tides_kwargs :
+        Optional parameters passed to the `eo_tides.model.model_tides`
+        function. Important parameters include `cutoff` (used to
+        extrapolate modelled tides away from the coast; defaults to
+        `np.inf`), `crop` (whether to crop tide model constituent files
+        on-the-fly to improve performance) etc.
+
+    Returns
+    -------
+    stats_df : pandas.Series
+        A `pandas.Series` containing the following statistics:
+
+        - `y`: latitude used for modelling tide heights
+        - `x`: longitude used for modelling tide heights
+        - `mot`: mean tide height observed by the satellite (metres)
+        - `mat`: mean modelled astronomical tide height (metres)
+        - `lot`: minimum tide height observed by the satellite (metres)
+        - `lat`: minimum tide height from modelled astronomical tidal range (metres)
+        - `hot`: maximum tide height observed by the satellite (metres)
+        - `hat`: maximum tide height from modelled astronomical tidal range (metres)
+        - `otr`: tidal range observed by the satellite (metres)
+        - `tr`: modelled astronomical tide range (metres)
+        - `spread`: proportion of the full modelled tidal range observed by the satellite
+        - `offset_low`: proportion of the lowest tides never observed by the satellite
+        - `offset_high`: proportion of the highest tides never observed by the satellite
+
+        If `linear_reg = True`, the output will also contain:
+
+        - `observed_slope`: slope of any relationship between observed tide heights and time
+        - `observed_pval`: significance/p-value of any relationship between observed tide heights and time
+    """
+    # Standardise data inputs, time and models
+    gbox, time_coords = _standardise_inputs(data, time)
+
+    # Verify that only one tide model is provided
+    if isinstance(model, list):
+        raise Exception("Only single tide models are supported by `tide_stats`.")
+
+    # If custom tide modelling locations are not provided, use the
+    # dataset centroid
+    if not tidepost_lat or not tidepost_lon:
+        tidepost_lon, tidepost_lat = gbox.geographic_extent.centroid.coords[0]
+
+    # Model tides for each observation in the supplied xarray object
+    assert time_coords is not None
+    obs_tides_da = tag_tides(
+        gbox,
+        time=time_coords,
+        model=model,
+        directory=directory,
+        tidepost_lat=tidepost_lat,  # type: ignore
+        tidepost_lon=tidepost_lon,  # type: ignore
+        return_tideposts=True,
+        **model_tides_kwargs,
+    )
+    if isinstance(data, (xr.Dataset, xr.DataArray)):
+        obs_tides_da = obs_tides_da.reindex_like(data)
+
+    # Generate range of times covering entire period of satellite record
+    all_timerange = pd.date_range(
+        start=time_coords.min().item(),
+        end=time_coords.max().item(),
+        freq=modelled_freq,
+    )
+
+    # Model tides for each timestep
+    all_tides_df = model_tides(
+        x=tidepost_lon,  # type: ignore
+        y=tidepost_lat,  # type: ignore
+        time=all_timerange,
+        model=model,
+        directory=directory,
+        crs="EPSG:4326",
+        **model_tides_kwargs,
+    )
+
+    # Get coarse statistics on all and observed tidal ranges
+    obs_mean = obs_tides_da.mean().item()
+    all_mean = all_tides_df.tide_height.mean()
+    obs_min, obs_max = obs_tides_da.quantile(min_max_q).values
+    all_min, all_max = all_tides_df.tide_height.quantile(min_max_q).values
+
+    # Calculate tidal range
+    obs_range = obs_max - obs_min
+    all_range = all_max - all_min
+
+    # Calculate Bishop-Taylor et al. 2018 tidal metrics
+    spread = obs_range / all_range
+    low_tide_offset_m = abs(all_min - obs_min)
+    high_tide_offset_m = abs(all_max - obs_max)
+    low_tide_offset = low_tide_offset_m / all_range
+    high_tide_offset = high_tide_offset_m / all_range
+
+    # Plain text descriptors
+    mean_diff = "higher" if obs_mean > all_mean else "lower"
+    mean_diff_icon = "⬆️" if obs_mean > all_mean else "⬇️"
+    spread_icon = "🟢" if spread >= 0.9 else "🟡" if 0.7 < spread <= 0.9 else "🔴"
+    low_tide_icon = "🟢" if low_tide_offset <= 0.1 else "🟡" if 0.1 <= low_tide_offset < 0.2 else "🔴"
+    high_tide_icon = "🟢" if high_tide_offset <= 0.1 else "🟡" if 0.1 <= high_tide_offset < 0.2 else "🔴"
+
+    # Extract x (time in decimal years) and y (distance) values
+    obs_x = (
+        obs_tides_da.time.dt.year + ((obs_tides_da.time.dt.dayofyear - 1) / 365) + ((obs_tides_da.time.dt.hour) / 24)
+    )
+    obs_y = obs_tides_da.values.astype(np.float32)
+
+    # Compute linear regression
+    obs_linreg = stats.linregress(x=obs_x, y=obs_y)
+
+    if plain_english:
+        print(f"\n\n🌊 Modelled astronomical tide range: {all_range:.2f} metres.")
+        print(f"🛰️ Observed tide range: {obs_range:.2f} metres.\n")
+        print(f"{spread_icon} {spread:.0%} of the modelled astronomical tide range was observed at this location.")
+        print(
+            f"{high_tide_icon} The highest {high_tide_offset:.0%} ({high_tide_offset_m:.2f} metres) of the tide range was never observed."
+        )
+        print(
+            f"{low_tide_icon} The lowest {low_tide_offset:.0%} ({low_tide_offset_m:.2f} metres) of the tide range was never observed.\n"
+        )
+        print(f"🌊 Mean modelled astronomical tide height: {all_mean:.2f} metres.")
+        print(f"🛰️ Mean observed tide height: {obs_mean:.2f} metres.\n")
+        print(
+            f"{mean_diff_icon} The mean observed tide height was {obs_mean - all_mean:.2f} metres {mean_diff} than the mean modelled astronomical tide height."
+        )
+
+        if linear_reg:
+            if obs_linreg.pvalue > 0.01:
+                print("➖ Observed tides showed no significant trends over time.")
+            else:
+                obs_slope_desc = "decreasing" if obs_linreg.slope < 0 else "increasing"
+                print(
+                    f"⚠️ Observed tides showed a significant {obs_slope_desc} trend over time (p={obs_linreg.pvalue:.3f}, {obs_linreg.slope:.2f} metres per year)"
+                )
+
+    if plot:
+        _plot_biases(
+            all_tides_df=all_tides_df,
+            obs_tides_da=obs_tides_da,
+            lat=all_min,
+            lot=obs_min,
+            hat=all_max,
+            hot=obs_max,
+            offset_low=low_tide_offset,
+            offset_high=high_tide_offset,
+            spread=spread,
+            plot_col=data[plot_col] if plot_col else None,
+            obs_linreg=obs_linreg if linear_reg else None,
+            obs_x=obs_x,
+            all_timerange=all_timerange,
+        )
+
+    # Export pandas.Series containing tidal stats
+    output_stats = {
+        "y": tidepost_lat,
+        "x": tidepost_lon,
+        "mot": obs_mean,
+        "mat": all_mean,
+        "lot": obs_min,
+        "lat": all_min,
+        "hot": obs_max,
+        "hat": all_max,
+        "otr": obs_range,
+        "tr": all_range,
+        "spread": spread,
+        "offset_low": low_tide_offset,
+        "offset_high": high_tide_offset,
+    }
+
+    if linear_reg:
+        output_stats.update({
+            "observed_slope": obs_linreg.slope,
+            "observed_pval": obs_linreg.pvalue,
+        })
+
+    # Return pandas data
+    stats_df = pd.Series(output_stats).round(round_stats)
+    return stats_df
+
+
+
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ eo_tides.validation + + +

+ +
+ + + + + + + + + +

Functions:

+ + + + + + + + + + + + + + + + + +
NameDescription
eval_metrics +
+

Calculate a set of common statistical metrics

+
+
load_gauge_gesla +
+

Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.

+
+
+ + + + + +
+ + + + + + + + + +
+ + +

+ eval_metrics + + +

+
eval_metrics(x, y, round=3, all_regress=False)
+
+ +
+ +

Calculate a set of common statistical metrics +based on two input actual and predicted vectors.

+

These include:

+
    +
  • Pearson correlation
  • +
  • Root Mean Squared Error
  • +
  • Mean Absolute Error
  • +
  • R-squared
  • +
  • Bias
  • +
  • Linear regression parameters (slope, p-value, intercept, standard error)
  • +
+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

x +

+ array + +
+

An array providing "actual" variable values.

+
+
+ required +
+

y +

+ array + +
+

An array providing "predicted" variable values.

+
+
+ required +
+

round +

+ int + +
+

Number of decimal places to round each metric +to. Defaults to 3.

+
+
+ 3 +
+

all_regress +

+ bool + +
+

Whether to return linear regression p-value, +intercept and standard error (in addition to +only regression slope). Defaults to False.

+
+
+ False +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Series + +
+

A pd.Series containing all calculated metrics.

+
+
+ +
+ Source code in eo_tides/validation.py +
16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
def eval_metrics(x, y, round=3, all_regress=False):
+    """
+    Calculate a set of common statistical metrics
+    based on two input actual and predicted vectors.
+
+    These include:
+
+    * Pearson correlation
+    * Root Mean Squared Error
+    * Mean Absolute Error
+    * R-squared
+    * Bias
+    * Linear regression parameters (slope, p-value, intercept, standard error)
+
+    Parameters
+    ----------
+    x : numpy.array
+        An array providing "actual" variable values.
+    y : numpy.array
+        An array providing "predicted" variable values.
+    round : int
+        Number of decimal places to round each metric
+        to. Defaults to 3.
+    all_regress : bool
+        Whether to return linear regression p-value,
+        intercept and standard error (in addition to
+        only regression slope). Defaults to False.
+
+    Returns
+    -------
+    pandas.Series
+        A `pd.Series` containing all calculated metrics.
+    """
+
+    # Create dataframe to drop na
+    xy_df = pd.DataFrame({"x": x, "y": y}).dropna()
+
+    # Compute linear regression
+    lin_reg = stats.linregress(x=xy_df.x, y=xy_df.y)
+
+    # Calculate statistics
+    stats_dict = {
+        "Correlation": xy_df.corr().iloc[0, 1],
+        "RMSE": sqrt(mean_squared_error(xy_df.x, xy_df.y)),
+        "MAE": mean_absolute_error(xy_df.x, xy_df.y),
+        "R-squared": lin_reg.rvalue**2,
+        "Bias": (xy_df.y - xy_df.x).mean(),
+        "Regression slope": lin_reg.slope,
+    }
+
+    # Additional regression params
+    if all_regress:
+        stats_dict.update({
+            "Regression p-value": lin_reg.pvalue,
+            "Regression intercept": lin_reg.intercept,
+            "Regression standard error": lin_reg.stderr,
+        })
+
+    # Return as
+    return pd.Series(stats_dict).round(round)
+
+
+
+ +
+ +
+ + +

+ load_gauge_gesla + + +

+
load_gauge_gesla(
+    x=None,
+    y=None,
+    site_code=None,
+    time=("2018", "2020"),
+    max_distance=None,
+    correct_mean=False,
+    filter_use_flag=True,
+    site_metadata=True,
+    data_path="/gdata1/data/sea_level/gesla/",
+    metadata_path="/gdata1/data/sea_level/GESLA3_ALL 2.csv",
+)
+
+ +
+ +

Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.

+

Load and process all available GESLA measured sea-level data +with an x, y, time spatio-temporal query, or from a list of +specific tide gauges. Can optionally filter by gauge quality +and append detailed gauge metadata.

+

Modified from original code in https://github.com/philiprt/GeslaDataset.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

x +

+ numeric or list / tuple + +
+

Coordinates (in degrees longitude, latitude) used to load GESLA +tide gauge observations. If provided as singular values +(e.g. x=150, y=-32), then the nearest tide gauge will be returned. +If provided as a list or tuple (e.g. x=(150, 152), y=(-32, -30)), +then all gauges within the provided bounding box will be loaded. +Leave as None to return all available gauges, or if providing a +list of site codes using site_code.

+
+
+ None +
+

y +

+ numeric or list / tuple + +
+

Coordinates (in degrees longitude, latitude) used to load GESLA +tide gauge observations. If provided as singular values +(e.g. x=150, y=-32), then the nearest tide gauge will be returned. +If provided as a list or tuple (e.g. x=(150, 152), y=(-32, -30)), +then all gauges within the provided bounding box will be loaded. +Leave as None to return all available gauges, or if providing a +list of site codes using site_code.

+
+
+ None +
+

site_code +

+ str or list of str + +
+

GESLA site code(s) for which to load data (e.g. site_code="62650"). +If site_code is provided, x and y will be ignored.

+
+
+ None +
+

time +

+ tuple or list of str + +
+

Time range to consider, given as a tuple of start and end dates, +e.g. time=("2020", "2021"). The default of None will return all +tide observations from the year 1800 onward.

+
+
+ ('2018', '2020') +
+

max_distance +

+ numeric + +
+

Optional max distance within which to return the nearest tide gauge +when x and y are provided as singular coordinates. Defaults to +None, which will always return a tide gauge no matter how far away +it is located from x and y.

+
+
+ None +
+

correct_mean +

+ bool + +
+

Whether to correct sea level measurements to a standardised mean +sea level by subtracting the mean of all observed sea level +observations. This can be useful when GESLA tide heights come +from different or unknown tide datums. Note: the observed mean +sea level calculated here may differ from true long-term/ +astronomical Mean Sea Level (MSL) datum.

+
+
+ False +
+

filter_use_flag +

+ bool + +
+

Whether to filter out low quality observations with a "use_flag" +value of 0 (do not use). Defaults to True.

+
+
+ True +
+

site_metadata +

+ bool + +
+

Whether to add tide gauge station metadata as additional columns +in the output DataFrame. Defaults to True.

+
+
+ True +
+

data_path +

+ str + +
+

Path to the raw GESLA data files. Default is +/gdata1/data/sea_level/gesla/.

+
+
+ '/gdata1/data/sea_level/gesla/' +
+

metadata_path +

+ str + +
+

Path to the GESLA station metadata file. +Default is /gdata1/data/sea_level/GESLA3_ALL 2.csv.

+
+
+ '/gdata1/data/sea_level/GESLA3_ALL 2.csv' +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ DataFrame + +
+

Processed GESLA data as a DataFrame with columns including:

+
    +
  • "time": Timestamps,
  • +
  • "sea_level": Observed sea level (m),
  • +
  • "qc_flag": Observed sea level QC flag,
  • +
  • "use_flag": Use-in-analysis flag (1 = use, 0 = do not use),
  • +
+

...and additional columns from station metadata.

+
+
+ +
+ Source code in eo_tides/validation.py +
def load_gauge_gesla(
+    x=None,
+    y=None,
+    site_code=None,
+    time=("2018", "2020"),
+    max_distance=None,
+    correct_mean=False,
+    filter_use_flag=True,
+    site_metadata=True,
+    data_path="/gdata1/data/sea_level/gesla/",
+    metadata_path="/gdata1/data/sea_level/GESLA3_ALL 2.csv",
+):
+    """
+    Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.
+
+    Load and process all available GESLA measured sea-level data
+    with an `x, y, time` spatio-temporal query, or from a list of
+    specific tide gauges. Can optionally filter by gauge quality
+    and append detailed gauge metadata.
+
+    Modified from original code in <https://github.com/philiprt/GeslaDataset>.
+
+    Parameters
+    ----------
+    x, y : numeric or list/tuple, optional
+        Coordinates (in degrees longitude, latitude) used to load GESLA
+        tide gauge observations. If provided as singular values
+        (e.g. `x=150, y=-32`), then the nearest tide gauge will be returned.
+        If provided as a list or tuple (e.g. `x=(150, 152), y=(-32, -30)`),
+        then all gauges within the provided bounding box will be loaded.
+        Leave as `None` to return all available gauges, or if providing a
+        list of site codes using `site_code`.
+    site_code : str or list of str, optional
+        GESLA site code(s) for which to load data (e.g. `site_code="62650"`).
+        If `site_code` is provided, `x` and `y` will be ignored.
+    time : tuple or list of str, optional
+        Time range to consider, given as a tuple of start and end dates,
+        e.g. `time=("2020", "2021")`. The default of None will return all
+        tide observations from the year 1800 onward.
+    max_distance : numeric, optional
+        Optional max distance within which to return the nearest tide gauge
+        when `x` and `y` are provided as singular coordinates. Defaults to
+        None, which will always return a tide gauge no matter how far away
+        it is located from `x` and `y`.
+    correct_mean : bool, optional
+        Whether to correct sea level measurements to a standardised mean
+        sea level by subtracting the mean of all observed sea level
+        observations. This can be useful when GESLA tide heights come
+        from different or unknown tide datums. Note: the observed mean
+        sea level calculated here may differ from true long-term/
+        astronomical Mean Sea Level (MSL) datum.
+    filter_use_flag : bool, optional
+        Whether to filter out low quality observations with a "use_flag"
+        value of 0 (do not use). Defaults to True.
+    site_metadata : bool, optional
+        Whether to add tide gauge station metadata as additional columns
+        in the output DataFrame. Defaults to True.
+    data_path : str, optional
+        Path to the raw GESLA data files. Default is
+        `/gdata1/data/sea_level/gesla/`.
+    metadata_path : str, optional
+        Path to the GESLA station metadata file.
+        Default is `/gdata1/data/sea_level/GESLA3_ALL 2.csv`.
+
+    Returns
+    -------
+    pd.DataFrame
+        Processed GESLA data as a DataFrame with columns including:
+
+        - "time": Timestamps,
+        - "sea_level": Observed sea level (m),
+        - "qc_flag": Observed sea level QC flag,
+        - "use_flag": Use-in-analysis flag (1 = use, 0 = do not use),
+
+        ...and additional columns from station metadata.
+    """
+    # Load tide gauge metadata
+    metadata_df, metadata_gdf = _load_gauge_metadata(metadata_path)
+
+    # Use supplied site codes if available
+    if site_code is not None:
+        site_code = [site_code] if not isinstance(site_code, list) else site_code
+
+    # If x and y are tuples, use xy bounds to identify sites
+    elif isinstance(x, (tuple, list)) & isinstance(y, (tuple, list)):
+        bbox = BoundingBox.from_xy(x, y)
+        site_code = metadata_gdf.cx[bbox.left : bbox.right, bbox.top : bbox.bottom].index
+
+    # If x and y are single numbers, select nearest row
+    elif isinstance(x, Number) & isinstance(y, Number):
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            site_code = (
+                _nearest_row(metadata_gdf, x, y, max_distance).rename({"index_right": "site_code"}, axis=1).site_code
+            )
+            # site_code = _nearest_row(metadata_gdf, x, y, max_distance).site_code
+
+        # Raise exception if no valid tide gauges are found
+        if site_code.isnull().all():
+            raise Exception(f"No tide gauge found within {max_distance} degrees of {x}, {y}.")
+
+    # Otherwise if all are None, return all available site codes
+    elif (site_code is None) & (x is None) & (y is None):
+        site_code = metadata_df.index.to_list()
+
+    else:
+        raise TypeError(
+            "`x` and `y` must be provided as either singular coordinates (e.g. `x=150`), or as a tuple bounding box (e.g. `x=(150, 152)`)."
+        )
+
+    # Prepare times
+    if time is None:
+        time = ["1800", str(datetime.datetime.now().year)]
+    time = [time] if not isinstance(time, (list, tuple)) else time
+    start_time = _round_date_strings(time[0], round_type="start")
+    end_time = _round_date_strings(time[-1], round_type="end")
+
+    # Identify paths to load and nodata values for each site
+    metadata_df["file_name"] = data_path + metadata_df["file_name"]
+    paths_na = metadata_df.loc[site_code, ["file_name", "null_value"]]
+
+    # Load and combine into a single dataframe
+    gauge_list = [
+        _load_gesla_dataset(s, p, na_value=na)
+        for s, p, na in tqdm.tqdm(paths_na.itertuples(), total=len(paths_na), desc="Loading GESLA gauges")
+    ]
+    data_df = pd.concat(gauge_list).sort_index().loc[slice(start_time, end_time)].reset_index().set_index("site_code")
+
+    # Optionally filter by use flag column
+    if filter_use_flag:
+        data_df = data_df.loc[data_df.use_flag == 1]
+
+    # Optionally insert metadata into dataframe
+    if site_metadata:
+        data_df[metadata_df.columns] = metadata_df.loc[site_code]
+
+    # Add time to index and remove duplicates
+    data_df = data_df.set_index("time", append=True)
+    duplicates = data_df.index.duplicated()
+    if duplicates.sum() > 0:
+        warnings.warn("Duplicate timestamps were removed.")
+        data_df = data_df.loc[~duplicates]
+
+    # Remove observed mean sea level if requested
+    if correct_mean:
+        data_df["sea_level"] = data_df["sea_level"].sub(data_df.groupby("site_code")["sea_level"].transform("mean"))
+
+    # Return data
+    return data_df
+
+
+
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ eo_tides.utils + + +

+ +
+ + + + + + + + + +

Functions:

+ + + + + + + + + + + + + + + + + + + + + +
NameDescription
clip_models +
+

Clip NetCDF-format ocean tide models to a bounding box.

+
+
idw +
+

Perform Inverse Distance Weighting (IDW) interpolation.

+
+
list_models +
+

List all tide models available for tide modelling.

+
+
+ + + + + +
+ + + + + + + + + +
+ + +

+ clip_models + + +

+
clip_models(
+    input_directory,
+    output_directory,
+    bbox,
+    model=None,
+    buffer=5,
+    overwrite=False,
+)
+
+ +
+ +

Clip NetCDF-format ocean tide models to a bounding box.

+

This function identifies all NetCDF-format tide models in a +given input directory, including "ATLAS-netcdf" (e.g. TPXO9-atlas-nc), +"FES-netcdf" (e.g. FES2022, EOT20), and "GOT-netcdf" (e.g. GOT5.5) +format files. Files for each model are then clipped to the extent of +the provided bounding box, handling model-specific file structures. +After each model is clipped, the result is exported to the output +directory and verified with pyTMD to ensure the clipped data is +suitable for tide modelling.

+

For instructions on accessing and downloading tide models, see: +https://geoscienceaustralia.github.io/eo-tides/setup/

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

input_directory +

+ str or PathLike + +
+

Path to directory containing input NetCDF-format tide model files.

+
+
+ required +
+

output_directory +

+ str or PathLike + +
+

Path to directory where clipped NetCDF files will be exported.

+
+
+ required +
+

bbox +

+ tuple of float + +
+

Bounding box for clipping the tide models in EPSG:4326 degrees +coordinates, specified as (left, bottom, right, top).

+
+
+ required +
+

model +

+ str or list of str + +
+

The tide model (or models) to clip. Defaults to None, which +will automatically identify and clip all NetCDF-format models +in the input directly.

+
+
+ None +
+

buffer +

+ float + +
+

Buffer distance (in degrees) added to the bounding box to provide +sufficient data on edges of study area. Defaults to 5 degrees.

+
+
+ 5 +
+

overwrite +

+ bool + +
+

If True, overwrite existing files in the output directory. +Defaults to False.

+
+
+ False +
+ + +

Examples:

+
>>> clip_models(
+...     input_directory="tide_models/",
+...     output_directory="tide_models_clipped/",
+...     bbox=(-8.968392, 50.070574, 2.447160, 59.367122),
+... )
+
+ +
+ Source code in eo_tides/utils.py +
def clip_models(
+    input_directory: str | os.PathLike,
+    output_directory: str | os.PathLike,
+    bbox: tuple[float, float, float, float],
+    model: list | None = None,
+    buffer: float = 5,
+    overwrite: bool = False,
+):
+    """
+    Clip NetCDF-format ocean tide models to a bounding box.
+
+    This function identifies all NetCDF-format tide models in a
+    given input directory, including "ATLAS-netcdf" (e.g. TPXO9-atlas-nc),
+    "FES-netcdf" (e.g. FES2022, EOT20), and "GOT-netcdf" (e.g. GOT5.5)
+    format files. Files for each model are then clipped to the extent of
+    the provided bounding box, handling model-specific file structures.
+    After each model is clipped, the result is exported to the output
+    directory and verified with `pyTMD` to ensure the clipped data is
+    suitable for tide modelling.
+
+    For instructions on accessing and downloading tide models, see:
+    <https://geoscienceaustralia.github.io/eo-tides/setup/>
+
+    Parameters
+    ----------
+    input_directory : str or os.PathLike
+        Path to directory containing input NetCDF-format tide model files.
+    output_directory : str or os.PathLike
+        Path to directory where clipped NetCDF files will be exported.
+    bbox : tuple of float
+        Bounding box for clipping the tide models in EPSG:4326 degrees
+        coordinates, specified as `(left, bottom, right, top)`.
+    model : str or list of str, optional
+        The tide model (or models) to clip. Defaults to None, which
+        will automatically identify and clip all NetCDF-format models
+        in the input directly.
+    buffer : float, optional
+        Buffer distance (in degrees) added to the bounding box to provide
+        sufficient data on edges of study area. Defaults to 5 degrees.
+    overwrite : bool, optional
+        If True, overwrite existing files in the output directory.
+        Defaults to False.
+
+    Examples
+    --------
+    >>> clip_models(
+    ...     input_directory="tide_models/",
+    ...     output_directory="tide_models_clipped/",
+    ...     bbox=(-8.968392, 50.070574, 2.447160, 59.367122),
+    ... )
+    """
+
+    # Get input and output paths
+    input_directory = _set_directory(input_directory)
+    output_directory = pathlib.Path(output_directory)
+
+    # Prepare bounding box
+    bbox = odc.geo.geom.BoundingBox(*bbox, crs="EPSG:4326").buffered(buffer)
+
+    # Identify NetCDF models
+    model_database = load_database()["elevation"]
+    netcdf_formats = ["ATLAS-netcdf", "FES-netcdf", "GOT-netcdf"]
+    netcdf_models = {k for k, v in model_database.items() if v["format"] in netcdf_formats}
+
+    # Identify subset of available and requested NetCDF models
+    available_models, _ = list_models(directory=input_directory, show_available=False, show_supported=False)
+    requested_models = list(np.atleast_1d(model)) if model is not None else available_models
+    available_netcdf_models = list(set(available_models) & set(requested_models) & set(netcdf_models))
+
+    # Raise error if no valid models found
+    if len(available_netcdf_models) == 0:
+        raise ValueError(f"No valid NetCDF models found in {input_directory}.")
+
+    # If model list is provided,
+    print(f"Preparing to clip suitable NetCDF models: {available_netcdf_models}\n")
+
+    # Loop through suitable models and export
+    for m in available_netcdf_models:
+        # Get model file and grid file list if they exist
+        model_files = model_database[m].get("model_file", [])
+        grid_file = model_database[m].get("grid_file", [])
+
+        # Convert to list if strings and combine
+        model_files = model_files if isinstance(model_files, list) else [model_files]
+        grid_file = grid_file if isinstance(grid_file, list) else [grid_file]
+        all_files = model_files + grid_file
+
+        # Loop through each model file and clip
+        for file in tqdm(all_files, desc=f"Clipping {m}"):
+            # Skip if it exists in output directory
+            if (output_directory / file).exists() and not overwrite:
+                continue
+
+            # Load model file
+            nc = xr.open_mfdataset(input_directory / file)
+
+            # Open file and clip according to model
+            if m in (
+                "GOT5.5",
+                "GOT5.5_load",
+                "GOT5.5_extrapolated",
+                "GOT5.5D",
+                "GOT5.5D_extrapolated",
+                "GOT5.6",
+                "GOT5.6_extrapolated",
+            ):
+                nc_clipped = _clip_model_file(
+                    nc,
+                    bbox,
+                    xdim="lon",
+                    ydim="lat",
+                    ycoord="latitude",
+                    xcoord="longitude",
+                )
+
+            elif m in ("HAMTIDE11",):
+                nc_clipped = _clip_model_file(nc, bbox, xdim="LON", ydim="LAT", ycoord="LAT", xcoord="LON")
+
+            elif m in (
+                "EOT20",
+                "EOT20_load",
+                "FES2012",
+                "FES2014",
+                "FES2014_extrapolated",
+                "FES2014_load",
+                "FES2022",
+                "FES2022_extrapolated",
+                "FES2022_load",
+            ):
+                nc_clipped = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="lat", xcoord="lon")
+
+            elif m in (
+                "TPXO8-atlas-nc",
+                "TPXO9-atlas-nc",
+                "TPXO9-atlas-v2-nc",
+                "TPXO9-atlas-v3-nc",
+                "TPXO9-atlas-v4-nc",
+                "TPXO9-atlas-v5-nc",
+                "TPXO10-atlas-v2-nc",
+            ):
+                nc_clipped = _clip_model_file(
+                    nc,
+                    bbox,
+                    xdim="nx",
+                    ydim="ny",
+                    ycoord="lat_z",
+                    xcoord="lon_z",
+                )
+
+            else:
+                raise Exception(f"Model {m} not supported")
+
+            # Create directory and export
+            (output_directory / file).parent.mkdir(parents=True, exist_ok=True)
+            nc_clipped.to_netcdf(output_directory / file, mode="w")
+
+        # Verify that models are ready
+        pytmd_model(directory=output_directory).elevation(m=m).verify
+        print(" ✅ Clipped model exported and verified")
+
+    print(f"\nOutputs exported to {output_directory}")
+    list_models(directory=output_directory, show_available=True, show_supported=False)
+
+
+
+ +
+ +
+ + +

+ idw + + +

+
idw(
+    input_z,
+    input_x,
+    input_y,
+    output_x,
+    output_y,
+    p=1,
+    k=10,
+    max_dist=None,
+    k_min=1,
+    epsilon=1e-12,
+)
+
+ +
+ +

Perform Inverse Distance Weighting (IDW) interpolation.

+

This function performs fast IDW interpolation by creating a KDTree +from the input coordinates then uses it to find the k nearest +neighbors for each output point. Weights are calculated based on the +inverse distance to each neighbor, with weights descreasing with +increasing distance.

+

Code inspired by: https://github.com/DahnJ/REM-xarray

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

input_z +

+ array - like + +
+

Array of values at the input points. This can be either a +1-dimensional array, or a 2-dimensional array where each column +(axis=1) represents a different set of values to be interpolated.

+
+
+ required +
+

input_x +

+ array - like + +
+

Array of x-coordinates of the input points.

+
+
+ required +
+

input_y +

+ array - like + +
+

Array of y-coordinates of the input points.

+
+
+ required +
+

output_x +

+ array - like + +
+

Array of x-coordinates where the interpolation is to be computed.

+
+
+ required +
+

output_y +

+ array - like + +
+

Array of y-coordinates where the interpolation is to be computed.

+
+
+ required +
+

p +

+ int or float + +
+

Power function parameter defining how rapidly weightings should +decrease as distance increases. Higher values of p will cause +weights for distant points to decrease rapidly, resulting in +nearby points having more influence on predictions. Defaults to 1.

+
+
+ 1 +
+

k +

+ int + +
+

Number of nearest neighbors to use for interpolation. k=1 is +equivalent to "nearest" neighbour interpolation. Defaults to 10.

+
+
+ 10 +
+

max_dist +

+ int or float + +
+

Restrict neighbouring points to less than this distance. +By default, no distance limit is applied.

+
+
+ None +
+

k_min +

+ int + +
+

If max_dist is provided, some points may end up with less than +k nearest neighbours, potentially producing less reliable +interpolations. Set k_min to set any points with less than +k_min neighbours to NaN. Defaults to 1.

+
+
+ 1 +
+

epsilon +

+ float + +
+

Small value added to distances to prevent division by zero +errors in the case that output coordinates are identical to +input coordinates. Defaults to 1e-12.

+
+
+ 1e-12 +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
interp_values + ndarray + +
+

Interpolated values at the output coordinates. If input_z is +1-dimensional, interp_values will also be 1-dimensional. If +input_z is 2-dimensional, interp_values will have the same +number of rows as input_z, with each column (axis=1) +representing interpolated values for one set of input data.

+
+
+ + +

Examples:

+
>>> input_z = [1, 2, 3, 4, 5]
+>>> input_x = [0, 1, 2, 3, 4]
+>>> input_y = [0, 1, 2, 3, 4]
+>>> output_x = [0.5, 1.5, 2.5]
+>>> output_y = [0.5, 1.5, 2.5]
+>>> idw(input_z, input_x, input_y, output_x, output_y, k=2)
+array([1.5, 2.5, 3.5])
+
+ +
+ Source code in eo_tides/utils.py +
def idw(
+    input_z,
+    input_x,
+    input_y,
+    output_x,
+    output_y,
+    p=1,
+    k=10,
+    max_dist=None,
+    k_min=1,
+    epsilon=1e-12,
+):
+    """Perform Inverse Distance Weighting (IDW) interpolation.
+
+    This function performs fast IDW interpolation by creating a KDTree
+    from the input coordinates then uses it to find the `k` nearest
+    neighbors for each output point. Weights are calculated based on the
+    inverse distance to each neighbor, with weights descreasing with
+    increasing distance.
+
+    Code inspired by: <https://github.com/DahnJ/REM-xarray>
+
+    Parameters
+    ----------
+    input_z : array-like
+        Array of values at the input points. This can be either a
+        1-dimensional array, or a 2-dimensional array where each column
+        (axis=1) represents a different set of values to be interpolated.
+    input_x : array-like
+        Array of x-coordinates of the input points.
+    input_y : array-like
+        Array of y-coordinates of the input points.
+    output_x : array-like
+        Array of x-coordinates where the interpolation is to be computed.
+    output_y : array-like
+        Array of y-coordinates where the interpolation is to be computed.
+    p : int or float, optional
+        Power function parameter defining how rapidly weightings should
+        decrease as distance increases. Higher values of `p` will cause
+        weights for distant points to decrease rapidly, resulting in
+        nearby points having more influence on predictions. Defaults to 1.
+    k : int, optional
+        Number of nearest neighbors to use for interpolation. `k=1` is
+        equivalent to "nearest" neighbour interpolation. Defaults to 10.
+    max_dist : int or float, optional
+        Restrict neighbouring points to less than this distance.
+        By default, no distance limit is applied.
+    k_min : int, optional
+        If `max_dist` is provided, some points may end up with less than
+        `k` nearest neighbours, potentially producing less reliable
+        interpolations. Set `k_min` to set any points with less than
+        `k_min` neighbours to NaN. Defaults to 1.
+    epsilon : float, optional
+        Small value added to distances to prevent division by zero
+        errors in the case that output coordinates are identical to
+        input coordinates. Defaults to 1e-12.
+
+    Returns
+    -------
+    interp_values : numpy.ndarray
+        Interpolated values at the output coordinates. If `input_z` is
+        1-dimensional, `interp_values` will also be 1-dimensional. If
+        `input_z` is 2-dimensional, `interp_values` will have the same
+        number of rows as `input_z`, with each column (axis=1)
+        representing interpolated values for one set of input data.
+
+    Examples
+    --------
+    >>> input_z = [1, 2, 3, 4, 5]
+    >>> input_x = [0, 1, 2, 3, 4]
+    >>> input_y = [0, 1, 2, 3, 4]
+    >>> output_x = [0.5, 1.5, 2.5]
+    >>> output_y = [0.5, 1.5, 2.5]
+    >>> idw(input_z, input_x, input_y, output_x, output_y, k=2)
+    array([1.5, 2.5, 3.5])
+
+    """
+    # Convert to numpy arrays
+    input_x = np.atleast_1d(input_x)
+    input_y = np.atleast_1d(input_y)
+    input_z = np.atleast_1d(input_z)
+    output_x = np.atleast_1d(output_x)
+    output_y = np.atleast_1d(output_y)
+
+    # Verify input and outputs have matching lengths
+    if not (input_z.shape[0] == len(input_x) == len(input_y)):
+        raise ValueError("All of `input_z`, `input_x` and `input_y` must be the same length.")
+    if not (len(output_x) == len(output_y)):
+        raise ValueError("Both `output_x` and `output_y` must be the same length.")
+
+    # Verify k is smaller than total number of points, and non-zero
+    if k > input_z.shape[0]:
+        raise ValueError(
+            f"The requested number of nearest neighbours (`k={k}`) "
+            f"is smaller than the total number of points ({input_z.shape[0]}).",
+        )
+    if k == 0:
+        raise ValueError("Interpolation based on `k=0` nearest neighbours is not valid.")
+
+    # Create KDTree to efficiently find nearest neighbours
+    points_xy = np.column_stack((input_y, input_x))
+    tree = KDTree(points_xy)
+
+    # Determine nearest neighbours and distances to each
+    grid_stacked = np.column_stack((output_y, output_x))
+    distances, indices = tree.query(grid_stacked, k=k, workers=-1)
+
+    # If k == 1, add an additional axis for consistency
+    if k == 1:
+        distances = distances[..., np.newaxis]
+        indices = indices[..., np.newaxis]
+
+    # Add small epsilon to distances to prevent division by zero errors
+    # if output coordinates are the same as input coordinates
+    distances = np.maximum(distances, epsilon)
+
+    # Set distances above max to NaN if specified
+    if max_dist is not None:
+        distances[distances > max_dist] = np.nan
+
+    # Calculate weights based on distance to k nearest neighbours.
+    weights = 1 / np.power(distances, p)
+    weights = weights / np.nansum(weights, axis=1).reshape(-1, 1)
+
+    # 1D case: Compute weighted sum of input_z values for each output point
+    if input_z.ndim == 1:
+        interp_values = np.nansum(weights * input_z[indices], axis=1)
+
+    # 2D case: Compute weighted sum for each set of input_z values
+    # weights[..., np.newaxis] adds a dimension for broadcasting
+    else:
+        interp_values = np.nansum(
+            weights[..., np.newaxis] * input_z[indices],
+            axis=1,
+        )
+
+    # Set any points with less than `k_min` valid weights to NaN
+    interp_values[np.isfinite(weights).sum(axis=1) < k_min] = np.nan
+
+    return interp_values
+
+
+
+ +
+ +
+ + +

+ list_models + + +

+
list_models(
+    directory=None,
+    show_available=True,
+    show_supported=True,
+    raise_error=False,
+)
+
+ +
+ +

List all tide models available for tide modelling.

+

This function scans the specified tide model directory +and returns a list of models that are available in the +directory as well as the full list of all models supported +by eo-tides and pyTMD.

+

For instructions on setting up tide models, see: +https://geoscienceaustralia.github.io/eo-tides/setup/

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+

directory +

+ str + +
+

The directory containing tide model data files. If no path is +provided, this will default to the environment variable +EO_TIDES_TIDE_MODELS if set, or raise an error if not. +Tide modelling files should be stored in sub-folders for each +model that match the structure required by pyTMD +(https://geoscienceaustralia.github.io/eo-tides/setup/).

+
+
+ None +
+

show_available +

+ bool + +
+

Whether to print a list of locally available models.

+
+
+ True +
+

show_supported +

+ bool + +
+

Whether to print a list of all supported models, in +addition to models available locally.

+
+
+ True +
+

raise_error +

+ bool + +
+

If True, raise an error if no available models are found. +If False, raise a warning.

+
+
+ False +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
available_models + list of str + +
+

A list of all tide models available within directory.

+
+
supported_models + list of str + +
+

A list of all tide models supported by eo-tides.

+
+
+ +
+ Source code in eo_tides/utils.py +
def list_models(
+    directory: str | os.PathLike | None = None,
+    show_available: bool = True,
+    show_supported: bool = True,
+    raise_error: bool = False,
+) -> tuple[list[str], list[str]]:
+    """
+    List all tide models available for tide modelling.
+
+    This function scans the specified tide model directory
+    and returns a list of models that are available in the
+    directory as well as the full list of all models supported
+    by `eo-tides` and `pyTMD`.
+
+    For instructions on setting up tide models, see:
+    <https://geoscienceaustralia.github.io/eo-tides/setup/>
+
+    Parameters
+    ----------
+    directory : str, optional
+        The directory containing tide model data files. If no path is
+        provided, this will default to the environment variable
+        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
+        Tide modelling files should be stored in sub-folders for each
+        model that match the structure required by `pyTMD`
+        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
+    show_available : bool, optional
+        Whether to print a list of locally available models.
+    show_supported : bool, optional
+        Whether to print a list of all supported models, in
+        addition to models available locally.
+    raise_error : bool, optional
+        If True, raise an error if no available models are found.
+        If False, raise a warning.
+
+    Returns
+    -------
+    available_models : list of str
+        A list of all tide models available within `directory`.
+    supported_models : list of str
+        A list of all tide models supported by `eo-tides`.
+    """
+    init()  # Initialize colorama
+
+    # Set tide modelling files directory. If no custom path is
+    # provided, try global environment variable.
+    directory = _set_directory(directory)
+
+    # Get full list of supported models from pyTMD database
+    model_database = load_database()["elevation"]
+    supported_models = list(model_database.keys())
+
+    # Extract expected model paths
+    expected_paths = {}
+    for m in supported_models:
+        model_file = model_database[m]["model_file"]
+
+        # Handle GOT5.6 differently to ensure we test for presence of GOT5.6 constituents
+        if m in ("GOT5.6", "GOT5.6_extrapolated"):
+            model_file = [file for file in model_file if "GOT5.6" in file][0]
+        else:
+            model_file = model_file[0] if isinstance(model_file, list) else model_file
+
+        # Add path to dict
+        expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
+
+    # Define column widths
+    status_width = 4  # Width for emoji
+    name_width = max(len(name) for name in supported_models)
+    path_width = max(len(path) for path in expected_paths.values())
+
+    # Print list of supported models, marking available and
+    # unavailable models and appending available to list
+    if show_available or show_supported:
+        total_width = min(status_width + name_width + path_width + 6, 80)
+        print("─" * total_width)
+        print(f"{'󠀠🌊':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}")
+        print("─" * total_width)
+
+    available_models = []
+    for m in supported_models:
+        try:
+            model_file = pytmd_model(directory=directory).elevation(m=m)
+            available_models.append(m)
+
+            if show_available:
+                # Mark available models with a green tick
+                status = "✅"
+                print(f"{status:^{status_width}}{m:<{name_width}}{expected_paths[m]:<{path_width}}")
+        except FileNotFoundError:
+            if show_supported:
+                # Mark unavailable models with a red cross
+                status = "❌"
+                print(
+                    f"{status:^{status_width}}{Style.DIM}{m:<{name_width}}{expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
+                )
+
+    if show_available or show_supported:
+        print("─" * total_width)
+
+        # Print summary
+        print(f"\n{Style.BRIGHT}Summary:{Style.RESET_ALL}")
+        print(f"Available models: {len(available_models)}/{len(supported_models)}")
+
+    # Raise error or warning if no models are available
+    if not available_models:
+        warning_msg = textwrap.dedent(
+            f"""
+            No valid tide models are available in `{directory}`.
+            Are you sure you have provided the correct `directory` path, or set the
+            `EO_TIDES_TIDE_MODELS` environment variable to point to the location of your
+            tide model directory?
+            """
+        ).strip()
+
+        if raise_error:
+            raise Exception(warning_msg)
+        else:
+            warnings.warn(warning_msg, UserWarning)
+
+    # Return list of available and supported models
+    return available_models, supported_models
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 0000000..b500381 --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,143 @@ + +/* Avoid breaking parameter names, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* No line break before first paragraph of descriptions. */ +.doc-md-description, +.doc-md-description>p:first-child { + display: inline; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} + +.doc .md-typeset__table tr { + display: table-row; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} + +/* Parameter headings must be inline, not blocks. */ +.doc-heading-parameter { + display: inline; +} + +/* Prefer space on the right, not the left of parameter permalinks. */ +.doc-heading-parameter .headerlink { + margin-left: 0 !important; + margin-right: 0.2rem; +} + +/* Backward-compatibility: docstring section titles in bold. */ +.doc-section-title { + font-weight: bold; +} + +/* Symbols in Navigation and ToC. */ +:root, :host, +[data-md-color-scheme="default"] { + --doc-symbol-parameter-fg-color: #df50af; + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-parameter-bg-color: #df50af1a; + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-parameter-fg-color: #ffa8cc; + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-parameter-bg-color: #ffa8cc1a; + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-parameter { + color: var(--doc-symbol-parameter-fg-color); + background-color: var(--doc-symbol-parameter-bg-color); +} + +code.doc-symbol-parameter::after { + content: "param"; +} + +code.doc-symbol-attribute { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after { + content: "attr"; +} + +code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + +code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} + +.doc-signature .autorefs { + color: inherit; + border-bottom: 1px dotted currentcolor; +} diff --git a/assets/eo-tides-abstract.gif b/assets/eo-tides-abstract.gif new file mode 100644 index 0000000..a48df48 Binary files /dev/null and b/assets/eo-tides-abstract.gif differ diff --git a/assets/eo-tides-logo-128.png b/assets/eo-tides-logo-128.png new file mode 100644 index 0000000..b074b2b Binary files /dev/null and b/assets/eo-tides-logo-128.png differ diff --git a/assets/eo-tides-logo-256.png b/assets/eo-tides-logo-256.png new file mode 100644 index 0000000..ac367d5 Binary files /dev/null and b/assets/eo-tides-logo-256.png differ diff --git a/assets/eo-tides-logo.gif b/assets/eo-tides-logo.gif new file mode 100644 index 0000000..e4d947b Binary files /dev/null and b/assets/eo-tides-logo.gif differ diff --git a/assets/eo-tides-logo.png b/assets/eo-tides-logo.png new file mode 100644 index 0000000..1c5f8e9 Binary files /dev/null and b/assets/eo-tides-logo.png differ diff --git a/assets/eot20_download.jpg b/assets/eot20_download.jpg new file mode 100644 index 0000000..bedbccd Binary files /dev/null and b/assets/eot20_download.jpg differ diff --git a/assets/fes_ftp.jpg b/assets/fes_ftp.jpg new file mode 100644 index 0000000..0ab973b Binary files /dev/null and b/assets/fes_ftp.jpg differ diff --git a/assets/fes_myproducts.jpg b/assets/fes_myproducts.jpg new file mode 100644 index 0000000..40471f5 Binary files /dev/null and b/assets/fes_myproducts.jpg differ diff --git a/assets/fes_productselection.jpg b/assets/fes_productselection.jpg new file mode 100644 index 0000000..8974007 Binary files /dev/null and b/assets/fes_productselection.jpg differ diff --git a/assets/fes_subscriptions.jpg b/assets/fes_subscriptions.jpg new file mode 100644 index 0000000..0dbb6e7 Binary files /dev/null and b/assets/fes_subscriptions.jpg differ diff --git a/assets/got_download.jpg b/assets/got_download.jpg new file mode 100644 index 0000000..d4e470f Binary files /dev/null and b/assets/got_download.jpg differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.83f73b43.min.js b/assets/javascripts/bundle.83f73b43.min.js new file mode 100644 index 0000000..43d8b70 --- /dev/null +++ b/assets/javascripts/bundle.83f73b43.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Wi=Object.create;var gr=Object.defineProperty;var Di=Object.getOwnPropertyDescriptor;var Vi=Object.getOwnPropertyNames,Vt=Object.getOwnPropertySymbols,Ni=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,ao=Object.prototype.propertyIsEnumerable;var io=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&io(e,r,t[r]);if(Vt)for(var r of Vt(t))ao.call(t,r)&&io(e,r,t[r]);return e};var so=(e,t)=>{var r={};for(var o in e)yr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Vt)for(var o of Vt(e))t.indexOf(o)<0&&ao.call(e,o)&&(r[o]=e[o]);return r};var xr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var zi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Vi(t))!yr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=Di(t,n))||o.enumerable});return e};var Mt=(e,t,r)=>(r=e!=null?Wi(Ni(e)):{},zi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var co=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var lo=xr((Er,po)=>{(function(e,t){typeof Er=="object"&&typeof po!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function p(k){var ft=k.type,qe=k.tagName;return!!(qe==="INPUT"&&a[ft]&&!k.readOnly||qe==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function c(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(k){o=!1}function d(k){s(k.target)&&(o||p(k.target))&&c(k.target)}function y(k){s(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function L(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function te(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,te())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",L,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",y,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var qr=xr((hy,On)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var $a=/["'&<>]/;On.exports=Pa;function Pa(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof It=="object"&&typeof Yr=="object"?Yr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof It=="object"?It.ClipboardJS=r():t.ClipboardJS=r()})(It,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ui}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(A){return!1}}var d=function(A){var M=f()(A);return u("cut"),M},y=d;function L(V){var A=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[A?"right":"left"]="-9999px";var F=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(F,"px"),M.setAttribute("readonly",""),M.value=V,M}var X=function(A,M){var F=L(A);M.container.appendChild(F);var D=f()(F);return u("copy"),F.remove(),D},te=function(A){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},F="";return typeof A=="string"?F=X(A,M):A instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(A==null?void 0:A.type)?F=X(A.value,M):(F=f()(A),u("copy")),F},J=te;function k(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(M){return typeof M}:k=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},k(V)}var ft=function(){var A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=A.action,F=M===void 0?"copy":M,D=A.container,Y=A.target,$e=A.text;if(F!=="copy"&&F!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&k(Y)==="object"&&Y.nodeType===1){if(F==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(F==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if($e)return J($e,{container:D});if(Y)return F==="cut"?y(Y):J(Y,{container:D})},qe=ft;function Fe(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(M){return typeof M}:Fe=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Fe(V)}function ki(V,A){if(!(V instanceof A))throw new TypeError("Cannot call a class as a function")}function no(V,A){for(var M=0;M0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Fe(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function($e){return Y.onClick($e)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,$e=this.action(Y)||"copy",Dt=qe({action:$e,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Dt?"success":"error",{action:$e,text:Dt,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return y(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,$e=!!document.queryCommandSupported;return Y.forEach(function(Dt){$e=$e&&!!document.queryCommandSupported(Dt)}),$e}}]),M}(s()),Ui=Fi},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,d,y){var L=c.apply(this,arguments);return l.addEventListener(u,L,y),{destroy:function(){l.removeEventListener(u,L,y)}}}function p(l,f,u,d,y){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(L){return s(L,f,u,d,y)}))}function c(l,f,u,d){return function(y){y.delegateTarget=a(y.target,f),y.delegateTarget&&d.call(l,y)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,d,y){if(!u&&!d&&!y)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(y))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,d,y);if(a.nodeList(u))return l(u,d,y);if(a.string(u))return f(u,d,y);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,d,y){return u.addEventListener(d,y),{destroy:function(){u.removeEventListener(d,y)}}}function l(u,d,y){return Array.prototype.forEach.call(u,function(L){L.addEventListener(d,y)}),{destroy:function(){Array.prototype.forEach.call(u,function(L){L.removeEventListener(d,y)})}}}function f(u,d,y){return s(document.body,u,d,y)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||p(d,L)})},y&&(n[d]=y(n[d])))}function p(d,y){try{c(o[d](y))}catch(L){u(i[0][3],L)}}function c(d){d.value instanceof nt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){p("next",d)}function f(d){p("throw",d)}function u(d,y){d(y),i.shift(),i.length&&p(i[0][0],i[0][1])}}function uo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof he=="function"?he(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function H(e){return typeof e=="function"}function ut(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ut(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ue=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=he(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(L){t={error:L}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(H(l))try{l()}catch(L){i=L instanceof zt?L.errors:[L]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=he(f),d=u.next();!d.done;d=u.next()){var y=d.value;try{ho(y)}catch(L){i=i!=null?i:[],L instanceof zt?i=q(q([],N(i)),N(L.errors)):i.push(L)}}}catch(L){o={error:L}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ho(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Ue.EMPTY;function qt(e){return e instanceof Ue||e&&"closed"in e&&H(e.remove)&&H(e.add)&&H(e.unsubscribe)}function ho(e){H(e)?e():e.unsubscribe()}var Pe={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var dt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Ue(function(){o.currentObservers=null,Qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(j);var To=function(e){oe(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){oe(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var At={now:function(){return(At.delegate||Date).now()},delegate:void 0};var Ct=function(e){oe(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=At);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(gt);var Lo=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(yt);var kr=new Lo(Oo);var Mo=function(e){oe(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=vt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(vt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(gt);var _o=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(yt);var me=new _o(Mo);var S=new j(function(e){return e.complete()});function Yt(e){return e&&H(e.schedule)}function Hr(e){return e[e.length-1]}function Xe(e){return H(Hr(e))?e.pop():void 0}function ke(e){return Yt(Hr(e))?e.pop():void 0}function Bt(e,t){return typeof Hr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return H(e==null?void 0:e.then)}function Jt(e){return H(e[bt])}function Xt(e){return Symbol.asyncIterator&&H(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Zi();function tr(e){return H(e==null?void 0:e[er])}function rr(e){return fo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return H(e==null?void 0:e.getReader)}function U(e){if(e instanceof j)return e;if(e!=null){if(Jt(e))return ea(e);if(xt(e))return ta(e);if(Gt(e))return ra(e);if(Xt(e))return Ao(e);if(tr(e))return oa(e);if(or(e))return na(e)}throw Zt(e)}function ea(e){return new j(function(t){var r=e[bt]();if(H(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ta(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?De(t):Qo(function(){return new ir}))}}function jr(e){return e<=0?function(){return S}:E(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,d=0,y=!1,L=!1,X=function(){f==null||f.unsubscribe(),f=void 0},te=function(){X(),l=u=void 0,y=L=!1},J=function(){var k=l;te(),k==null||k.unsubscribe()};return E(function(k,ft){d++,!L&&!y&&X();var qe=u=u!=null?u:r();ft.add(function(){d--,d===0&&!L&&!y&&(f=Ur(J,p))}),qe.subscribe(ft),!l&&d>0&&(l=new at({next:function(Fe){return qe.next(Fe)},error:function(Fe){L=!0,X(),f=Ur(te,n,Fe),qe.error(Fe)},complete:function(){y=!0,X(),f=Ur(te,a),qe.complete()}}),U(k).subscribe(l))})(c)}}function Ur(e,t){for(var r=[],o=2;oe.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function R(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Ie(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var wa=O(h(document.body,"focusin"),h(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Ie()||document.body),G(1));function et(e){return wa.pipe(m(t=>e.contains(t)),K())}function $t(e,t){return C(()=>O(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Le(+!r*t)):le,Q(e.matches(":hover"))))}function Jo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Jo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Jo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Tt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),O(h(t,"load"),h(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),Te(1))))}var Xo=new g,Ta=C(()=>typeof ResizeObserver=="undefined"?Tt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Xo.next(t)))),v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ta.pipe(w(r=>r.observe(t)),v(r=>Xo.pipe(b(o=>o.target===t),_(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function St(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Zo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ve(e){return{x:e.offsetLeft,y:e.offsetTop}}function en(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function tn(e){return O(h(window,"load"),h(window,"resize")).pipe(Me(0,me),m(()=>Ve(e)),Q(Ve(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ne(e){return O(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe(Me(0,me),m(()=>pr(e)),Q(pr(e)))}var rn=new g,Sa=C(()=>I(new IntersectionObserver(e=>{for(let t of e)rn.next(t)},{threshold:0}))).pipe(v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function tt(e){return Sa.pipe(w(t=>t.observe(e)),v(t=>rn.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function on(e,t=16){return Ne(e).pipe(m(({y:r})=>{let o=ce(e),n=St(e);return r>=n.height-o.height-t}),K())}var lr={drawer:R("[data-md-toggle=drawer]"),search:R("[data-md-toggle=search]")};function nn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function ze(e){let t=lr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function Oa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function La(){return O(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function an(){let e=h(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:nn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Ie();if(typeof o!="undefined")return!Oa(o,r)}return!0}),pe());return La().pipe(v(t=>t?S:e))}function ye(){return new URL(location.href)}function lt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function sn(){return new g}function cn(){return location.hash.slice(1)}function pn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ma(e){return O(h(window,"hashchange"),e).pipe(m(cn),Q(cn()),b(t=>t.length>0),G(1))}function ln(e){return Ma(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function Pt(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function mn(){let e=matchMedia("print");return O(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():S))}function zr(e,t){return new j(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function je(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function fn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function un(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function dn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function hn(){return O(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(dn),Q(dn()))}function bn(){return{width:innerWidth,height:innerHeight}}function vn(){return h(window,"resize",{passive:!0}).pipe(m(bn),Q(bn()))}function gn(){return z([hn(),vn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(ee("size")),n=z([o,r]).pipe(m(()=>Ve(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function _a(e){return h(e,"message",t=>t.data)}function Aa(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function yn(e,t=new Worker(e)){let r=_a(t),o=Aa(t),n=new g;n.subscribe(o);let i=o.pipe(Z(),ie(!0));return n.pipe(Z(),Re(r.pipe(W(i))),pe())}var Ca=R("#__config"),Ot=JSON.parse(Ca.textContent);Ot.base=`${new URL(Ot.base,ye())}`;function xe(){return Ot}function B(e){return Ot.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?Ot.translations[e].replace("#",t.toString()):Ot.translations[e]}function Se(e,t=document){return R(`[data-md-component=${e}]`,t)}function ae(e,t=document){return P(`[data-md-component=${e}]`,t)}function ka(e){let t=R(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>R(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function xn(e){if(!B("announce.dismiss")||!e.childElementCount)return S;if(!e.hidden){let t=R(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),ka(e).pipe(w(r=>t.next(r)),_(()=>t.complete()),m(r=>$({ref:e},r)))})}function Ha(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function En(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Ha(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))}function Rt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Tn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Sn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ln=Mt(qr());function Qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,(0,Ln.default)(c))," "],[]).slice(0,-1),i=xe(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=xe();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Mn(e){let t=e[0].score,r=[...e],o=xe(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreQr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>Qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function _n(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Kr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function An(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ra(e){var o;let t=xe(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Cn(e,t){var o;let r=xe();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ra)))}var Ia=0;function ja(e){let t=z([et(e),$t(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Zo(e)).pipe(ne(Ne),pt(1),He(t),m(()=>en(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function Fa(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ia++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(Z(),ie(!1)).subscribe(a);let s=a.pipe(Ht(c=>Le(+!c*250,kr)),K(),v(c=>c?r:S),w(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>$t(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),re(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),d=u.width/2;if(l.role==="tooltip")return{x:d,y:8+u.height};if(u.y>=f.height/2){let{height:y}=ce(l);return{x:d,y:-16-y}}else return{x:d,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),re(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(R(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),ve(me),re(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ja(e).pipe(w(c=>i.next(c)),_(()=>i.complete()),m(c=>$({ref:e},c)))})}function mt(e,{viewport$:t},r=document.body){return Fa(e,{content$:new j(o=>{let n=e.title,i=wn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Ua(e,t){let r=C(()=>z([tn(e),Ne(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function kn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(W(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),O(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Me(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),h(n,"mousedown").pipe(W(a),re(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Ie())==null||c.blur()}}),r.pipe(W(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Ua(e,t).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function Wa(e){return e.tagName==="CODE"?P(".c, .c1, .cm",e):[e]}function Da(e){let t=[];for(let r of Wa(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function Hn(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Da(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,Tn(p,i)),s.replaceWith(a.get(p)))}return a.size===0?S:C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=[];for(let[l,f]of a)c.push([R(".md-typeset",f),R(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?Hn(f,u):Hn(u,f)}),O(...[...a].map(([,l])=>kn(l,t,{target$:r}))).pipe(_(()=>s.complete()),pe())})}function $n(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return $n(t)}}function Pn(e,t){return C(()=>{let r=$n(e);return typeof r!="undefined"?fr(r,e,t):S})}var Rn=Mt(Br());var Va=0;function In(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return In(t)}}function Na(e){return ge(e).pipe(m(({width:t})=>({scrollable:St(e).width>t})),ee("scrollable"))}function jn(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(jr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Rn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Va++}`;let l=Sn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(mt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=In(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(W(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:S)))}}return P(":scope > span[id]",e).length&&e.classList.add("md-code__content"),Na(e).pipe(w(c=>n.next(c)),_(()=>n.complete()),m(c=>$({ref:e},c)),Re(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function za(e,{target$:t,print$:r}){let o=!0;return O(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),w(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Fn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),za(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}var Un=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Gr,Qa=0;function Ka(){return typeof mermaid=="undefined"||mermaid instanceof Element?Tt("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):I(void 0)}function Wn(e){return e.classList.remove("mermaid"),Gr||(Gr=Ka().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Un,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Gr.subscribe(()=>co(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Qa++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Gr.pipe(m(()=>({ref:e})))}var Dn=x("table");function Vn(e){return e.replaceWith(Dn),Dn.replaceWith(An(e)),I({ref:e})}function Ya(e){let t=e.find(r=>r.checked)||e[0];return O(...e.map(r=>h(r,"change").pipe(m(()=>R(`label[for="${r.id}"]`))))).pipe(Q(R(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Nn(e,{viewport$:t,target$:r}){let o=R(".tabbed-labels",e),n=P(":scope > input",e),i=Kr("prev");e.append(i);let a=Kr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(Z(),ie(!0));z([s,ge(e),tt(e)]).pipe(W(p),Me(1,me)).subscribe({next([{active:c},l]){let f=Ve(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=pr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ne(o),ge(o)]).pipe(W(p)).subscribe(([c,l])=>{let f=St(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),O(h(i,"click").pipe(m(()=>-1)),h(a,"click").pipe(m(()=>1))).pipe(W(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(W(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=R(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(p),b(f=>!(f.metaKey||f.ctrlKey)),w(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),re(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let y of P("[data-tabs]"))for(let L of P(":scope > input",y)){let X=R(`label[for="${L.id}"]`);if(X!==c&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),L.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),s.pipe(W(p)).subscribe(()=>{for(let c of P("audio, video",e))c.pause()}),Ya(n).pipe(w(c=>s.next(c)),_(()=>s.complete()),m(c=>$({ref:e},c)))}).pipe(Ke(se))}function zn(e,{viewport$:t,target$:r,print$:o}){return O(...P(".annotate:not(.highlight)",e).map(n=>Pn(n,{target$:r,print$:o})),...P("pre:not(.mermaid) > code",e).map(n=>jn(n,{target$:r,print$:o})),...P("pre.mermaid",e).map(n=>Wn(n)),...P("table:not([class])",e).map(n=>Vn(n)),...P("details",e).map(n=>Fn(n,{target$:r,print$:o})),...P("[data-tabs]",e).map(n=>Nn(n,{viewport$:t,target$:r})),...P("[title]",e).filter(()=>B("content.tooltips")).map(n=>mt(n,{viewport$:t})))}function Ba(e,{alert$:t}){return t.pipe(v(r=>O(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function qn(e,t){let r=R(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ba(e,t).pipe(w(n=>o.next(n)),_(()=>o.complete()),m(n=>$({ref:e},n)))})}var Ga=0;function Ja(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?Ne(o):I({x:0,y:0}),i=O(et(t),$t(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ve(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Qn(e){let t=e.title;if(!t.length)return S;let r=`__tooltip_${Ga++}`,o=Rt(r,"inline"),n=R(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),O(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Me(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Ja(o,e).pipe(w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))}).pipe(Ke(se))}function Xa({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Be(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=ze("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Kn(e,t){return C(()=>z([ge(e),Xa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function Yn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(Z(),ie(!0));o.pipe(ee("active"),He(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue(P("[title]",e)).pipe(b(()=>B("content.tooltips")),ne(a=>Qn(a)));return r.subscribe(o),t.pipe(W(n),m(a=>$({ref:e},a)),Re(i.pipe(W(n))))})}function Za(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),ee("active"))}function Bn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?S:Za(o,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))})}function Gn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ee("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function es(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(ne(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Jn(e){let t=P("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Pt("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),re(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(ve(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),es(t).pipe(W(n.pipe(Ce(1))),ct(),w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))})}function Xn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(w(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Jr=Mt(Br());function ts(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Zn({alert$:e}){Jr.default.isSupported()&&new j(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ts(R(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function ei(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function rs(e,t){let r=new Map;for(let o of P("url",e)){let n=R("loc",o),i=[ei(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(ei(new URL(s),t))}}return r}function ur(e){return un(new URL("sitemap.xml",e)).pipe(m(t=>rs(t,new URL(e))),de(()=>I(new Map)))}function os(e,t){if(!(e.target instanceof Element))return S;let r=e.target.closest("a");if(r===null)return S;if(r.target||e.metaKey||e.ctrlKey)return S;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):S}function ti(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function ri(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function ns(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=ti(document);for(let[o,n]of ti(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return We(P("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),S}),Z(),ie(document))}function oi({location$:e,viewport$:t,progress$:r}){let o=xe();if(location.protocol==="file:")return S;let n=ur(o.base);I(document).subscribe(ri);let i=h(document.body,"click").pipe(He(n),v(([p,c])=>os(p,c)),pe()),a=h(window,"popstate").pipe(m(ye),pe());i.pipe(re(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),O(i,a).subscribe(e);let s=e.pipe(ee("pathname"),v(p=>fn(p,{progress$:r}).pipe(de(()=>(lt(p,!0),S)))),v(ri),v(ns),pe());return O(s.pipe(re(e,(p,c)=>c)),s.pipe(v(()=>e),ee("pathname"),v(()=>e),ee("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),w(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",pn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(ee("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ni=Mt(qr());function ii(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ni.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function jt(e){return e.type===1}function dr(e){return e.type===3}function ai(e,t){let r=yn(e);return O(I(location.protocol!=="file:"),ze("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function si(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=Xr(n))==null?void 0:l.pathname;if(i===void 0)return;let a=ss(o.pathname,i);if(a===void 0)return;let s=ps(t.keys());if(!t.has(s))return;let p=Xr(a,s);if(!p||!t.has(p.href))return;let c=Xr(a,r);if(c)return c.hash=o.hash,c.search=o.search,c}function Xr(e,t){try{return new URL(e,t)}catch(r){return}}function ss(e,t){if(e.startsWith(t))return e.slice(t.length)}function cs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oS)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>h(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),re(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?S:(i.preventDefault(),I(new URL(p)))}}return S}),v(i=>ur(i).pipe(m(a=>{var s;return(s=si({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(n=>lt(n,!0)),z([r,o]).subscribe(([n,i])=>{R(".md-header__topic").appendChild(Cn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ls(e,{worker$:t}){let{searchParams:r}=ye();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),ze("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=ye();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=O(t.pipe(Ae(jt)),h(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function pi(e,{worker$:t}){let r=new g,o=r.pipe(Z(),ie(!0));z([t.pipe(Ae(jt)),r],(i,a)=>a).pipe(ee("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ee("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=R("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ls(e,{worker$:t}).pipe(w(i=>r.next(i)),_(()=>r.complete()),m(i=>$({ref:e},i)),G(1))}function li(e,{worker$:t,query$:r}){let o=new g,n=on(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=R(":scope > :first-child",e),s=R(":scope > :last-child",e);ze("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(re(r),Wr(t.pipe(Ae(jt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(w(()=>s.innerHTML=""),v(({items:l})=>O(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Be(4),Vr(n),v(([f])=>f)))),m(Mn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(ne(l=>{let f=fe("details",l);return typeof f=="undefined"?S:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(w(l=>o.next(l)),_(()=>o.complete()),m(l=>$({ref:e},l)))}function ms(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ye();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function mi(e,t){let r=new g,o=r.pipe(Z(),ie(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),ms(e,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))}function fi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=O(h(n,"keydown"),h(n,"focus")).pipe(ve(se),m(()=>n.value),K());return o.pipe(He(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(w(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ui(e,{index$:t,keyboard$:r}){let o=xe();try{let n=ai(o.search,t),i=Se("search-query",e),a=Se("search-result",e);h(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Ie();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of P(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...P(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Ie()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=pi(i,{worker$:n});return O(s,li(a,{worker$:n,query$:s})).pipe(Re(...ae("search-share",e).map(p=>mi(p,{query$:s})),...ae("search-suggest",e).map(p=>fi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ye}}function di(e,{index$:t,location$:r}){return z([t,r.pipe(Q(ye()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>ii(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function fs(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Zr(e,o){var n=o,{header$:t}=n,r=so(n,["header$"]);let i=R(".md-sidebar__scrollwrap",e),{y:a}=Ve(i);return C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=s.pipe(Me(0,me));return c.pipe(re(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of P(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2})}}}),ue(P("label[tabindex]",e)).pipe(ne(l=>h(l,"click").pipe(ve(se),m(()=>l),W(p)))).subscribe(l=>{let f=R(`[id="${l.htmlFor}"]`);R(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),fs(e,r).pipe(w(l=>s.next(l)),_(()=>s.complete()),m(l=>$({ref:e},l)))})}function hi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return st(je(`${r}/releases/latest`).pipe(de(()=>S),m(o=>({version:o.tag_name})),De({})),je(r).pipe(de(()=>S),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return je(r).pipe(m(o=>({repositories:o.public_repos})),De({}))}}function bi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return st(je(`${r}/releases/permalink/latest`).pipe(de(()=>S),m(({tag_name:o})=>({version:o})),De({})),je(r).pipe(de(()=>S),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}function vi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return hi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return bi(r,o)}return S}var us;function ds(e){return us||(us=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return S}return vi(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(de(()=>S),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function gi(e){let t=R(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(_n(o)),t.classList.add("md-source__repository--active")}),ds(e).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function hs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ee("hidden"))}function yi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):hs(e,t)).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){let o=new Map,n=P(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(ee("height"),m(({height:s})=>{let p=Se("main"),c=R(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(ee("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),He(i),v(([p,c])=>t.pipe(Fr(([l,f],{offset:{y:u},size:d})=>{let y=u+d.height>=Math.floor(s.height);for(;f.length;){let[,L]=f[0];if(L-c=u&&!y)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Be(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(Z(),ie(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=O(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),He(o.pipe(ve(se))),re(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(W(a),ee("offset"),_e(250),Ce(1),W(n.pipe(Ce(1))),ct({delay:250}),re(i)).subscribe(([,{prev:s}])=>{let p=ye(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),bs(e,{viewport$:t,header$:r}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function vs(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Be(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),W(o.pipe(Ce(1))),ie(!0),ct({delay:250}),m(a=>({hidden:a})))}function Ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(a),ee("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),h(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),vs(e,{viewport$:t,main$:o,target$:n}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))}function wi({document$:e,viewport$:t}){e.pipe(v(()=>P(".md-ellipsis")),ne(r=>tt(r).pipe(W(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?mt(n,{viewport$:t}).pipe(W(e.pipe(Ce(1))),_(()=>n.removeAttribute("title"))):S})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>P(".md-status")),ne(r=>mt(r,{viewport$:t}))).subscribe()}function Ti({document$:e,tablet$:t}){e.pipe(v(()=>P(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),ne(r=>h(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),re(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function gs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Si({document$:e}){e.pipe(v(()=>P("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),b(gs),ne(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Oi({viewport$:e,tablet$:t}){z([ze("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),re(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ys(){return location.protocol==="file:"?Tt(`${new URL("search/search_index.js",eo.base)}`).pipe(m(()=>__index),G(1)):je(new URL("search/search_index.json",eo.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Go(),Ut=sn(),Lt=ln(Ut),to=an(),Oe=gn(),hr=Pt("(min-width: 960px)"),Mi=Pt("(min-width: 1220px)"),_i=mn(),eo=xe(),Ai=document.forms.namedItem("search")?ys():Ye,ro=new g;Zn({alert$:ro});var oo=new g;B("navigation.instant")&&oi({location$:Ut,viewport$:Oe,progress$:oo}).subscribe(ot);var Li;((Li=eo.version)==null?void 0:Li.provider)==="mike"&&ci({document$:ot});O(Ut,Lt).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});to.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&<(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&<(r);break;case"Enter":let o=Ie();o instanceof HTMLLabelElement&&o.click()}});wi({viewport$:Oe,document$:ot});Ti({document$:ot,tablet$:hr});Si({document$:ot});Oi({viewport$:Oe,tablet$:hr});var rt=Kn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Gn(e,{viewport$:Oe,header$:rt})),G(1)),xs=O(...ae("consent").map(e=>En(e,{target$:Lt})),...ae("dialog").map(e=>qn(e,{alert$:ro})),...ae("palette").map(e=>Jn(e)),...ae("progress").map(e=>Xn(e,{progress$:oo})),...ae("search").map(e=>ui(e,{index$:Ai,keyboard$:to})),...ae("source").map(e=>gi(e))),Es=C(()=>O(...ae("announce").map(e=>xn(e)),...ae("content").map(e=>zn(e,{viewport$:Oe,target$:Lt,print$:_i})),...ae("content").map(e=>B("search.highlight")?di(e,{index$:Ai,location$:Ut}):S),...ae("header").map(e=>Yn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("header-title").map(e=>Bn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Mi,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>yi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>xi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})),...ae("top").map(e=>Ei(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})))),Ci=ot.pipe(v(()=>Es),Re(xs),G(1));Ci.subscribe();window.document$=ot;window.location$=Ut;window.target$=Lt;window.keyboard$=to;window.viewport$=Oe;window.tablet$=hr;window.screen$=Mi;window.print$=_i;window.alert$=ro;window.progress$=oo;window.component$=Ci;})(); +//# sourceMappingURL=bundle.83f73b43.min.js.map + diff --git a/assets/javascripts/bundle.83f73b43.min.js.map b/assets/javascripts/bundle.83f73b43.min.js.map new file mode 100644 index 0000000..fe920b7 --- /dev/null +++ b/assets/javascripts/bundle.83f73b43.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/notebooks/Model_tides/index.html b/notebooks/Model_tides/index.html new file mode 100644 index 0000000..7c24708 --- /dev/null +++ b/notebooks/Model_tides/index.html @@ -0,0 +1,3499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Modelling tides - eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/notebooks/Satellite_data/index.html b/notebooks/Satellite_data/index.html new file mode 100644 index 0000000..1c0e8df --- /dev/null +++ b/notebooks/Satellite_data/index.html @@ -0,0 +1,2831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Combining tides with satellite data - eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/notebooks/Tide_statistics/index.html b/notebooks/Tide_statistics/index.html new file mode 100644 index 0000000..e1a83c3 --- /dev/null +++ b/notebooks/Tide_statistics/index.html @@ -0,0 +1,2596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Calculating tide statistics and satellite biases - eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/notebooks/Validating_tides/index.html b/notebooks/Validating_tides/index.html new file mode 100644 index 0000000..c29d773 --- /dev/null +++ b/notebooks/Validating_tides/index.html @@ -0,0 +1,2530 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Validating modelled tide heights - eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..8b6cf1c --- /dev/null +++ b/objects.inv @@ -0,0 +1,6 @@ +# Sphinx inventory version 2 +# Project: eo-tides +# Version: 0.0.0 +# The remainder of this file is compressed using zlib. +xڭݎ ڋRj/2cۛI<&J0@3/jp/uv Bݟ7W/}/?A9OuF#JQAx1yzǏ1;mAp;!4 XSqY=UI >ĪcP"Pw&x a^h8!4Ă㝑PFQdX0Cy[pa5(bO+Ҕ\8``d7.$?H(]0F[kVw]d nfC{1N^a +پ๧Dv_i;Tb̃ƾ4!=Xby7U[dx1qհUDK 7J?>1 \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000..c41b07d --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":""},{"location":"#eo-tides-tide-modelling-tools-for-large-scale-satellite-earth-observation-analysis","title":"eo-tides: Tide modelling tools for large-scale satellite Earth observation analysis","text":"

Warning

Note: This package is a work in progress, and not currently ready for operational use.

eo-tides provides provides powerful parallelized tools for integrating satellite Earth observation data with tide modelling. \ud83d\udee0\ufe0f\ud83c\udf0a\ud83d\udef0\ufe0f

eo-tides combines advanced tide modelling functionality from the pyTMD package with pandas, xarray and odc-geo, providing a suite of flexible tools for efficient analysis of coastal and ocean Earth observation data \u2013 from regional, continental, to global scale.

These tools can be applied to petabytes of freely available satellite data (e.g. from Digital Earth Australia or Microsoft Planetary Computer) loaded via Open Data Cube's odc-stac or datacube packages, supporting coastal and ocean earth observation analysis for any time period or location globally.

"},{"location":"#highlights","title":"Highlights","text":"
  • \ud83c\udf0a Model tide heights and phases (e.g. high, low, ebb, flow) from multiple global ocean tide models in parallel, and return a pandas.DataFrame for further analysis
  • \ud83d\udef0\ufe0f \"Tag\" satellite data with tide heights based on the exact moment of image acquisition
  • \ud83c\udf10 Model tides for every individual satellite pixel through time, producing three-dimensional \"tide height\" xarray-format datacubes that can be integrated with satellite data
  • \ud83d\udcc8 Calculate statistics describing local tide dynamics, as well as biases caused by interactions between tidal processes and satellite orbits
  • \ud83d\udee0\ufe0f Validate modelled tides using measured sea levels from coastal tide gauges (e.g. GESLA Global Extreme Sea Level Analysis)
"},{"location":"#supported-tide-models","title":"Supported tide models","text":"

eo-tides supports all ocean tide models supported by pyTMD. These include:

  • Empirical Ocean Tide model (EOT20)
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)

For instructions on how to set up these models for use in eo-tides, refer to Setting up tide models.

"},{"location":"#citing-eo-tides","title":"Citing eo-tides","text":"

To cite eo-tides in your work, please use the following citation:

Bishop-Taylor, R., Sagar, S., Phillips, C., & Newey, V. (2024). eo-tides: Tide modelling tools for large-scale satellite earth observation analysis. https://github.com/GeoscienceAustralia/eo-tides\n

In addition, please consider also citing the underlying pyTMD Python package which powers the tide modelling functionality behind eo-tides:

Sutterley, T. C., Alley, K., Brunt, K., Howard, S., Padman, L., Siegfried, M. (2017) pyTMD: Python-based tidal prediction software. 10.5281/zenodo.5555395\n
"},{"location":"#next-steps","title":"Next steps","text":"

To get started, first follow the guide to installing eo-tides, and then set up one or multiple global ocean tide models.

"},{"location":"api/","title":"API reference","text":""},{"location":"api/#eo_tides.model","title":"eo_tides.model","text":"

Functions:

Name Description ensemble_tides

Combine multiple tide models into a single locally optimised

model_phases

Model tide phases (low-flow, high-flow, high-ebb, low-ebb)

model_tides

Model tide heights at multiple coordinates and/or timesteps

"},{"location":"api/#eo_tides.model.ensemble_tides","title":"ensemble_tides","text":"
ensemble_tides(\n    tide_df,\n    crs,\n    ensemble_models,\n    ensemble_func=None,\n    ensemble_top_n=3,\n    ranking_points=\"https://dea-public-data-dev.s3-ap-southeast-2.amazonaws.com/derivative/dea_intertidal/supplementary/rankings_ensemble_2017-2019.fgb\",\n    ranking_valid_perc=0.02,\n    **idw_kwargs\n)\n

Combine multiple tide models into a single locally optimised ensemble tide model using external model ranking data (e.g. satellite altimetry or NDWI-tide correlations along the coastline) to inform the selection of the best local models.

This function performs the following steps: 1. Takes a dataframe of tide heights from multiple tide models, as produced by eo_tides.model.model_tides 2. Loads model ranking points from an external file, filters them based on the valid data percentage, and retains relevant columns 3. Interpolates the model rankings into the coordinates of the original dataframe using Inverse Weighted Interpolation (IDW) 4. Uses rankings to combine multiple tide models into a single optimised ensemble model (by default, by taking the mean of the top 3 ranked models) 5. Returns a new dataFrame with the combined ensemble model predictions

Parameters:

Name Type Description Default DataFrame

DataFrame produced by eo_tides.model.model_tides, containing tide model predictions in long format with columns: [\"time\", \"x\", \"y\", \"tide_height\", \"tide_model\"].

required string

Coordinate reference system for the \"x\" and \"y\" coordinates in tide_df. Used to ensure that interpolations are performed in the correct CRS.

required list

A list of models to include in the ensemble modelling process. All values must exist as columns with the prefix \"rank_\" in ranking_points.

required dict

By default, a simple ensemble model will be calculated by taking the mean of the ensemble_top_n tide models at each location. However, a dictionary containing more complex ensemble calculations can also be provided. Dictionary keys are used to name output ensemble models; functions should take a column named \"rank\" and convert it to a weighting, e.g.: ensemble_func = {\"ensemble-custom\": lambda x: x[\"rank\"] <= 3}

None int

If ensemble_func is None, this sets the number of top models to include in the mean ensemble calculation. Defaults to 3.

3 str

Path to the file containing model ranking points. This dataset should include columns containing rankings for each tide model, named with the prefix \"rank_\". e.g. \"rank_EOT20\". Low values should represent high rankings (e.g. 1 = top ranked). The default value points to an example file covering Australia.

'https://dea-public-data-dev.s3-ap-southeast-2.amazonaws.com/derivative/dea_intertidal/supplementary/rankings_ensemble_2017-2019.fgb' float

Minimum percentage of valid data required to include a model rank point in the analysis, as defined in a column named \"valid_perc\". Defaults to 0.02.

0.02

Optional keyword arguments to pass to the idw function used for interpolation. Useful values include k (number of nearest neighbours to use in interpolation), max_dist (maximum distance to nearest neighbours), and k_min (minimum number of neighbours required after max_dist is applied).

{}

Returns:

Type Description DataFrame

DataFrame containing the ensemble model predictions, matching the format of the input tide_df (e.g. columns [\"time\", \"x\", \"y\", \"tide_height\", \"tide_model\"]. By default the 'tide_model' column will be labeled \"ensemble\" for the combined model predictions (but if a custom dictionary of ensemble functions is provided via ensemble_func, each ensemble will be named using the provided dictionary keys).

Source code in eo_tides/model.py
def ensemble_tides(\n    tide_df,\n    crs,\n    ensemble_models,\n    ensemble_func=None,\n    ensemble_top_n=3,\n    ranking_points=\"https://dea-public-data-dev.s3-ap-southeast-2.amazonaws.com/derivative/dea_intertidal/supplementary/rankings_ensemble_2017-2019.fgb\",\n    ranking_valid_perc=0.02,\n    **idw_kwargs,\n):\n    \"\"\"Combine multiple tide models into a single locally optimised\n    ensemble tide model using external model ranking data (e.g.\n    satellite altimetry or NDWI-tide correlations along the coastline)\n    to inform the selection of the best local models.\n\n    This function performs the following steps:\n    1. Takes a dataframe of tide heights from multiple tide models, as\n       produced by `eo_tides.model.model_tides`\n    2. Loads model ranking points from an external file, filters them\n       based on the valid data percentage, and retains relevant columns\n    3. Interpolates the model rankings into the coordinates of the\n       original dataframe using Inverse Weighted Interpolation (IDW)\n    4. Uses rankings to combine multiple tide models into a single\n       optimised ensemble model (by default, by taking the mean of the\n       top 3 ranked models)\n    5. Returns a new dataFrame with the combined ensemble model predictions\n\n    Parameters\n    ----------\n    tide_df : pandas.DataFrame\n        DataFrame produced by `eo_tides.model.model_tides`, containing\n        tide model predictions in long format with columns:\n        `[\"time\", \"x\", \"y\", \"tide_height\", \"tide_model\"]`.\n    crs : string\n        Coordinate reference system for the \"x\" and \"y\" coordinates in\n        `tide_df`. Used to ensure that interpolations are performed\n        in the correct CRS.\n    ensemble_models : list\n        A list of models to include in the ensemble modelling process.\n        All values must exist as columns with the prefix \"rank_\" in\n        `ranking_points`.\n    ensemble_func : dict, optional\n        By default, a simple ensemble model will be calculated by taking\n        the mean of the `ensemble_top_n` tide models at each location.\n        However, a dictionary containing more complex ensemble\n        calculations can also be provided. Dictionary keys are used\n        to name output ensemble models; functions should take a column\n        named \"rank\" and convert it to a weighting, e.g.:\n        `ensemble_func = {\"ensemble-custom\": lambda x: x[\"rank\"] <= 3}`\n    ensemble_top_n : int, optional\n        If `ensemble_func` is None, this sets the number of top models\n        to include in the mean ensemble calculation. Defaults to 3.\n    ranking_points : str, optional\n        Path to the file containing model ranking points. This dataset\n        should include columns containing rankings for each tide\n        model, named with the prefix \"rank_\". e.g. \"rank_EOT20\".\n        Low values should represent high rankings (e.g. 1 = top ranked).\n        The default value points to an example file covering Australia.\n    ranking_valid_perc : float, optional\n        Minimum percentage of valid data required to include a model\n        rank point in the analysis, as defined in a column named\n        \"valid_perc\". Defaults to 0.02.\n    **idw_kwargs\n        Optional keyword arguments to pass to the `idw` function used\n        for interpolation. Useful values include `k` (number of nearest\n        neighbours to use in interpolation), `max_dist` (maximum\n        distance to nearest neighbours), and `k_min` (minimum number of\n        neighbours required after `max_dist` is applied).\n\n    Returns\n    -------\n    pandas.DataFrame\n        DataFrame containing the ensemble model predictions, matching\n        the format of the input `tide_df` (e.g. columns `[\"time\", \"x\",\n        \"y\", \"tide_height\", \"tide_model\"]`. By default the 'tide_model'\n        column will be labeled \"ensemble\" for the combined model\n        predictions (but if a custom dictionary of ensemble functions is\n        provided via `ensemble_func`, each ensemble will be named using\n        the provided dictionary keys).\n\n    \"\"\"\n    # Raise data if `tide_df` provided in wide format\n    if \"tide_model\" not in tide_df:\n        raise Exception(\n            \"`tide_df` does not contain the expected 'tide_model' and \"\n            \"'tide_height' columns. Ensure that tides were modelled in \"\n            \"long format (i.e. `output_format='long'` in `model_tides`).\"\n        )\n\n    # Extract x and y coords from dataframe\n    x = tide_df.index.get_level_values(level=\"x\")\n    y = tide_df.index.get_level_values(level=\"y\")\n\n    # Load model ranks points and reproject to same CRS as x and y\n    model_ranking_cols = [f\"rank_{m}\" for m in ensemble_models]\n    try:\n        model_ranks_gdf = (\n            gpd.read_file(ranking_points, engine=\"pyogrio\")\n            .to_crs(crs)\n            .query(f\"valid_perc > {ranking_valid_perc}\")\n            .dropna(how=\"all\")[model_ranking_cols + [\"geometry\"]]\n        )\n    except KeyError:\n        error_msg = f\"\"\"\n        Not all of the expected \"rank_\" columns {model_ranking_cols} were\n        found in the columns of the ranking points file ({ranking_points}).\n        Consider passing a custom list of models using `ensemble_models`.\n        \"\"\"\n        raise Exception(textwrap.dedent(error_msg).strip()) from None\n\n    # Use points to interpolate model rankings into requested x and y\n    id_kwargs_str = \"\" if idw_kwargs == {} else idw_kwargs\n    print(f\"Interpolating model rankings using IDW interpolation {id_kwargs_str}\")\n    ensemble_ranks_df = (\n        # Run IDW interpolation on subset of ranking columns\n        pd.DataFrame(\n            idw(\n                input_z=model_ranks_gdf[model_ranking_cols],\n                input_x=model_ranks_gdf.geometry.x,\n                input_y=model_ranks_gdf.geometry.y,\n                output_x=x,\n                output_y=y,\n                **idw_kwargs,\n            ),\n            columns=model_ranking_cols,\n        )\n        .assign(x=x, y=y)\n        # Drop any duplicates then melt columns into long format\n        .drop_duplicates()\n        .melt(id_vars=[\"x\", \"y\"], var_name=\"tide_model\", value_name=\"rank\")\n        # Remore \"rank_\" prefix to get plain model names\n        .replace({\"^rank_\": \"\"}, regex=True)\n        # Set index columns and rank across groups\n        .set_index([\"tide_model\", \"x\", \"y\"])\n        .groupby([\"x\", \"y\"])\n        .rank()\n    )\n\n    # If no custom ensemble funcs are provided, use a default ensemble\n    # calculation that takes the mean of the top N tide models\n    if ensemble_func is None:\n        ensemble_func = {\"ensemble\": lambda x: x[\"rank\"] <= ensemble_top_n}\n\n    # Create output list to hold computed ensemble model outputs\n    ensemble_list = []\n\n    # Loop through all provided ensemble generation functions\n    for ensemble_n, ensemble_f in ensemble_func.items():\n        print(f\"Combining models into single {ensemble_n} model\")\n\n        # Join ranks to input tide data, compute weightings and group\n        grouped = (\n            # Add tide model as an index so we can join with model ranks\n            tide_df.set_index(\"tide_model\", append=True)\n            .join(ensemble_ranks_df)\n            # Add temp columns containing weightings and weighted values\n            .assign(\n                weights=ensemble_f,  # use custom func to compute weights\n                weighted=lambda i: i.tide_height * i.weights,\n            )\n            # Groupby is specified in a weird order here as this seems\n            # to be the easiest way to preserve correct index sorting\n            .groupby([\"x\", \"y\", \"time\"])\n        )\n\n        # Use weightings to combine multiple models into single ensemble\n        ensemble_df = (\n            # Calculate weighted mean and convert back to dataframe\n            grouped.weighted.sum()\n            .div(grouped.weights.sum())\n            .to_frame(\"tide_height\")\n            # Label ensemble model and ensure indexes are in expected order\n            .assign(tide_model=ensemble_n)\n            .reorder_levels([\"time\", \"x\", \"y\"], axis=0)\n        )\n\n        ensemble_list.append(ensemble_df)\n\n    # Combine all ensemble models and return as a single dataframe\n    return pd.concat(ensemble_list)\n
"},{"location":"api/#eo_tides.model.ensemble_tides(tide_df)","title":"tide_df","text":""},{"location":"api/#eo_tides.model.ensemble_tides(crs)","title":"crs","text":""},{"location":"api/#eo_tides.model.ensemble_tides(ensemble_models)","title":"ensemble_models","text":""},{"location":"api/#eo_tides.model.ensemble_tides(ensemble_func)","title":"ensemble_func","text":""},{"location":"api/#eo_tides.model.ensemble_tides(ensemble_top_n)","title":"ensemble_top_n","text":""},{"location":"api/#eo_tides.model.ensemble_tides(ranking_points)","title":"ranking_points","text":""},{"location":"api/#eo_tides.model.ensemble_tides(ranking_valid_perc)","title":"ranking_valid_perc","text":""},{"location":"api/#eo_tides.model.ensemble_tides(**idw_kwargs)","title":"**idw_kwargs","text":""},{"location":"api/#eo_tides.model.model_phases","title":"model_phases","text":"
model_phases(\n    x,\n    y,\n    time,\n    model=\"EOT20\",\n    directory=None,\n    time_offset=\"15 min\",\n    return_tides=False,\n    **model_tides_kwargs\n)\n

Model tide phases (low-flow, high-flow, high-ebb, low-ebb) at multiple coordinates and/or timesteps using using one or more ocean tide models.

Ebb and low phases are calculated by running the eo_tides.model.model_tides function twice, once for the requested timesteps, and again after subtracting a small time offset (by default, 15 minutes). If tides increased over this period, they are assigned as \"flow\"; if they decreased, they are assigned as \"ebb\". Tides are considered \"high\" if equal or greater than 0 metres tide height, otherwise \"low\".

This function supports all parameters that are supported by model_tides.

Parameters:

Name Type Description Default float or list of float

One or more x and y coordinates used to define the location at which to model tide phases. By default these coordinates should be lat/lon; use \"crs\" if they are in a custom coordinate reference system.

required float or list of float

One or more x and y coordinates used to define the location at which to model tide phases. By default these coordinates should be lat/lon; use \"crs\" if they are in a custom coordinate reference system.

required DatetimeLike

Times at which to model tide phases (in UTC). Accepts any format that can be converted by pandas.to_datetime(); e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp, datetime.datetime and strings (e.g. \"2020-01-01 23:00\"). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")

required str or list of str

The tide model (or list of models) to use to model tides. Defaults to \"EOT20\"; specify \"all\" to use all models available in directory. For a full list of available and supported models, run eo_tides.model.list_models.

'EOT20' str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None str

The time offset/delta used to generate a time series of offset tide heights required for phase calculation. Defeaults to \"15 min\"; can be any string passed to pandas.Timedelta.

'15 min' bool

Whether to return intermediate modelled tide heights as a \"tide_height\" column in the output dataframe. Defaults to False.

False

Optional parameters passed to the eo_tides.model.model_tides function. Important parameters include output_format (e.g. whether to return results in wide or long format), crop (whether to crop tide model constituent files on-the-fly to improve performance) etc.

{}

Returns:

Type Description DataFrame

A dataframe containing modelled tide phases.

Source code in eo_tides/model.py
def model_phases(\n    x: float | list[float] | xr.DataArray,\n    y: float | list[float] | xr.DataArray,\n    time: DatetimeLike,\n    model: str | list[str] = \"EOT20\",\n    directory: str | os.PathLike | None = None,\n    time_offset: str = \"15 min\",\n    return_tides: bool = False,\n    **model_tides_kwargs,\n) -> pd.DataFrame:\n    \"\"\"\n    Model tide phases (low-flow, high-flow, high-ebb, low-ebb)\n    at multiple coordinates and/or timesteps using using one\n    or more ocean tide models.\n\n    Ebb and low phases are calculated by running the\n    `eo_tides.model.model_tides` function twice, once for\n    the requested timesteps, and again after subtracting a\n    small time offset (by default, 15 minutes). If tides\n    increased over this period, they are assigned as \"flow\";\n    if they decreased, they are assigned as \"ebb\".\n    Tides are considered \"high\" if equal or greater than 0\n    metres tide height, otherwise \"low\".\n\n    This function supports all parameters that are supported\n    by `model_tides`.\n\n    Parameters\n    ----------\n    x, y : float or list of float\n        One or more x and y coordinates used to define\n        the location at which to model tide phases. By default\n        these coordinates should be lat/lon; use \"crs\" if they\n        are in a custom coordinate reference system.\n    time : DatetimeLike\n        Times at which to model tide phases (in UTC). Accepts\n        any format that can be converted by `pandas.to_datetime()`;\n        e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp,\n        datetime.datetime and strings (e.g. \"2020-01-01 23:00\").\n        For example: `time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")`\n    model : str or list of str, optional\n        The tide model (or list of models) to use to model tides.\n        Defaults to \"EOT20\"; specify \"all\" to use all models available\n        in `directory`. For a full list of available and supported models,\n        run `eo_tides.model.list_models`.\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    time_offset: str, optional\n        The time offset/delta used to generate a time series of\n        offset tide heights required for phase calculation. Defeaults\n        to \"15 min\"; can be any string passed to `pandas.Timedelta`.\n    return_tides: bool, optional\n        Whether to return intermediate modelled tide heights as a\n        \"tide_height\" column in the output dataframe. Defaults to False.\n    **model_tides_kwargs :\n        Optional parameters passed to the `eo_tides.model.model_tides`\n        function. Important parameters include `output_format` (e.g.\n        whether to return results in wide or long format), `crop`\n        (whether to crop tide model constituent files on-the-fly to\n        improve performance) etc.\n\n    Returns\n    -------\n    pandas.DataFrame\n        A dataframe containing modelled tide phases.\n\n    \"\"\"\n\n    # Pop output format and mode for special handling\n    output_format = model_tides_kwargs.pop(\"output_format\", \"long\")\n    mode = model_tides_kwargs.pop(\"mode\", \"one-to-many\")\n\n    # Model tides\n    tide_df = model_tides(\n        x=x,\n        y=y,\n        time=time,\n        model=model,\n        directory=directory,\n        **model_tides_kwargs,\n    )\n\n    # Model tides for a time 15 minutes prior to each previously\n    # modelled satellite acquisition time. This allows us to compare\n    # tide heights to see if they are rising or falling.\n    pre_df = model_tides(\n        x=x,\n        y=y,\n        time=time - pd.Timedelta(time_offset),\n        model=model,\n        directory=directory,\n        **model_tides_kwargs,\n    )\n\n    # Compare tides computed for each timestep. If the previous tide\n    # was higher than the current tide, the tide is 'ebbing'. If the\n    # previous tide was lower, the tide is 'flowing'\n    ebb_flow = (tide_df.tide_height < pre_df.tide_height.values).replace({True: \"ebb\", False: \"flow\"})\n\n    # If tides are greater than 0, then \"high\", otherwise \"low\"\n    high_low = (tide_df.tide_height >= 0).replace({True: \"high\", False: \"low\"})\n\n    # Combine into one string and add to data\n    tide_df[\"tide_phase\"] = high_low.astype(str) + \"-\" + ebb_flow.astype(str)\n\n    # Optionally convert to a wide format dataframe with a tide model in\n    # each dataframe column\n    if output_format == \"wide\":\n        # Pivot into wide format with each time model as a column\n        print(\"Converting to a wide format dataframe\")\n        tide_df = tide_df.pivot(columns=\"tide_model\")\n\n        # If in 'one-to-one' mode, reindex using our input time/x/y\n        # values to ensure the output is sorted the same as our inputs\n        if mode == \"one-to-one\":\n            output_indices = pd.MultiIndex.from_arrays([time, x, y], names=[\"time\", \"x\", \"y\"])\n            tide_df = tide_df.reindex(output_indices)\n\n        # Optionally drop tides\n        if not return_tides:\n            return tide_df.drop(\"tide_height\", axis=1)[\"tide_phase\"]\n\n    # Optionally drop tide heights\n    if not return_tides:\n        return tide_df.drop(\"tide_height\", axis=1)\n\n    return tide_df\n
"},{"location":"api/#eo_tides.model.model_phases(x)","title":"x","text":""},{"location":"api/#eo_tides.model.model_phases(y)","title":"y","text":""},{"location":"api/#eo_tides.model.model_phases(time)","title":"time","text":""},{"location":"api/#eo_tides.model.model_phases(model)","title":"model","text":""},{"location":"api/#eo_tides.model.model_phases(directory)","title":"directory","text":""},{"location":"api/#eo_tides.model.model_phases(time_offset)","title":"time_offset","text":""},{"location":"api/#eo_tides.model.model_phases(return_tides)","title":"return_tides","text":""},{"location":"api/#eo_tides.model.model_phases(**model_tides_kwargs)","title":"**model_tides_kwargs","text":""},{"location":"api/#eo_tides.model.model_tides","title":"model_tides","text":"
model_tides(\n    x,\n    y,\n    time,\n    model=\"EOT20\",\n    directory=None,\n    crs=\"EPSG:4326\",\n    mode=\"one-to-many\",\n    output_format=\"long\",\n    output_units=\"m\",\n    method=\"linear\",\n    extrapolate=True,\n    cutoff=None,\n    crop=True,\n    crop_buffer=5,\n    parallel=True,\n    parallel_splits=\"auto\",\n    parallel_max=None,\n    ensemble_models=None,\n    **ensemble_kwargs\n)\n

Model tide heights at multiple coordinates and/or timesteps using using one or more ocean tide models.

This function is parallelised to improve performance, and supports all tidal models supported by pyTMD, including:

  • Empirical Ocean Tide model (EOT20)
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)

This function requires access to tide model data files. These should be placed in a folder with subfolders matching the structure required by pyTMD. For more details: https://geoscienceaustralia.github.io/eo-tides/setup/ https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories

This function is a modification of the pyTMD package's pyTMD.compute.tide_elevations function. For more info: https://pytmd.readthedocs.io/en/latest/api_reference/compute.html#pyTMD.compute.tide_elevations

Parameters:

Name Type Description Default float or list of float

One or more x and y coordinates used to define the location at which to model tides. By default these coordinates should be lat/lon; use \"crs\" if they are in a custom coordinate reference system.

required float or list of float

One or more x and y coordinates used to define the location at which to model tides. By default these coordinates should be lat/lon; use \"crs\" if they are in a custom coordinate reference system.

required DatetimeLike

Times at which to model tide heights (in UTC). Accepts any format that can be converted by pandas.to_datetime(); e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp, datetime.datetime and strings (e.g. \"2020-01-01 23:00\"). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")

required str or list of str

The tide model (or list of models) to use to model tides. Defaults to \"EOT20\"; specify \"all\" to use all models available in directory. For a full list of available and supported models, run eo_tides.model.list_models.

'EOT20' str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None str

Input coordinate reference system for x and y coordinates. Defaults to \"EPSG:4326\" (WGS84; degrees latitude, longitude).

'EPSG:4326' str

The analysis mode to use for tide modelling. Supports two options:

  • \"one-to-many\": Models tides for every timestep in \"time\" at every input x and y coordinate point. This is useful if you want to model tides for a specific list of timesteps across multiple spatial points (e.g. for the same set of satellite acquisition times at various locations across your study area).
  • \"one-to-one\": Model tides using a unique timestep for each set of x and y coordinates. In this mode, the number of x and y points must equal the number of timesteps provided in \"time\".
'one-to-many' str

Whether to return the output dataframe in long format (with results stacked vertically along \"tide_model\" and \"tide_height\" columns), or wide format (with a column for each tide model). Defaults to \"long\".

'long' str

Whether to return modelled tides in floating point metre units, or integer centimetre units (i.e. scaled by 100) or integer millimetre units (i.e. scaled by 1000. Returning outputs in integer units can be useful for reducing memory usage. Defaults to \"m\" for metres; set to \"cm\" for centimetres or \"mm\" for millimetres.

'm' str

Method used to interpolate tidal constituents from model files. Defaults to \"linear\"; options include:

  • \"linear\", \"nearest\": scipy regular grid interpolations
  • \"spline\": scipy bivariate spline interpolation
  • \"bilinear\": quick bilinear interpolation
'linear' bool

Whether to extrapolate tides for x and y coordinates outside of the valid tide modelling domain using nearest-neighbor.

True float

Extrapolation cutoff in kilometers. The default is None, which will extrapolate for all points regardless of distance from the valid tide modelling domain.

None bool

Whether to crop tide model constituent files on-the-fly to improve performance. Defaults to True; use crop_buffer to customise the buffer distance used to crop the files.

True int or float

The buffer distance in degrees used to crop tide model constituent files around the modelling area. Defaults to 5, which will crop constituents using a five degree buffer on either side of the analysis extent.

5 bool

Whether to parallelise tide modelling. If multiple tide models are requested, these will be run in parallel using concurrent.futures. If enough workers are available, the analysis will also be split into spatial chunks for additional parallelisation (see \"parallel_splits\" below). Default is True.

True str or int

Whether to split the input x and y coordinates into smaller, evenly-sized chunks that are processed in parallel. This can provide a large performance boost when processing large numbers of coordinates. The default is \"auto\", which will automatically attempt to determine optimal splits based on available CPUs, the number of input points, and the number of models.

'auto' int

Maximum number of processes to run in parallel. The default of None will automatically determine this from your available CPUs.

None list of str

An optional list of models used to generate the ensemble tide model if \"ensemble\" tide modelling is requested. Defaults to [\"EOT20\", \"FES2012\", \"FES2014_extrapolated\", \"FES2022_extrapolated\", \"GOT4.10\", \"GOT5.5_extrapolated\", \"GOT5.6_extrapolated\", \"TPXO10-atlas-v2-nc\", \"TPXO8-atlas-nc\", \"TPXO9-atlas-v5-nc\"].

None

Keyword arguments used to customise the generation of optional ensemble tide models if \"ensemble\" modelling are requested. These are passed to the underlying _ensemble_model function. Useful parameters include ranking_points (path to model rankings data), k (for controlling how model rankings are interpolated), and ensemble_top_n (how many top models to use in the ensemble calculation).

{}

Returns:

Type Description DataFrame

A dataframe containing modelled tide heights.

Source code in eo_tides/model.py
def model_tides(\n    x: float | list[float] | xr.DataArray,\n    y: float | list[float] | xr.DataArray,\n    time: DatetimeLike,\n    model: str | list[str] = \"EOT20\",\n    directory: str | os.PathLike | None = None,\n    crs: str = \"EPSG:4326\",\n    mode: str = \"one-to-many\",\n    output_format: str = \"long\",\n    output_units: str = \"m\",\n    method: str = \"linear\",\n    extrapolate: bool = True,\n    cutoff: float | None = None,\n    crop: bool = True,\n    crop_buffer: float | None = 5,\n    parallel: bool = True,\n    parallel_splits: int | str = \"auto\",\n    parallel_max: int | None = None,\n    ensemble_models: list[str] | None = None,\n    **ensemble_kwargs,\n) -> pd.DataFrame:\n    \"\"\"\n    Model tide heights at multiple coordinates and/or timesteps\n    using using one or more ocean tide models.\n\n    This function is parallelised to improve performance, and\n    supports all tidal models supported by `pyTMD`, including:\n\n    - Empirical Ocean Tide model (EOT20)\n    - Finite Element Solution tide models (FES2022, FES2014, FES2012)\n    - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)\n    - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)\n    - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)\n\n    This function requires access to tide model data files.\n    These should be placed in a folder with subfolders matching\n    the structure required by `pyTMD`. For more details:\n    <https://geoscienceaustralia.github.io/eo-tides/setup/>\n    <https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>\n\n    This function is a modification of the `pyTMD` package's\n    `pyTMD.compute.tide_elevations` function. For more info:\n    <https://pytmd.readthedocs.io/en/latest/api_reference/compute.html#pyTMD.compute.tide_elevations>\n\n    Parameters\n    ----------\n    x, y : float or list of float\n        One or more x and y coordinates used to define\n        the location at which to model tides. By default these\n        coordinates should be lat/lon; use \"crs\" if they\n        are in a custom coordinate reference system.\n    time : DatetimeLike\n        Times at which to model tide heights (in UTC). Accepts\n        any format that can be converted by `pandas.to_datetime()`;\n        e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp,\n        datetime.datetime and strings (e.g. \"2020-01-01 23:00\").\n        For example: `time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")`\n    model : str or list of str, optional\n        The tide model (or list of models) to use to model tides.\n        Defaults to \"EOT20\"; specify \"all\" to use all models available\n        in `directory`. For a full list of available and supported models,\n        run `eo_tides.model.list_models`.\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    crs : str, optional\n        Input coordinate reference system for x and y coordinates.\n        Defaults to \"EPSG:4326\" (WGS84; degrees latitude, longitude).\n    mode : str, optional\n        The analysis mode to use for tide modelling. Supports two options:\n\n        - \"one-to-many\": Models tides for every timestep in \"time\" at\n        every input x and y coordinate point. This is useful if you\n        want to model tides for a specific list of timesteps across\n        multiple spatial points (e.g. for the same set of satellite\n        acquisition times at various locations across your study area).\n        - \"one-to-one\": Model tides using a unique timestep for each\n        set of x and y coordinates. In this mode, the number of x and\n        y points must equal the number of timesteps provided in \"time\".\n    output_format : str, optional\n        Whether to return the output dataframe in long format (with\n        results stacked vertically along \"tide_model\" and \"tide_height\"\n        columns), or wide format (with a column for each tide model).\n        Defaults to \"long\".\n    output_units : str, optional\n        Whether to return modelled tides in floating point metre units,\n        or integer centimetre units (i.e. scaled by 100) or integer\n        millimetre units (i.e. scaled by 1000. Returning outputs in\n        integer units can be useful for reducing memory usage.\n        Defaults to \"m\" for metres; set to \"cm\" for centimetres or \"mm\"\n        for millimetres.\n    method : str, optional\n        Method used to interpolate tidal constituents\n        from model files. Defaults to \"linear\"; options include:\n\n        - \"linear\", \"nearest\": scipy regular grid interpolations\n        - \"spline\": scipy bivariate spline interpolation\n        - \"bilinear\": quick bilinear interpolation\n    extrapolate : bool, optional\n        Whether to extrapolate tides for x and y coordinates outside of\n        the valid tide modelling domain using nearest-neighbor.\n    cutoff : float, optional\n        Extrapolation cutoff in kilometers. The default is None, which\n        will extrapolate for all points regardless of distance from the\n        valid tide modelling domain.\n    crop : bool, optional\n        Whether to crop tide model constituent files on-the-fly to\n        improve performance. Defaults to True; use `crop_buffer`\n        to customise the buffer distance used to crop the files.\n    crop_buffer : int or float, optional\n        The buffer distance in degrees used to crop tide model\n        constituent files around the modelling area. Defaults to 5,\n        which will crop constituents using a five degree buffer on either\n        side of the analysis extent.\n    parallel : bool, optional\n        Whether to parallelise tide modelling. If multiple tide models are\n        requested, these will be run in parallel using `concurrent.futures`.\n        If enough workers are available, the analysis will also be split\n        into spatial chunks for additional parallelisation (see \"parallel_splits\"\n        below). Default is True.\n    parallel_splits : str or int, optional\n        Whether to split the input x and y coordinates into smaller,\n        evenly-sized chunks that are processed in parallel. This can\n        provide a large performance boost when processing large numbers\n        of coordinates. The default is \"auto\", which will automatically\n        attempt to determine optimal splits based on available CPUs,\n        the number of input points, and the number of models.\n    parallel_max : int, optional\n        Maximum number of processes to run in parallel. The default of\n        None will automatically determine this from your available CPUs.\n    ensemble_models : list of str, optional\n        An optional list of models used to generate the ensemble tide\n        model if \"ensemble\" tide modelling is requested. Defaults to\n        `[\"EOT20\", \"FES2012\", \"FES2014_extrapolated\", \"FES2022_extrapolated\",\n        \"GOT4.10\", \"GOT5.5_extrapolated\", \"GOT5.6_extrapolated\",\n        \"TPXO10-atlas-v2-nc\", \"TPXO8-atlas-nc\", \"TPXO9-atlas-v5-nc\"]`.\n    **ensemble_kwargs :\n        Keyword arguments used to customise the generation of optional\n        ensemble tide models if \"ensemble\" modelling are requested.\n        These are passed to the underlying `_ensemble_model` function.\n        Useful parameters include `ranking_points` (path to model\n        rankings data), `k` (for controlling how model rankings are\n        interpolated), and `ensemble_top_n` (how many top models to use\n        in the ensemble calculation).\n\n    Returns\n    -------\n    pandas.DataFrame\n        A dataframe containing modelled tide heights.\n\n    \"\"\"\n    # Turn inputs into arrays for consistent handling\n    x = np.atleast_1d(x)\n    y = np.atleast_1d(y)\n    time = _standardise_time(time)\n\n    # Validate input arguments\n    assert time is not None, \"Times for modelling tides muyst be provided via `time`.\"\n    assert method in (\"bilinear\", \"spline\", \"linear\", \"nearest\")\n    assert output_units in (\n        \"m\",\n        \"cm\",\n        \"mm\",\n    ), \"Output units must be either 'm', 'cm', or 'mm'.\"\n    assert output_format in (\n        \"long\",\n        \"wide\",\n    ), \"Output format must be either 'long' or 'wide'.\"\n    assert len(x) == len(y), \"x and y must be the same length.\"\n    if mode == \"one-to-one\":\n        assert len(x) == len(time), (\n            \"The number of supplied x and y points and times must be \"\n            \"identical in 'one-to-one' mode. Use 'one-to-many' mode if \"\n            \"you intended to model multiple timesteps at each point.\"\n        )\n\n    # Set tide modelling files directory. If no custom path is\n    # provided, try global environment variable.\n    directory = _set_directory(directory)\n\n    # Standardise model list, handling \"all\" and \"ensemble\" functionality\n    models_to_process, models_requested, ensemble_models = _standardise_models(\n        model=model,\n        directory=directory,\n        ensemble_models=ensemble_models,\n    )\n\n    # Update tide modelling func to add default keyword arguments that\n    # are used for every iteration during parallel processing\n    iter_func = partial(\n        _model_tides,\n        directory=directory,\n        crs=crs,\n        mode=mode,\n        output_units=output_units,\n        method=method,\n        extrapolate=extrapolate,\n        cutoff=np.inf if cutoff is None else cutoff,\n        crop=crop,\n        crop_buffer=crop_buffer,\n    )\n\n    # If automatic parallel splits, calculate optimal value\n    # based on available parallelisation, number of points\n    # and number of models\n    if parallel_splits == \"auto\":\n        parallel_splits = _parallel_splits(\n            total_points=len(x),\n            model_count=len(models_to_process),\n            parallel_max=parallel_max,\n        )\n\n    # Verify that parallel splits are not larger than number of points\n    assert isinstance(parallel_splits, int)\n    if parallel_splits > len(x):\n        raise ValueError(f\"Parallel splits ({parallel_splits}) cannot be larger than the number of points ({len(x)}).\")\n\n    # Parallelise if either multiple models or multiple splits requested\n    if parallel & ((len(models_to_process) > 1) | (parallel_splits > 1)):\n        with ProcessPoolExecutor(max_workers=parallel_max) as executor:\n            print(\n                f\"Modelling tides with {', '.join(models_to_process)} in parallel (models: {len(models_to_process)}, splits: {parallel_splits})\"\n            )\n\n            # Optionally split lon/lat points into `splits_n` chunks\n            # that will be applied in parallel\n            x_split = np.array_split(x, parallel_splits)\n            y_split = np.array_split(y, parallel_splits)\n\n            # Get every combination of models and lat/lon points, and\n            # extract as iterables that can be passed to `executor.map()`\n            # In \"one-to-many\" mode, pass entire set of timesteps to each\n            # parallel iteration by repeating timesteps by number of total\n            # parallel iterations. In \"one-to-one\" mode, split up\n            # timesteps into smaller parallel chunks too.\n            if mode == \"one-to-many\":\n                model_iters, x_iters, y_iters = zip(\n                    *[(m, x_split[i], y_split[i]) for m in models_to_process for i in range(parallel_splits)],\n                )\n                time_iters = [time] * len(model_iters)\n            elif mode == \"one-to-one\":\n                time_split = np.array_split(time, parallel_splits)\n                model_iters, x_iters, y_iters, time_iters = zip(\n                    *[\n                        (m, x_split[i], y_split[i], time_split[i])\n                        for m in models_to_process\n                        for i in range(parallel_splits)\n                    ],\n                )\n\n            # Apply func in parallel, iterating through each input param\n            try:\n                model_outputs = list(\n                    tqdm(\n                        executor.map(iter_func, model_iters, x_iters, y_iters, time_iters),\n                        total=len(model_iters),\n                    ),\n                )\n            except BrokenProcessPool:\n                error_msg = (\n                    \"Parallelised tide modelling failed, likely to to an out-of-memory error. \"\n                    \"Try reducing the size of your analysis, or set `parallel=False`.\"\n                )\n                raise RuntimeError(error_msg)\n\n    # Model tides in series if parallelisation is off\n    else:\n        model_outputs = []\n\n        for model_i in models_to_process:\n            print(f\"Modelling tides with {model_i}\")\n            tide_df = iter_func(model_i, x, y, time)\n            model_outputs.append(tide_df)\n\n    # Combine outputs into a single dataframe\n    tide_df = pd.concat(model_outputs, axis=0)\n\n    # Optionally compute ensemble model and add to dataframe\n    if \"ensemble\" in models_requested:\n        ensemble_df = ensemble_tides(tide_df, crs, ensemble_models, **ensemble_kwargs)\n\n        # Update requested models with any custom ensemble models, then\n        # filter the dataframe to keep only models originally requested\n        models_requested = list(np.union1d(models_requested, ensemble_df.tide_model.unique()))\n        tide_df = pd.concat([tide_df, ensemble_df]).query(\"tide_model in @models_requested\")\n\n    # Optionally convert to a wide format dataframe with a tide model in\n    # each dataframe column\n    if output_format == \"wide\":\n        # Pivot into wide format with each time model as a column\n        print(\"Converting to a wide format dataframe\")\n        tide_df = tide_df.pivot(columns=\"tide_model\", values=\"tide_height\")\n\n        # If in 'one-to-one' mode, reindex using our input time/x/y\n        # values to ensure the output is sorted the same as our inputs\n        if mode == \"one-to-one\":\n            output_indices = pd.MultiIndex.from_arrays([time, x, y], names=[\"time\", \"x\", \"y\"])\n            tide_df = tide_df.reindex(output_indices)\n\n    return tide_df\n
"},{"location":"api/#eo_tides.model.model_tides(x)","title":"x","text":""},{"location":"api/#eo_tides.model.model_tides(y)","title":"y","text":""},{"location":"api/#eo_tides.model.model_tides(time)","title":"time","text":""},{"location":"api/#eo_tides.model.model_tides(model)","title":"model","text":""},{"location":"api/#eo_tides.model.model_tides(directory)","title":"directory","text":""},{"location":"api/#eo_tides.model.model_tides(crs)","title":"crs","text":""},{"location":"api/#eo_tides.model.model_tides(mode)","title":"mode","text":""},{"location":"api/#eo_tides.model.model_tides(output_format)","title":"output_format","text":""},{"location":"api/#eo_tides.model.model_tides(output_units)","title":"output_units","text":""},{"location":"api/#eo_tides.model.model_tides(method)","title":"method","text":""},{"location":"api/#eo_tides.model.model_tides(extrapolate)","title":"extrapolate","text":""},{"location":"api/#eo_tides.model.model_tides(cutoff)","title":"cutoff","text":""},{"location":"api/#eo_tides.model.model_tides(crop)","title":"crop","text":""},{"location":"api/#eo_tides.model.model_tides(crop_buffer)","title":"crop_buffer","text":""},{"location":"api/#eo_tides.model.model_tides(parallel)","title":"parallel","text":""},{"location":"api/#eo_tides.model.model_tides(parallel_splits)","title":"parallel_splits","text":""},{"location":"api/#eo_tides.model.model_tides(parallel_max)","title":"parallel_max","text":""},{"location":"api/#eo_tides.model.model_tides(ensemble_models)","title":"ensemble_models","text":""},{"location":"api/#eo_tides.model.model_tides(**ensemble_kwargs)","title":"**ensemble_kwargs","text":""},{"location":"api/#eo_tides.eo","title":"eo_tides.eo","text":"

Functions:

Name Description pixel_tides

Model tide heights for every pixel in a multi-dimensional

tag_tides

Model tide heights for every timestep in a multi-dimensional

"},{"location":"api/#eo_tides.eo.pixel_tides","title":"pixel_tides","text":"
pixel_tides(\n    data,\n    time=None,\n    model=\"EOT20\",\n    directory=None,\n    resample=True,\n    calculate_quantiles=None,\n    resolution=None,\n    buffer=None,\n    resample_method=\"bilinear\",\n    dask_chunks=None,\n    dask_compute=True,\n    **model_tides_kwargs\n)\n

Model tide heights for every pixel in a multi-dimensional dataset, using one or more ocean tide models.

This function models tides into a low-resolution tide modelling grid covering the spatial extent of the input data (buffered to reduce potential edge effects). These modelled tides can then be resampled back into the original higher resolution dataset's extent and resolution to produce a modelled tide height for every pixel through time.

This function uses the parallelised model_tides function under the hood. It supports all tidal models supported by pyTMD, including:

  • Empirical Ocean Tide model (EOT20)
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)

This function requires access to tide model data files. These should be placed in a folder with subfolders matching the structure required by pyTMD. For more details: https://geoscienceaustralia.github.io/eo-tides/setup/ https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories

Parameters:

Name Type Description Default Dataset or DataArray or GeoBox

A multi-dimensional dataset or GeoBox pixel grid that will be used to define the spatial tide modelling grid. If data is an xarray object, it should include a \"time\" dimension. If no \"time\" dimension exists or if data is a GeoBox, then times must be passed using the time parameter.

required DatetimeLike

By default, tides will be modelled using times from the \"time\" dimension of data. Alternatively, this param can be used to provide a custom set of times. Accepts any format that can be converted by pandas.to_datetime(). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")

None str or list of str

The tide model (or list of models) to use to model tides. If a list is provided, a new \"tide_model\" dimension will be added to the xarray.DataArray outputs. Defaults to \"EOT20\"; specify \"all\" to use all models available in directory. For a full list of available and supported models, run eo_tides.model.list_models.

'EOT20' str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None bool

Whether to resample low resolution tides back into data's original higher resolution grid. Set this to False if you do not want low resolution tides to be re-projected back to higher resolution.

True tuple of float or numpy.ndarray

Rather than returning all individual tides, low-resolution tides can be first aggregated using a quantile calculation by passing in a tuple or array of quantiles to compute. For example, this could be used to calculate the min/max tide across all times: calculate_quantiles=(0.0, 1.0).

None float

The desired resolution of the low-resolution grid used for tide modelling. The default None will create a 5000 m resolution grid if data has a projected CRS (i.e. metre units), or a 0.05 degree resolution grid if data has a geographic CRS (e.g. degree units). Note: higher resolutions do not necessarily provide better tide modelling performance, as results will be limited by the resolution of the underlying global tide model (e.g. 1/16th degree / ~5 km resolution grid for FES2014).

None float

The amount by which to buffer the higher resolution grid extent when creating the new low resolution grid. This buffering ensures that modelled tides are seamless across analysis boundaries. This buffer is eventually be clipped away when the low-resolution modelled tides are re-projected back to the original resolution and extent of data. To ensure that at least two low-resolution grid pixels occur outside of the dataset bounds, the default None applies a 12000 m buffer if data has a projected CRS (i.e. metre units), or a 0.12 degree buffer if data has a geographic CRS (e.g. degree units).

None str

If resampling is requested (see resample above), use this resampling method when converting from low resolution to high resolution pixels. Defaults to \"bilinear\"; valid options include \"nearest\", \"cubic\", \"min\", \"max\", \"average\" etc.

'bilinear' tuple of float

Can be used to configure custom Dask chunking for the final resampling step. By default, chunks will be automatically set to match y/x chunks from data if they exist; otherwise chunks will be chosen to cover the entire y/x extent of the dataset. For custom chunks, provide a tuple in the form (y, x), e.g. (2048, 2048).

None bool

Whether to compute results of the resampling step using Dask. If False, tides_highres will be returned as a Dask array.

True

Optional parameters passed to the eo_tides.model.model_tides function. Important parameters include cutoff (used to extrapolate modelled tides away from the coast; defaults to np.inf), crop (whether to crop tide model constituent files on-the-fly to improve performance) etc.

{}

Returns:

Name Type Description tides_da DataArray

A three-dimensional tide height array. If resample=True (default), a high-resolution array of tide heights will be returned that matches the exact spatial resolution and extents of data. This will contain either tide heights for every timestep in data (or in times if provided), or tide height quantiles for every quantile provided by calculate_quantiles. If resample=False, results for the intermediate low-resolution tide modelling grid will be returned instead.

Source code in eo_tides/eo.py
def pixel_tides(\n    data: xr.Dataset | xr.DataArray | GeoBox,\n    time: DatetimeLike | None = None,\n    model: str | list[str] = \"EOT20\",\n    directory: str | os.PathLike | None = None,\n    resample: bool = True,\n    calculate_quantiles: np.ndarray | tuple[float, float] | None = None,\n    resolution: float | None = None,\n    buffer: float | None = None,\n    resample_method: str = \"bilinear\",\n    dask_chunks: tuple[float, float] | None = None,\n    dask_compute: bool = True,\n    **model_tides_kwargs,\n) -> xr.DataArray:\n    \"\"\"\n    Model tide heights for every pixel in a multi-dimensional\n    dataset, using one or more ocean tide models.\n\n    This function models tides into a low-resolution tide\n    modelling grid covering the spatial extent of the input\n    data (buffered to reduce potential edge effects). These\n    modelled tides can then be resampled back into the original\n    higher resolution dataset's extent and resolution to\n    produce a modelled tide height for every pixel through time.\n\n    This function uses the parallelised `model_tides` function\n    under the hood. It supports all tidal models supported by\n    `pyTMD`, including:\n\n    - Empirical Ocean Tide model (EOT20)\n    - Finite Element Solution tide models (FES2022, FES2014, FES2012)\n    - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)\n    - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)\n    - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)\n\n    This function requires access to tide model data files.\n    These should be placed in a folder with subfolders matching\n    the structure required by `pyTMD`. For more details:\n    <https://geoscienceaustralia.github.io/eo-tides/setup/>\n    <https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>\n\n    Parameters\n    ----------\n    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox\n        A multi-dimensional dataset or GeoBox pixel grid that will\n        be used to define the spatial tide modelling grid. If `data`\n        is an xarray object, it should include a \"time\" dimension.\n        If no \"time\" dimension exists or if `data` is a GeoBox,\n        then times must be passed using the `time` parameter.\n    time : DatetimeLike, optional\n        By default, tides will be modelled using times from the\n        \"time\" dimension of `data`. Alternatively, this param can\n        be used to provide a custom set of times. Accepts any format\n        that can be converted by `pandas.to_datetime()`. For example:\n        `time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")`\n    model : str or list of str, optional\n        The tide model (or list of models) to use to model tides.\n        If a list is provided, a new \"tide_model\" dimension will be\n        added to the `xarray.DataArray` outputs. Defaults to \"EOT20\";\n        specify \"all\" to use all models available in `directory`.\n        For a full list of available and supported models, run\n        `eo_tides.model.list_models`.\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    resample : bool, optional\n        Whether to resample low resolution tides back into `data`'s original\n        higher resolution grid. Set this to `False` if you do not want\n        low resolution tides to be re-projected back to higher resolution.\n    calculate_quantiles : tuple of float or numpy.ndarray, optional\n        Rather than returning all individual tides, low-resolution tides\n        can be first aggregated using a quantile calculation by passing in\n        a tuple or array of quantiles to compute. For example, this could\n        be used to calculate the min/max tide across all times:\n        `calculate_quantiles=(0.0, 1.0)`.\n    resolution : float, optional\n        The desired resolution of the low-resolution grid used for tide\n        modelling. The default None will create a 5000 m resolution grid\n        if `data` has a projected CRS (i.e. metre units), or a 0.05 degree\n        resolution grid if `data` has a geographic CRS (e.g. degree units).\n        Note: higher resolutions do not necessarily provide better\n        tide modelling performance, as results will be limited by the\n        resolution of the underlying global tide model (e.g. 1/16th\n        degree / ~5 km resolution grid for FES2014).\n    buffer : float, optional\n        The amount by which to buffer the higher resolution grid extent\n        when creating the new low resolution grid. This buffering\n        ensures that modelled tides are seamless across analysis\n        boundaries. This buffer is eventually be clipped away when\n        the low-resolution modelled tides are re-projected back to the\n        original resolution and extent of `data`. To ensure that at least\n        two low-resolution grid pixels occur outside of the dataset\n        bounds, the default None applies a 12000 m buffer if `data` has a\n        projected CRS (i.e. metre units), or a 0.12 degree buffer if\n        `data` has a geographic CRS (e.g. degree units).\n    resample_method : str, optional\n        If resampling is requested (see `resample` above), use this\n        resampling method when converting from low resolution to high\n        resolution pixels. Defaults to \"bilinear\"; valid options include\n        \"nearest\", \"cubic\", \"min\", \"max\", \"average\" etc.\n    dask_chunks : tuple of float, optional\n        Can be used to configure custom Dask chunking for the final\n        resampling step. By default, chunks will be automatically set\n        to match y/x chunks from `data` if they exist; otherwise chunks\n        will be chosen to cover the entire y/x extent of the dataset.\n        For custom chunks, provide a tuple in the form `(y, x)`, e.g.\n        `(2048, 2048)`.\n    dask_compute : bool, optional\n        Whether to compute results of the resampling step using Dask.\n        If False, `tides_highres` will be returned as a Dask array.\n    **model_tides_kwargs :\n        Optional parameters passed to the `eo_tides.model.model_tides`\n        function. Important parameters include `cutoff` (used to\n        extrapolate modelled tides away from the coast; defaults to\n        `np.inf`), `crop` (whether to crop tide model constituent files\n        on-the-fly to improve performance) etc.\n    Returns\n    -------\n    tides_da : xr.DataArray\n        A three-dimensional tide height array.\n        If `resample=True` (default), a high-resolution array of tide\n        heights will be returned that matches the exact spatial resolution\n        and extents of `data`. This will contain either tide heights for\n        every timestep in `data` (or in `times` if provided), or tide height\n        quantiles for every quantile provided by `calculate_quantiles`.\n        If `resample=False`, results for the intermediate low-resolution\n        tide modelling grid will be returned instead.\n    \"\"\"\n    # Standardise data inputs, time and models\n    gbox, time_coords = _standardise_inputs(data, time)\n    dask_chunks = _resample_chunks(data, dask_chunks)\n    model = [model] if isinstance(model, str) else model\n\n    # Determine spatial dimensions\n    y_dim, x_dim = gbox.dimensions\n\n    # Determine resolution and buffer, using different defaults for\n    # geographic (i.e. degrees) and projected (i.e. metres) CRSs:\n    assert gbox.crs is not None\n    crs_units = gbox.crs.units[0][0:6]\n    if gbox.crs.geographic:\n        if resolution is None:\n            resolution = 0.05\n        elif resolution > 360:\n            raise ValueError(\n                f\"A resolution of greater than 360 was \"\n                f\"provided, but `data` has a geographic CRS \"\n                f\"in {crs_units} units. Did you accidently \"\n                f\"provide a resolution in projected \"\n                f\"(i.e. metre) units?\",\n            )\n        if buffer is None:\n            buffer = 0.12\n    else:\n        if resolution is None:\n            resolution = 5000\n        elif resolution < 1:\n            raise ValueError(\n                f\"A resolution of less than 1 was provided, \"\n                f\"but `data` has a projected CRS in \"\n                f\"{crs_units} units. Did you accidently \"\n                f\"provide a resolution in geographic \"\n                f\"(degree) units?\",\n            )\n        if buffer is None:\n            buffer = 12000\n\n    # Raise error if resolution is less than dataset resolution\n    dataset_res = gbox.resolution.x\n    if resolution < dataset_res:\n        raise ValueError(\n            f\"The resolution of the low-resolution tide \"\n            f\"modelling grid ({resolution:.2f}) is less \"\n            f\"than `data`'s pixel resolution ({dataset_res:.2f}). \"\n            f\"This can cause extremely slow tide modelling \"\n            f\"performance. Please select provide a resolution \"\n            f\"greater than {dataset_res:.2f} using \"\n            f\"`pixel_tides`'s 'resolution' parameter.\",\n        )\n\n    # Create a new reduced resolution tide modelling grid after\n    # first buffering the grid\n    print(f\"Creating reduced resolution {resolution} x {resolution} {crs_units} tide modelling array\")\n    buffered_geobox = gbox.buffered(buffer)\n    rescaled_geobox = GeoBox.from_bbox(bbox=buffered_geobox.boundingbox, resolution=resolution)\n    rescaled_ds = odc.geo.xr.xr_zeros(rescaled_geobox)\n\n    # Flatten grid to 1D, then add time dimension\n    flattened_ds = rescaled_ds.stack(z=(x_dim, y_dim))\n    flattened_ds = flattened_ds.expand_dims(dim={\"time\": time_coords})\n\n    # Model tides in parallel, returning a pandas.DataFrame\n    tide_df = model_tides(\n        x=flattened_ds[x_dim],\n        y=flattened_ds[y_dim],\n        time=flattened_ds.time,\n        crs=f\"EPSG:{gbox.crs.epsg}\",\n        model=model,\n        directory=directory,\n        **model_tides_kwargs,\n    )\n\n    # Convert our pandas.DataFrame tide modelling outputs to xarray\n    tides_lowres = (\n        # Rename x and y dataframe indexes to match x and y xarray dims\n        tide_df.rename_axis([\"time\", x_dim, y_dim])\n        # Add tide model column to dataframe indexes so we can convert\n        # our dataframe to a multidimensional xarray\n        .set_index(\"tide_model\", append=True)\n        # Convert to xarray and select our tide modelling xr.DataArray\n        .to_xarray()\n        .tide_height\n        # Re-index and transpose into our input coordinates and dim order\n        .reindex_like(rescaled_ds)\n        .transpose(\"tide_model\", \"time\", y_dim, x_dim)\n    )\n\n    # Optionally calculate and return quantiles rather than raw data.\n    # Set dtype to dtype of the input data as quantile always returns\n    # float64 (memory intensive)\n    if calculate_quantiles is not None:\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\")\n            print(\"Computing tide quantiles\")\n            tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim=\"time\").astype(tides_lowres.dtype)\n\n    # If only one tidal model exists, squeeze out \"tide_model\" dim\n    if len(tides_lowres.tide_model) == 1:\n        tides_lowres = tides_lowres.squeeze(\"tide_model\")\n\n    # Ensure CRS is present before we apply any resampling\n    tides_lowres = tides_lowres.odc.assign_crs(gbox.crs)\n\n    # Reproject into original high resolution grid\n    if resample:\n        print(\"Reprojecting tides into original resolution\")\n        tides_highres = _pixel_tides_resample(\n            tides_lowres,\n            gbox,\n            resample_method,\n            dask_chunks,\n            dask_compute,\n        )\n        return tides_highres\n\n    print(\"Returning low resolution tide array\")\n    return tides_lowres\n
"},{"location":"api/#eo_tides.eo.pixel_tides(data)","title":"data","text":""},{"location":"api/#eo_tides.eo.pixel_tides(time)","title":"time","text":""},{"location":"api/#eo_tides.eo.pixel_tides(model)","title":"model","text":""},{"location":"api/#eo_tides.eo.pixel_tides(directory)","title":"directory","text":""},{"location":"api/#eo_tides.eo.pixel_tides(resample)","title":"resample","text":""},{"location":"api/#eo_tides.eo.pixel_tides(calculate_quantiles)","title":"calculate_quantiles","text":""},{"location":"api/#eo_tides.eo.pixel_tides(resolution)","title":"resolution","text":""},{"location":"api/#eo_tides.eo.pixel_tides(buffer)","title":"buffer","text":""},{"location":"api/#eo_tides.eo.pixel_tides(resample_method)","title":"resample_method","text":""},{"location":"api/#eo_tides.eo.pixel_tides(dask_chunks)","title":"dask_chunks","text":""},{"location":"api/#eo_tides.eo.pixel_tides(dask_compute)","title":"dask_compute","text":""},{"location":"api/#eo_tides.eo.pixel_tides(**model_tides_kwargs)","title":"**model_tides_kwargs","text":""},{"location":"api/#eo_tides.eo.tag_tides","title":"tag_tides","text":"
tag_tides(\n    data,\n    time=None,\n    model=\"EOT20\",\n    directory=None,\n    tidepost_lat=None,\n    tidepost_lon=None,\n    **model_tides_kwargs\n)\n

Model tide heights for every timestep in a multi-dimensional dataset, and return a new tide_height array that can be used to \"tag\" each observation with tide heights.

The function models tides at the centroid of the dataset by default, but a custom tidal modelling location can be specified using tidepost_lat and tidepost_lon.

This function uses the parallelised model_tides function under the hood. It supports all tidal models supported by pyTMD, including:

  • Empirical Ocean Tide model (EOT20)
  • Finite Element Solution tide models (FES2022, FES2014, FES2012)
  • TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
  • Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
  • Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)

Parameters:

Name Type Description Default Dataset or DataArray or GeoBox

A multi-dimensional dataset or GeoBox pixel grid that will be used to define the tide modelling location. If data is an xarray object, it should include a \"time\" dimension. If no \"time\" dimension exists or if data is a GeoBox, then times must be passed using the time parameter.

required DatetimeLike

By default, tides will be modelled using times from the \"time\" dimension of data. Alternatively, this param can be used to provide a custom set of times. Accepts any format that can be converted by pandas.to_datetime(). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")

None str or list of str

The tide model (or list of models) to use to model tides. If a list is provided, a new \"tide_model\" dimension will be added to the xarray.DataArray outputs. Defaults to \"EOT20\"; specify \"all\" to use all models available in directory. For a full list of available and supported models, run eo_tides.model.list_models.

'EOT20' str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None float

Optional coordinates used to model tides. The default is None, which uses the centroid of the dataset as the tide modelling location.

None float

Optional coordinates used to model tides. The default is None, which uses the centroid of the dataset as the tide modelling location.

None

Optional parameters passed to the eo_tides.model.model_tides function. Important parameters include cutoff (used to extrapolate modelled tides away from the coast; defaults to np.inf), crop (whether to crop tide model constituent files on-the-fly to improve performance) etc.

{}

Returns:

Name Type Description tides_da DataArray

A one-dimensional tide height array. This will contain either tide heights for every timestep in data, or for every time in times if provided.

Source code in eo_tides/eo.py
def tag_tides(\n    data: xr.Dataset | xr.DataArray | GeoBox,\n    time: DatetimeLike | None = None,\n    model: str | list[str] = \"EOT20\",\n    directory: str | os.PathLike | None = None,\n    tidepost_lat: float | None = None,\n    tidepost_lon: float | None = None,\n    **model_tides_kwargs,\n) -> xr.DataArray:\n    \"\"\"\n    Model tide heights for every timestep in a multi-dimensional\n    dataset, and return a new `tide_height` array that can\n    be used to \"tag\" each observation with tide heights.\n\n    The function models tides at the centroid of the dataset\n    by default, but a custom tidal modelling location can\n    be specified using `tidepost_lat` and `tidepost_lon`.\n\n    This function uses the parallelised `model_tides` function\n    under the hood. It supports all tidal models supported by\n    `pyTMD`, including:\n\n    - Empirical Ocean Tide model (EOT20)\n    - Finite Element Solution tide models (FES2022, FES2014, FES2012)\n    - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)\n    - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)\n    - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)\n\n    Parameters\n    ----------\n    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox\n        A multi-dimensional dataset or GeoBox pixel grid that will\n        be used to define the tide modelling location. If `data`\n        is an xarray object, it should include a \"time\" dimension.\n        If no \"time\" dimension exists or if `data` is a GeoBox,\n        then times must be passed using the `time` parameter.\n    time : DatetimeLike, optional\n        By default, tides will be modelled using times from the\n        \"time\" dimension of `data`. Alternatively, this param can\n        be used to provide a custom set of times. Accepts any format\n        that can be converted by `pandas.to_datetime()`. For example:\n        `time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")`\n    model : str or list of str, optional\n        The tide model (or list of models) to use to model tides.\n        If a list is provided, a new \"tide_model\" dimension will be\n        added to the `xarray.DataArray` outputs. Defaults to \"EOT20\";\n        specify \"all\" to use all models available in `directory`.\n        For a full list of available and supported models, run\n        `eo_tides.model.list_models`.\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    tidepost_lat, tidepost_lon : float, optional\n        Optional coordinates used to model tides. The default is None,\n        which uses the centroid of the dataset as the tide modelling\n        location.\n    **model_tides_kwargs :\n        Optional parameters passed to the `eo_tides.model.model_tides`\n        function. Important parameters include `cutoff` (used to\n        extrapolate modelled tides away from the coast; defaults to\n        `np.inf`), `crop` (whether to crop tide model constituent files\n        on-the-fly to improve performance) etc.\n\n    Returns\n    -------\n    tides_da : xr.DataArray\n        A one-dimensional tide height array. This will contain either\n        tide heights for every timestep in `data`, or for every time in\n        `times` if provided.\n    \"\"\"\n    # Standardise data inputs, time and models\n    gbox, time_coords = _standardise_inputs(data, time)\n    model = [model] if isinstance(model, str) else model\n\n    # If custom tide posts are not provided, use dataset centroid\n    if tidepost_lat is None or tidepost_lon is None:\n        lon, lat = gbox.geographic_extent.centroid.coords[0]\n        print(f\"Setting tide modelling location from dataset centroid: {lon:.2f}, {lat:.2f}\")\n    else:\n        lon, lat = tidepost_lon, tidepost_lat\n        print(f\"Using tide modelling location: {lon:.2f}, {lat:.2f}\")\n\n    # Model tide heights for each observation:\n    tide_df = model_tides(\n        x=lon,  # type: ignore\n        y=lat,  # type: ignore\n        time=time_coords,\n        model=model,\n        directory=directory,\n        crs=\"EPSG:4326\",\n        **model_tides_kwargs,\n    )\n\n    # If tides cannot be successfully modeled (e.g. if the centre of the\n    # xarray dataset is located is over land), raise an exception\n    if tide_df.tide_height.isnull().all():\n        raise ValueError(\n            f\"Tides could not be modelled for dataset centroid located \"\n            f\"at {tidepost_lon:.2f}, {tidepost_lat:.2f}. This can occur if \"\n            f\"this coordinate occurs over land. Please manually specify \"\n            f\"a tide modelling location located over water using the \"\n            f\"`tidepost_lat` and `tidepost_lon` parameters.\"\n        )\n\n    # Convert to xarray format\n    tides_da = tide_df.reset_index().set_index([\"time\", \"tide_model\"]).drop([\"x\", \"y\"], axis=1).tide_height.to_xarray()\n\n    # If only one tidal model exists, squeeze out \"tide_model\" dim\n    if len(tides_da.tide_model) == 1:\n        tides_da = tides_da.squeeze(\"tide_model\")\n\n    return tides_da\n
"},{"location":"api/#eo_tides.eo.tag_tides(data)","title":"data","text":""},{"location":"api/#eo_tides.eo.tag_tides(time)","title":"time","text":""},{"location":"api/#eo_tides.eo.tag_tides(model)","title":"model","text":""},{"location":"api/#eo_tides.eo.tag_tides(directory)","title":"directory","text":""},{"location":"api/#eo_tides.eo.tag_tides(tidepost_lat)","title":"tidepost_lat","text":""},{"location":"api/#eo_tides.eo.tag_tides(tidepost_lon)","title":"tidepost_lon","text":""},{"location":"api/#eo_tides.eo.tag_tides(**model_tides_kwargs)","title":"**model_tides_kwargs","text":""},{"location":"api/#eo_tides.stats","title":"eo_tides.stats","text":"

Functions:

Name Description pixel_stats

Takes a multi-dimensional dataset and generate two-dimensional

tide_stats

Takes a multi-dimensional dataset and generate tide statistics

"},{"location":"api/#eo_tides.stats.pixel_stats","title":"pixel_stats","text":"
pixel_stats(\n    data,\n    time=None,\n    model=\"EOT20\",\n    directory=None,\n    resample=False,\n    modelled_freq=\"3h\",\n    min_max_q=(0.0, 1.0),\n    extrapolate=True,\n    cutoff=10,\n    **pixel_tides_kwargs\n)\n

Takes a multi-dimensional dataset and generate two-dimensional tide statistics and satellite-observed tide bias metrics, calculated based on every timestep in the satellte data and modelled into the spatial extent of the imagery.

By comparing the subset of tides observed by satellites against the full astronomical tidal range, we can evaluate whether the tides observed by satellites are biased (e.g. fail to observe either the highest or lowest tides).

Compared to tide_stats, this function models tide metrics spatially to produce a two-dimensional output.

For more information about the tidal statistics computed by this function, refer to Figure 8 in Bishop-Taylor et al. 2018: https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8

Parameters:

Name Type Description Default Dataset or DataArray or GeoBox

A multi-dimensional dataset or GeoBox pixel grid that will be used to calculate 2D tide statistics. If data is an xarray object, it should include a \"time\" dimension. If no \"time\" dimension exists or if data is a GeoBox, then times must be passed using the time parameter.

required DatetimeLike

By default, tides will be modelled using times from the \"time\" dimension of data. Alternatively, this param can be used to provide a custom set of times. Accepts any format that can be converted by pandas.to_datetime(). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")

None str or list of str

The tide model (or list of models) to use to model tides. If a list is provided, a new \"tide_model\" dimension will be added to the xarray.DataArray outputs. Defaults to \"EOT20\"; specify \"all\" to use all models available in directory. For a full list of available and supported models, run eo_tides.model.list_models.

'EOT20' str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None bool

Whether to resample tide statistics back into data's original higher resolution grid. Defaults to False, which will return lower-resolution statistics that are typically sufficient for most purposes.

False str

An optional string giving the frequency at which to model tides when computing the full modelled tidal range. Defaults to '3h', which computes a tide height for every three hours across the temporal extent of data.

'3h' tuple

Quantiles used to calculate max and min observed and modelled astronomical tides. By default (0.0, 1.0) which is equivalent to minimum and maximum; to use a softer threshold that is more robust to outliers, use e.g. (0.1, 0.9).

(0.0, 1.0) bool

Whether to extrapolate tides for x and y coordinates outside of the valid tide modelling domain using nearest-neighbor. Defaults to True.

True float

Extrapolation cutoff in kilometers. To avoid producing tide statistics too far inland, the default is 10 km.

10

Optional parameters passed to the eo_tides.eo.pixel_tides function.

{}

Returns:

Name Type Description stats_ds Dataset

An xarray.Dataset containing the following statistics as two-dimensional data variables:

  • lot: minimum tide height observed by the satellite (metres)
  • lat: minimum tide height from modelled astronomical tidal range (metres)
  • hot: maximum tide height observed by the satellite (metres)
  • hat: maximum tide height from modelled astronomical tidal range (metres)
  • otr: tidal range observed by the satellite (metres)
  • tr: modelled astronomical tide range (metres)
  • spread: proportion of the full modelled tidal range observed by the satellite
  • offset_low: proportion of the lowest tides never observed by the satellite
  • offset_high: proportion of the highest tides never observed by the satellite
Source code in eo_tides/stats.py
def pixel_stats(\n    data: xr.Dataset | xr.DataArray | GeoBox,\n    time: DatetimeLike | None = None,\n    model: str | list[str] = \"EOT20\",\n    directory: str | os.PathLike | None = None,\n    resample: bool = False,\n    modelled_freq: str = \"3h\",\n    min_max_q: tuple[float, float] = (0.0, 1.0),\n    extrapolate: bool = True,\n    cutoff: float = 10,\n    **pixel_tides_kwargs,\n) -> xr.Dataset:\n    \"\"\"\n    Takes a multi-dimensional dataset and generate two-dimensional\n    tide statistics and satellite-observed tide bias metrics,\n    calculated based on every timestep in the satellte data and\n    modelled into the spatial extent of the imagery.\n\n    By comparing the subset of tides observed by satellites\n    against the full astronomical tidal range, we can evaluate\n    whether the tides observed by satellites are biased\n    (e.g. fail to observe either the highest or lowest tides).\n\n    Compared to `tide_stats`, this function models tide metrics\n    spatially to produce a two-dimensional output.\n\n    For more information about the tidal statistics computed by this\n    function, refer to Figure 8 in Bishop-Taylor et al. 2018:\n    <https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8>\n\n    Parameters\n    ----------\n    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox\n        A multi-dimensional dataset or GeoBox pixel grid that will\n        be used to calculate 2D tide statistics. If `data`\n        is an xarray object, it should include a \"time\" dimension.\n        If no \"time\" dimension exists or if `data` is a GeoBox,\n        then times must be passed using the `time` parameter.\n    time : DatetimeLike, optional\n        By default, tides will be modelled using times from the\n        \"time\" dimension of `data`. Alternatively, this param can\n        be used to provide a custom set of times. Accepts any format\n        that can be converted by `pandas.to_datetime()`. For example:\n        `time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")`\n    model : str or list of str, optional\n        The tide model (or list of models) to use to model tides.\n        If a list is provided, a new \"tide_model\" dimension will be\n        added to the `xarray.DataArray` outputs. Defaults to \"EOT20\";\n        specify \"all\" to use all models available in `directory`.\n        For a full list of available and supported models, run\n        `eo_tides.model.list_models`.\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    resample : bool, optional\n        Whether to resample tide statistics back into `data`'s original\n        higher resolution grid. Defaults to False, which will return\n        lower-resolution statistics that are typically sufficient for\n        most purposes.\n    modelled_freq : str, optional\n        An optional string giving the frequency at which to model tides\n        when computing the full modelled tidal range. Defaults to '3h',\n        which computes a tide height for every three hours across the\n        temporal extent of `data`.\n    min_max_q : tuple, optional\n        Quantiles used to calculate max and min observed and modelled\n        astronomical tides. By default `(0.0, 1.0)` which is equivalent\n        to minimum and maximum; to use a softer threshold that is more\n        robust to outliers, use e.g. `(0.1, 0.9)`.\n    extrapolate : bool, optional\n        Whether to extrapolate tides for x and y coordinates outside of\n        the valid tide modelling domain using nearest-neighbor. Defaults\n        to True.\n    cutoff : float, optional\n        Extrapolation cutoff in kilometers. To avoid producing tide\n        statistics too far inland, the default is 10 km.\n    **pixel_tides_kwargs :\n        Optional parameters passed to the `eo_tides.eo.pixel_tides`\n        function.\n\n    Returns\n    -------\n    stats_ds : xarray.Dataset\n        An `xarray.Dataset` containing the following statistics as two-dimensional data variables:\n\n        - `lot`: minimum tide height observed by the satellite (metres)\n        - `lat`: minimum tide height from modelled astronomical tidal range (metres)\n        - `hot`: maximum tide height observed by the satellite (metres)\n        - `hat`: maximum tide height from modelled astronomical tidal range (metres)\n        - `otr`: tidal range observed by the satellite (metres)\n        - `tr`: modelled astronomical tide range (metres)\n        - `spread`: proportion of the full modelled tidal range observed by the satellite\n        - `offset_low`: proportion of the lowest tides never observed by the satellite\n        - `offset_high`: proportion of the highest tides never observed by the satellite\n\n    \"\"\"\n    # Standardise data inputs, time and models\n    gbox, time_coords = _standardise_inputs(data, time)\n    model = [model] if isinstance(model, str) else model\n\n    # Model observed tides\n    assert time_coords is not None\n    obs_tides = pixel_tides(\n        gbox,\n        time=time_coords,\n        resample=False,\n        model=model,\n        directory=directory,\n        calculate_quantiles=min_max_q,\n        extrapolate=extrapolate,\n        cutoff=cutoff,\n        **pixel_tides_kwargs,\n    )\n\n    # Generate times covering entire period of satellite record\n    all_timerange = pd.date_range(\n        start=time_coords.min().item(),\n        end=time_coords.max().item(),\n        freq=modelled_freq,\n    )\n\n    # Model all tides\n    all_tides = pixel_tides(\n        gbox,\n        time=all_timerange,\n        model=model,\n        directory=directory,\n        calculate_quantiles=min_max_q,\n        resample=False,\n        extrapolate=extrapolate,\n        cutoff=cutoff,\n        **pixel_tides_kwargs,\n    )\n\n    # # Calculate means\n    # TODO: Find way to make this work with `calculate_quantiles`\n    # mot = obs_tides.mean(dim=\"time\")\n    # mat = all_tides.mean(dim=\"time\")\n\n    # Calculate min and max tides\n    lot = obs_tides.isel(quantile=0)\n    hot = obs_tides.isel(quantile=-1)\n    lat = all_tides.isel(quantile=0)\n    hat = all_tides.isel(quantile=-1)\n\n    # Calculate tidal range\n    otr = hot - lot\n    tr = hat - lat\n\n    # Calculate Bishop-Taylor et al. 2018 tidal metrics\n    spread = otr / tr\n    offset_low_m = abs(lat - lot)\n    offset_high_m = abs(hat - hot)\n    offset_low = offset_low_m / tr\n    offset_high = offset_high_m / tr\n\n    # Combine into a single dataset\n    stats_ds = (\n        xr.merge(\n            [\n                # mot.rename(\"mot\"),\n                # mat.rename(\"mat\"),\n                hot.rename(\"hot\"),\n                hat.rename(\"hat\"),\n                lot.rename(\"lot\"),\n                lat.rename(\"lat\"),\n                otr.rename(\"otr\"),\n                tr.rename(\"tr\"),\n                spread.rename(\"spread\"),\n                offset_low.rename(\"offset_low\"),\n                offset_high.rename(\"offset_high\"),\n            ],\n            compat=\"override\",\n        )\n        .drop_vars(\"quantile\")\n        .odc.assign_crs(crs=gbox.crs)\n    )\n\n    # Optionally resample into the original pixel grid of `data`\n    if resample:\n        stats_ds = stats_ds.odc.reproject(how=gbox, resample_method=\"bilinear\")\n\n    return stats_ds\n
"},{"location":"api/#eo_tides.stats.pixel_stats(data)","title":"data","text":""},{"location":"api/#eo_tides.stats.pixel_stats(time)","title":"time","text":""},{"location":"api/#eo_tides.stats.pixel_stats(model)","title":"model","text":""},{"location":"api/#eo_tides.stats.pixel_stats(directory)","title":"directory","text":""},{"location":"api/#eo_tides.stats.pixel_stats(resample)","title":"resample","text":""},{"location":"api/#eo_tides.stats.pixel_stats(modelled_freq)","title":"modelled_freq","text":""},{"location":"api/#eo_tides.stats.pixel_stats(min_max_q)","title":"min_max_q","text":""},{"location":"api/#eo_tides.stats.pixel_stats(extrapolate)","title":"extrapolate","text":""},{"location":"api/#eo_tides.stats.pixel_stats(cutoff)","title":"cutoff","text":""},{"location":"api/#eo_tides.stats.pixel_stats(**pixel_tides_kwargs)","title":"**pixel_tides_kwargs","text":""},{"location":"api/#eo_tides.stats.tide_stats","title":"tide_stats","text":"
tide_stats(\n    data,\n    time=None,\n    model=\"EOT20\",\n    directory=None,\n    tidepost_lat=None,\n    tidepost_lon=None,\n    plain_english=True,\n    plot=True,\n    plot_col=None,\n    modelled_freq=\"3h\",\n    linear_reg=False,\n    min_max_q=(0.0, 1.0),\n    round_stats=3,\n    **model_tides_kwargs\n)\n

Takes a multi-dimensional dataset and generate tide statistics and satellite-observed tide bias metrics, calculated based on every timestep in the satellte data and the geographic centroid of the imagery.

By comparing the subset of tides observed by satellites against the full astronomical tidal range, we can evaluate whether the tides observed by satellites are biased (e.g. fail to observe either the highest or lowest tides).

For more information about the tidal statistics computed by this function, refer to Figure 8 in Bishop-Taylor et al. 2018: https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8

Parameters:

Name Type Description Default Dataset or DataArray or GeoBox

A multi-dimensional dataset or GeoBox pixel grid that will be used to calculate tide statistics. If data is an xarray object, it should include a \"time\" dimension. If no \"time\" dimension exists or if data is a GeoBox, then times must be passed using the time parameter.

required DatetimeLike

By default, tides will be modelled using times from the \"time\" dimension of data. Alternatively, this param can be used to provide a custom set of times. Accepts any format that can be converted by pandas.to_datetime(). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")

None str

The tide model to use to model tides. Defaults to \"EOT20\"; for a full list of available/supported models, run eo_tides.model.list_models.

'EOT20' str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None float or int

Optional coordinates used to model tides. The default is None, which uses the centroid of the dataset as the tide modelling location.

None float or int

Optional coordinates used to model tides. The default is None, which uses the centroid of the dataset as the tide modelling location.

None bool

An optional boolean indicating whether to print a plain english version of the tidal statistics to the screen. Defaults to True.

True bool

An optional boolean indicating whether to plot how satellite- observed tide heights compare against the full tidal range. Defaults to True.

True str

Optional name of a coordinate, dimension or variable in the array that will be used to plot observations with unique symbols. Defaults to None, which will plot all observations as circles.

None str

An optional string giving the frequency at which to model tides when computing the full modelled tidal range. Defaults to '3h', which computes a tide height for every three hours across the temporal extent of data.

'3h' bool

Whether to return linear regression statistics that assess whether satellite-observed tides show any decreasing or increasing trends over time. This may indicate whether your satellite data may produce misleading trends based on uneven sampling of the local tide regime.

False tuple

Quantiles used to calculate max and min observed and modelled astronomical tides. By default (0.0, 1.0) which is equivalent to minimum and maximum; to use a softer threshold that is more robust to outliers, use e.g. (0.1, 0.9).

(0.0, 1.0) int

The number of decimal places used to round the output statistics. Defaults to 3.

3

Optional parameters passed to the eo_tides.model.model_tides function. Important parameters include cutoff (used to extrapolate modelled tides away from the coast; defaults to np.inf), crop (whether to crop tide model constituent files on-the-fly to improve performance) etc.

{}

Returns:

Name Type Description stats_df Series

A pandas.Series containing the following statistics:

  • y: latitude used for modelling tide heights
  • x: longitude used for modelling tide heights
  • mot: mean tide height observed by the satellite (metres)
  • mat: mean modelled astronomical tide height (metres)
  • lot: minimum tide height observed by the satellite (metres)
  • lat: minimum tide height from modelled astronomical tidal range (metres)
  • hot: maximum tide height observed by the satellite (metres)
  • hat: maximum tide height from modelled astronomical tidal range (metres)
  • otr: tidal range observed by the satellite (metres)
  • tr: modelled astronomical tide range (metres)
  • spread: proportion of the full modelled tidal range observed by the satellite
  • offset_low: proportion of the lowest tides never observed by the satellite
  • offset_high: proportion of the highest tides never observed by the satellite

If linear_reg = True, the output will also contain:

  • observed_slope: slope of any relationship between observed tide heights and time
  • observed_pval: significance/p-value of any relationship between observed tide heights and time
Source code in eo_tides/stats.py
def tide_stats(\n    data: xr.Dataset | xr.DataArray | GeoBox,\n    time: DatetimeLike | None = None,\n    model: str = \"EOT20\",\n    directory: str | os.PathLike | None = None,\n    tidepost_lat: float | None = None,\n    tidepost_lon: float | None = None,\n    plain_english: bool = True,\n    plot: bool = True,\n    plot_col: str | None = None,\n    modelled_freq: str = \"3h\",\n    linear_reg: bool = False,\n    min_max_q: tuple = (0.0, 1.0),\n    round_stats: int = 3,\n    **model_tides_kwargs,\n) -> pd.Series:\n    \"\"\"\n    Takes a multi-dimensional dataset and generate tide statistics\n    and satellite-observed tide bias metrics, calculated based on\n    every timestep in the satellte data and the geographic centroid\n    of the imagery.\n\n    By comparing the subset of tides observed by satellites\n    against the full astronomical tidal range, we can evaluate\n    whether the tides observed by satellites are biased\n    (e.g. fail to observe either the highest or lowest tides).\n\n    For more information about the tidal statistics computed by this\n    function, refer to Figure 8 in Bishop-Taylor et al. 2018:\n    <https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8>\n\n    Parameters\n    ----------\n    data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox\n        A multi-dimensional dataset or GeoBox pixel grid that will\n        be used to calculate tide statistics. If `data` is an\n        xarray object, it should include a \"time\" dimension.\n        If no \"time\" dimension exists or if `data` is a GeoBox,\n        then times must be passed using the `time` parameter.\n    time : DatetimeLike, optional\n        By default, tides will be modelled using times from the\n        \"time\" dimension of `data`. Alternatively, this param can\n        be used to provide a custom set of times. Accepts any format\n        that can be converted by `pandas.to_datetime()`. For example:\n        `time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\")`\n    model : str, optional\n        The tide model to use to model tides. Defaults to \"EOT20\";\n        for a full list of available/supported models, run\n        `eo_tides.model.list_models`.\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    tidepost_lat, tidepost_lon : float or int, optional\n        Optional coordinates used to model tides. The default is None,\n        which uses the centroid of the dataset as the tide modelling\n        location.\n    plain_english : bool, optional\n        An optional boolean indicating whether to print a plain english\n        version of the tidal statistics to the screen. Defaults to True.\n    plot : bool, optional\n        An optional boolean indicating whether to plot how satellite-\n        observed tide heights compare against the full tidal range.\n        Defaults to True.\n    plot_col : str, optional\n        Optional name of a coordinate, dimension or variable in the array\n        that will be used to plot observations with unique symbols.\n        Defaults to None, which will plot all observations as circles.\n    modelled_freq : str, optional\n        An optional string giving the frequency at which to model tides\n        when computing the full modelled tidal range. Defaults to '3h',\n        which computes a tide height for every three hours across the\n        temporal extent of `data`.\n    linear_reg: bool, optional\n        Whether to return linear regression statistics that assess\n        whether satellite-observed tides show any decreasing  or\n        increasing trends over time. This may indicate whether your\n        satellite data may produce misleading trends based on uneven\n        sampling of the local tide regime.\n    min_max_q : tuple, optional\n        Quantiles used to calculate max and min observed and modelled\n        astronomical tides. By default `(0.0, 1.0)` which is equivalent\n        to minimum and maximum; to use a softer threshold that is more\n        robust to outliers, use e.g. `(0.1, 0.9)`.\n    round_stats : int, optional\n        The number of decimal places used to round the output statistics.\n        Defaults to 3.\n    **model_tides_kwargs :\n        Optional parameters passed to the `eo_tides.model.model_tides`\n        function. Important parameters include `cutoff` (used to\n        extrapolate modelled tides away from the coast; defaults to\n        `np.inf`), `crop` (whether to crop tide model constituent files\n        on-the-fly to improve performance) etc.\n\n    Returns\n    -------\n    stats_df : pandas.Series\n        A `pandas.Series` containing the following statistics:\n\n        - `y`: latitude used for modelling tide heights\n        - `x`: longitude used for modelling tide heights\n        - `mot`: mean tide height observed by the satellite (metres)\n        - `mat`: mean modelled astronomical tide height (metres)\n        - `lot`: minimum tide height observed by the satellite (metres)\n        - `lat`: minimum tide height from modelled astronomical tidal range (metres)\n        - `hot`: maximum tide height observed by the satellite (metres)\n        - `hat`: maximum tide height from modelled astronomical tidal range (metres)\n        - `otr`: tidal range observed by the satellite (metres)\n        - `tr`: modelled astronomical tide range (metres)\n        - `spread`: proportion of the full modelled tidal range observed by the satellite\n        - `offset_low`: proportion of the lowest tides never observed by the satellite\n        - `offset_high`: proportion of the highest tides never observed by the satellite\n\n        If `linear_reg = True`, the output will also contain:\n\n        - `observed_slope`: slope of any relationship between observed tide heights and time\n        - `observed_pval`: significance/p-value of any relationship between observed tide heights and time\n    \"\"\"\n    # Standardise data inputs, time and models\n    gbox, time_coords = _standardise_inputs(data, time)\n\n    # Verify that only one tide model is provided\n    if isinstance(model, list):\n        raise Exception(\"Only single tide models are supported by `tide_stats`.\")\n\n    # If custom tide modelling locations are not provided, use the\n    # dataset centroid\n    if not tidepost_lat or not tidepost_lon:\n        tidepost_lon, tidepost_lat = gbox.geographic_extent.centroid.coords[0]\n\n    # Model tides for each observation in the supplied xarray object\n    assert time_coords is not None\n    obs_tides_da = tag_tides(\n        gbox,\n        time=time_coords,\n        model=model,\n        directory=directory,\n        tidepost_lat=tidepost_lat,  # type: ignore\n        tidepost_lon=tidepost_lon,  # type: ignore\n        return_tideposts=True,\n        **model_tides_kwargs,\n    )\n    if isinstance(data, (xr.Dataset, xr.DataArray)):\n        obs_tides_da = obs_tides_da.reindex_like(data)\n\n    # Generate range of times covering entire period of satellite record\n    all_timerange = pd.date_range(\n        start=time_coords.min().item(),\n        end=time_coords.max().item(),\n        freq=modelled_freq,\n    )\n\n    # Model tides for each timestep\n    all_tides_df = model_tides(\n        x=tidepost_lon,  # type: ignore\n        y=tidepost_lat,  # type: ignore\n        time=all_timerange,\n        model=model,\n        directory=directory,\n        crs=\"EPSG:4326\",\n        **model_tides_kwargs,\n    )\n\n    # Get coarse statistics on all and observed tidal ranges\n    obs_mean = obs_tides_da.mean().item()\n    all_mean = all_tides_df.tide_height.mean()\n    obs_min, obs_max = obs_tides_da.quantile(min_max_q).values\n    all_min, all_max = all_tides_df.tide_height.quantile(min_max_q).values\n\n    # Calculate tidal range\n    obs_range = obs_max - obs_min\n    all_range = all_max - all_min\n\n    # Calculate Bishop-Taylor et al. 2018 tidal metrics\n    spread = obs_range / all_range\n    low_tide_offset_m = abs(all_min - obs_min)\n    high_tide_offset_m = abs(all_max - obs_max)\n    low_tide_offset = low_tide_offset_m / all_range\n    high_tide_offset = high_tide_offset_m / all_range\n\n    # Plain text descriptors\n    mean_diff = \"higher\" if obs_mean > all_mean else \"lower\"\n    mean_diff_icon = \"\u2b06\ufe0f\" if obs_mean > all_mean else \"\u2b07\ufe0f\"\n    spread_icon = \"\ud83d\udfe2\" if spread >= 0.9 else \"\ud83d\udfe1\" if 0.7 < spread <= 0.9 else \"\ud83d\udd34\"\n    low_tide_icon = \"\ud83d\udfe2\" if low_tide_offset <= 0.1 else \"\ud83d\udfe1\" if 0.1 <= low_tide_offset < 0.2 else \"\ud83d\udd34\"\n    high_tide_icon = \"\ud83d\udfe2\" if high_tide_offset <= 0.1 else \"\ud83d\udfe1\" if 0.1 <= high_tide_offset < 0.2 else \"\ud83d\udd34\"\n\n    # Extract x (time in decimal years) and y (distance) values\n    obs_x = (\n        obs_tides_da.time.dt.year + ((obs_tides_da.time.dt.dayofyear - 1) / 365) + ((obs_tides_da.time.dt.hour) / 24)\n    )\n    obs_y = obs_tides_da.values.astype(np.float32)\n\n    # Compute linear regression\n    obs_linreg = stats.linregress(x=obs_x, y=obs_y)\n\n    if plain_english:\n        print(f\"\\n\\n\ud83c\udf0a Modelled astronomical tide range: {all_range:.2f} metres.\")\n        print(f\"\ud83d\udef0\ufe0f Observed tide range: {obs_range:.2f} metres.\\n\")\n        print(f\"{spread_icon} {spread:.0%} of the modelled astronomical tide range was observed at this location.\")\n        print(\n            f\"{high_tide_icon} The highest {high_tide_offset:.0%} ({high_tide_offset_m:.2f} metres) of the tide range was never observed.\"\n        )\n        print(\n            f\"{low_tide_icon} The lowest {low_tide_offset:.0%} ({low_tide_offset_m:.2f} metres) of the tide range was never observed.\\n\"\n        )\n        print(f\"\ud83c\udf0a Mean modelled astronomical tide height: {all_mean:.2f} metres.\")\n        print(f\"\ud83d\udef0\ufe0f Mean observed tide height: {obs_mean:.2f} metres.\\n\")\n        print(\n            f\"{mean_diff_icon} The mean observed tide height was {obs_mean - all_mean:.2f} metres {mean_diff} than the mean modelled astronomical tide height.\"\n        )\n\n        if linear_reg:\n            if obs_linreg.pvalue > 0.01:\n                print(\"\u2796 Observed tides showed no significant trends over time.\")\n            else:\n                obs_slope_desc = \"decreasing\" if obs_linreg.slope < 0 else \"increasing\"\n                print(\n                    f\"\u26a0\ufe0f Observed tides showed a significant {obs_slope_desc} trend over time (p={obs_linreg.pvalue:.3f}, {obs_linreg.slope:.2f} metres per year)\"\n                )\n\n    if plot:\n        _plot_biases(\n            all_tides_df=all_tides_df,\n            obs_tides_da=obs_tides_da,\n            lat=all_min,\n            lot=obs_min,\n            hat=all_max,\n            hot=obs_max,\n            offset_low=low_tide_offset,\n            offset_high=high_tide_offset,\n            spread=spread,\n            plot_col=data[plot_col] if plot_col else None,\n            obs_linreg=obs_linreg if linear_reg else None,\n            obs_x=obs_x,\n            all_timerange=all_timerange,\n        )\n\n    # Export pandas.Series containing tidal stats\n    output_stats = {\n        \"y\": tidepost_lat,\n        \"x\": tidepost_lon,\n        \"mot\": obs_mean,\n        \"mat\": all_mean,\n        \"lot\": obs_min,\n        \"lat\": all_min,\n        \"hot\": obs_max,\n        \"hat\": all_max,\n        \"otr\": obs_range,\n        \"tr\": all_range,\n        \"spread\": spread,\n        \"offset_low\": low_tide_offset,\n        \"offset_high\": high_tide_offset,\n    }\n\n    if linear_reg:\n        output_stats.update({\n            \"observed_slope\": obs_linreg.slope,\n            \"observed_pval\": obs_linreg.pvalue,\n        })\n\n    # Return pandas data\n    stats_df = pd.Series(output_stats).round(round_stats)\n    return stats_df\n
"},{"location":"api/#eo_tides.stats.tide_stats(data)","title":"data","text":""},{"location":"api/#eo_tides.stats.tide_stats(time)","title":"time","text":""},{"location":"api/#eo_tides.stats.tide_stats(model)","title":"model","text":""},{"location":"api/#eo_tides.stats.tide_stats(directory)","title":"directory","text":""},{"location":"api/#eo_tides.stats.tide_stats(tidepost_lat)","title":"tidepost_lat","text":""},{"location":"api/#eo_tides.stats.tide_stats(tidepost_lon)","title":"tidepost_lon","text":""},{"location":"api/#eo_tides.stats.tide_stats(plain_english)","title":"plain_english","text":""},{"location":"api/#eo_tides.stats.tide_stats(plot)","title":"plot","text":""},{"location":"api/#eo_tides.stats.tide_stats(plot_col)","title":"plot_col","text":""},{"location":"api/#eo_tides.stats.tide_stats(modelled_freq)","title":"modelled_freq","text":""},{"location":"api/#eo_tides.stats.tide_stats(linear_reg)","title":"linear_reg","text":""},{"location":"api/#eo_tides.stats.tide_stats(min_max_q)","title":"min_max_q","text":""},{"location":"api/#eo_tides.stats.tide_stats(round_stats)","title":"round_stats","text":""},{"location":"api/#eo_tides.stats.tide_stats(**model_tides_kwargs)","title":"**model_tides_kwargs","text":""},{"location":"api/#eo_tides.validation","title":"eo_tides.validation","text":"

Functions:

Name Description eval_metrics

Calculate a set of common statistical metrics

load_gauge_gesla

Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.

"},{"location":"api/#eo_tides.validation.eval_metrics","title":"eval_metrics","text":"
eval_metrics(x, y, round=3, all_regress=False)\n

Calculate a set of common statistical metrics based on two input actual and predicted vectors.

These include:

  • Pearson correlation
  • Root Mean Squared Error
  • Mean Absolute Error
  • R-squared
  • Bias
  • Linear regression parameters (slope, p-value, intercept, standard error)

Parameters:

Name Type Description Default array

An array providing \"actual\" variable values.

required array

An array providing \"predicted\" variable values.

required int

Number of decimal places to round each metric to. Defaults to 3.

3 bool

Whether to return linear regression p-value, intercept and standard error (in addition to only regression slope). Defaults to False.

False

Returns:

Type Description Series

A pd.Series containing all calculated metrics.

Source code in eo_tides/validation.py
def eval_metrics(x, y, round=3, all_regress=False):\n    \"\"\"\n    Calculate a set of common statistical metrics\n    based on two input actual and predicted vectors.\n\n    These include:\n\n    * Pearson correlation\n    * Root Mean Squared Error\n    * Mean Absolute Error\n    * R-squared\n    * Bias\n    * Linear regression parameters (slope, p-value, intercept, standard error)\n\n    Parameters\n    ----------\n    x : numpy.array\n        An array providing \"actual\" variable values.\n    y : numpy.array\n        An array providing \"predicted\" variable values.\n    round : int\n        Number of decimal places to round each metric\n        to. Defaults to 3.\n    all_regress : bool\n        Whether to return linear regression p-value,\n        intercept and standard error (in addition to\n        only regression slope). Defaults to False.\n\n    Returns\n    -------\n    pandas.Series\n        A `pd.Series` containing all calculated metrics.\n    \"\"\"\n\n    # Create dataframe to drop na\n    xy_df = pd.DataFrame({\"x\": x, \"y\": y}).dropna()\n\n    # Compute linear regression\n    lin_reg = stats.linregress(x=xy_df.x, y=xy_df.y)\n\n    # Calculate statistics\n    stats_dict = {\n        \"Correlation\": xy_df.corr().iloc[0, 1],\n        \"RMSE\": sqrt(mean_squared_error(xy_df.x, xy_df.y)),\n        \"MAE\": mean_absolute_error(xy_df.x, xy_df.y),\n        \"R-squared\": lin_reg.rvalue**2,\n        \"Bias\": (xy_df.y - xy_df.x).mean(),\n        \"Regression slope\": lin_reg.slope,\n    }\n\n    # Additional regression params\n    if all_regress:\n        stats_dict.update({\n            \"Regression p-value\": lin_reg.pvalue,\n            \"Regression intercept\": lin_reg.intercept,\n            \"Regression standard error\": lin_reg.stderr,\n        })\n\n    # Return as\n    return pd.Series(stats_dict).round(round)\n
"},{"location":"api/#eo_tides.validation.eval_metrics(x)","title":"x","text":""},{"location":"api/#eo_tides.validation.eval_metrics(y)","title":"y","text":""},{"location":"api/#eo_tides.validation.eval_metrics(round)","title":"round","text":""},{"location":"api/#eo_tides.validation.eval_metrics(all_regress)","title":"all_regress","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla","title":"load_gauge_gesla","text":"
load_gauge_gesla(\n    x=None,\n    y=None,\n    site_code=None,\n    time=(\"2018\", \"2020\"),\n    max_distance=None,\n    correct_mean=False,\n    filter_use_flag=True,\n    site_metadata=True,\n    data_path=\"/gdata1/data/sea_level/gesla/\",\n    metadata_path=\"/gdata1/data/sea_level/GESLA3_ALL 2.csv\",\n)\n

Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.

Load and process all available GESLA measured sea-level data with an x, y, time spatio-temporal query, or from a list of specific tide gauges. Can optionally filter by gauge quality and append detailed gauge metadata.

Modified from original code in https://github.com/philiprt/GeslaDataset.

Parameters:

Name Type Description Default numeric or list / tuple

Coordinates (in degrees longitude, latitude) used to load GESLA tide gauge observations. If provided as singular values (e.g. x=150, y=-32), then the nearest tide gauge will be returned. If provided as a list or tuple (e.g. x=(150, 152), y=(-32, -30)), then all gauges within the provided bounding box will be loaded. Leave as None to return all available gauges, or if providing a list of site codes using site_code.

None numeric or list / tuple

Coordinates (in degrees longitude, latitude) used to load GESLA tide gauge observations. If provided as singular values (e.g. x=150, y=-32), then the nearest tide gauge will be returned. If provided as a list or tuple (e.g. x=(150, 152), y=(-32, -30)), then all gauges within the provided bounding box will be loaded. Leave as None to return all available gauges, or if providing a list of site codes using site_code.

None str or list of str

GESLA site code(s) for which to load data (e.g. site_code=\"62650\"). If site_code is provided, x and y will be ignored.

None tuple or list of str

Time range to consider, given as a tuple of start and end dates, e.g. time=(\"2020\", \"2021\"). The default of None will return all tide observations from the year 1800 onward.

('2018', '2020') numeric

Optional max distance within which to return the nearest tide gauge when x and y are provided as singular coordinates. Defaults to None, which will always return a tide gauge no matter how far away it is located from x and y.

None bool

Whether to correct sea level measurements to a standardised mean sea level by subtracting the mean of all observed sea level observations. This can be useful when GESLA tide heights come from different or unknown tide datums. Note: the observed mean sea level calculated here may differ from true long-term/ astronomical Mean Sea Level (MSL) datum.

False bool

Whether to filter out low quality observations with a \"use_flag\" value of 0 (do not use). Defaults to True.

True bool

Whether to add tide gauge station metadata as additional columns in the output DataFrame. Defaults to True.

True str

Path to the raw GESLA data files. Default is /gdata1/data/sea_level/gesla/.

'/gdata1/data/sea_level/gesla/' str

Path to the GESLA station metadata file. Default is /gdata1/data/sea_level/GESLA3_ALL 2.csv.

'/gdata1/data/sea_level/GESLA3_ALL 2.csv'

Returns:

Type Description DataFrame

Processed GESLA data as a DataFrame with columns including:

  • \"time\": Timestamps,
  • \"sea_level\": Observed sea level (m),
  • \"qc_flag\": Observed sea level QC flag,
  • \"use_flag\": Use-in-analysis flag (1 = use, 0 = do not use),

...and additional columns from station metadata.

Source code in eo_tides/validation.py
def load_gauge_gesla(\n    x=None,\n    y=None,\n    site_code=None,\n    time=(\"2018\", \"2020\"),\n    max_distance=None,\n    correct_mean=False,\n    filter_use_flag=True,\n    site_metadata=True,\n    data_path=\"/gdata1/data/sea_level/gesla/\",\n    metadata_path=\"/gdata1/data/sea_level/GESLA3_ALL 2.csv\",\n):\n    \"\"\"\n    Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.\n\n    Load and process all available GESLA measured sea-level data\n    with an `x, y, time` spatio-temporal query, or from a list of\n    specific tide gauges. Can optionally filter by gauge quality\n    and append detailed gauge metadata.\n\n    Modified from original code in <https://github.com/philiprt/GeslaDataset>.\n\n    Parameters\n    ----------\n    x, y : numeric or list/tuple, optional\n        Coordinates (in degrees longitude, latitude) used to load GESLA\n        tide gauge observations. If provided as singular values\n        (e.g. `x=150, y=-32`), then the nearest tide gauge will be returned.\n        If provided as a list or tuple (e.g. `x=(150, 152), y=(-32, -30)`),\n        then all gauges within the provided bounding box will be loaded.\n        Leave as `None` to return all available gauges, or if providing a\n        list of site codes using `site_code`.\n    site_code : str or list of str, optional\n        GESLA site code(s) for which to load data (e.g. `site_code=\"62650\"`).\n        If `site_code` is provided, `x` and `y` will be ignored.\n    time : tuple or list of str, optional\n        Time range to consider, given as a tuple of start and end dates,\n        e.g. `time=(\"2020\", \"2021\")`. The default of None will return all\n        tide observations from the year 1800 onward.\n    max_distance : numeric, optional\n        Optional max distance within which to return the nearest tide gauge\n        when `x` and `y` are provided as singular coordinates. Defaults to\n        None, which will always return a tide gauge no matter how far away\n        it is located from `x` and `y`.\n    correct_mean : bool, optional\n        Whether to correct sea level measurements to a standardised mean\n        sea level by subtracting the mean of all observed sea level\n        observations. This can be useful when GESLA tide heights come\n        from different or unknown tide datums. Note: the observed mean\n        sea level calculated here may differ from true long-term/\n        astronomical Mean Sea Level (MSL) datum.\n    filter_use_flag : bool, optional\n        Whether to filter out low quality observations with a \"use_flag\"\n        value of 0 (do not use). Defaults to True.\n    site_metadata : bool, optional\n        Whether to add tide gauge station metadata as additional columns\n        in the output DataFrame. Defaults to True.\n    data_path : str, optional\n        Path to the raw GESLA data files. Default is\n        `/gdata1/data/sea_level/gesla/`.\n    metadata_path : str, optional\n        Path to the GESLA station metadata file.\n        Default is `/gdata1/data/sea_level/GESLA3_ALL 2.csv`.\n\n    Returns\n    -------\n    pd.DataFrame\n        Processed GESLA data as a DataFrame with columns including:\n\n        - \"time\": Timestamps,\n        - \"sea_level\": Observed sea level (m),\n        - \"qc_flag\": Observed sea level QC flag,\n        - \"use_flag\": Use-in-analysis flag (1 = use, 0 = do not use),\n\n        ...and additional columns from station metadata.\n    \"\"\"\n    # Load tide gauge metadata\n    metadata_df, metadata_gdf = _load_gauge_metadata(metadata_path)\n\n    # Use supplied site codes if available\n    if site_code is not None:\n        site_code = [site_code] if not isinstance(site_code, list) else site_code\n\n    # If x and y are tuples, use xy bounds to identify sites\n    elif isinstance(x, (tuple, list)) & isinstance(y, (tuple, list)):\n        bbox = BoundingBox.from_xy(x, y)\n        site_code = metadata_gdf.cx[bbox.left : bbox.right, bbox.top : bbox.bottom].index\n\n    # If x and y are single numbers, select nearest row\n    elif isinstance(x, Number) & isinstance(y, Number):\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\")\n            site_code = (\n                _nearest_row(metadata_gdf, x, y, max_distance).rename({\"index_right\": \"site_code\"}, axis=1).site_code\n            )\n            # site_code = _nearest_row(metadata_gdf, x, y, max_distance).site_code\n\n        # Raise exception if no valid tide gauges are found\n        if site_code.isnull().all():\n            raise Exception(f\"No tide gauge found within {max_distance} degrees of {x}, {y}.\")\n\n    # Otherwise if all are None, return all available site codes\n    elif (site_code is None) & (x is None) & (y is None):\n        site_code = metadata_df.index.to_list()\n\n    else:\n        raise TypeError(\n            \"`x` and `y` must be provided as either singular coordinates (e.g. `x=150`), or as a tuple bounding box (e.g. `x=(150, 152)`).\"\n        )\n\n    # Prepare times\n    if time is None:\n        time = [\"1800\", str(datetime.datetime.now().year)]\n    time = [time] if not isinstance(time, (list, tuple)) else time\n    start_time = _round_date_strings(time[0], round_type=\"start\")\n    end_time = _round_date_strings(time[-1], round_type=\"end\")\n\n    # Identify paths to load and nodata values for each site\n    metadata_df[\"file_name\"] = data_path + metadata_df[\"file_name\"]\n    paths_na = metadata_df.loc[site_code, [\"file_name\", \"null_value\"]]\n\n    # Load and combine into a single dataframe\n    gauge_list = [\n        _load_gesla_dataset(s, p, na_value=na)\n        for s, p, na in tqdm.tqdm(paths_na.itertuples(), total=len(paths_na), desc=\"Loading GESLA gauges\")\n    ]\n    data_df = pd.concat(gauge_list).sort_index().loc[slice(start_time, end_time)].reset_index().set_index(\"site_code\")\n\n    # Optionally filter by use flag column\n    if filter_use_flag:\n        data_df = data_df.loc[data_df.use_flag == 1]\n\n    # Optionally insert metadata into dataframe\n    if site_metadata:\n        data_df[metadata_df.columns] = metadata_df.loc[site_code]\n\n    # Add time to index and remove duplicates\n    data_df = data_df.set_index(\"time\", append=True)\n    duplicates = data_df.index.duplicated()\n    if duplicates.sum() > 0:\n        warnings.warn(\"Duplicate timestamps were removed.\")\n        data_df = data_df.loc[~duplicates]\n\n    # Remove observed mean sea level if requested\n    if correct_mean:\n        data_df[\"sea_level\"] = data_df[\"sea_level\"].sub(data_df.groupby(\"site_code\")[\"sea_level\"].transform(\"mean\"))\n\n    # Return data\n    return data_df\n
"},{"location":"api/#eo_tides.validation.load_gauge_gesla(x)","title":"x","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(y)","title":"y","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(site_code)","title":"site_code","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(time)","title":"time","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(max_distance)","title":"max_distance","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(correct_mean)","title":"correct_mean","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(filter_use_flag)","title":"filter_use_flag","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(site_metadata)","title":"site_metadata","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(data_path)","title":"data_path","text":""},{"location":"api/#eo_tides.validation.load_gauge_gesla(metadata_path)","title":"metadata_path","text":""},{"location":"api/#eo_tides.utils","title":"eo_tides.utils","text":"

Functions:

Name Description clip_models

Clip NetCDF-format ocean tide models to a bounding box.

idw

Perform Inverse Distance Weighting (IDW) interpolation.

list_models

List all tide models available for tide modelling.

"},{"location":"api/#eo_tides.utils.clip_models","title":"clip_models","text":"
clip_models(\n    input_directory,\n    output_directory,\n    bbox,\n    model=None,\n    buffer=5,\n    overwrite=False,\n)\n

Clip NetCDF-format ocean tide models to a bounding box.

This function identifies all NetCDF-format tide models in a given input directory, including \"ATLAS-netcdf\" (e.g. TPXO9-atlas-nc), \"FES-netcdf\" (e.g. FES2022, EOT20), and \"GOT-netcdf\" (e.g. GOT5.5) format files. Files for each model are then clipped to the extent of the provided bounding box, handling model-specific file structures. After each model is clipped, the result is exported to the output directory and verified with pyTMD to ensure the clipped data is suitable for tide modelling.

For instructions on accessing and downloading tide models, see: https://geoscienceaustralia.github.io/eo-tides/setup/

Parameters:

Name Type Description Default str or PathLike

Path to directory containing input NetCDF-format tide model files.

required str or PathLike

Path to directory where clipped NetCDF files will be exported.

required tuple of float

Bounding box for clipping the tide models in EPSG:4326 degrees coordinates, specified as (left, bottom, right, top).

required str or list of str

The tide model (or models) to clip. Defaults to None, which will automatically identify and clip all NetCDF-format models in the input directly.

None float

Buffer distance (in degrees) added to the bounding box to provide sufficient data on edges of study area. Defaults to 5 degrees.

5 bool

If True, overwrite existing files in the output directory. Defaults to False.

False

Examples:

>>> clip_models(\n...     input_directory=\"tide_models/\",\n...     output_directory=\"tide_models_clipped/\",\n...     bbox=(-8.968392, 50.070574, 2.447160, 59.367122),\n... )\n
Source code in eo_tides/utils.py
def clip_models(\n    input_directory: str | os.PathLike,\n    output_directory: str | os.PathLike,\n    bbox: tuple[float, float, float, float],\n    model: list | None = None,\n    buffer: float = 5,\n    overwrite: bool = False,\n):\n    \"\"\"\n    Clip NetCDF-format ocean tide models to a bounding box.\n\n    This function identifies all NetCDF-format tide models in a\n    given input directory, including \"ATLAS-netcdf\" (e.g. TPXO9-atlas-nc),\n    \"FES-netcdf\" (e.g. FES2022, EOT20), and \"GOT-netcdf\" (e.g. GOT5.5)\n    format files. Files for each model are then clipped to the extent of\n    the provided bounding box, handling model-specific file structures.\n    After each model is clipped, the result is exported to the output\n    directory and verified with `pyTMD` to ensure the clipped data is\n    suitable for tide modelling.\n\n    For instructions on accessing and downloading tide models, see:\n    <https://geoscienceaustralia.github.io/eo-tides/setup/>\n\n    Parameters\n    ----------\n    input_directory : str or os.PathLike\n        Path to directory containing input NetCDF-format tide model files.\n    output_directory : str or os.PathLike\n        Path to directory where clipped NetCDF files will be exported.\n    bbox : tuple of float\n        Bounding box for clipping the tide models in EPSG:4326 degrees\n        coordinates, specified as `(left, bottom, right, top)`.\n    model : str or list of str, optional\n        The tide model (or models) to clip. Defaults to None, which\n        will automatically identify and clip all NetCDF-format models\n        in the input directly.\n    buffer : float, optional\n        Buffer distance (in degrees) added to the bounding box to provide\n        sufficient data on edges of study area. Defaults to 5 degrees.\n    overwrite : bool, optional\n        If True, overwrite existing files in the output directory.\n        Defaults to False.\n\n    Examples\n    --------\n    >>> clip_models(\n    ...     input_directory=\"tide_models/\",\n    ...     output_directory=\"tide_models_clipped/\",\n    ...     bbox=(-8.968392, 50.070574, 2.447160, 59.367122),\n    ... )\n    \"\"\"\n\n    # Get input and output paths\n    input_directory = _set_directory(input_directory)\n    output_directory = pathlib.Path(output_directory)\n\n    # Prepare bounding box\n    bbox = odc.geo.geom.BoundingBox(*bbox, crs=\"EPSG:4326\").buffered(buffer)\n\n    # Identify NetCDF models\n    model_database = load_database()[\"elevation\"]\n    netcdf_formats = [\"ATLAS-netcdf\", \"FES-netcdf\", \"GOT-netcdf\"]\n    netcdf_models = {k for k, v in model_database.items() if v[\"format\"] in netcdf_formats}\n\n    # Identify subset of available and requested NetCDF models\n    available_models, _ = list_models(directory=input_directory, show_available=False, show_supported=False)\n    requested_models = list(np.atleast_1d(model)) if model is not None else available_models\n    available_netcdf_models = list(set(available_models) & set(requested_models) & set(netcdf_models))\n\n    # Raise error if no valid models found\n    if len(available_netcdf_models) == 0:\n        raise ValueError(f\"No valid NetCDF models found in {input_directory}.\")\n\n    # If model list is provided,\n    print(f\"Preparing to clip suitable NetCDF models: {available_netcdf_models}\\n\")\n\n    # Loop through suitable models and export\n    for m in available_netcdf_models:\n        # Get model file and grid file list if they exist\n        model_files = model_database[m].get(\"model_file\", [])\n        grid_file = model_database[m].get(\"grid_file\", [])\n\n        # Convert to list if strings and combine\n        model_files = model_files if isinstance(model_files, list) else [model_files]\n        grid_file = grid_file if isinstance(grid_file, list) else [grid_file]\n        all_files = model_files + grid_file\n\n        # Loop through each model file and clip\n        for file in tqdm(all_files, desc=f\"Clipping {m}\"):\n            # Skip if it exists in output directory\n            if (output_directory / file).exists() and not overwrite:\n                continue\n\n            # Load model file\n            nc = xr.open_mfdataset(input_directory / file)\n\n            # Open file and clip according to model\n            if m in (\n                \"GOT5.5\",\n                \"GOT5.5_load\",\n                \"GOT5.5_extrapolated\",\n                \"GOT5.5D\",\n                \"GOT5.5D_extrapolated\",\n                \"GOT5.6\",\n                \"GOT5.6_extrapolated\",\n            ):\n                nc_clipped = _clip_model_file(\n                    nc,\n                    bbox,\n                    xdim=\"lon\",\n                    ydim=\"lat\",\n                    ycoord=\"latitude\",\n                    xcoord=\"longitude\",\n                )\n\n            elif m in (\"HAMTIDE11\",):\n                nc_clipped = _clip_model_file(nc, bbox, xdim=\"LON\", ydim=\"LAT\", ycoord=\"LAT\", xcoord=\"LON\")\n\n            elif m in (\n                \"EOT20\",\n                \"EOT20_load\",\n                \"FES2012\",\n                \"FES2014\",\n                \"FES2014_extrapolated\",\n                \"FES2014_load\",\n                \"FES2022\",\n                \"FES2022_extrapolated\",\n                \"FES2022_load\",\n            ):\n                nc_clipped = _clip_model_file(nc, bbox, xdim=\"lon\", ydim=\"lat\", ycoord=\"lat\", xcoord=\"lon\")\n\n            elif m in (\n                \"TPXO8-atlas-nc\",\n                \"TPXO9-atlas-nc\",\n                \"TPXO9-atlas-v2-nc\",\n                \"TPXO9-atlas-v3-nc\",\n                \"TPXO9-atlas-v4-nc\",\n                \"TPXO9-atlas-v5-nc\",\n                \"TPXO10-atlas-v2-nc\",\n            ):\n                nc_clipped = _clip_model_file(\n                    nc,\n                    bbox,\n                    xdim=\"nx\",\n                    ydim=\"ny\",\n                    ycoord=\"lat_z\",\n                    xcoord=\"lon_z\",\n                )\n\n            else:\n                raise Exception(f\"Model {m} not supported\")\n\n            # Create directory and export\n            (output_directory / file).parent.mkdir(parents=True, exist_ok=True)\n            nc_clipped.to_netcdf(output_directory / file, mode=\"w\")\n\n        # Verify that models are ready\n        pytmd_model(directory=output_directory).elevation(m=m).verify\n        print(\" \u2705 Clipped model exported and verified\")\n\n    print(f\"\\nOutputs exported to {output_directory}\")\n    list_models(directory=output_directory, show_available=True, show_supported=False)\n
"},{"location":"api/#eo_tides.utils.clip_models(input_directory)","title":"input_directory","text":""},{"location":"api/#eo_tides.utils.clip_models(output_directory)","title":"output_directory","text":""},{"location":"api/#eo_tides.utils.clip_models(bbox)","title":"bbox","text":""},{"location":"api/#eo_tides.utils.clip_models(model)","title":"model","text":""},{"location":"api/#eo_tides.utils.clip_models(buffer)","title":"buffer","text":""},{"location":"api/#eo_tides.utils.clip_models(overwrite)","title":"overwrite","text":""},{"location":"api/#eo_tides.utils.idw","title":"idw","text":"
idw(\n    input_z,\n    input_x,\n    input_y,\n    output_x,\n    output_y,\n    p=1,\n    k=10,\n    max_dist=None,\n    k_min=1,\n    epsilon=1e-12,\n)\n

Perform Inverse Distance Weighting (IDW) interpolation.

This function performs fast IDW interpolation by creating a KDTree from the input coordinates then uses it to find the k nearest neighbors for each output point. Weights are calculated based on the inverse distance to each neighbor, with weights descreasing with increasing distance.

Code inspired by: https://github.com/DahnJ/REM-xarray

Parameters:

Name Type Description Default array - like

Array of values at the input points. This can be either a 1-dimensional array, or a 2-dimensional array where each column (axis=1) represents a different set of values to be interpolated.

required array - like

Array of x-coordinates of the input points.

required array - like

Array of y-coordinates of the input points.

required array - like

Array of x-coordinates where the interpolation is to be computed.

required array - like

Array of y-coordinates where the interpolation is to be computed.

required int or float

Power function parameter defining how rapidly weightings should decrease as distance increases. Higher values of p will cause weights for distant points to decrease rapidly, resulting in nearby points having more influence on predictions. Defaults to 1.

1 int

Number of nearest neighbors to use for interpolation. k=1 is equivalent to \"nearest\" neighbour interpolation. Defaults to 10.

10 int or float

Restrict neighbouring points to less than this distance. By default, no distance limit is applied.

None int

If max_dist is provided, some points may end up with less than k nearest neighbours, potentially producing less reliable interpolations. Set k_min to set any points with less than k_min neighbours to NaN. Defaults to 1.

1 float

Small value added to distances to prevent division by zero errors in the case that output coordinates are identical to input coordinates. Defaults to 1e-12.

1e-12

Returns:

Name Type Description interp_values ndarray

Interpolated values at the output coordinates. If input_z is 1-dimensional, interp_values will also be 1-dimensional. If input_z is 2-dimensional, interp_values will have the same number of rows as input_z, with each column (axis=1) representing interpolated values for one set of input data.

Examples:

>>> input_z = [1, 2, 3, 4, 5]\n>>> input_x = [0, 1, 2, 3, 4]\n>>> input_y = [0, 1, 2, 3, 4]\n>>> output_x = [0.5, 1.5, 2.5]\n>>> output_y = [0.5, 1.5, 2.5]\n>>> idw(input_z, input_x, input_y, output_x, output_y, k=2)\narray([1.5, 2.5, 3.5])\n
Source code in eo_tides/utils.py
def idw(\n    input_z,\n    input_x,\n    input_y,\n    output_x,\n    output_y,\n    p=1,\n    k=10,\n    max_dist=None,\n    k_min=1,\n    epsilon=1e-12,\n):\n    \"\"\"Perform Inverse Distance Weighting (IDW) interpolation.\n\n    This function performs fast IDW interpolation by creating a KDTree\n    from the input coordinates then uses it to find the `k` nearest\n    neighbors for each output point. Weights are calculated based on the\n    inverse distance to each neighbor, with weights descreasing with\n    increasing distance.\n\n    Code inspired by: <https://github.com/DahnJ/REM-xarray>\n\n    Parameters\n    ----------\n    input_z : array-like\n        Array of values at the input points. This can be either a\n        1-dimensional array, or a 2-dimensional array where each column\n        (axis=1) represents a different set of values to be interpolated.\n    input_x : array-like\n        Array of x-coordinates of the input points.\n    input_y : array-like\n        Array of y-coordinates of the input points.\n    output_x : array-like\n        Array of x-coordinates where the interpolation is to be computed.\n    output_y : array-like\n        Array of y-coordinates where the interpolation is to be computed.\n    p : int or float, optional\n        Power function parameter defining how rapidly weightings should\n        decrease as distance increases. Higher values of `p` will cause\n        weights for distant points to decrease rapidly, resulting in\n        nearby points having more influence on predictions. Defaults to 1.\n    k : int, optional\n        Number of nearest neighbors to use for interpolation. `k=1` is\n        equivalent to \"nearest\" neighbour interpolation. Defaults to 10.\n    max_dist : int or float, optional\n        Restrict neighbouring points to less than this distance.\n        By default, no distance limit is applied.\n    k_min : int, optional\n        If `max_dist` is provided, some points may end up with less than\n        `k` nearest neighbours, potentially producing less reliable\n        interpolations. Set `k_min` to set any points with less than\n        `k_min` neighbours to NaN. Defaults to 1.\n    epsilon : float, optional\n        Small value added to distances to prevent division by zero\n        errors in the case that output coordinates are identical to\n        input coordinates. Defaults to 1e-12.\n\n    Returns\n    -------\n    interp_values : numpy.ndarray\n        Interpolated values at the output coordinates. If `input_z` is\n        1-dimensional, `interp_values` will also be 1-dimensional. If\n        `input_z` is 2-dimensional, `interp_values` will have the same\n        number of rows as `input_z`, with each column (axis=1)\n        representing interpolated values for one set of input data.\n\n    Examples\n    --------\n    >>> input_z = [1, 2, 3, 4, 5]\n    >>> input_x = [0, 1, 2, 3, 4]\n    >>> input_y = [0, 1, 2, 3, 4]\n    >>> output_x = [0.5, 1.5, 2.5]\n    >>> output_y = [0.5, 1.5, 2.5]\n    >>> idw(input_z, input_x, input_y, output_x, output_y, k=2)\n    array([1.5, 2.5, 3.5])\n\n    \"\"\"\n    # Convert to numpy arrays\n    input_x = np.atleast_1d(input_x)\n    input_y = np.atleast_1d(input_y)\n    input_z = np.atleast_1d(input_z)\n    output_x = np.atleast_1d(output_x)\n    output_y = np.atleast_1d(output_y)\n\n    # Verify input and outputs have matching lengths\n    if not (input_z.shape[0] == len(input_x) == len(input_y)):\n        raise ValueError(\"All of `input_z`, `input_x` and `input_y` must be the same length.\")\n    if not (len(output_x) == len(output_y)):\n        raise ValueError(\"Both `output_x` and `output_y` must be the same length.\")\n\n    # Verify k is smaller than total number of points, and non-zero\n    if k > input_z.shape[0]:\n        raise ValueError(\n            f\"The requested number of nearest neighbours (`k={k}`) \"\n            f\"is smaller than the total number of points ({input_z.shape[0]}).\",\n        )\n    if k == 0:\n        raise ValueError(\"Interpolation based on `k=0` nearest neighbours is not valid.\")\n\n    # Create KDTree to efficiently find nearest neighbours\n    points_xy = np.column_stack((input_y, input_x))\n    tree = KDTree(points_xy)\n\n    # Determine nearest neighbours and distances to each\n    grid_stacked = np.column_stack((output_y, output_x))\n    distances, indices = tree.query(grid_stacked, k=k, workers=-1)\n\n    # If k == 1, add an additional axis for consistency\n    if k == 1:\n        distances = distances[..., np.newaxis]\n        indices = indices[..., np.newaxis]\n\n    # Add small epsilon to distances to prevent division by zero errors\n    # if output coordinates are the same as input coordinates\n    distances = np.maximum(distances, epsilon)\n\n    # Set distances above max to NaN if specified\n    if max_dist is not None:\n        distances[distances > max_dist] = np.nan\n\n    # Calculate weights based on distance to k nearest neighbours.\n    weights = 1 / np.power(distances, p)\n    weights = weights / np.nansum(weights, axis=1).reshape(-1, 1)\n\n    # 1D case: Compute weighted sum of input_z values for each output point\n    if input_z.ndim == 1:\n        interp_values = np.nansum(weights * input_z[indices], axis=1)\n\n    # 2D case: Compute weighted sum for each set of input_z values\n    # weights[..., np.newaxis] adds a dimension for broadcasting\n    else:\n        interp_values = np.nansum(\n            weights[..., np.newaxis] * input_z[indices],\n            axis=1,\n        )\n\n    # Set any points with less than `k_min` valid weights to NaN\n    interp_values[np.isfinite(weights).sum(axis=1) < k_min] = np.nan\n\n    return interp_values\n
"},{"location":"api/#eo_tides.utils.idw(input_z)","title":"input_z","text":""},{"location":"api/#eo_tides.utils.idw(input_x)","title":"input_x","text":""},{"location":"api/#eo_tides.utils.idw(input_y)","title":"input_y","text":""},{"location":"api/#eo_tides.utils.idw(output_x)","title":"output_x","text":""},{"location":"api/#eo_tides.utils.idw(output_y)","title":"output_y","text":""},{"location":"api/#eo_tides.utils.idw(p)","title":"p","text":""},{"location":"api/#eo_tides.utils.idw(k)","title":"k","text":""},{"location":"api/#eo_tides.utils.idw(max_dist)","title":"max_dist","text":""},{"location":"api/#eo_tides.utils.idw(k_min)","title":"k_min","text":""},{"location":"api/#eo_tides.utils.idw(epsilon)","title":"epsilon","text":""},{"location":"api/#eo_tides.utils.list_models","title":"list_models","text":"
list_models(\n    directory=None,\n    show_available=True,\n    show_supported=True,\n    raise_error=False,\n)\n

List all tide models available for tide modelling.

This function scans the specified tide model directory and returns a list of models that are available in the directory as well as the full list of all models supported by eo-tides and pyTMD.

For instructions on setting up tide models, see: https://geoscienceaustralia.github.io/eo-tides/setup/

Parameters:

Name Type Description Default str

The directory containing tide model data files. If no path is provided, this will default to the environment variable EO_TIDES_TIDE_MODELS if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure required by pyTMD (https://geoscienceaustralia.github.io/eo-tides/setup/).

None bool

Whether to print a list of locally available models.

True bool

Whether to print a list of all supported models, in addition to models available locally.

True bool

If True, raise an error if no available models are found. If False, raise a warning.

False

Returns:

Name Type Description available_models list of str

A list of all tide models available within directory.

supported_models list of str

A list of all tide models supported by eo-tides.

Source code in eo_tides/utils.py
def list_models(\n    directory: str | os.PathLike | None = None,\n    show_available: bool = True,\n    show_supported: bool = True,\n    raise_error: bool = False,\n) -> tuple[list[str], list[str]]:\n    \"\"\"\n    List all tide models available for tide modelling.\n\n    This function scans the specified tide model directory\n    and returns a list of models that are available in the\n    directory as well as the full list of all models supported\n    by `eo-tides` and `pyTMD`.\n\n    For instructions on setting up tide models, see:\n    <https://geoscienceaustralia.github.io/eo-tides/setup/>\n\n    Parameters\n    ----------\n    directory : str, optional\n        The directory containing tide model data files. If no path is\n        provided, this will default to the environment variable\n        `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.\n        Tide modelling files should be stored in sub-folders for each\n        model that match the structure required by `pyTMD`\n        (<https://geoscienceaustralia.github.io/eo-tides/setup/>).\n    show_available : bool, optional\n        Whether to print a list of locally available models.\n    show_supported : bool, optional\n        Whether to print a list of all supported models, in\n        addition to models available locally.\n    raise_error : bool, optional\n        If True, raise an error if no available models are found.\n        If False, raise a warning.\n\n    Returns\n    -------\n    available_models : list of str\n        A list of all tide models available within `directory`.\n    supported_models : list of str\n        A list of all tide models supported by `eo-tides`.\n    \"\"\"\n    init()  # Initialize colorama\n\n    # Set tide modelling files directory. If no custom path is\n    # provided, try global environment variable.\n    directory = _set_directory(directory)\n\n    # Get full list of supported models from pyTMD database\n    model_database = load_database()[\"elevation\"]\n    supported_models = list(model_database.keys())\n\n    # Extract expected model paths\n    expected_paths = {}\n    for m in supported_models:\n        model_file = model_database[m][\"model_file\"]\n\n        # Handle GOT5.6 differently to ensure we test for presence of GOT5.6 constituents\n        if m in (\"GOT5.6\", \"GOT5.6_extrapolated\"):\n            model_file = [file for file in model_file if \"GOT5.6\" in file][0]\n        else:\n            model_file = model_file[0] if isinstance(model_file, list) else model_file\n\n        # Add path to dict\n        expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)\n\n    # Define column widths\n    status_width = 4  # Width for emoji\n    name_width = max(len(name) for name in supported_models)\n    path_width = max(len(path) for path in expected_paths.values())\n\n    # Print list of supported models, marking available and\n    # unavailable models and appending available to list\n    if show_available or show_supported:\n        total_width = min(status_width + name_width + path_width + 6, 80)\n        print(\"\u2500\" * total_width)\n        print(f\"{'\udb40\udc20\ud83c\udf0a':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}\")\n        print(\"\u2500\" * total_width)\n\n    available_models = []\n    for m in supported_models:\n        try:\n            model_file = pytmd_model(directory=directory).elevation(m=m)\n            available_models.append(m)\n\n            if show_available:\n                # Mark available models with a green tick\n                status = \"\u2705\"\n                print(f\"{status:^{status_width}}\u2502 {m:<{name_width}} \u2502 {expected_paths[m]:<{path_width}}\")\n        except FileNotFoundError:\n            if show_supported:\n                # Mark unavailable models with a red cross\n                status = \"\u274c\"\n                print(\n                    f\"{status:^{status_width}}\u2502 {Style.DIM}{m:<{name_width}} \u2502 {expected_paths[m]:<{path_width}}{Style.RESET_ALL}\"\n                )\n\n    if show_available or show_supported:\n        print(\"\u2500\" * total_width)\n\n        # Print summary\n        print(f\"\\n{Style.BRIGHT}Summary:{Style.RESET_ALL}\")\n        print(f\"Available models: {len(available_models)}/{len(supported_models)}\")\n\n    # Raise error or warning if no models are available\n    if not available_models:\n        warning_msg = textwrap.dedent(\n            f\"\"\"\n            No valid tide models are available in `{directory}`.\n            Are you sure you have provided the correct `directory` path, or set the\n            `EO_TIDES_TIDE_MODELS` environment variable to point to the location of your\n            tide model directory?\n            \"\"\"\n        ).strip()\n\n        if raise_error:\n            raise Exception(warning_msg)\n        else:\n            warnings.warn(warning_msg, UserWarning)\n\n    # Return list of available and supported models\n    return available_models, supported_models\n
"},{"location":"api/#eo_tides.utils.list_models(directory)","title":"directory","text":""},{"location":"api/#eo_tides.utils.list_models(show_available)","title":"show_available","text":""},{"location":"api/#eo_tides.utils.list_models(show_supported)","title":"show_supported","text":""},{"location":"api/#eo_tides.utils.list_models(raise_error)","title":"raise_error","text":""},{"location":"changelog/","title":"Changelog","text":""},{"location":"changelog/#v032-upcoming","title":"v0.3.2 (upcoming)","text":""},{"location":"changelog/#new-features","title":"New features","text":"
  • Publishes ensemble tide modelling code for combining multiple global ocean tide models into a single locally optimised ensemble tide model using external model ranking data (e.g. satellite altimetry or NDWI-tide correlations along the coastline)
  • Update ensemble code to latest version that includes FES2022, GOT5.6 and TPXO10
  • Make ensemble model calculation function a top level function (i.e. rename from _ensemble_model to ensemble_tides)
  • Load tide model ranking points from external flatgeobuff format file for faster cloud access
  • Make buffer distance applied when cropping model files configurable via the crop_buffer param, with a default of 5 degrees
  • Reorder model_tides params to provide more logical flow and move more common params like mode, output_format and output_units higher
"},{"location":"changelog/#bug-fixes","title":"Bug fixes","text":"
  • Fix warnings from load_gauge_gesla function
"},{"location":"changelog/#v031-2024-11-15","title":"v0.3.1 (2024-11-15)","text":""},{"location":"changelog/#new-features_1","title":"New features","text":"
  • Add new \"all\" option to model param in model_tides, pixel_tides etc, which will model tides using all available tide models in your provided directory.
"},{"location":"changelog/#bug-fixes_1","title":"Bug fixes","text":"
  • Fix bug where GOT5.6 was not detected as a valid model because it contains files in multiple directories (e.g. both \"GOT5.6\" and \"GOT5.5\"). This also affected clipping GOT5.6 data using the eo_tides.utils.clip_models function.
"},{"location":"changelog/#v030-2024-11-11","title":"v0.3.0 (2024-11-11)","text":""},{"location":"changelog/#new-features_2","title":"New features","text":"
  • Added new eo_tides.utils.clip_models function for clipping tide models to a smaller spatial extent. This can have a major positive impact on performance, sometimes producing more than a 10 x speedup. This function identifies all NetCDF-format tide models in a given input directory, including \"ATLAS-netcdf\" (e.g. TPXO9-atlas-nc), \"FES-netcdf\" (e.g. FES2022, EOT20), and \"GOT-netcdf\" (e.g. GOT5.5) format files. Files for each model are then clipped to the extent of the provided bounding box, handling model-specific file structures. After each model is clipped, the result is exported to the output directory and verified with pyTMD to ensure the clipped data is suitable for tide modelling.
"},{"location":"changelog/#major-changes","title":"Major changes","text":"
  • The parallel_splits parameter that controls the number of chunks data is broken into for parallel analysis has been refactored to use a new default of \"auto\". This now attempts to automatically determine a sensible value based on available CPU, number of points, and number of models being run. All CPUs will be used where possible, unless this will produce splits with less than 1000 points in each (which would increase overhead). Parallel splits will be reduced if multiple models are requested, as these are run in parallel too and will compete for the same resources.
  • Changed the default interpolation method from \"spline\" to \"linear\". This appears to produce the same results, but works considerably faster.
  • Updates to enable correct cropping, recently resolved in PyTMD 2.1.8
"},{"location":"changelog/#breaking-changes","title":"Breaking changes","text":"
  • The list_models function has been relocated to eo_tides.utils (from eo_tides.model)
"},{"location":"changelog/#v020-2024-10-30","title":"v0.2.0 (2024-10-30)","text":""},{"location":"changelog/#new-features_3","title":"New features","text":"
  • New model_phases function for calculating tidal phases (\"low-flow\", high-flow\", \"high-ebb\", \"low-ebb\") for each tide height in a timeseries. Ebb and low phases are calculated by running the eo_tides.model.model_tides function twice, once for the requested timesteps, and again after subtracting a small time offset (by default, 15 minutes). If tides increased over this period, they are assigned as \"flow\"; if they decreased, they are assigned as \"ebb\". Tides are considered \"high\" if equal or greater than 0 metres tide height, otherwise \"low\".
  • Major refactor to use consistent input parameters across all EO focused functions: input can now be either xr.DataArray or xr.Dataset or odc.geo.geobox.GeoBox; if an xarray object is passed, it must have a \"time\" dimension; if GeoBox is passed, time must be provided by the time parameter.
  • time parameters now accept any format that can be converted by pandas.to_datetime(); e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp, datetime.datetime and strings (e.g. \"2020-01-01 23:00\").
  • model_tides now uses default cropping approach from pyTMD, rather than applying a bespoke 1 degree buffer around the selected analysis area
  • model_tides refactored to use simpler approach to loading tide consistuents enabled in pyTMD==2.1.7
"},{"location":"changelog/#breaking-changes_1","title":"Breaking changes","text":"
  • The ds param in all satellite data functions (tag_tides, pixel_tides, tide_stats, pixel_tides) has been renamed to a more generic name data (to account for now accepting either xarray.Dataset, xarray.DataArray or a odc.geo.geobox.GeoBox inputs).
"},{"location":"changelog/#v010-2024-10-18","title":"v0.1.0 (2024-10-18)","text":""},{"location":"changelog/#new-features_4","title":"New features","text":"
  • Initial creation of eo-tides repo
"},{"location":"changelog/#breaking-changes_2","title":"Breaking changes","text":"

See Migrating from DEA Tools for a guide to updating your code from the original Digital Earth Australia Notebooks and Tools repository.

"},{"location":"credits/","title":"Citations and credits","text":""},{"location":"credits/#citing-eo-tides","title":"Citing eo-tides","text":"

To cite eo-tides in your work, please use the following software citation:

Bishop-Taylor, R., Sagar, S., Phillips, C., & Newey, V. (2024). eo-tides: Tide modelling tools for large-scale satellite earth observation analysis. https://github.com/GeoscienceAustralia/eo-tides\n

In addition, please consider also citing the underlying pyTMD Python package which powers the tide modelling functionality behind eo-tides:

Sutterley, T. C., Alley, K., Brunt, K., Howard, S., Padman, L., Siegfried, M. (2017) pyTMD: Python-based tidal prediction software. 10.5281/zenodo.5555395\n
"},{"location":"credits/#credits","title":"Credits","text":"

eo-tides builds on (and wouldn't be possible without!) fundamental tide modelling tools provided by pyTMD. The authors wish to thank Dr. Tyler Sutterley for his ongoing development and support of this incredible modelling tool.

Functions from eo-tides were originally developed in the Digital Earth Australia Notebooks and Tools repository. The authors would like to thank all DEA Notebooks contributers and maintainers for their invaluable assistance with code review, feature suggestions and code edits.

FES Finite Element Solution tide models were developed, validated by the CTOH/LEGOS, France and distributed by Aviso+: https://www.aviso.altimetry.fr/en/data/products/sea-surface-height-products/regional/x-track-sla/x-track-l2p-sla-version-2022.html

This repository was initialised using the cookiecutter-uv package.

"},{"location":"credits/#references","title":"References","text":"

Carrere et al., OSTST 2022: A new barotropic tide model for global ocean: FES2022, https://doi.org/10.24400/527896/a03-2022.3287

Egbert, Gary D., and Svetlana Y. Erofeeva. \"Efficient inverse modeling of barotropic ocean tides.\" Journal of Atmospheric and Oceanic Technology 19.2 (2002): 183-204.

Florent H. Lyard, Loren Carrere, Ergane Fouchet, Mathilde Cancet, David Greenberg, G\u00e9rald Dibarboure and Nicolas Picot: \u201cFES2022 a step towards a SWOT-compliant tidal correction\u201d, to be submitted to Ocean Sciences.

Hart-Davis Michael, Piccioni Gaia, Dettmering Denise, Schwatke Christian, Passaro Marcello, Seitz Florian (2021). EOT20 - A global Empirical Ocean Tide model from multi-mission satellite altimetry. SEANOE. https://doi.org/10.17882/79489

Hart-Davis Michael G., Piccioni Gaia, Dettmering Denise, Schwatke Christian, Passaro Marcello, Seitz Florian (2021). EOT20: a global ocean tide model from multi-mission satellite altimetry. Earth System Science Data, 13 (8), 3869-3884. https://doi.org/10.5194/essd-13-3869-2021

Sutterley, T. C., Markus, T., Neumann, T. A., van den Broeke, M., van Wessem, J. M., and Ligtenberg, S. R. M.: Antarctic ice shelf thickness change from multimission lidar mapping, The Cryosphere, 13, 1801\u20131817, https://doi.org/10.5194/tc-13-1801-2019, 2019.

"},{"location":"install/","title":"Installing eo-tides","text":""},{"location":"install/#stable-version","title":"Stable version","text":"

The latest stable release of eo-tides can be installed into your Python environment from PyPI using pip:

python3 -m pip install eo-tides\n

By default, only essential package dependencies are installed. To install all packages required for running the included Jupyter Notebook examples (including odc-stac and pystac-client for loading freely available satellite data), run:

python3 -m pip install eo-tides[notebooks]\n
"},{"location":"install/#unstable-development-version","title":"Unstable development version","text":"

To install the latest unstable development version of eo-tides directly from Github, run:

python3 -m pip install git+https://github.com/GeoscienceAustralia/eo-tides.git\n

Important

Unstable development versions of eo-tides may contain bugs and untested new features. Unless you need access to a specific unpublished feature, we recommend installing the latest stable version instead.

"},{"location":"install/#cloning-locally","title":"Cloning locally","text":"

To clone the eo-tides repository locally:

git clone https://github.com/GeoscienceAustralia/eo-tides.git\n

Navigate to the project directory and install in editable mode from your local copy of the repository:

cd eo-tides\npython3 -m pip install -e .\n
"},{"location":"install/#next-steps","title":"Next steps","text":"

Once you have installed eo-tides, you will need to download and set up at least one tide model before you can model tides.

"},{"location":"migration/","title":"Migrating from dea_tools","text":"

The eo-tides package contains functions that were previously available in the Digital Earth Australia Notebooks and Tools repository. To migrate your code from DEA Tools to eo-tides, please be aware of the following breaking changes:

"},{"location":"migration/#breaking-changes","title":"Breaking changes","text":""},{"location":"migration/#tide_m-renamed-to-tide_height","title":"\"tide_m\" renamed to \"tide_height\"","text":"

The default output tide heights column generated by the model_tides function and the xarray.DataArray outputs generated by tag_tides and pixel_tides have been renamed from tide_m to tide_height. This more clearly describes the data, particularly when used in combination with output_units=\"cm\" or output_units=\"mm\" which returns tide heights in non-metre units.

Action required

Update any references of tide_m to tide_height

"},{"location":"migration/#default-model-changed-to-eot20","title":"Default model changed to \"EOT20\"","text":"

The default tide model has been changed from \"FES2014\" to the open source \"EOT20\" Empirical Ocean Tide tide model (CC BY 4.0):

Hart-Davis Michael, Piccioni Gaia, Dettmering Denise, Schwatke Christian, Passaro Marcello, Seitz Florian (2021). EOT20 - A global Empirical Ocean Tide model from multi-mission satellite altimetry. SEANOE. https://doi.org/10.17882/79489

Note that this change in default is primarily due to the permissive license of this model; the choice of optimal model will vary by study area and application.

Action required

To use FES2014, set model=\"FES2014\" instead of leaving the default model=None.

"},{"location":"migration/#tidal_tag-renamed-to-tag_tides","title":"tidal_tag renamed to tag_tides","text":"

Renamed for consistency with model_tides and pixel_tides.

Action required

Update references to tidal_tag to tag_tides.

"},{"location":"migration/#ds-param-renamed-to-data-now-accepts-geobox","title":"ds param renamed to data, now accepts GeoBox","text":"

The ds param in all satellite data functions (tag_tides, pixel_tides, tide_stats, pixel_tides) has been updated to accept either xarray.Dataset, xarray.DataArray or a odc.geo.geobox.GeoBox. To account for this change, the ds param has been renamed to a more generic name data.

Action required

Update:

tag_tides(ds=your_data)\n
To:
tag_tides(data=your_data)\n

"},{"location":"migration/#times-param-renamed-to-time-accepts-any-format-supported-by-pandasto_datetime","title":"times param renamed to time, accepts any format supported by pandas.to_datetime()","text":"

The times parameter has been renamed to time, and updated to more flexibly accept any time format that can be converted by pandas.to_datetime(); e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp, datetime.datetime and strings (e.g. \"2020-01-01 23:00\"). For example: time=pd.date_range(start=\"2000\", end=\"2001\", freq=\"5h\").

Action required

Update:

model_tides(..., times=...)\n
To:
model_tides(..., time=...)\n

"},{"location":"migration/#tag_tides-now-returns-an-array-instead-of-updating-data-in-place","title":"tag_tides now returns an array instead of updating data in-place","text":"

The tag_tides function now returns an xarray.DataArray output containing tide heights, rather than appending tide height data to the original input dataset in-place. This change provides better consistency with pixel_tides, which also returns an array of tide heights.

Action required

Update:

data = tag_tides(data, ...)\n
To:
data[\"tide_height\"] = tag_tides(data, ...)\n

"},{"location":"migration/#pixel_tides-only-returns-a-single-array","title":"pixel_tides only returns a single array","text":"

The pixel_tides function has been updated to only ever return a single array as an output: a high-resolution tide height array matching the resolution of the input data by default, and a low-resolution tide height array if resample=False.

Action required

Update code to handle a single tide height array output from pixel_tides, instead of a tuple of high-resolution and low-resolution modelled tide height arrays.

"},{"location":"migration/#tide-model-directory-environment-variable-updated","title":"Tide model directory environment variable updated","text":"

The DEA_TOOLS_TIDE_MODELS environmental variable has been renamed to EO_TIDES_TIDE_MODELS.

Action required

Set the EO_TIDES_TIDE_MODELS environment variable instead of DEA_TOOLS_TIDE_MODELS.

"},{"location":"migration/#error-raised-if-both-directory-parameter-and-environment-variable-are-missing","title":"Error raised if both directory parameter and environment variable are missing","text":"

Previously, tide modelling functions used a fall-back tide modelling directory (/var/share/tide_models) if both the directory parameter and the tide model directory environment variable were absent. This fall-back has been removed, and an error will now be raised if no tide model directory is specified.

Action required

Ensure that either the directory parameter or the EO_TIDES_TIDE_MODELS environment variable are provided.

"},{"location":"setup/","title":"Setting up tide models","text":"

Important

eo-tides provides tools for modelling tides using global ocean tide models but does not host or maintain the models themselves. Users are responsible for accessing, using, and citing ocean tide models in compliance with each model's licensing terms.

Once you have installed eo-tides, we need to download and set up the external global ocean tide models required for eo-tides to work. The following documentation provides instructions for getting started with several common global ocean tide models.

Tip

Please refer to the pyTMD documentation for additional instructions covering all other supported tide models.

"},{"location":"setup/#setting-up-a-tide-model-directory","title":"Setting up a tide model directory","text":"

As a first step, we need to create a directory that will contain our tide model data. This directory will be accessed by all eo-tides functions. For example, we might want to store our tide models in a directory called tide_models/:

tide_models/\n

Tip

This directory doesn't need to be called tide_models; use any name and/or location that is convenient to you and accessible from your Python environment. Please refer to the documentation below for further details on configuring eo-tides to use this directory.

"},{"location":"setup/#downloading-tide-model-data","title":"Downloading tide model data","text":"

Now we need to download some data from one or more models, and save this into our tide model directory. Follow the guides below for some of the most commonly used global ocean tide models:

EOT20 Empirical Ocean Tide model (default) FES2022 Finite Element Solution tide models FES2014 Finite Element Solution tide models GOT Global Ocean Tide models TPXO Global Tidal Models

Tip

To allow you to improve tide modelling performance by clipping your tide model files (see below), we recommend downloading NetCDF-format versions of tide models wherever possible.

"},{"location":"setup/#eot20-empirical-ocean-tide-model-default","title":"EOT20 Empirical Ocean Tide model (default)","text":"
  1. Visit EOT20 - A global Empirical Ocean Tide model from multi-mission satellite altimetry
  2. Under Data, click Download:

  3. Create a new directory inside your tide model directory called EOT20/ to store the EOT20 model files.

  4. Extract the 85762.zip and then ocean_tides.zip into this new directory. You should end up with the following directory structure containing the extracted NetCDF files:

    tide_models/EOT20/ocean_tides/\n   |- 2N2_ocean_eot20.nc\n   |- ...\n   |- T2_ocean_eot20.nc\n
"},{"location":"setup/#fes2022-finite-element-solution-tide-models","title":"FES2022 Finite Element Solution tide models","text":"
  1. Register with AVISO+, and select FES (Finite Element Solution - Oceanic Tides Heights) from the Licence Agreement and product selection section:

  2. Scroll to the bottom of the page and agree to the licence agreement. Your request will be sent for approval (this may take several days).

  3. Once you are notified via email that your registration and access is approved, login to MY AVISO+.
  4. Once logged in, select My products in the left-hand menu:

  5. FES (Finite Element Solution - Oceanic Tides Heights) should appear under Your current subscriptions. Right click on Ftp, and copy the FTP address.

  6. Using an FTP client like FileZilla, log in to the FTP using your AVISO+ username and password:

  7. Navigate to /auxiliary/tide_model/, and download the contents of one or more of the following directories:

    • fes2022b/ocean_tide/
    • fes2022b/ocean_tide_extrapolated/

    Tip

    The \"extrapolated\" version of FES models have been extended inland using a simple \"nearest\" extrapolation method to ensure data coverage across the entire coastal zone. This can be useful for ensuring you always return a modelled tide, but can also introduce uncertainty into your modelling (particularly in complex regions such as narrow peninsulas or inlets/embayments).

  8. Create new nested directories inside your tide model directory called fes2022b/ocean_tide/ (if using standard model data) or fes2022b/ocean_tide_extrapolated/ (if using extrapolated model data) to store the FES2022 model files.

  9. Extract your ...nc.xz files into this directory (e.g. tar -xf m2_fes2022.nc.xz). You should end up with the following directory structure containing the extracted NetCDF files:

    tide_models/fes2022b/ocean_tide/\n   |- 2n2_fes2022.nc\n   |- ...\n   |- t2_fes2022.nc\n
    Or:
    tide_models/fes2022b/ocean_tide_extrapolated/\n   |- 2n2_fes2022.nc\n   |- ...\n   |- t2_fes2022.nc\n

"},{"location":"setup/#fes2014-finite-element-solution-tide-models","title":"FES2014 Finite Element Solution tide models","text":"
  1. Register with AVISO+, and select FES (Finite Element Solution - Oceanic Tides Heights) from the Licence Agreement and product selection section:

  2. Scroll to the bottom of the page and agree to the licence agreement. Your request will be sent for approval (this may take several days).

  3. Once you are notified via email that your registration and access is approved, login to MY AVISO+.
  4. Once logged in, select My products in the left-hand menu:

  5. FES (Finite Element Solution - Oceanic Tides Heights) should appear under Your current subscriptions. Right click on Ftp, and copy the FTP address.

  6. Using an FTP client like FileZilla, log in to the FTP using your AVISO+ username and password:

  7. Navigate to /auxiliary/tide_model/, and download the contents of one or more of the following directories:

    • fes2014_elevations_and_load/fes2014b_elevations/
    • fes2014_elevations_and_load/fes2014b_elevations_extrapolated/

    Tip

    The \"extrapolated\" version of FES have been extended inland using a simple \"nearest\" extrapolation method to ensure data coverage across the entire coastal zone. This can be useful for ensuring you always return a modelled tide, but can also introduce uncertainty into your modelling (particularly in complex regions such as narrow peninsulas or inlets/embayments).

  8. Create a new directory inside your tide model directory called fes2014/ to store the FES2014 model files.

  9. Extract ocean_tide.tar.xz or ocean_tide_extrapolated.tar.xz into this directory (e.g. tar -xf ocean_tide.tar.xz). You should end up with the following directory structure containing the extracted NetCDF files:

    tide_models/fes2014/ocean_tide/\n   |- 2n2.nc\n   |- ...\n   |- t2.nc\n
    Or:
    tide_models/fes2014/ocean_tide_extrapolated/\n   |- 2n2.nc\n   |- ...\n   |- t2.nc\n

"},{"location":"setup/#got-global-ocean-tide-models","title":"GOT Global Ocean Tide models","text":"
  1. Visit Ocean tide models
  2. Under Short-period (diurnal/semidiurnal) tides, click choose your desired GOT model:

  3. Create a new directory inside your tide model directory called either GOT4.7/, got4.8/, GOT4.10c/, GOT5.5/ or GOT5.6/ to store the GOT model files.

  4. Extract your downloaded .tar.gz file into this new directory. You should end up with the following directory structure containing the extracted NetCDF files:

    tide_models/GOT5.6/ocean_tides/\n   |- ...\n
    Or:
    tide_models/GOT5.5/ocean_tides/\n   |- ...\n

    Important

    Note that GOT5.6 requires that both GOT5.6 and GOT5.5 model files are downloaded and extracted.

    Or:

    tide_models/GOT4.10c/grids_oceantide/\n   |- ...\n
    Or:
    tide_models/got4.8/grids_oceantide/\n   |- ...\n
    Or:
    tide_models/GOT4.7/grids_oceantide/\n   |- ...\n

"},{"location":"setup/#tpxo-global-tidal-models","title":"TPXO Global Tidal Models","text":"
  1. Visit TPXO Registration
  2. Follow instructions to email TPXO authors for access, providing your name, institution, your intended application/use case, and which TPXO models you need (\"TPXO10-atlas-v2 netcdf\" or \"TPXO9-atlas-v5 netcdf\" are recommended to enable clipping).
  3. If your request is approved, you will be emailed an invite to an app.box.com folder. Open this link, then click \"Download\" on the top-right to download your zipped model files.

  4. Create a new directory inside your tide model directory called either TPXO10_atlas_v2/ or TPXO9_atlas_v5/ to store the TPXO model files.

  5. Extract your zipped model files (e.g. TPXO10_atlas_v2_nc.zip or TPXO9_atlas_v5_nc.zip) into this new directory. You should end up with the following directory structure containing the extracted NetCDF files depending on the model you downloaded:

    tide_models/TPXO10_atlas_v2/\n   |- grid_tpxo10atlas_v2.nc\n   |- ...\n   |- u_s2_tpxo10_atlas_30_v2.nc\n
    Or:
    tide_models/TPXO9_atlas_v5/\n   |- grid_tpxo9_atlas_30_v5.nc\n   |- ...\n   |- u_s2_tpxo9_atlas_30_v5.nc\n

"},{"location":"setup/#configuring-eo-tides-to-use-tide-model-directory","title":"Configuring eo-tides to use tide model directory","text":"

eo-tides can be pointed to the location of your tide model directory and your downloaded tide model data in two ways:

"},{"location":"setup/#1-using-the-directory-function-parameter","title":"1. Using the directory function parameter","text":"

All tide modelling functions from eo-tides provide a directory parameter that can be used to specify the location of your tide model directory. For example, using the eo_tides.model.model_tides function:

import pandas as pd\nfrom eo_tides.model import model_tides\n\nmodel_tides(\n        x=155,\n        y=-35,\n        time=pd.date_range(\"2022-01-01\", \"2022-01-04\", freq=\"1D\"),\n        directory=\"tide_models/\"\n)\n
"},{"location":"setup/#2-advanced-setting-the-eo_tides_tide_models-environmental-variable","title":"2. (Advanced) Setting the EO_TIDES_TIDE_MODELS environmental variable","text":"

For more advanced usage, you can set the path to your tide model directory by setting the EO_TIDES_TIDE_MODELS environment variable:

import os\nos.environ[\"EO_TIDES_TIDE_MODELS\"] = \"tide_models/\"\n

All tide modelling functions from eo-tides will check for the presence of the EO_TIDES_TIDE_MODELS environment variable, and use it as the default directory path if available (the EO_TIDES_TIDE_MODELS environment variable will be overuled by the directory parameter if provided).

Tip

Setting the EO_TIDES_TIDE_MODELS environment variable can be useful when the location of your tide model directory might change between different environments, and you want to avoid hard-coding a single location via the directory parameter.

"},{"location":"setup/#verifying-available-and-supported-models","title":"Verifying available and supported models","text":"

You can check what tide models have been correctly set up for use by eo-tides using the eo_tides.utils.list_models function:

from eo_tides.utils import list_models\n\navailable_models, supported_models = list_models(directory=\"tide_models/\")\n

This will print out a useful summary, with available models marked with a \u2705:

\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n \udb40\udc20\ud83c\udf0a  | Model        | Expected path\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n \u2705  \u2502 EOT20        \u2502 tide_models/EOT20/ocean_tides\n \u274c  \u2502 FES2014      \u2502 tide_models/fes2014/ocean_tide\n \u2705  \u2502 HAMTIDE11    \u2502 tide_models/hamtide\n \u274c  \u2502 TPXO9.1      \u2502 tide_models/TPXO9.1/DATA\n ...   ...            ...\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSummary:\nAvailable models: 2/50\n

Tip

If the models that you saved are not marked as available in this summary, check that they were saved to the expected path as identified in the summary table.

"},{"location":"setup/#clipping-model-files-to-improve-performance","title":"Clipping model files to improve performance","text":"

Highly recommended

Clipping your model files to a smaller spatial extent is highly recommended, unless you are specifically running global-scale analyses.

Running tide modelling on the default tide modelling data provided by external providers can be slow due to the large size of these files (especially for high-resolution models like FES2022). To improve performance, it can be extremely useful to clip your model files to a smaller region of interest (e.g. the extents of your country or coastal region). This can greatly improve run-times: potentially speeding up your tide modelling by over 10 times for large models!

Once you have downloaded and verified your tide model data, you can use the eo_tides.utils.clip_models function to automatically clip your data, and export them to a new tide modelling directory:

from eo_tides.utils import clip_models\n\nclip_models(\n    input_directory=\"tide_models/\",\n    output_directory=\"tide_models_clipped/\",\n    bbox=(113.3, -43.6, 153.6, -10.7),\n)\n

When you run clip_models, the function will automatically identify suitable NetCDF-format models in your input directory, and clip each of them to the extent of your bounding box (specified as (left, bottom, right, top) in EPSG:4326 degree coordinates). After each model is clipped, the result is exported to your selected output directory and verified to ensure the clipped data is suitable for tide modelling:

Preparing to clip suitable NetCDF models: ['HAMTIDE11', 'EOT20']\n\nClipping HAMTIDE11: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 9/9 [00:03<00:00,  2.60it/s]\n\u2705 Clipped model exported and verified\nClipping EOT20: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 17/17 [00:07<00:00,  2.36it/s]\n\u2705 Clipped model exported and verified\n\nOutputs exported to tide_models_clipped/\n

You can now pass this new clipped tide model directory to all future eo_tides function calls for improved tide modelling performance, e.g.:

model_tides(\n        x=155,\n        y=-35,\n        time=pd.date_range(\"2022-01-01\", \"2022-01-04\", freq=\"1D\"),\n        directory=\"tide_models_clipped/\"\n)\n

Example of tide model data for the M2 tidal constituent from EOT20 clipped to Australia:

Tip

Because only NetCDF-format tide models can be clipped, we recommend downloading NetCDF versions of your tide models wherever possible.

"},{"location":"setup/#next-steps","title":"Next steps","text":"

Now that you have installed eo-tides and set up some tide models, you can learn how to use eo-tides for modelling tides and analysing satellite data!

"},{"location":"notebooks/Case_study_intertidal/","title":"Mapping the intertidal zone","text":"In\u00a0[1]: Copied!
import odc.stac\nimport pystac_client\nimport planetary_computer\nimport matplotlib.pyplot as plt\n\nfrom eo_tides.eo import tag_tides\nfrom eo_tides.stats import tide_stats\nfrom eo_tides.utils import list_models\n
import odc.stac import pystac_client import planetary_computer import matplotlib.pyplot as plt from eo_tides.eo import tag_tides from eo_tides.stats import tide_stats from eo_tides.utils import list_models In\u00a0[2]: Copied!
directory = \"../../tests/data/tide_models\"\n\n# Confirm we have model data\nlist_models(directory=directory, show_supported=False);\n
directory = \"../../tests/data/tide_models\" # Confirm we have model data list_models(directory=directory, show_supported=False);
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n \udb40\udc20\ud83c\udf0a  | Model                | Expected path                                                \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n \u2705  \u2502 EOT20                \u2502 ../../tests/data/tide_models/EOT20/ocean_tides               \n \u2705  \u2502 GOT5.5               \u2502 ../../tests/data/tide_models/GOT5.5/ocean_tides              \n \u2705  \u2502 HAMTIDE11            \u2502 ../../tests/data/tide_models/hamtide                         \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSummary:\nAvailable models: 3/50\n
In\u00a0[3]: Copied!
# Set the study area (xmin, ymin, xmax, ymax)\nbbox = [122.12, -18.25, 122.43, -17.93]\n\n# Set the time period\nstart_date = \"2022-01-01\"\nend_date = \"2023-12-31\"\n\n# Satellite products and bands to load\nsatellite_sensors = [\"landsat-c2-l2\"]\nbands = [\"green\", \"nir08\"]\n\n# Tide model to use\ntide_model = \"EOT20\"\n
# Set the study area (xmin, ymin, xmax, ymax) bbox = [122.12, -18.25, 122.43, -17.93] # Set the time period start_date = \"2022-01-01\" end_date = \"2023-12-31\" # Satellite products and bands to load satellite_sensors = [\"landsat-c2-l2\"] bands = [\"green\", \"nir08\"] # Tide model to use tide_model = \"EOT20\" In\u00a0[4]: Copied!
# Connect to STAC catalog\ncatalog = pystac_client.Client.open(\n    \"https://planetarycomputer.microsoft.com/api/stac/v1\",\n    modifier=planetary_computer.sign_inplace,\n)\n\n# Set cloud access defaults\nodc.stac.configure_rio(\n    cloud_defaults=True,\n    aws={\"aws_unsigned\": True},\n)\n\n# Build a query and search the STAC catalog for all matching items\nquery = catalog.search(\n    bbox=bbox,\n    collections=satellite_sensors,\n    datetime=f\"{start_date}/{end_date}\",\n    query={\n        \"eo:cloud_cover\": {\"lt\": 10},  # Filter to images with <5% cloud\n        \"platform\": {\"in\": [\"landsat-8\", \"landsat-9\"]},  # No Landsat 7\n    },\n)\n\n# Load data into xarray format\nds = odc.stac.load(\n    items=query.item_collection(),\n    bands=bands,\n    crs=\"utm\",\n    resolution=50,\n    groupby=\"solar_day\",\n    bbox=bbox,\n    fail_on_error=False,\n    chunks={},\n)\n\n# Apply USGS Landsat Collection 2 scaling factors to convert\n# surface reflectance to between 0.0 and 1.0. See:\n# https://www.usgs.gov/faqs/how-do-i-use-a-scale-factor-landsat-level-2-science-products\nds = (ds.where(ds != 0) * 0.0000275 + -0.2).clip(0, 1)\nprint(ds)\n
# Connect to STAC catalog catalog = pystac_client.Client.open( \"https://planetarycomputer.microsoft.com/api/stac/v1\", modifier=planetary_computer.sign_inplace, ) # Set cloud access defaults odc.stac.configure_rio( cloud_defaults=True, aws={\"aws_unsigned\": True}, ) # Build a query and search the STAC catalog for all matching items query = catalog.search( bbox=bbox, collections=satellite_sensors, datetime=f\"{start_date}/{end_date}\", query={ \"eo:cloud_cover\": {\"lt\": 10}, # Filter to images with <5% cloud \"platform\": {\"in\": [\"landsat-8\", \"landsat-9\"]}, # No Landsat 7 }, ) # Load data into xarray format ds = odc.stac.load( items=query.item_collection(), bands=bands, crs=\"utm\", resolution=50, groupby=\"solar_day\", bbox=bbox, fail_on_error=False, chunks={}, ) # Apply USGS Landsat Collection 2 scaling factors to convert # surface reflectance to between 0.0 and 1.0. See: # https://www.usgs.gov/faqs/how-do-i-use-a-scale-factor-landsat-level-2-science-products ds = (ds.where(ds != 0) * 0.0000275 + -0.2).clip(0, 1) print(ds)
<xarray.Dataset> Size: 454MB\nDimensions:      (time: 121, y: 712, x: 659)\nCoordinates:\n  * y            (y) float64 6kB 8.017e+06 8.017e+06 ... 7.982e+06 7.982e+06\n  * x            (x) float64 5kB 4.068e+05 4.069e+05 ... 4.397e+05 4.397e+05\n    spatial_ref  int32 4B 32751\n  * time         (time) datetime64[ns] 968B 2022-01-01T01:55:34.757654 ... 20...\nData variables:\n    green        (time, y, x) float32 227MB dask.array<chunksize=(1, 712, 659), meta=np.ndarray>\n    nir08        (time, y, x) float32 227MB dask.array<chunksize=(1, 712, 659), meta=np.ndarray>\n
In\u00a0[5]: Copied!
# Calculate NDWI\nds[[\"ndwi\"]] = (ds.green - ds.nir08) / (ds.green + ds.nir08) \n\n# Plot a single timestep\nds.ndwi.isel(time=1).odc.explore(vmin=-0.5, vmax=0.5, cmap=\"RdBu\")\n
# Calculate NDWI ds[[\"ndwi\"]] = (ds.green - ds.nir08) / (ds.green + ds.nir08) # Plot a single timestep ds.ndwi.isel(time=1).odc.explore(vmin=-0.5, vmax=0.5, cmap=\"RdBu\")
/env/lib/python3.10/site-packages/dask/core.py:133: RuntimeWarning: invalid value encountered in divide\n  return func(*(_execute_task(a, cache) for a in args))\n/env/lib/python3.10/site-packages/rasterio/warp.py:387: NotGeoreferencedWarning: Dataset has no geotransform, gcps, or rpcs. The identity matrix will be returned.\n  dest = _reproject(\n
Out[5]: Make this Notebook Trusted to load map: File -> Trust Notebook In\u00a0[6]: Copied!
ds[\"tide_height\"] = tag_tides(\n    data=ds,\n    model=tide_model,\n    directory=directory,\n)\n
ds[\"tide_height\"] = tag_tides( data=ds, model=tide_model, directory=directory, )
Setting tide modelling location from dataset centroid: 122.27, -18.09\nModelling tides using EOT20\n

We can plot a histogram of the tide heights of our satellite images. This shows that we have decent coverage of the tide range between approximately -2.8 to 3.5 m Above Mean Sea Level (AMSL):

In\u00a0[7]: Copied!
ds[\"tide_height\"].plot.hist(bins=10, figsize=(5, 3))\nplt.title(\"Histogram of tide heights in satellite timeseries\");\n
ds[\"tide_height\"].plot.hist(bins=10, figsize=(5, 3)) plt.title(\"Histogram of tide heights in satellite timeseries\");

It is also important to compare our satellite tide observations against the full range of astronomical tides at our location. This lets us become aware of any major biases in our data. For example, here we can see that our data is biased away from low tide observations. This means that our final intertidal maps will underestimate the lower extent of the intertidal zone.

In\u00a0[8]: Copied!
tide_stats(\n    data=ds,\n    model=tide_model,\n    directory=directory,\n);\n
tide_stats( data=ds, model=tide_model, directory=directory, );
Using tide modelling location: 122.27, -18.09\nModelling tides using EOT20\nModelling tides using EOT20\n\n\n\ud83c\udf0a Modelled astronomical tide range: 9.46 metres.\n\ud83d\udef0\ufe0f Observed tide range: 6.37 metres.\n\n\ud83d\udd34 67% of the modelled astronomical tide range was observed at this location.\n\ud83d\udfe1 The highest 12% (1.09 metres) of the tide range was never observed.\n\ud83d\udd34 The lowest 21% (2.00 metres) of the tide range was never observed.\n\n\ud83c\udf0a Mean modelled astronomical tide height: -0.00 metres.\n\ud83d\udef0\ufe0f Mean observed tide height: 0.65 metres.\n\n\u2b06\ufe0f The mean observed tide height was 0.65 metres higher than the mean modelled astronomical tide height.\n
In\u00a0[9]: Copied!
# Calculate low and high tide height thresholds\nlowtide_cutoff = 0.2\nhightide_cutoff = 0.8\nlowtide_thresh, hightide_thresh = ds.tide_height.quantile([lowtide_cutoff, hightide_cutoff])\nprint(f\"Low tide threshold: {lowtide_thresh:.2f} metres AMSL\")\nprint(f\"High tide threshold: {hightide_thresh:.2f} metres AMSL\")\n
# Calculate low and high tide height thresholds lowtide_cutoff = 0.2 hightide_cutoff = 0.8 lowtide_thresh, hightide_thresh = ds.tide_height.quantile([lowtide_cutoff, hightide_cutoff]) print(f\"Low tide threshold: {lowtide_thresh:.2f} metres AMSL\") print(f\"High tide threshold: {hightide_thresh:.2f} metres AMSL\")
Low tide threshold: -1.00 metres AMSL\nHigh tide threshold: 2.51 metres AMSL\n

We can now use these thresholds to select just the subset of our data with tide heights lower and higher than our thresholds:

In\u00a0[10]: Copied!
# Extract subset of low and high tide images\nds_lowtide = ds.sel(time=ds.tide_height <= lowtide_thresh)\nds_hightide = ds.sel(time=ds.tide_height >= hightide_thresh)\n\n# Plot extracted images over all images\nds.tide_height.plot(marker=\"o\", linewidth=0, label=\"Other satellite images\")\nds_hightide.tide_height.plot(marker=\"o\", linewidth=0, label=\"High tide images\")\nds_lowtide.tide_height.plot(marker=\"o\", linewidth=0, label=\"Low tide images\")\nplt.axhline(lowtide_thresh, color=\"black\", linestyle=\"dashed\")\nplt.axhline(hightide_thresh, color=\"black\", linestyle=\"dashed\")\nplt.legend(bbox_to_anchor=(1.01, 0.6))\nplt.title(\"Low and high tide satellite images\");\n
# Extract subset of low and high tide images ds_lowtide = ds.sel(time=ds.tide_height <= lowtide_thresh) ds_hightide = ds.sel(time=ds.tide_height >= hightide_thresh) # Plot extracted images over all images ds.tide_height.plot(marker=\"o\", linewidth=0, label=\"Other satellite images\") ds_hightide.tide_height.plot(marker=\"o\", linewidth=0, label=\"High tide images\") ds_lowtide.tide_height.plot(marker=\"o\", linewidth=0, label=\"Low tide images\") plt.axhline(lowtide_thresh, color=\"black\", linestyle=\"dashed\") plt.axhline(hightide_thresh, color=\"black\", linestyle=\"dashed\") plt.legend(bbox_to_anchor=(1.01, 0.6)) plt.title(\"Low and high tide satellite images\"); In\u00a0[11]: Copied!
# Combine NDWI into single median composites\nndwi_lowtide_median = ds_lowtide.ndwi.median(dim=\"time\")\nndwi_hightide_median = ds_hightide.ndwi.median(dim=\"time\")\n\n# Process our data and load into memory\nndwi_lowtide_median.load()\nndwi_hightide_median.load();\n
# Combine NDWI into single median composites ndwi_lowtide_median = ds_lowtide.ndwi.median(dim=\"time\") ndwi_hightide_median = ds_hightide.ndwi.median(dim=\"time\") # Process our data and load into memory ndwi_lowtide_median.load() ndwi_hightide_median.load();

Now that we have processed our median NDWI composites, we can plot them on a map:

In\u00a0[12]: Copied!
ndwi_lowtide_median.odc.explore(vmin=-0.5, vmax=0.5, cmap=\"RdBu\")\n
ndwi_lowtide_median.odc.explore(vmin=-0.5, vmax=0.5, cmap=\"RdBu\") Out[12]: Make this Notebook Trusted to load map: File -> Trust Notebook In\u00a0[13]: Copied!
ndwi_hightide_median.odc.explore(vmin=-0.5, vmax=0.5, cmap=\"RdBu\")\n
ndwi_hightide_median.odc.explore(vmin=-0.5, vmax=0.5, cmap=\"RdBu\") Out[13]: Make this Notebook Trusted to load map: File -> Trust Notebook

Or plot them together for a direct comparison. We can see that the low tide NDWI composite contains additional exposed intertidal terrain along the coastline that becomes inundated at high tide:

In\u00a0[14]: Copied!
fig, axes = plt.subplots(1, 2, figsize=(12, 5))\nndwi_lowtide_median.plot.imshow(ax=axes[0], vmin=-0.5, vmax=0.5, cmap=\"RdBu\")\nndwi_hightide_median.plot.imshow(ax=axes[1], vmin=-0.5, vmax=0.5, cmap=\"RdBu\")\naxes[0].set_title(\"Low tide NDWI composite\")\naxes[1].set_title(\"High tide NDWI composite\");\n
fig, axes = plt.subplots(1, 2, figsize=(12, 5)) ndwi_lowtide_median.plot.imshow(ax=axes[0], vmin=-0.5, vmax=0.5, cmap=\"RdBu\") ndwi_hightide_median.plot.imshow(ax=axes[1], vmin=-0.5, vmax=0.5, cmap=\"RdBu\") axes[0].set_title(\"Low tide NDWI composite\") axes[1].set_title(\"High tide NDWI composite\"); In\u00a0[15]: Copied!
# Classify our NDWI layers\nndwi_lowtide_bool = ndwi_lowtide_median > 0.0\nndwi_hightide_bool = ndwi_hightide_median > 0.0\n\n# Plot outputs\nfig, axes = plt.subplots(1, 2, figsize=(12, 5))\nndwi_lowtide_bool.plot.imshow(ax=axes[0])\nndwi_hightide_bool.plot.imshow(ax=axes[1])\naxes[0].set_title(\"Low tide classified NDWI\")\naxes[1].set_title(\"High tide classified NDWI\");\n
# Classify our NDWI layers ndwi_lowtide_bool = ndwi_lowtide_median > 0.0 ndwi_hightide_bool = ndwi_hightide_median > 0.0 # Plot outputs fig, axes = plt.subplots(1, 2, figsize=(12, 5)) ndwi_lowtide_bool.plot.imshow(ax=axes[0]) ndwi_hightide_bool.plot.imshow(ax=axes[1]) axes[0].set_title(\"Low tide classified NDWI\") axes[1].set_title(\"High tide classified NDWI\");

Now, we can identify pixels that are True (dry) in our low tide data, and False (wet) in our high tide data. Yellow pixels in our resulting map represent likely intertidal pixels that are exposed at low tide and inundated at high tide!

In\u00a0[16]: Copied!
intertidal = ~ndwi_lowtide_bool & ndwi_hightide_bool\nintertidal.odc.explore()\n
intertidal = ~ndwi_lowtide_bool & ndwi_hightide_bool intertidal.odc.explore() Out[16]: Make this Notebook Trusted to load map: File -> Trust Notebook In\u00a0[18]: Copied!
intertidal.astype(\"int16\").odc.write_cog(\"intertidal_map.tif\", overwrite=True);\n
intertidal.astype(\"int16\").odc.write_cog(\"intertidal_map.tif\", overwrite=True);"},{"location":"notebooks/Case_study_intertidal/#mapping-the-intertidal-zone","title":"Mapping the intertidal zone\u00b6","text":"

The intertidal zone (i.e. the land along the coast that is periodically inundated by the tide) support important ecological habitats (e.g. sandy beaches and shores, tidal flats and rocky shores and reefs), and provide many valuable benefits such as storm surge protection, carbon storage and natural resources for recreational and commercial use. However, intertidal zones are faced with increasing threats from coastal erosion, land reclamation (e.g. port construction), and sea level rise. Accurate mapping data describing the spatial extents of the intertidal zone are essential for managing these environments, and predicting when and where these threats will have the greatest impact. However, the intertidal zone is challenging and expensive to map at large scale using intensive manual survey methods - particularly across large coastal regions.

Satellite Earth observation (EO) data is freely available for the entire planet, making satellite imagery a powerful and cost-effective tool for mapping the intertidal zone at regional, national scale or global scale. This case study will demonstrate a simple intertidal mapping workflow that combines free and open Landsat satellite data with tide modelling from eo-tides. The workflow includes:

  1. Loading a time-series of cloud-free satellite data from the cloud using odc-stac
  2. Converting our satellite data to a remote sensing water index (NDWI)
  3. Modelling tides for each satellite image and inspecting how these observed tides match up to the full local astronomical tide range
  4. Filtering our satellite imagery to low and high tide observations
  5. Combining noisy individual images into clean low and high tide median NDWI composites
  6. Using these composites to extract the extent of the intertidal zone

More information

For more information about the workflows described below, refer to Sagar et al. 2017, Sagar et al. 2018, and Bishop-Taylor et al. 2019.

"},{"location":"notebooks/Case_study_intertidal/#getting-started","title":"Getting started\u00b6","text":"

Import any Python packages we need for the analysis:

"},{"location":"notebooks/Case_study_intertidal/#tide-model-directory","title":"Tide model directory\u00b6","text":"

We need to tell eo-tides the location of our tide model directory (if you haven't set this up, refer to the setup instructions here):

"},{"location":"notebooks/Case_study_intertidal/#analysis-parameters","title":"Analysis parameters\u00b6","text":"

To make our analysis more re-usable, we can define some important parameters up-front. The default will load Landsat 8 and 9 satellite data from 2022-23 over the city of Broome, Western Australia - a macrotidal region with extensive intertidal coastal habitats.

Tip

Leave the defaults below unchanged the first time you run through this notebook.

"},{"location":"notebooks/Case_study_intertidal/#load-satellite-data-using-odc-stac","title":"Load satellite data using odc-stac\u00b6","text":"

Now we can load a time-series of satellite data over our area of interest using the Open Data Cube's odc-stac package. This powerful package allows us to load open satellite data (e.g ESA Sentinel-2 or NASA/USGS Landsat) for any time period and location on the planet, and load our data into a multi-dimensional xarray.Dataset format dataset.

In this example, we will load our data from the Microsoft Planetary Computer STAC catalogue, and restrict our data to cloud-free images with less than 10% cloud (eo:cloud_cover\": {\"lt\": 10}\"), and load our data at low resolution (resolution=50) to improve load times.

Tip

For a more detailed guide to using STAC metadata and odc-stac to find and load satellite data, refer to this STAC user guide.

"},{"location":"notebooks/Case_study_intertidal/#converting-satellite-data-to-ndwi-water-index","title":"Converting satellite data to NDWI water index\u00b6","text":"

To help us map the distribution of tidal inundation across our study area, we can convert our spectral bands to a remote sensing water index that enhances contrast between water and dry land. We will use the Normalised Difference Water Index (NDWI), which is particularly useful for intertidal mapping as it is less vulnerable to misclassifications caused by wet sand and mud after high tide:

$$ \\text{NDWI} = \\frac{\\text{Green} - \\text{NIR}}{\\text{Green} + \\text{NIR}} $$

In the image below, red pixels represent dry land pixels, while blue pixels represent water:

"},{"location":"notebooks/Case_study_intertidal/#modelling-tide-heights-for-each-satellite-image","title":"Modelling tide heights for each satellite image\u00b6","text":"

We can now use the tag_tides function to model tides for each image in our satellite timeseries:

"},{"location":"notebooks/Case_study_intertidal/#exploring-tide-biases","title":"Exploring tide biases\u00b6","text":""},{"location":"notebooks/Case_study_intertidal/#extracting-low-and-high-tide-images","title":"Extracting low and high tide images\u00b6","text":"

Now that we have satellite images and associated tide heights, we can filter our data to extract only images from specific tidal stages.

In this example, we will focus on extracting low tide and high tide images. We can identify low and high tide images by calculating quantiles of tide heights across time, selecting images from the lowest and highest 20% of tide heights.

Tip

Try modifying the lowtide_cutoff and hightide_cutoff tide cutoffs below to customise the tidal stages we will analyse.

"},{"location":"notebooks/Case_study_intertidal/#creating-low-and-high-tide-ndwi-composites","title":"Creating low and high tide NDWI composites\u00b6","text":"

Individual satellite images can be affected by many sources of noise, including clouds, saltspray, whitewater and sunglint. These sources of noise can make it difficult to map coastal environments consistently across time and along the coastline.

To produce consistent analysis outputs, it can be useful to combine multiple satellite images into a single, cleaner \"composite\" image. This can improve the signal-to-noise ratio of our data, and allow us to map coastal features and change more accurately (at the cost of lower temporal resolution).

In this example, we will combine our low and high tide NDWI data into two clean, relatively noise-free low tide and high tide median composites. We use a median calculation for this analysis as medians are extremely robust to noisy data.

Important

Note the use of .load() below. Up to this point, our entire analysis has been \"lazy\", which means we haven't loaded the majority of our satellite data - we have simply \"queued\" up our analysis to run in a single step. This makes it quick and easy to write code without having to wait for every step of our workflow to run every time, or ever worrying about running out of memory. Running .load() triggers our entire analysis to run, and then load our final outputs into memory for further use. For more information about lazy loading and processing, see Parallel processing with Dask.

Important

Be patient; this step may take several minutes to complete.

"},{"location":"notebooks/Case_study_intertidal/#mapping-intertidal-extent","title":"Mapping intertidal extent\u00b6","text":"

We can now perform a simple comparison of our two outputs to identify intertidal pixels as any pixels that are dry in our low tide composite, but wet in our high tide composite.

First, let's classify our NDWI outputs into binary dry (True or 1.0) vs. wet (False or 0.0) using a standard 0.0 NDWI threshold. In the images below, yellow pixels represent water, and purple pixels represent land:

"},{"location":"notebooks/Case_study_intertidal/#exporting-data","title":"Exporting data\u00b6","text":"

As a final step, we can export our classified intertidal map as a GeoTIFF raster that we can load into a GIS like QGIS or ArcGIS Pro:

"},{"location":"notebooks/Case_study_intertidal/#next-steps","title":"Next steps\u00b6","text":"

Now that you have completed running this example, here's some possible next steps:

  • Download the exported intertidal_map.tif and load it into a GIS software (QGIS, ArcGIS Pro) to inspect the output classification.
  • Return to Analysis parameters, and re-run the analysis for a different location (bbox) or time period (start_date, end_date).
  • Try modifying Analysis parameters to select a different tide model (e.g. tide_model=\"GOT5.5\" or tide_model=\"HAMTIDE11\").
  • Return to Extracting low and high tide images, and change the percentage thresholds (lowtide_cutoff, hightide_cutoff) used to identify low and high tide images.
  • Advanced: In this simple analysis, we filtered to cloud-free images by discarding entire images with more than 10% cloud. Rather than filtering cloud-free images, consider masking clouds at the pixel-level using cloud masking bands that are packaged with the data (e.g. qa_pixel).
  • Advanced: NDWI is just one of many possible remote sensing water indices that can be used for coastal analysis. Update the workflow to use a different water index (e.g. MNDWI), ensuring that you load any new bands required for the index calculation.

In addition, consider the following questions:

  • What implications do the tide biases calculated in Exploring tide biases have for the outputs of this analysis?
  • Are our outputs likely to fully map the entire intertidal zone? What areas of the intertidal zone are likely to be better or poorly mapped?
  • If you experimented with running the analysis using a different tide model, how does this influence our results?
"},{"location":"notebooks/Model_tides/","title":"Modelling tides","text":"In\u00a0[1]: Copied!
directory = \"../../tests/data/tide_models/\"\n
directory = \"../../tests/data/tide_models/\"

Important

The directory above is for demo purposes only. Update the directory path to point to the location of your own tide model directory.

We can use the eo_tides.utils.list_models function to verify that we have some tide model data available in this directory:

In\u00a0[2]: Copied!
from eo_tides.utils import list_models\n\nlist_models(directory=directory, show_supported=False);\n
from eo_tides.utils import list_models list_models(directory=directory, show_supported=False);
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n \udb40\udc20\ud83c\udf0a  | Model                | Expected path                                                   \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n \u2705  \u2502 EOT20                \u2502 ../../tests/data/tide_models/EOT20/ocean_tides                  \n \u2705  \u2502 GOT5.5               \u2502 ../../tests/data/tide_models/GOT5.5/ocean_tides                 \n \u2705  \u2502 HAMTIDE11            \u2502 ../../tests/data/tide_models/hamtide                            \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSummary:\nAvailable models: 3/51\n
In\u00a0[3]: Copied!
from eo_tides.model import model_tides\nimport pandas as pd\n\ntide_df = model_tides(\n    x=122.2186,\n    y=-18.0008,\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", freq=\"1h\"),\n    directory=directory,\n)\n\n# Print outputs\ntide_df.head()\n
from eo_tides.model import model_tides import pandas as pd tide_df = model_tides( x=122.2186, y=-18.0008, time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", freq=\"1h\"), directory=directory, ) # Print outputs tide_df.head()
Modelling tides with EOT20\n
Out[3]: tide_model tide_height time x y 2018-01-01 00:00:00 122.2186 -18.0008 EOT20 1.229286 2018-01-01 01:00:00 122.2186 -18.0008 EOT20 2.162897 2018-01-01 02:00:00 122.2186 -18.0008 EOT20 2.476600 2018-01-01 03:00:00 122.2186 -18.0008 EOT20 2.112390 2018-01-01 04:00:00 122.2186 -18.0008 EOT20 1.181283

The resulting pandas.DataFrame contains:

  • time, x, y: Our original input timesteps and coordinates
  • tide_model: a column listing the tide model used
  • tide_height: modelled tide heights representing tide height in metres relative to Mean Sea Level

We can plot our modelled outputs to view how tides changed across the month. Looking at the y-axis, we can see that tides at this macrotidal region ranged from -4 metres up to a maximum of +4 metres relative to Mean Sea Level:

In\u00a0[4]: Copied!
tide_df.droplevel([\"x\", \"y\"]).tide_height.plot();\n
tide_df.droplevel([\"x\", \"y\"]).tide_height.plot(); In\u00a0[5]: Copied!
tide_df_multiple = model_tides(\n    x=122.2186,\n    y=-18.0008,\n    model=[\"EOT20\", \"HAMTIDE11\", \"GOT5.5\"],\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", freq=\"1h\"),\n    output_format=\"wide\",\n    directory=directory,\n)\n\n# Print outputs\ntide_df_multiple.head()\n
tide_df_multiple = model_tides( x=122.2186, y=-18.0008, model=[\"EOT20\", \"HAMTIDE11\", \"GOT5.5\"], time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", freq=\"1h\"), output_format=\"wide\", directory=directory, ) # Print outputs tide_df_multiple.head()
Modelling tides with EOT20, HAMTIDE11, GOT5.5 in parallel (models: 3, splits: 1)\n
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 18.09it/s]\n
Converting to a wide format dataframe\n
Out[5]: tide_model EOT20 GOT5.5 HAMTIDE11 time x y 2018-01-01 00:00:00 122.2186 -18.0008 1.229286 1.298427 1.422702 2018-01-01 01:00:00 122.2186 -18.0008 2.162897 2.287205 2.302042 2018-01-01 02:00:00 122.2186 -18.0008 2.476600 2.618187 2.537032 2018-01-01 03:00:00 122.2186 -18.0008 2.112390 2.228044 2.072846 2018-01-01 04:00:00 122.2186 -18.0008 1.181283 1.241291 1.034931

Plot our outputs to see all our models on a graph:

In\u00a0[6]: Copied!
# Print outputs\ntide_df_multiple.droplevel([\"x\", \"y\"]).plot(legend=True)\n
# Print outputs tide_df_multiple.droplevel([\"x\", \"y\"]).plot(legend=True) Out[6]:
<Axes: xlabel='time'>
In\u00a0[7]: Copied!
model_tides(\n    x=[122.21, 122.22],\n    y=[-18.20, -18.21],\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2),\n    mode=\"one-to-many\",\n    directory=directory,\n)\n
model_tides( x=[122.21, 122.22], y=[-18.20, -18.21], time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2), mode=\"one-to-many\", directory=directory, )
Modelling tides with EOT20\n
Out[7]: tide_model tide_height time x y 2018-01-01 122.21 -18.20 EOT20 1.231424 2018-01-31 122.21 -18.20 EOT20 0.368303 2018-01-01 122.22 -18.21 EOT20 1.231424 2018-01-31 122.22 -18.21 EOT20 0.368303

However, another common use case is having a list of locations you want to use to model tides for, each with a single timestep. Using \"one-to-one\" mode, we can model tides for each pair of locations and times:

2 timesteps at 2 locations = 2 modelled tides\n

For example, you may have a pandas.DataFrame containing x, y and time values:

In\u00a0[8]: Copied!
df = pd.DataFrame(\n    {\n        \"time\": pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2),\n        \"x\": [122.21, 122.22],\n        \"y\": [-18.20, -18.21],\n    }\n)\ndf\n
df = pd.DataFrame( { \"time\": pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2), \"x\": [122.21, 122.22], \"y\": [-18.20, -18.21], } ) df Out[8]: time x y 0 2018-01-01 122.21 -18.20 1 2018-01-31 122.22 -18.21

We can pass these values to model_tides directly, and run the function in \"one-to-one\" mode to return a tide height for each row:

In\u00a0[9]: Copied!
# Model tides and add back into dataframe\ndf[\"tide_height\"] = model_tides(\n    x=df.x,\n    y=df.y,\n    time=df.time,\n    mode=\"one-to-one\",\n    directory=directory,\n).tide_height.values\n\n# Print dataframe with added tide height data:\ndf.head()\n
# Model tides and add back into dataframe df[\"tide_height\"] = model_tides( x=df.x, y=df.y, time=df.time, mode=\"one-to-one\", directory=directory, ).tide_height.values # Print dataframe with added tide height data: df.head()
Modelling tides with EOT20\n
Out[9]: time x y tide_height 0 2018-01-01 122.21 -18.20 1.231424 1 2018-01-31 122.22 -18.21 0.368303 In\u00a0[10]: Copied!
model_tides(\n    x=[122.21, 122.22],\n    y=[-18.20, -18.21],\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2),\n    model=[\"EOT20\", \"GOT5.5\", \"HAMTIDE11\"],\n    output_format=\"long\",\n    directory=directory,\n)\n
model_tides( x=[122.21, 122.22], y=[-18.20, -18.21], time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2), model=[\"EOT20\", \"GOT5.5\", \"HAMTIDE11\"], output_format=\"long\", directory=directory, )
Modelling tides with EOT20, GOT5.5, HAMTIDE11 in parallel (models: 3, splits: 1)\n
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 18.19it/s]\n
Out[10]: tide_model tide_height time x y 2018-01-01 122.21 -18.20 EOT20 1.231424 2018-01-31 122.21 -18.20 EOT20 0.368303 2018-01-01 122.22 -18.21 EOT20 1.231424 2018-01-31 122.22 -18.21 EOT20 0.368303 2018-01-01 122.21 -18.20 GOT5.5 1.271818 2018-01-31 122.21 -18.20 GOT5.5 0.397214 2018-01-01 122.22 -18.21 GOT5.5 1.271818 2018-01-31 122.22 -18.21 GOT5.5 0.397214 2018-01-01 122.21 -18.20 HAMTIDE11 1.435844 2018-01-31 122.21 -18.20 HAMTIDE11 0.588129 2018-01-01 122.22 -18.21 HAMTIDE11 1.435844 2018-01-31 122.22 -18.21 HAMTIDE11 0.588129

We can also run the function in \"wide\" format, which will return a new column for each tide model (e.g. EOT20, GOT5.5, HAMTIDE11 etc):

In\u00a0[11]: Copied!
model_tides(\n    x=[122.21, 122.22],\n    y=[-18.20, -18.21],\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2),\n    model=[\"EOT20\", \"GOT5.5\", \"HAMTIDE11\"],\n    output_format=\"wide\",\n    directory=directory,\n)\n
model_tides( x=[122.21, 122.22], y=[-18.20, -18.21], time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-31\", periods=2), model=[\"EOT20\", \"GOT5.5\", \"HAMTIDE11\"], output_format=\"wide\", directory=directory, )
Modelling tides with EOT20, GOT5.5, HAMTIDE11 in parallel (models: 3, splits: 1)\n
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 18.12it/s]\n
Converting to a wide format dataframe\n
Out[11]: tide_model EOT20 GOT5.5 HAMTIDE11 time x y 2018-01-01 122.21 -18.20 1.231424 1.271818 1.435844 122.22 -18.21 1.231424 1.271818 1.435844 2018-01-31 122.21 -18.20 0.368303 0.397214 0.588129 122.22 -18.21 0.368303 0.397214 0.588129 In\u00a0[12]: Copied!
from eo_tides.model import model_phases\n\nmodel_phases(\n    x=122.2186,\n    y=-18.0008,\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"5h\"),\n    directory=directory,\n)\n
from eo_tides.model import model_phases model_phases( x=122.2186, y=-18.0008, time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"5h\"), directory=directory, )
Modelling tides with EOT20\nModelling tides with EOT20\n
Out[12]: tide_model tide_phase time x y 2018-01-01 00:00:00 122.2186 -18.0008 EOT20 high-flow 2018-01-01 05:00:00 122.2186 -18.0008 EOT20 low-ebb 2018-01-01 10:00:00 122.2186 -18.0008 EOT20 low-flow 2018-01-01 15:00:00 122.2186 -18.0008 EOT20 high-ebb 2018-01-01 20:00:00 122.2186 -18.0008 EOT20 low-ebb

The resulting pandas.DataFrame contains:

  • time, x, y: Our original input timesteps and coordinates
  • tide_model: a column listing the tide model used
  • tide_phase: the modelled tidal phase (\"high-flow\", \"high-ebb\", \"low-ebb\", \"low-flow\").

model_phases accepts all parameters accepted by model_tides; e.g. model and output_format:

In\u00a0[13]: Copied!
model_phases(\n    x=122.2186,\n    y=-18.0008,\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"5h\"),\n    model=[\"EOT20\", \"GOT5.5\"],\n    output_format=\"wide\",\n    directory=directory,\n)\n
model_phases( x=122.2186, y=-18.0008, time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"5h\"), model=[\"EOT20\", \"GOT5.5\"], output_format=\"wide\", directory=directory, )
Modelling tides with EOT20, GOT5.5 in parallel (models: 2, splits: 1)\n
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 12.91it/s]\n
Modelling tides with EOT20, GOT5.5 in parallel (models: 2, splits: 1)\n
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 13.08it/s]\n
Converting to a wide format dataframe\n
Out[13]: tide_model EOT20 GOT5.5 time x y 2018-01-01 00:00:00 122.2186 -18.0008 high-flow high-flow 2018-01-01 05:00:00 122.2186 -18.0008 low-ebb low-ebb 2018-01-01 10:00:00 122.2186 -18.0008 low-flow low-flow 2018-01-01 15:00:00 122.2186 -18.0008 high-ebb high-ebb 2018-01-01 20:00:00 122.2186 -18.0008 low-ebb low-ebb

To change the default 15 minute time offset used to calculate tide phases, pass a custom value to time_offset:

In\u00a0[14]: Copied!
model_phases(\n    x=122.2186,\n    y=-18.0008,\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"5h\"),\n    time_offset='30 min',\n    directory=directory,\n)\n
model_phases( x=122.2186, y=-18.0008, time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"5h\"), time_offset='30 min', directory=directory, )
Modelling tides with EOT20\nModelling tides with EOT20\n
Out[14]: tide_model tide_phase time x y 2018-01-01 00:00:00 122.2186 -18.0008 EOT20 high-flow 2018-01-01 05:00:00 122.2186 -18.0008 EOT20 low-ebb 2018-01-01 10:00:00 122.2186 -18.0008 EOT20 low-flow 2018-01-01 15:00:00 122.2186 -18.0008 EOT20 high-ebb 2018-01-01 20:00:00 122.2186 -18.0008 EOT20 low-ebb

Optionally, we can choose to return also tide heights by providing return_tides=True. This will include an additional tide_height column in our dataframe:

In\u00a0[15]: Copied!
phase_df = model_phases(\n    x=122.2186,\n    y=-18.0008,\n    time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"30min\"),\n    return_tides=True,\n    directory=directory,\n)\n\n# Print outputs\nphase_df.head()\n
phase_df = model_phases( x=122.2186, y=-18.0008, time=pd.date_range(start=\"2018-01-01\", end=\"2018-01-02\", freq=\"30min\"), return_tides=True, directory=directory, ) # Print outputs phase_df.head()
Modelling tides with EOT20\nModelling tides with EOT20\n
Out[15]: tide_model tide_height tide_phase time x y 2018-01-01 00:00:00 122.2186 -18.0008 EOT20 1.229286 high-flow 2018-01-01 00:30:00 122.2186 -18.0008 EOT20 1.763103 high-flow 2018-01-01 01:00:00 122.2186 -18.0008 EOT20 2.162897 high-flow 2018-01-01 01:30:00 122.2186 -18.0008 EOT20 2.405015 high-flow 2018-01-01 02:00:00 122.2186 -18.0008 EOT20 2.476600 high-flow

If we plot our tide_height and tide_phase data, we can see it follows a logical progression from high-flow -> high-ebb -> low-ebb -> low-flow -> ..., repeating with every tide cycle:

In\u00a0[16]: Copied!
# Plot tide heights\nax = phase_df.droplevel([\"x\", \"y\"]).tide_height.plot(color=\"black\")\n\n# Define plotting parameters for each phase\nplot_params = {\n    \"high-flow\": {\"marker\": \"^\", \"color\": \"tab:blue\"},\n    \"high-ebb\": {\"marker\": \"v\", \"color\": \"tab:blue\"},\n    \"low-flow\": {\"marker\": \"^\", \"color\": \"tab:orange\"},\n    \"low-ebb\": {\"marker\": \"v\", \"color\": \"tab:orange\"},\n}\n\n# Plot each phase\nfor phase, params in plot_params.items():\n    phase_df.droplevel([\"x\", \"y\"]).query(f\"tide_phase == '{phase}'\").tide_height.plot(\n        marker=params[\"marker\"],\n        linewidth=0.0,\n        color=params[\"color\"],\n        markersize=10,\n        label=phase,\n    )\nax.legend();\n
# Plot tide heights ax = phase_df.droplevel([\"x\", \"y\"]).tide_height.plot(color=\"black\") # Define plotting parameters for each phase plot_params = { \"high-flow\": {\"marker\": \"^\", \"color\": \"tab:blue\"}, \"high-ebb\": {\"marker\": \"v\", \"color\": \"tab:blue\"}, \"low-flow\": {\"marker\": \"^\", \"color\": \"tab:orange\"}, \"low-ebb\": {\"marker\": \"v\", \"color\": \"tab:orange\"}, } # Plot each phase for phase, params in plot_params.items(): phase_df.droplevel([\"x\", \"y\"]).query(f\"tide_phase == '{phase}'\").tide_height.plot( marker=params[\"marker\"], linewidth=0.0, color=params[\"color\"], markersize=10, label=phase, ) ax.legend();"},{"location":"notebooks/Model_tides/#modelling-tides","title":"Modelling tides\u00b6","text":"

This guide demonstrates how to use the model_tides and model_phases functions from the eo_tides.model module to model tide heights and phases at multiple coordinates or time steps, using one or more ocean tide models.

The model_tides function supports tide modelling based on a wide range of ocean tide models using a single line of code, parallelising this modelling where possible and returning data in a standardised pandas.Dataframe format. The model_tides function can be used independently of Earth observation (EO) data, e.g. for any application where you need to generate a time series of tide heights. However, it also underpins the more complex EO-related functions demonstrated in Combining tides with satellite data.

The model_phases function can be used to model the phase of the tide at any location and time. This can be used to classify tides into high and low tide observations, or determine whether the tide was rising (i.e. flow tide) or falling (i.e. ebb tide).

Tip

The model_tides function is based on the pyTMD.compute.tide_elevations function from the pyTMD tide modelling package that underpins eo-tides, with modifications to support parallel processing and integration with pandas and xarray workflows. We highly recommend exploring the more advanced tide modelling functionality available in pyTMD for more custom tide modelling applications.

"},{"location":"notebooks/Model_tides/#getting-started","title":"Getting started\u00b6","text":"

As a first step, we need to tell eo-tides the location of our tide model or clipped tide model directory (if you haven't set this up, refer to the setup instructions here).

We will pass this path to eo-tides functions using the directory parameter.

"},{"location":"notebooks/Model_tides/#using-model_tides","title":"Using model_tides\u00b6","text":"

In the example below, we use the model_tides function to model hourly tides for the city of Broome, Western Australia across January 2018:

"},{"location":"notebooks/Model_tides/#multiple-models","title":"Multiple models\u00b6","text":"

By default, model_tides will model tides using the EOT20 tide model \u2013 a leading open-source global ocean model with a permissive CC BY 4.0 licence:

Hart-Davis Michael, Piccioni Gaia, Dettmering Denise, Schwatke Christian, Passaro Marcello, Seitz Florian (2021). EOT20 - A global Empirical Ocean Tide model from multi-mission satellite altimetry. SEANOE. https://doi.org/10.17882/79489

However, we can easily model tides using multiple models by passing a list of models to the model parameter. eo-tides will process these in parallel where possible, and return the data into a single pandas.DataFrame. For example, we can model tides using the EOT20, GOT5.5 and HAMTIDE11 models:

Note

Here we also set output_format=\"wide\", which will place data from each model into a new column. This can make it easier to plot our data. For more details, see below.

"},{"location":"notebooks/Model_tides/#one-to-many-and-one-to-one-modes","title":"\"One-to-many\" and \"one-to-one\" modes\u00b6","text":"

By default, the model_tides function operates in \"one-to-many\" mode, which will model tides at every requested location, for every requested timestep. This is particularly useful for satellite Earth observation applications where we may want to model tides for a large set of satellite pixels, for every satellite acquisition through time.

For example, if we provide two locations and two timesteps, the function will return four modelled tides:

2 locations * 2 timesteps = 4 modelled tides\n
"},{"location":"notebooks/Model_tides/#wide-and-long-output-formats","title":"\"Wide\" and \"long\" output formats\u00b6","text":"

By default, modelled tides will be returned in \"long\" format, with multiple models stacked under a tide_models column and tide heights in the tide_height column:

"},{"location":"notebooks/Model_tides/#using-model_phases","title":"Using model_phases\u00b6","text":"

In addition to tide height, it can be useful to obtain information about the phase of the tide at each observation. For example, we may want to know whether tides were low or high, or whether water levels were rising (\"flow\" tide) or falling (\"ebb\" tide). Tide phase data can provide valuable contextual information for interpreting satellite imagery, particularly in tidal flat or mangrove forest environments where water may remain in the landscape for considerable time after the tidal peak.

The model_phases function calculates ebb and low phases by modelling tides once for the requested timesteps, and again after subtracting a small time offset (by default, 15 minutes). If tides increased over this period, they are assigned as \"flow\"; if they decreased, they are assigned as \"ebb\". Tides are considered \"high\" if equal or greater than 0 metres tide height, otherwise \"low\".

We can run model_phases by providing x and y coordinates and time:

"},{"location":"notebooks/Model_tides/#next-steps","title":"Next steps\u00b6","text":"

Now that we have demonstrated how to model tide heights and phases, we can learn how to combine modelled tides with satellite data for further analysis.

"},{"location":"notebooks/Satellite_data/","title":"Combining tides with satellite data","text":"In\u00a0[1]: Copied!
directory = \"../../tests/data/tide_models/\"\n
directory = \"../../tests/data/tide_models/\" In\u00a0[2]: Copied!
import odc.stac\nimport pystac_client\n\n# Connect to STAC catalog\ncatalog = pystac_client.Client.open(\"https://explorer.dea.ga.gov.au/stac\")\n\n# Set cloud access defaults\nodc.stac.configure_rio(\n    cloud_defaults=True,\n    aws={\"aws_unsigned\": True},\n)\n\n# Build a query and search the STAC catalog for all matching items\nbbox = [122.12, -18.25, 122.43, -17.93]\nquery = catalog.search(\n    bbox=bbox,\n    collections=[\"ga_ls8c_ard_3\", \"ga_ls9c_ard_3\"],\n    datetime=\"2024-01-01/2024-12-31\",\n    filter = \"eo:cloud_cover < 5\"  # Filter to images with <5% cloud\n)\n\n# Load data into xarray format\nds = odc.stac.load(\n    items=list(query.items()),\n    bands=[\"nbart_red\", \"nbart_green\", \"nbart_blue\"],\n    crs=\"utm\",\n    resolution=30,\n    groupby=\"solar_day\",\n    bbox=bbox,\n    fail_on_error=False,\n    chunks={},\n)\n\n# Plot the first image\nds.isel(time=0).odc.explore(vmin=50, vmax=3000)\n
import odc.stac import pystac_client # Connect to STAC catalog catalog = pystac_client.Client.open(\"https://explorer.dea.ga.gov.au/stac\") # Set cloud access defaults odc.stac.configure_rio( cloud_defaults=True, aws={\"aws_unsigned\": True}, ) # Build a query and search the STAC catalog for all matching items bbox = [122.12, -18.25, 122.43, -17.93] query = catalog.search( bbox=bbox, collections=[\"ga_ls8c_ard_3\", \"ga_ls9c_ard_3\"], datetime=\"2024-01-01/2024-12-31\", filter = \"eo:cloud_cover < 5\" # Filter to images with <5% cloud ) # Load data into xarray format ds = odc.stac.load( items=list(query.items()), bands=[\"nbart_red\", \"nbart_green\", \"nbart_blue\"], crs=\"utm\", resolution=30, groupby=\"solar_day\", bbox=bbox, fail_on_error=False, chunks={}, ) # Plot the first image ds.isel(time=0).odc.explore(vmin=50, vmax=3000)
/env/lib/python3.10/site-packages/odc/geo/_rgba.py:56: RuntimeWarning: invalid value encountered in cast\n  return x.astype(\"uint8\")\n/env/lib/python3.10/site-packages/rasterio/warp.py:387: NotGeoreferencedWarning: Dataset has no geotransform, gcps, or rpcs. The identity matrix will be returned.\n  dest = _reproject(\n
Out[2]: Make this Notebook Trusted to load map: File -> Trust Notebook In\u00a0[3]: Copied!
from eo_tides.eo import tag_tides\n\ntides_da = tag_tides(\n    data=ds,\n    directory=directory,\n)\n\n# Print modelled tides\nprint(tides_da)\n
from eo_tides.eo import tag_tides tides_da = tag_tides( data=ds, directory=directory, ) # Print modelled tides print(tides_da)
Setting tide modelling location from dataset centroid: 122.27, -18.09\nModelling tides with EOT20\n<xarray.DataArray 'tide_height' (time: 45)> Size: 180B\narray([-0.25108945,  0.5275667 ,  1.5171705 ,  2.011433  ,  0.36809078,\n        1.9124153 ,  1.0965405 ,  1.1305977 , -0.21305004,  1.7526643 ,\n       -0.2175682 , -0.80696225, -0.9524483 ,  3.0443938 , -1.3169092 ,\n        3.3964403 ,  2.625125  , -0.9654651 , -0.4368508 ,  2.581808  ,\n        1.7244624 , -1.0423656 ,  0.16597499,  0.73508024, -0.33408186,\n       -0.10130765,  0.7978594 , -1.8499157 ,  1.6090035 , -1.2717861 ,\n       -1.8342571 ,  2.162794  ,  2.7683735 , -2.6152036 , -2.39916   ,\n        2.5937896 ,  2.1230242 , -1.6377252 ,  3.2850509 , -0.3772273 ,\n        0.6212255 ,  1.6580964 ,  0.71566176, -0.03352478,  1.0641807 ],\n      dtype=float32)\nCoordinates:\n  * time        (time) datetime64[ns] 360B 2024-01-07T01:55:31.679580 ... 202...\n    tide_model  <U5 20B 'EOT20'\n

We can easily combine these modelled tides with our original satellite data for further analysis. The code below adds our modelled tides as a new tide_height variable under Data variables.

In\u00a0[4]: Copied!
ds[\"tide_height\"] = tides_da\nprint(ds)\n
ds[\"tide_height\"] = tides_da print(ds)
<xarray.Dataset> Size: 703MB\nDimensions:      (y: 1185, x: 1099, time: 45)\nCoordinates:\n  * y            (y) float64 9kB 8.017e+06 8.017e+06 ... 7.982e+06 7.982e+06\n  * x            (x) float64 9kB 4.068e+05 4.068e+05 ... 4.397e+05 4.398e+05\n    spatial_ref  int32 4B 32751\n  * time         (time) datetime64[ns] 360B 2024-01-07T01:55:31.679580 ... 20...\n    tide_model   <U5 20B 'EOT20'\nData variables:\n    nbart_red    (time, y, x) float32 234MB dask.array<chunksize=(1, 1185, 1099), meta=np.ndarray>\n    nbart_green  (time, y, x) float32 234MB dask.array<chunksize=(1, 1185, 1099), meta=np.ndarray>\n    nbart_blue   (time, y, x) float32 234MB dask.array<chunksize=(1, 1185, 1099), meta=np.ndarray>\n    tide_height  (time) float32 180B -0.2511 0.5276 1.517 ... -0.03352 1.064\n

Tip

You could also model tides and insert tide heights into ds in a single step via: ds[\"tide_height\"] = tag_tides(ds, ...)

We can plot this new tide_height variable over time to inspect the tide heights observed by the satellites in our time series:

In\u00a0[5]: Copied!
ds.tide_height.plot()\n
ds.tide_height.plot() Out[5]:
[<matplotlib.lines.Line2D at 0x7f6056bfc850>]
In\u00a0[6]: Copied!
# Sort by tide and plot the first and last image\nds_sorted = ds.sortby(\"tide_height\")\nds_sorted.isel(time=[0, -1]).odc.to_rgba(vmin=50, vmax=3000).plot.imshow(col=\"time\")\n
# Sort by tide and plot the first and last image ds_sorted = ds.sortby(\"tide_height\") ds_sorted.isel(time=[0, -1]).odc.to_rgba(vmin=50, vmax=3000).plot.imshow(col=\"time\") Out[6]:
<xarray.plot.facetgrid.FacetGrid at 0x7f6056ae61a0>
In\u00a0[7]: Copied!
# Load data into xarray format\nds = odc.stac.load(\n    items=list(query.items()),\n    bands=[\"nbart_red\", \"nbart_green\", \"nbart_blue\"],\n    crs=\"utm\",\n    resolution=30,\n    groupby=\"solar_day\",\n    bbox=bbox,\n    fail_on_error=False,\n    chunks={},\n)\n
# Load data into xarray format ds = odc.stac.load( items=list(query.items()), bands=[\"nbart_red\", \"nbart_green\", \"nbart_blue\"], crs=\"utm\", resolution=30, groupby=\"solar_day\", bbox=bbox, fail_on_error=False, chunks={}, )

Now run pixel_tides, passing our satellite dataset ds as an input:

In\u00a0[8]: Copied!
from eo_tides.eo import pixel_tides\n\n# Model tides spatially\ntides_lowres = pixel_tides(\n    data=ds,\n    resample=False,\n    directory=directory,\n)\n\n# Print output\nprint(tides_lowres)\n
from eo_tides.eo import pixel_tides # Model tides spatially tides_lowres = pixel_tides( data=ds, resample=False, directory=directory, ) # Print output print(tides_lowres)
Creating reduced resolution 5000 x 5000 metre tide modelling array\nModelling tides with EOT20\nReturning low resolution tide array\n<xarray.DataArray 'tide_height' (time: 45, y: 13, x: 13)> Size: 30kB\narray([[[-0.30606133, -0.30717686, -0.30773783, ..., -0.25615942,\n         -0.25615942, -0.25615942],\n        [-0.30401564, -0.30424863, -0.30387282, ..., -0.25615942,\n         -0.25615942, -0.25615942],\n        [-0.30068266, -0.29933682, -0.29704148, ..., -0.25615942,\n         -0.25615942, -0.25615942],\n        ...,\n        [-0.27365166, -0.27505815, -0.2606054 , ..., -0.25108945,\n         -0.25108945, -0.25108945],\n        [-0.27030516, -0.26545134, -0.26545134, ..., -0.25108945,\n         -0.25108945, -0.25108945],\n        [-0.26545134, -0.26545134, -0.26545134, ..., -0.25108945,\n         -0.25108945, -0.25108945]],\n\n       [[ 0.4048146 ,  0.40729165,  0.4100286 , ...,  0.51402086,\n          0.51402086,  0.51402086],\n        [ 0.41507572,  0.41939604,  0.42421374, ...,  0.51402086,\n          0.51402086,  0.51402086],\n        [ 0.42650127,  0.43261963,  0.43943623, ...,  0.51402086,\n          0.51402086,  0.51402086],\n...\n        [-0.05945582, -0.06040129, -0.04247594, ..., -0.03352478,\n         -0.03352478, -0.03352478],\n        [-0.05551685, -0.04903932, -0.04903932, ..., -0.03352478,\n         -0.03352478, -0.03352478],\n        [-0.04903932, -0.04903932, -0.04903932, ..., -0.03352478,\n         -0.03352478, -0.03352478]],\n\n       [[ 0.88739955,  0.90003395,  0.91273254, ...,  1.0558591 ,\n          1.0558591 ,  1.0558591 ],\n        [ 0.9003904 ,  0.91464955,  0.9293343 , ...,  1.0558591 ,\n          1.0558591 ,  1.0558591 ],\n        [ 0.9152668 ,  0.93159544,  0.9488063 , ...,  1.0558591 ,\n          1.0558591 ,  1.0558591 ],\n        ...,\n        [ 1.0099615 ,  1.0095476 ,  1.0438287 , ...,  1.0641807 ,\n          1.0641807 ,  1.0641807 ],\n        [ 1.0166272 ,  1.026946  ,  1.026946  , ...,  1.0641807 ,\n          1.0641807 ,  1.0641807 ],\n        [ 1.026946  ,  1.026946  ,  1.026946  , ...,  1.0641807 ,\n          1.0641807 ,  1.0641807 ]]], dtype=float32)\nCoordinates:\n  * time         (time) datetime64[ns] 360B 2024-01-07T01:55:31.679580 ... 20...\n  * x            (x) float64 104B 3.925e+05 3.975e+05 ... 4.475e+05 4.525e+05\n  * y            (y) float64 104B 8.028e+06 8.022e+06 ... 7.972e+06 7.968e+06\n    tide_model   <U5 20B 'EOT20'\n    spatial_ref  int32 4B 32751\n

If we plot the resulting data, we can see that we now have two-dimensional tide surfaces for each timestep in our data (instead of the single tide height per timestamp returned by the tag_tides function).

Blue values below indicate low tide pixels, while red indicates high tide pixels. If you look closely, you may see some spatial variability in tide heights within each timestep, with slight variations in tide heights along the north-west side of the study area:

In\u00a0[9]: Copied!
# Plot the first four timesteps in our data\ntides_lowres.isel(time=slice(0, 4)).plot.imshow(col=\"time\", vmin=-1, vmax=1, cmap=\"RdBu\")\n
# Plot the first four timesteps in our data tides_lowres.isel(time=slice(0, 4)).plot.imshow(col=\"time\", vmin=-1, vmax=1, cmap=\"RdBu\") Out[9]:
<xarray.plot.facetgrid.FacetGrid at 0x7f60811c18a0>
In\u00a0[10]: Copied!
# Model tides spatially\ntides_highres = pixel_tides(\n    data=ds,\n    resample=True,\n    directory=directory,\n)\n\n# Plot the first four timesteps in our data\ntides_highres.isel(time=slice(0, 4)).plot.imshow(col=\"time\", vmin=-1, vmax=1, cmap=\"RdBu\")\n
# Model tides spatially tides_highres = pixel_tides( data=ds, resample=True, directory=directory, ) # Plot the first four timesteps in our data tides_highres.isel(time=slice(0, 4)).plot.imshow(col=\"time\", vmin=-1, vmax=1, cmap=\"RdBu\")
Creating reduced resolution 5000 x 5000 metre tide modelling array\nModelling tides with EOT20\nReprojecting tides into original resolution\n
Out[10]:
<xarray.plot.facetgrid.FacetGrid at 0x7f6047d99fc0>

tides_highres will have exactly the same dimensions as ds, with a unique tide height for every satellite pixel:

In\u00a0[11]: Copied!
ds.sizes\n
ds.sizes Out[11]:
Frozen({'y': 1185, 'x': 1099, 'time': 45})
In\u00a0[12]: Copied!
tides_highres.sizes\n
tides_highres.sizes Out[12]:
Frozen({'time': 45, 'y': 1185, 'x': 1099})

Because of this, our stack of tides can be added as an additional 3D variable in our dataset:

In\u00a0[13]: Copied!
ds[\"tide_height_pixel\"] = tides_highres\nprint(ds)\n
ds[\"tide_height_pixel\"] = tides_highres print(ds)
<xarray.Dataset> Size: 938MB\nDimensions:            (y: 1185, x: 1099, time: 45)\nCoordinates:\n  * y                  (y) float64 9kB 8.017e+06 8.017e+06 ... 7.982e+06\n  * x                  (x) float64 9kB 4.068e+05 4.068e+05 ... 4.398e+05\n    spatial_ref        int32 4B 32751\n  * time               (time) datetime64[ns] 360B 2024-01-07T01:55:31.679580 ...\n    tide_model         <U5 20B 'EOT20'\nData variables:\n    nbart_red          (time, y, x) float32 234MB dask.array<chunksize=(1, 1185, 1099), meta=np.ndarray>\n    nbart_green        (time, y, x) float32 234MB dask.array<chunksize=(1, 1185, 1099), meta=np.ndarray>\n    nbart_blue         (time, y, x) float32 234MB dask.array<chunksize=(1, 1185, 1099), meta=np.ndarray>\n    tide_height_pixel  (time, y, x) float32 234MB -0.3038 -0.3039 ... 1.064\n
In\u00a0[14]: Copied!
# Model tides spatially\ntides_highres_quantiles = pixel_tides(\n    data=ds,\n    calculate_quantiles=(0, 0.5, 1),\n    directory=directory,\n)\n\n# Plot quantiles\ntides_highres_quantiles.plot.imshow(col=\"quantile\")\n
# Model tides spatially tides_highres_quantiles = pixel_tides( data=ds, calculate_quantiles=(0, 0.5, 1), directory=directory, ) # Plot quantiles tides_highres_quantiles.plot.imshow(col=\"quantile\")
Creating reduced resolution 5000 x 5000 metre tide modelling array\nModelling tides with EOT20\nComputing tide quantiles\nReprojecting tides into original resolution\n
Out[14]:
<xarray.plot.facetgrid.FacetGrid at 0x7f6047cf2350>
In\u00a0[15]: Copied!
import pandas as pd\n\ncustom_times = pd.date_range(\n    start=\"2022-01-01\", \n    end=\"2022-01-02\", \n    freq=\"6H\",\n)\n\n# Model tides spatially\ntides_highres = pixel_tides(\n    data=ds, \n    time=custom_times,\n    directory=directory,\n)\n\n# Plot custom timesteps\ntides_highres.plot.imshow(col=\"time\")\n
import pandas as pd custom_times = pd.date_range( start=\"2022-01-01\", end=\"2022-01-02\", freq=\"6H\", ) # Model tides spatially tides_highres = pixel_tides( data=ds, time=custom_times, directory=directory, ) # Plot custom timesteps tides_highres.plot.imshow(col=\"time\")
Creating reduced resolution 5000 x 5000 metre tide modelling array\nModelling tides with EOT20\nReprojecting tides into original resolution\n
Out[15]:
<xarray.plot.facetgrid.FacetGrid at 0x7f6047be1000>
"},{"location":"notebooks/Satellite_data/#combining-tides-with-satellite-data","title":"Combining tides with satellite data\u00b6","text":"

This guide demonstrates how to combine tide modelling with satellite Earth observation (EO) data using the tag_tides and pixel_tides functions from eo_tides.eo.

Both these functions allow you to model the height of the tide at the exact moment of satellite image acquisition. This can then allow you to analyse satellite EO data by tidal conditions - for example, filter your data to satellite imagery collected during specific tidal stages (e.g. low or high tide).

Although both functions perform a similar function, they differ in complexity and performance. tag_tides assigns a single tide height to each timestep/satellite image, which is fast and efficient, and suitable for small-scale applications where tides are unlikely to vary across your study area. In constrast, pixel_tide models tides both through time and spatially, returning a tide height for every satellite pixel. This can be critical for producing seamless coastal EO datasets at large scale - however comes at the cost of performance.

Table 1. Comparison of tag_tides and pixel_tides

tag_tides pixel_tides Assigns a single tide height to each timestep/satellite image Assigns a tide height to every individual pixel through time to capture spatial tide dynamics \ud83d\udd0e Ideal for local or site-scale analysis \ud83c\udf0f Ideal for regional to global-scale coastal product generation \u2705 Fast, low memory use \u274c Slower, higher memory use \u274c Single tide height per image can produce artefacts in complex tidal regions \u2705 Produce spatially seamless results across large extents by applying analyses at the pixel level"},{"location":"notebooks/Satellite_data/#getting-started","title":"Getting started\u00b6","text":"

As in the previous example, our first step is to tell eo-tides the location of our tide model or clipped tide model directory (if you haven't set this up, refer to the setup instructions here):

"},{"location":"notebooks/Satellite_data/#load-satellite-data-using-odc-stac","title":"Load satellite data using odc-stac\u00b6","text":"

Now we can load a time-series of satellite data over our area of interest using the Open Data Cube's odc-stac package. This powerful package allows us to load open satellite data (e.g ESA Sentinel-2 or NASA/USGS Landsat) for any time period and location on the planet, and load our data into a multi-dimensional xarray.Dataset format dataset.

In this example, we will load Landsat 8 and 9 satellite data from 2024 over the city of Broome, Western Australia - a macrotidal region with extensive intertidal coastal habitats. We will load this data from the Digital Earth Australia STAC catalogue.

Tip

For a more detailed guide to using STAC metadata and odc-stac to find and load satellite data, refer to the Digital Earth Australia STAC user guide.

"},{"location":"notebooks/Satellite_data/#using-tag_tides","title":"Using tag_tides\u00b6","text":"

We can pass our satellite dataset ds to the tag_tides function to model a tide for each timestep in our dataset. This can help sort and filter images by tide height, allowing us to learn more about how coastal environments respond to the effect of changing tides.

The tag_tides function uses the time and date of acquisition and the geographic centroid of each satellite observation as inputs for the selected tide model (EOT20 by default). It returns an xarray.DataArray called tide_height, with a modelled tide for every timestep in our satellite dataset:

"},{"location":"notebooks/Satellite_data/#selecting-and-analysing-satellite-data-by-tide","title":"Selecting and analysing satellite data by tide\u00b6","text":"

Having tide_height as a variable allows us to select and analyse our satellite data using information about tides. For example, we could sort by tide_height, then plot the lowest and highest tide images in our time series:

"},{"location":"notebooks/Satellite_data/#using-pixel_tides","title":"Using pixel_tides\u00b6","text":"

The previous examples show how to model a single tide height for each satellite image using the centroid of the image as a tide modelling location. However, in reality tides vary spatially \u2013 potentially by several metres in areas of complex tidal dynamics. This means that an individual satellite image can capture a range of tide conditions.

We can use the pixel_tides function to capture this spatial variability in tide heights. For efficient processing, this function first models tides into a low resolution grid surrounding each satellite image in our time series. This lower resolution data includes a buffer around the extent of our satellite data so that tides can be modelled seamlessly across analysis boundaries.

First, let's reload our satellite data for a fresh start:

"},{"location":"notebooks/Satellite_data/#reprojecting-into-original-high-resolution-spatial-grid","title":"Reprojecting into original high-resolution spatial grid\u00b6","text":"

By setting resample=True, we can use interpolation to re-project our low resolution tide data back into the resolution of our satellite image, resulting in an individual tide height for every pixel in our dataset through time and space:

"},{"location":"notebooks/Satellite_data/#calculating-tide-height-minmaxmedianquantiles-for-each-pixel","title":"Calculating tide height min/max/median/quantiles for each pixel\u00b6","text":"

Min, max or any specific quantile of all tide heights observed over a region can be calculated for each pixel by passing in a list of quantiles/percentiles using the calculate_quantiles parameter.

This calculation is performed on the low resolution modelled tide data before reprojecting to higher resolution, so should be faster than calculating min/max/median tide at high resolution:

"},{"location":"notebooks/Satellite_data/#modelling-custom-times","title":"Modelling custom times\u00b6","text":"

Instead of using times contained in the time dimension of our dataset, we can also calculate pixel-based tides for a custom set of times:

"},{"location":"notebooks/Satellite_data/#next-steps","title":"Next steps\u00b6","text":"

Now that we have learnt to combine tide modelling with satellite data, we can learn how to calculate statistics describing local tide dynamics, as well as biases caused by interactions between tidal processes and satellite orbits.

"},{"location":"notebooks/Tide_statistics/","title":"Calculating tide statistics and satellite biases","text":"In\u00a0[1]: Copied!
directory = \"../../tests/data/tide_models/\"\n
directory = \"../../tests/data/tide_models/\" In\u00a0[2]: Copied!
import odc.stac\nimport pystac_client\nimport planetary_computer\n\n# Connect to STAC catalog\ncatalog = pystac_client.Client.open(\n    \"https://planetarycomputer.microsoft.com/api/stac/v1\",\n    modifier=planetary_computer.sign_inplace,\n)\n\n# Set cloud access defaults\nodc.stac.configure_rio(\n    cloud_defaults=True,\n    aws={\"aws_unsigned\": True},\n)\n\n# Build a query and search the STAC catalog for all matching items\nbbox = [122.160, -18.05, 122.260, -17.95]\nquery = catalog.search(\n    bbox=bbox,\n    collections=[\"sentinel-2-l2a\"],\n    datetime=\"2021/2023\",\n)\n\n# Load data into xarray format\nds_s2 = odc.stac.load(\n    items=list(query.items()),\n    bands=[\"red\"],\n    crs=\"utm\",\n    resolution=30,\n    groupby=\"solar_day\",\n    bbox=bbox,\n    fail_on_error=False,\n    chunks={},\n)\n\nprint(ds_s2)\n
import odc.stac import pystac_client import planetary_computer # Connect to STAC catalog catalog = pystac_client.Client.open( \"https://planetarycomputer.microsoft.com/api/stac/v1\", modifier=planetary_computer.sign_inplace, ) # Set cloud access defaults odc.stac.configure_rio( cloud_defaults=True, aws={\"aws_unsigned\": True}, ) # Build a query and search the STAC catalog for all matching items bbox = [122.160, -18.05, 122.260, -17.95] query = catalog.search( bbox=bbox, collections=[\"sentinel-2-l2a\"], datetime=\"2021/2023\", ) # Load data into xarray format ds_s2 = odc.stac.load( items=list(query.items()), bands=[\"red\"], crs=\"utm\", resolution=30, groupby=\"solar_day\", bbox=bbox, fail_on_error=False, chunks={}, ) print(ds_s2)
<xarray.Dataset> Size: 111MB\nDimensions:      (y: 371, x: 356, time: 211)\nCoordinates:\n  * y            (y) float64 3kB 8.015e+06 8.015e+06 ... 8.004e+06 8.004e+06\n  * x            (x) float64 3kB 4.11e+05 4.111e+05 ... 4.217e+05 4.217e+05\n    spatial_ref  int32 4B 32751\n  * time         (time) datetime64[ns] 2kB 2021-01-03T02:04:51.024000 ... 202...\nData variables:\n    red          (time, y, x) float32 111MB dask.array<chunksize=(1, 371, 356), meta=np.ndarray>\n
In\u00a0[3]: Copied!
from eo_tides.stats import tide_stats\n\nstatistics_df = tide_stats(\n    data=ds_s2,\n    directory=directory,\n)\n
from eo_tides.stats import tide_stats statistics_df = tide_stats( data=ds_s2, directory=directory, )
Using tide modelling location: 122.21, -18.00\nModelling tides with EOT20\nModelling tides with EOT20\n\n\n\ud83c\udf0a Modelled astronomical tide range: 9.30 metres.\n\ud83d\udef0\ufe0f Observed tide range: 6.29 metres.\n\n\ud83d\udd34 68% of the modelled astronomical tide range was observed at this location.\n\ud83d\udfe2 The highest 8% (0.77 metres) of the tide range was never observed.\n\ud83d\udd34 The lowest 24% (2.25 metres) of the tide range was never observed.\n\n\ud83c\udf0a Mean modelled astronomical tide height: -0.00 metres.\n\ud83d\udef0\ufe0f Mean observed tide height: 0.69 metres.\n\n\u2b06\ufe0f The mean observed tide height was 0.69 metres higher than the mean modelled astronomical tide height.\n

As we can see in the graph, Sentinel-2 captured a biased proportion of the tide range at this location: only observing ~64% of the tide range, and never observing the lowest 26% of tides.

The tide_stats function also outputs a pandas.Series object containing statistics for the results above, including:

  • y: latitude used for modelling tide heights
  • x: longitude used for modelling tide heights
  • mot: mean tide height observed by the satellite (metres)
  • mat: mean modelled astronomical tide height (metres)
  • lot: minimum tide height observed by the satellite (metres)
  • lat: minimum tide height from modelled astronomical tidal range (metres)
  • hot: maximum tide height observed by the satellite (metres)
  • hat: maximum tide height from modelled astronomical tidal range (metres)
  • otr: tidal range observed by the satellite (metres)
  • tr: modelled astronomical tide range (metres)
  • spread: proportion of the full modelled tidal range observed by the satellite
  • offset_low: proportion of the lowest tides never observed by the satellite
  • offset_high: proportion of the highest tides never observed by the satellite
In\u00a0[4]: Copied!
statistics_df\n
statistics_df Out[4]:
y              -18.000\nx              122.210\nmot              0.691\nmat             -0.000\nlot             -2.355\nlat             -4.604\nhot              3.930\nhat              4.696\notr              6.285\ntr               9.300\nspread           0.676\noffset_low       0.242\noffset_high      0.082\ndtype: float64
In\u00a0[5]: Copied!
# Build a query and search the STAC catalog for all matching items\nbbox = [122.160, -18.05, 122.260, -17.95]\nquery = catalog.search(\n    bbox=bbox,\n    collections=[\"sentinel-1-rtc\"],\n    datetime=\"2021/2023\",\n)\n\n# Load data into xarray format\nds_s1 = odc.stac.load(\n    items=list(query.items()),\n    bands=[\"vv\"],\n    crs=\"utm\",\n    resolution=30,\n    groupby=\"solar_day\",\n    bbox=bbox,\n    fail_on_error=False,\n    chunks={},\n)\n\nprint(ds_s1)\n
# Build a query and search the STAC catalog for all matching items bbox = [122.160, -18.05, 122.260, -17.95] query = catalog.search( bbox=bbox, collections=[\"sentinel-1-rtc\"], datetime=\"2021/2023\", ) # Load data into xarray format ds_s1 = odc.stac.load( items=list(query.items()), bands=[\"vv\"], crs=\"utm\", resolution=30, groupby=\"solar_day\", bbox=bbox, fail_on_error=False, chunks={}, ) print(ds_s1)
<xarray.Dataset> Size: 47MB\nDimensions:      (y: 371, x: 356, time: 89)\nCoordinates:\n  * y            (y) float64 3kB 8.015e+06 8.015e+06 ... 8.004e+06 8.004e+06\n  * x            (x) float64 3kB 4.11e+05 4.111e+05 ... 4.217e+05 4.217e+05\n    spatial_ref  int32 4B 32751\n  * time         (time) datetime64[ns] 712B 2021-01-07T21:23:20.996123 ... 20...\nData variables:\n    vv           (time, y, x) float32 47MB dask.array<chunksize=(1, 371, 356), meta=np.ndarray>\n

When we run tide_stats, we can see a very different pattern: Sentinel-1 data is biased towards low tide observations, and never observes high tide at our location!

In\u00a0[6]: Copied!
statistics_df = tide_stats(\n    data=ds_s1,\n    directory=directory,\n)\n
statistics_df = tide_stats( data=ds_s1, directory=directory, )
Using tide modelling location: 122.21, -18.00\nModelling tides with EOT20\nModelling tides with EOT20\n\n\n\ud83c\udf0a Modelled astronomical tide range: 9.55 metres.\n\ud83d\udef0\ufe0f Observed tide range: 6.40 metres.\n\n\ud83d\udd34 67% of the modelled astronomical tide range was observed at this location.\n\ud83d\udd34 The highest 28% (2.72 metres) of the tide range was never observed.\n\ud83d\udfe2 The lowest 4% (0.43 metres) of the tide range was never observed.\n\n\ud83c\udf0a Mean modelled astronomical tide height: -0.00 metres.\n\ud83d\udef0\ufe0f Mean observed tide height: -1.31 metres.\n\n\u2b07\ufe0f The mean observed tide height was -1.31 metres lower than the mean modelled astronomical tide height.\n
In\u00a0[7]: Copied!
import xarray as xr\n\n# Give each observation a \"satellite_name\" based on its satellite\nds_s1 = ds_s1.assign_coords(satellite_name=(\"time\", [\"Sentinel-1\"] * ds_s1.sizes[\"time\"]))\nds_s2 = ds_s2.assign_coords(satellite_name=(\"time\", [\"Sentinel-2\"] * ds_s2.sizes[\"time\"]))\n\n# Combine both Sentinel-1 and Sentinel-2 data into a single dataset\nds_all = xr.concat([ds_s1, ds_s2], dim=\"time\")\nprint(ds_all)\n
import xarray as xr # Give each observation a \"satellite_name\" based on its satellite ds_s1 = ds_s1.assign_coords(satellite_name=(\"time\", [\"Sentinel-1\"] * ds_s1.sizes[\"time\"])) ds_s2 = ds_s2.assign_coords(satellite_name=(\"time\", [\"Sentinel-2\"] * ds_s2.sizes[\"time\"])) # Combine both Sentinel-1 and Sentinel-2 data into a single dataset ds_all = xr.concat([ds_s1, ds_s2], dim=\"time\") print(ds_all)
<xarray.Dataset> Size: 317MB\nDimensions:         (time: 300, y: 371, x: 356)\nCoordinates:\n  * y               (y) float64 3kB 8.015e+06 8.015e+06 ... 8.004e+06 8.004e+06\n  * x               (x) float64 3kB 4.11e+05 4.111e+05 ... 4.217e+05 4.217e+05\n    spatial_ref     int32 4B 32751\n  * time            (time) datetime64[ns] 2kB 2021-01-07T21:23:20.996123 ... ...\n    satellite_name  (time) <U10 12kB 'Sentinel-1' 'Sentinel-1' ... 'Sentinel-2'\nData variables:\n    vv              (time, y, x) float32 158MB dask.array<chunksize=(1, 371, 356), meta=np.ndarray>\n    red             (time, y, x) float32 158MB dask.array<chunksize=(1, 371, 356), meta=np.ndarray>\n

We can now run tide_stats again. This time, we pass our satellite name coordinate to the function using the plot_col=\"satellite_name\" parameter. This will plot data from each of our satellites using a different symbol.

In\u00a0[8]: Copied!
statistics_df = tide_stats(\n    data=ds_all,\n    plot_col=\"satellite_name\",\n    directory=directory,\n)\n
statistics_df = tide_stats( data=ds_all, plot_col=\"satellite_name\", directory=directory, )
Using tide modelling location: 122.21, -18.00\nModelling tides with EOT20\nModelling tides with EOT20\n\n\n\ud83c\udf0a Modelled astronomical tide range: 9.30 metres.\n\ud83d\udef0\ufe0f Observed tide range: 8.32 metres.\n\n\ud83d\udfe1 89% of the modelled astronomical tide range was observed at this location.\n\ud83d\udfe2 The highest 8% (0.77 metres) of the tide range was never observed.\n\ud83d\udfe2 The lowest 2% (0.22 metres) of the tide range was never observed.\n\n\ud83c\udf0a Mean modelled astronomical tide height: -0.00 metres.\n\ud83d\udef0\ufe0f Mean observed tide height: 0.10 metres.\n\n\u2b06\ufe0f The mean observed tide height was 0.10 metres higher than the mean modelled astronomical tide height.\n

We can see that at this location, combining Sentinel-2 and Sentinel-1 data greatly improves our biases: our satellite data now covers ~85% of the modelled astronomical tide range, and only fails to observe 10% of the highest tides and 6% of the lowest tides!

In\u00a0[9]: Copied!
from eo_tides.stats import pixel_stats\n\nstats_ds = pixel_stats(\n    data=ds_s2,\n    directory=directory,\n)\nprint(stats_ds)\n
from eo_tides.stats import pixel_stats stats_ds = pixel_stats( data=ds_s2, directory=directory, ) print(stats_ds)
Creating reduced resolution 5000 x 5000 metre tide modelling array\nModelling tides with EOT20\nComputing tide quantiles\nReturning low resolution tide array\nCreating reduced resolution 5000 x 5000 metre tide modelling array\nModelling tides with EOT20\nComputing tide quantiles\nReturning low resolution tide array\n<xarray.Dataset> Size: 2kB\nDimensions:      (x: 8, y: 8)\nCoordinates:\n  * x            (x) float64 64B 3.975e+05 4.025e+05 ... 4.275e+05 4.325e+05\n  * y            (y) float64 64B 8.028e+06 8.022e+06 ... 7.998e+06 7.992e+06\n    tide_model   <U5 20B 'EOT20'\n    spatial_ref  int32 4B 32751\nData variables:\n    hot          (y, x) float32 256B 3.773 3.794 3.844 3.844 ... 3.953 3.953 nan\n    hat          (y, x) float32 256B 4.282 4.318 4.408 4.408 ... 4.797 4.797 nan\n    lot          (y, x) float32 256B -2.109 -2.128 -2.18 ... -2.418 -2.418 nan\n    lat          (y, x) float32 256B -4.222 -4.264 -4.353 ... -4.692 -4.692 nan\n    otr          (y, x) float32 256B 5.882 5.922 6.023 6.023 ... 6.372 6.372 nan\n    tr           (y, x) float32 256B 8.504 8.582 8.761 8.761 ... 9.49 9.49 nan\n    spread       (y, x) float32 256B 0.6917 0.69 0.6875 ... 0.6714 0.6714 nan\n    offset_low   (y, x) float32 256B 0.2485 0.2489 0.2481 ... 0.2396 0.2396 nan\n    offset_high  (y, x) float32 256B 0.0598 0.06103 0.06443 ... 0.08894 nan\n

We can explore these statistics on a map:

In\u00a0[10]: Copied!
stats_ds.spread.odc.explore()\n
stats_ds.spread.odc.explore() Out[10]: Make this Notebook Trusted to load map: File -> Trust Notebook

Or plot them directly:

In\u00a0[11]: Copied!
import matplotlib.pyplot as plt\n\nfig, axes = plt.subplots(1, 3, figsize=(16, 4))\nstats_ds.spread.plot(ax=axes[0], vmin=0.5, vmax=0.9, cmap=\"magma_r\", add_labels=False)\nstats_ds.offset_low.plot(ax=axes[1], vmin=0, vmax=0.35, cmap=\"magma\", add_labels=False)\nstats_ds.offset_high.plot(ax=axes[2], vmin=0, vmax=0.35, cmap=\"magma\", add_labels=False)\naxes[0].set_title(\"Spread\")\naxes[1].set_title(\"Low tide offset\")\naxes[2].set_title(\"High tide offset\");\n
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 3, figsize=(16, 4)) stats_ds.spread.plot(ax=axes[0], vmin=0.5, vmax=0.9, cmap=\"magma_r\", add_labels=False) stats_ds.offset_low.plot(ax=axes[1], vmin=0, vmax=0.35, cmap=\"magma\", add_labels=False) stats_ds.offset_high.plot(ax=axes[2], vmin=0, vmax=0.35, cmap=\"magma\", add_labels=False) axes[0].set_title(\"Spread\") axes[1].set_title(\"Low tide offset\") axes[2].set_title(\"High tide offset\");"},{"location":"notebooks/Tide_statistics/#calculating-tide-statistics-and-satellite-biases","title":"Calculating tide statistics and satellite biases\u00b6","text":"

This guide demonstrates how to use the tide_stats and pixel_stats functions from eo_tides.stats to calculate local tide statistics and identify biases caused by interactions between tidal processes and satellite orbits.

Complex interactions between temporal tide dynamics and the regular mid-morning overpass timing of sun-synchronous sensors like Landsat or Sentinel-2 mean that satellites often do not observe the entire tidal cycle. Biases in satellite coverage of the tidal cycle can mean that tidal extremes (e.g. the lowest or highest tides at a location) may either never be captured by satellites, or be over-represented in the satellite EO record. Local tide dynamics can cause these biases to vary greatly both through time and spatially (Figure 1), making it challenging to consistently analyse and compare coastal processes consistently - particularly for large-scale (e.g. regional or global) analyses.

To ensure that coastal EO analyses are not inadvertently affected by tide biases, it is important to compare how well the tides observed by satellites match the full range of tides at a location. The tidal_stats and pixel_stats functions compares the subset of tides observed by satellite data against the full range of tides modelled at a regular interval through time (every two hours by default) across the entire time period covered by the satellite dataset. This comparison is used to calculate several useful statistics and plots that summarise how well your satellite data capture real-world tidal conditions.

Figure 1: Example of tide biases in Landsat satellite data across coastal Australia (Bishop-Taylor et al. 2018). \"Spread\" represents the proportion of the astronomical tide range observed by satellites; low and high tide \"offsets\" represent the proportion of highest and lowest tides never observed.

Tip

For a more detailed discussion of the issue of tidal bias in sun-synchronous satellite observations of the coastline, refer to the 'Limitations and future work' section in Bishop-Taylor et al. 2018.

"},{"location":"notebooks/Tide_statistics/#getting-started","title":"Getting started\u00b6","text":"

As in the previous examples, our first step is to tell eo-tides the location of our tide model or clipped tide model directory (if you haven't set this up, refer to the setup instructions here):

"},{"location":"notebooks/Tide_statistics/#load-sentinel-2-satellite-data-using-odc-stac","title":"Load Sentinel-2 satellite data using odc-stac\u00b6","text":"

We can now load a time-series of satellite data over our area of interest using the Open Data Cube's odc-stac package. In this example, we will load Sentinel-2 satellite data from 2021-2023 over the city of Broome, Western Australia. We will load this data from the Microsoft Planetary Computer STAC catalogue.

Tip

For a more detailed guide to using STAC metadata and odc-stac to find and load satellite data, refer to the STAC user guide here.

"},{"location":"notebooks/Tide_statistics/#using-tide_stats","title":"Using tide_stats\u00b6","text":"

Once we have loaded some satellite data, we can pass this to the tide_stats function to calculate local tide statistics and reveal any potential tidal biases. The tide_stats function will return a plain-text summary below, as well as a visual plot that compares the distribution of satellite-observed tides (black dots) against the full range of modelled astronomical tide conditions (blue) using three useful metrics:

  1. Spread: The proportion of the full modelled astronomical tidal range that was observed by satellites. A high value indicating good coverage of the tide range.
  2. Offset high: The proportion of the highest tides not observed by satellites at any time, as a proportion of the full modelled astronomical tidal range. A high value indicates that the satellite data is biased towards never capturing high tides.
  3. Offset low: The proportion of the lowest tides not observed by satellites at any time, as a proportion of the full modelled astronomical tidal range. A high value indicates that the satellite data is biased towards never capturing low tides.

Tip

For a more detailed description of these biases, see Bishop-Taylor et al. 2018.

"},{"location":"notebooks/Tide_statistics/#compare-against-sentinel-1-tide-biases","title":"Compare against Sentinel-1 tide biases\u00b6","text":"

In the previous example, we saw that Sentinel-2 data was biased towards never capturing low tide at our location. These biases are caused by the consistent 10:30 am local overpass time of the Sentinel-2 satellites, which due to a phenomenon called \"tide-aliasing\" means that certain tides never occur when the satellite overpasses.

One possible way around these biases is to use different satellite data from satellites that overpass at different times. For example, Sentinel-1 radar satellites follow a different orbit to Sentinel-2, overpassing at a local time of 6:00 pm instead of 10:30 am. This diference in overpass time potentially means that Sentinel-1 satellite data may capture different tides to Sentinel-2.

In the next example, we run the tide_stats function on data loaded from Sentinel-1 for the same location and time period to see if this data is affected by the same tide biases.

"},{"location":"notebooks/Tide_statistics/#calculate-biases-for-multiple-satellite-sensors","title":"Calculate biases for multiple satellite sensors\u00b6","text":"

At our location, Sentinel-2 optical satellites are biased towards high tide observations, while Sentinel-1 radar satellites are biased towards low tide observations. Could combining data from multiple EO sensors help us capture a more complete view of tides at this location?

To test this theory, we can combine Sentinel-2 and Sentinel-1 data into a single xarray.Dataset, recording the name of each sensor using a new satellite_name coordinate in our data:

"},{"location":"notebooks/Tide_statistics/#using-pixel_stats","title":"Using pixel_stats\u00b6","text":""},{"location":"notebooks/Tide_statistics/#modelling-tide-statistics-and-biases-spatially","title":"Modelling tide statistics and biases spatially\u00b6","text":"

Because tide regimes and satellite biases can vary greatly along the coast, it can be useful to plot these biases spatially. To do this, we can use the pixel_stats function.

pixel_stats works similarly to tide_stats, except statistics are calculated across the entire extent of your satellite dataset. The function will generate an xarray.Dataset output containing the statistics discussed above as two-dimensional arrays.

Tip

The pixel_stats function uses eo_tides.eo.pixel_tides to model tides spatially. You can experiment passing in parameters like resolution and buffer to customise the modelling grid used for calculating tide biases. Be warned however that you can quickly run out of memory with large analyses, given the number of timesteps required to model astronomical low and high tide.

"},{"location":"notebooks/Tide_statistics/#next-steps","title":"Next steps\u00b6","text":"

We have explored calculating tide statistics and biases in EO data. Now we can learn how to validate modelled tides against measured tide gauge data to ensure the tides we are modelling are accurate.

"},{"location":"notebooks/Validating_tides/","title":"Validating modelled tide heights","text":"In\u00a0[1]: Copied!
directory = \"../../tests/data/tide_models/\"\n
directory = \"../../tests/data/tide_models/\" In\u00a0[2]: Copied!
from eo_tides.model import model_tides\nimport pandas as pd\n\nx, y = 122.2186, -18.0008\nstart_time = \"2018-01-01\"\nend_time = \"2018-01-31\"\n\nmodelled_df = model_tides(\n    x=x,\n    y=y,\n    time=pd.date_range(start=start_time, end=end_time, freq=\"1h\"),\n    directory=directory,\n)\n\n# Print outputs\nmodelled_df.head()\n
from eo_tides.model import model_tides import pandas as pd x, y = 122.2186, -18.0008 start_time = \"2018-01-01\" end_time = \"2018-01-31\" modelled_df = model_tides( x=x, y=y, time=pd.date_range(start=start_time, end=end_time, freq=\"1h\"), directory=directory, ) # Print outputs modelled_df.head()
Modelling tides using EOT20\n
Out[2]: tide_model tide_height time x y 2018-01-01 00:00:00 122.2186 -18.0008 EOT20 1.229286 2018-01-01 01:00:00 122.2186 -18.0008 EOT20 2.162897 2018-01-01 02:00:00 122.2186 -18.0008 EOT20 2.476600 2018-01-01 03:00:00 122.2186 -18.0008 EOT20 2.112390 2018-01-01 04:00:00 122.2186 -18.0008 EOT20 1.181283 In\u00a0[3]: Copied!
gesla_data_path = \"../../tests/data/\"\ngesla_metadata_path = \"../../tests/data/GESLA3_ALL 2.csv\"\n
gesla_data_path = \"../../tests/data/\" gesla_metadata_path = \"../../tests/data/GESLA3_ALL 2.csv\"

To load GESLA measured sea-level data for our location, we can pass in the same x and y location and time period that we used to originally model our tides. This will ensure that we load only the gauge data we actually need.

Tip

The load_gauge_gesla function will automatically identify the nearest GESLA tide gauge to an x, y coordinate; pass a set of bounding box tuples (e.g. x=(120, 130), y=(-20, -30) to return all tide gauges within a bounding box instead.

In\u00a0[4]: Copied!
from eo_tides.validation import load_gauge_gesla\n\n# Load gauge data\ngauge_df = load_gauge_gesla(\n    x=x,\n    y=y,\n    time=(start_time, end_time),\n    correct_mean=True,\n    data_path=gesla_data_path,\n    metadata_path=gesla_metadata_path,\n)\ngauge_df.head()\n
from eo_tides.validation import load_gauge_gesla # Load gauge data gauge_df = load_gauge_gesla( x=x, y=y, time=(start_time, end_time), correct_mean=True, data_path=gesla_data_path, metadata_path=gesla_metadata_path, ) gauge_df.head() Out[4]: sea_level qc_flag use_flag file_name site_name country contributor_abbreviated contributor_full contributor_website contributor_contact ... start_date_time end_date_time number_of_years time_zone_hours datum_information instrument precision null_value gauge_type overall_record_quality site_code time 62650 2018-01-01 00:00:00 1.208329 1 1 ../../tests/data/broome-62650-aus-bom Broome AUS BOM Bureau of Meteorology http://www.bom.gov.au/oceanography/projects/nt... tides@bom.gov.au ... 2/07/1966 0:00 31/12/2019 23:00 51 0 Chart Datum / Lowest Astronomical Tide Unspecified Unspecified -99.9999 Coastal No obvious issues 2018-01-01 01:00:00 2.311329 1 1 ../../tests/data/broome-62650-aus-bom Broome AUS BOM Bureau of Meteorology http://www.bom.gov.au/oceanography/projects/nt... tides@bom.gov.au ... 2/07/1966 0:00 31/12/2019 23:00 51 0 Chart Datum / Lowest Astronomical Tide Unspecified Unspecified -99.9999 Coastal No obvious issues 2018-01-01 02:00:00 2.712329 1 1 ../../tests/data/broome-62650-aus-bom Broome AUS BOM Bureau of Meteorology http://www.bom.gov.au/oceanography/projects/nt... tides@bom.gov.au ... 2/07/1966 0:00 31/12/2019 23:00 51 0 Chart Datum / Lowest Astronomical Tide Unspecified Unspecified -99.9999 Coastal No obvious issues 2018-01-01 03:00:00 2.137329 1 1 ../../tests/data/broome-62650-aus-bom Broome AUS BOM Bureau of Meteorology http://www.bom.gov.au/oceanography/projects/nt... tides@bom.gov.au ... 2/07/1966 0:00 31/12/2019 23:00 51 0 Chart Datum / Lowest Astronomical Tide Unspecified Unspecified -99.9999 Coastal No obvious issues 2018-01-01 04:00:00 1.049329 1 1 ../../tests/data/broome-62650-aus-bom Broome AUS BOM Bureau of Meteorology http://www.bom.gov.au/oceanography/projects/nt... tides@bom.gov.au ... 2/07/1966 0:00 31/12/2019 23:00 51 0 Chart Datum / Lowest Astronomical Tide Unspecified Unspecified -99.9999 Coastal No obvious issues

5 rows \u00d7 26 columns

We have successfully loaded data for the Broome tide gauge (GESLA site code 62650)! We can now plot sea levels over time - note that the gauge dataset is missing some data in late January 2018:

In\u00a0[5]: Copied!
gauge_df.droplevel(\"site_code\").sea_level.plot()\n
gauge_df.droplevel(\"site_code\").sea_level.plot() Out[5]:
<Axes: xlabel='time'>
In\u00a0[6]: Copied!
import matplotlib.pyplot as plt\n\n# Join our modelled data to the timesteps in our gauge data\njoined_df = gauge_df.join(modelled_df).dropna()\n\n# Plot as a scatterplot with 1:1 line\nax = joined_df.plot.scatter(x=\"sea_level\", y=\"tide_height\")\nplt.plot([-5, 5], [-5, 5], c=\"red\", linestyle=\"dashed\")\nax.set_aspect(1.0)\nax.set_ylabel(\"Modelled tide (m)\")\nax.set_xlabel(\"GESLA sea level (m)\");\n
import matplotlib.pyplot as plt # Join our modelled data to the timesteps in our gauge data joined_df = gauge_df.join(modelled_df).dropna() # Plot as a scatterplot with 1:1 line ax = joined_df.plot.scatter(x=\"sea_level\", y=\"tide_height\") plt.plot([-5, 5], [-5, 5], c=\"red\", linestyle=\"dashed\") ax.set_aspect(1.0) ax.set_ylabel(\"Modelled tide (m)\") ax.set_xlabel(\"GESLA sea level (m)\");

We can see that both datasets are highly correlated. To quantify this, we can use the eo_tides.validation.eval_metrics function to compare them and calculate some useful accuracy statistics, including Root Mean Square Error (RMSE), Mean Absolute Error (MAE), R-squared and bias.

Our results show that our modelled tides closely reproduced observed sea levels at this location:

In\u00a0[7]: Copied!
from eo_tides.validation import eval_metrics\n\n# Calculate accuracy metrics\naccuracy_metrics = eval_metrics(x=joined_df.sea_level, y=joined_df.tide_height)\naccuracy_metrics\n
from eo_tides.validation import eval_metrics # Calculate accuracy metrics accuracy_metrics = eval_metrics(x=joined_df.sea_level, y=joined_df.tide_height) accuracy_metrics Out[7]:
Correlation         0.997\nRMSE                0.159\nMAE                 0.126\nR-squared           0.994\nBias               -0.005\nRegression slope    0.979\ndtype: float64
In\u00a0[8]: Copied!
models = [\"EOT20\", \"GOT5.5\", \"HAMTIDE11\"]\n\nmodelled_df = model_tides(\n    x=x,\n    y=y,\n    time=pd.date_range(start=start_time, end=end_time, freq=\"1h\"),\n    model=models,\n    output_format=\"wide\",\n    directory=directory,\n)\nmodelled_df.head()\n
models = [\"EOT20\", \"GOT5.5\", \"HAMTIDE11\"] modelled_df = model_tides( x=x, y=y, time=pd.date_range(start=start_time, end=end_time, freq=\"1h\"), model=models, output_format=\"wide\", directory=directory, ) modelled_df.head()
Modelling tides using EOT20, GOT5.5, HAMTIDE11 in parallel\n
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 25.57it/s]\n
Converting to a wide format dataframe\n
Out[8]: tide_model EOT20 GOT5.5 HAMTIDE11 time x y 2018-01-01 00:00:00 122.2186 -18.0008 1.229286 1.292904 1.422702 2018-01-01 01:00:00 122.2186 -18.0008 2.162897 2.290366 2.302042 2018-01-01 02:00:00 122.2186 -18.0008 2.476600 2.629404 2.537032 2018-01-01 03:00:00 122.2186 -18.0008 2.112390 2.244390 2.072846 2018-01-01 04:00:00 122.2186 -18.0008 1.181283 1.258280 1.034931

We can now merge these modelled tides with our measured gauge data:

In\u00a0[9]: Copied!
# Join our modelled data to the timesteps in our gauge data\njoined_df = gauge_df.join(modelled_df).dropna()\n\n# Plot measured sea levels and modelled data\njoined_df.droplevel([\"site_code\", \"x\", \"y\"])[[\"sea_level\"] + models].plot()\n
# Join our modelled data to the timesteps in our gauge data joined_df = gauge_df.join(modelled_df).dropna() # Plot measured sea levels and modelled data joined_df.droplevel([\"site_code\", \"x\", \"y\"])[[\"sea_level\"] + models].plot() Out[9]:
<Axes: xlabel='time'>

Now, we can loop through each of our models and calculate accuracy metrics compared to our gauge data for each of them:

In\u00a0[10]: Copied!
# Calculate accuracy metrics for each model\naccuracy_dict = {}\nfor model in models:\n    accuracy_dict[model] = eval_metrics(x=joined_df.sea_level, y=joined_df[model])\n\n# Merge into a single dataframe\ncombined_accuracy_df = pd.DataFrame.from_dict(accuracy_dict)\ncombined_accuracy_df\n
# Calculate accuracy metrics for each model accuracy_dict = {} for model in models: accuracy_dict[model] = eval_metrics(x=joined_df.sea_level, y=joined_df[model]) # Merge into a single dataframe combined_accuracy_df = pd.DataFrame.from_dict(accuracy_dict) combined_accuracy_df Out[10]: EOT20 GOT5.5 HAMTIDE11 Correlation 0.997 0.997 0.993 RMSE 0.159 0.152 0.239 MAE 0.126 0.118 0.193 R-squared 0.994 0.994 0.986 Bias -0.005 -0.010 -0.011 Regression slope 0.979 0.996 0.963

As we can see above, at this location GOT5.5 has the best overall accuracy as measured by RMSE and MAE, while results from HAMTIDE11 are less accurate and slightly less correlated with our measured gauge data.

"},{"location":"notebooks/Validating_tides/#validating-modelled-tide-heights","title":"Validating modelled tide heights\u00b6","text":"

This guide demonstrates how to use the load_gauge_gesla function from eo_tides.stats to validate modelled tides from eo-tides using tide gauge data.

The tide models used by eo-tides can vary significantly in accuracy across the world's coastlines. Evaluating the accuracy of your modelled tides is critical for ensuring that resulting marine or coastal EO analyses are reliable and useful.

The load_gauge_gesla function provides a convenient tool for loading high-quality sea-level measurements from the GESLA Global Extreme Sea Level Analysis archive \u2013 a global archive of almost 90,713 years of sea level data from 5,119 records across the world. This data can be used to compare against tides modelled using eo-tides to calculate the accuracy of your tide modelling and identify the optimal tide models to use for your study area.

"},{"location":"notebooks/Validating_tides/#getting-started","title":"Getting started\u00b6","text":"

As in the previous examples, our first step is to tell eo-tides the location of our tide model directory (if you haven't set this up, refer to the setup instructions here):

"},{"location":"notebooks/Validating_tides/#example-modelled-tides","title":"Example modelled tides\u00b6","text":"

First, we can model hourly tides for a location (Broome, Western Australia) and time period (January 2018) of interest using the eo_tides.model.model_tides function:

"},{"location":"notebooks/Validating_tides/#loading-gesla-tide-gauge-data","title":"Loading GESLA tide gauge data\u00b6","text":"

To evaluate the accuracy of these modelled tides, we can load measured sea-level data from the nearest GESLA tide gauge using load_gauge_gesla.

To obtain GESLA data, you will need to download both \"GESLA-3 data\" and \"GESLA-3 CSV META-DATA FILE\" from the Downloads page of the GESLA website, and save these to a convenient location.

We have provided an example below; replace these paths to point to your downloaded data.

"},{"location":"notebooks/Validating_tides/#validation-against-gesla-tide-gauges","title":"Validation against GESLA tide gauges\u00b6","text":"

Now we have modelled some tides and loaded some measured sea-level data, we can compare them. Note that because the timeseries above is missing some data in late January, we need to \"join\" our modelled modelled_df data to the timesteps present in gauge_df.

Now let's generate a scatterplot with our measured data on the x-axis, and our modelled tides on the y-axis:

"},{"location":"notebooks/Validating_tides/#identifying-best-local-tide-models","title":"Identifying best local tide models\u00b6","text":"

Because different ocean tide models can perform better or worse in different locations, it can be valuable to compare the accuracy of different models against measured gauge data. This can help us make an informed decision about the best model to use for a given application or study area.

In the example below, we will use model_tides to model tides using three different models: EOT20, GOT5.5, and HAMTIDE11:

"}]} \ No newline at end of file diff --git a/setup/index.html b/setup/index.html new file mode 100644 index 0000000..1a4ace1 --- /dev/null +++ b/setup/index.html @@ -0,0 +1,1493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Setting up tide models - eo-tides + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Setting up tide models

+
+

Important

+

eo-tides provides tools for modelling tides using global ocean tide models but does not host or maintain the models themselves. Users are responsible for accessing, using, and citing ocean tide models in compliance with each model's licensing terms.

+
+

Once you have installed eo-tides, we need to download and set up the external global ocean tide models required for eo-tides to work. +The following documentation provides instructions for getting started with several common global ocean tide models.

+
+

Tip

+

Please refer to the pyTMD documentation for additional instructions covering all other supported tide models.

+
+

Setting up a tide model directory

+

As a first step, we need to create a directory that will contain our tide model data. +This directory will be accessed by all eo-tides functions. +For example, we might want to store our tide models in a directory called tide_models/:

+
tide_models/
+
+
+

Tip

+

This directory doesn't need to be called tide_models; use any name and/or location that is convenient to you and accessible from your Python environment. Please refer to the documentation below for further details on configuring eo-tides to use this directory.

+
+

Downloading tide model data

+

Now we need to download some data from one or more models, and save this into our tide model directory. +Follow the guides below for some of the most commonly used global ocean tide models:

+
+EOT20 Empirical Ocean Tide model (default) +

EOT20 Empirical Ocean Tide model (default)

+
    +
  1. Visit EOT20 - A global Empirical Ocean Tide model from multi-mission satellite altimetry
  2. +
  3. +

    Under Data, click Download:

    +

    image

    +
  4. +
  5. +

    Create a new directory inside your tide model directory called EOT20/ to store the EOT20 model files.

    +
  6. +
  7. +

    Extract the 85762.zip and then ocean_tides.zip into this new directory. You should end up with the following directory structure containing the extracted NetCDF files:

    +
    tide_models/EOT20/ocean_tides/
    +   |- 2N2_ocean_eot20.nc
    +   |- ...
    +   |- T2_ocean_eot20.nc
    +
    +
  8. +
+
+
+FES2022 Finite Element Solution tide models +

FES2022 Finite Element Solution tide models

+
    +
  1. +

    Register with AVISO+, and select FES (Finite Element Solution - Oceanic Tides Heights) from the Licence Agreement and product selection section:

    +

    image

    +
  2. +
  3. +

    Scroll to the bottom of the page and agree to the licence agreement. Your request will be sent for approval (this may take several days).

    +
  4. +
  5. Once you are notified via email that your registration and access is approved, login to MY AVISO+.
  6. +
  7. +

    Once logged in, select My products in the left-hand menu:

    +

    image

    +
  8. +
  9. +

    FES (Finite Element Solution - Oceanic Tides Heights) should appear under Your current subscriptions. Right click on Ftp, and copy the FTP address.

    +

    image

    +
  10. +
  11. +

    Using an FTP client like FileZilla, log in to the FTP using your AVISO+ username and password:

    +

    image

    +
  12. +
  13. +

    Navigate to /auxiliary/tide_model/, and download the contents of one or more of the following directories:

    +
      +
    • fes2022b/ocean_tide/
    • +
    • fes2022b/ocean_tide_extrapolated/
    • +
    +
    +

    Tip

    +

    The "extrapolated" version of FES models have been extended inland using a simple "nearest" extrapolation method to ensure data coverage across the entire coastal zone. This can be useful for ensuring you always return a modelled tide, but can also introduce uncertainty into your modelling (particularly in complex regions such as narrow peninsulas or inlets/embayments).

    +
    +
  14. +
  15. +

    Create new nested directories inside your tide model directory called fes2022b/ocean_tide/ (if using standard model data) or fes2022b/ocean_tide_extrapolated/ (if using extrapolated model data) to store the FES2022 model files.

    +
  16. +
  17. +

    Extract your ...nc.xz files into this directory (e.g. tar -xf m2_fes2022.nc.xz). You should end up with the following directory structure containing the extracted NetCDF files:

    +

    tide_models/fes2022b/ocean_tide/
    +   |- 2n2_fes2022.nc
    +   |- ...
    +   |- t2_fes2022.nc
    +
    +Or: +
    tide_models/fes2022b/ocean_tide_extrapolated/
    +   |- 2n2_fes2022.nc
    +   |- ...
    +   |- t2_fes2022.nc
    +

    +
  18. +
+
+
+FES2014 Finite Element Solution tide models +

FES2014 Finite Element Solution tide models

+
    +
  1. +

    Register with AVISO+, and select FES (Finite Element Solution - Oceanic Tides Heights) from the Licence Agreement and product selection section:

    +

    image

    +
  2. +
  3. +

    Scroll to the bottom of the page and agree to the licence agreement. Your request will be sent for approval (this may take several days).

    +
  4. +
  5. Once you are notified via email that your registration and access is approved, login to MY AVISO+.
  6. +
  7. +

    Once logged in, select My products in the left-hand menu:

    +

    image

    +
  8. +
  9. +

    FES (Finite Element Solution - Oceanic Tides Heights) should appear under Your current subscriptions. Right click on Ftp, and copy the FTP address.

    +

    image

    +
  10. +
  11. +

    Using an FTP client like FileZilla, log in to the FTP using your AVISO+ username and password:

    +

    image

    +
  12. +
  13. +

    Navigate to /auxiliary/tide_model/, and download the contents of one or more of the following directories:

    +
      +
    • fes2014_elevations_and_load/fes2014b_elevations/
    • +
    • fes2014_elevations_and_load/fes2014b_elevations_extrapolated/
    • +
    +
    +

    Tip

    +

    The "extrapolated" version of FES have been extended inland using a simple "nearest" extrapolation method to ensure data coverage across the entire coastal zone. This can be useful for ensuring you always return a modelled tide, but can also introduce uncertainty into your modelling (particularly in complex regions such as narrow peninsulas or inlets/embayments).

    +
    +
  14. +
  15. +

    Create a new directory inside your tide model directory called fes2014/ to store the FES2014 model files.

    +
  16. +
  17. +

    Extract ocean_tide.tar.xz or ocean_tide_extrapolated.tar.xz into this directory (e.g. tar -xf ocean_tide.tar.xz). You should end up with the following directory structure containing the extracted NetCDF files:

    +

    tide_models/fes2014/ocean_tide/
    +   |- 2n2.nc
    +   |- ...
    +   |- t2.nc
    +
    +Or: +
    tide_models/fes2014/ocean_tide_extrapolated/
    +   |- 2n2.nc
    +   |- ...
    +   |- t2.nc
    +

    +
  18. +
+
+
+GOT Global Ocean Tide models +

GOT Global Ocean Tide models

+
    +
  1. Visit Ocean tide models
  2. +
  3. +

    Under Short-period (diurnal/semidiurnal) tides, click choose your desired GOT model:

    +

    image

    +
  4. +
  5. +

    Create a new directory inside your tide model directory called either GOT4.7/, got4.8/, GOT4.10c/, GOT5.5/ or GOT5.6/ to store the GOT model files.

    +
  6. +
  7. +

    Extract your downloaded .tar.gz file into this new directory. You should end up with the following directory structure containing the extracted NetCDF files:

    +

    tide_models/GOT5.6/ocean_tides/
    +   |- ...
    +
    +Or: +
    tide_models/GOT5.5/ocean_tides/
    +   |- ...
    +

    +
    +

    Important

    +

    Note that GOT5.6 requires that both GOT5.6 and GOT5.5 model files are downloaded and extracted.

    +
    +

    Or: +

    tide_models/GOT4.10c/grids_oceantide/
    +   |- ...
    +
    +Or: +
    tide_models/got4.8/grids_oceantide/
    +   |- ...
    +
    +Or: +
    tide_models/GOT4.7/grids_oceantide/
    +   |- ...
    +

    +
  8. +
+
+
+TPXO Global Tidal Models +

TPXO Global Tidal Models

+
    +
  1. Visit TPXO Registration
  2. +
  3. Follow instructions to email TPXO authors for access, providing your name, institution, your intended application/use case, and which TPXO models you need ("TPXO10-atlas-v2 netcdf" or "TPXO9-atlas-v5 netcdf" are recommended to enable clipping).
  4. +
  5. +

    If your request is approved, you will be emailed an invite to an app.box.com folder. Open this link, then click "Download" on the top-right to download your zipped model files.

    +

    image

    +
  6. +
  7. +

    Create a new directory inside your tide model directory called either TPXO10_atlas_v2/ or TPXO9_atlas_v5/ to store the TPXO model files.

    +
  8. +
  9. +

    Extract your zipped model files (e.g. TPXO10_atlas_v2_nc.zip or TPXO9_atlas_v5_nc.zip) into this new directory. You should end up with the following directory structure containing the extracted NetCDF files depending on the model you downloaded:

    +

    tide_models/TPXO10_atlas_v2/
    +   |- grid_tpxo10atlas_v2.nc
    +   |- ...
    +   |- u_s2_tpxo10_atlas_30_v2.nc
    +
    +Or: +
    tide_models/TPXO9_atlas_v5/
    +   |- grid_tpxo9_atlas_30_v5.nc
    +   |- ...
    +   |- u_s2_tpxo9_atlas_30_v5.nc
    +

    +
  10. +
+
+
+

Tip

+

To allow you to improve tide modelling performance by clipping your tide model files (see below), we recommend downloading NetCDF-format versions of tide models wherever possible.

+
+

Configuring eo-tides to use tide model directory

+

eo-tides can be pointed to the location of your tide model directory and your downloaded tide model data in two ways:

+

1. Using the directory function parameter

+

All tide modelling functions from eo-tides provide a directory parameter that can be used to specify the location of your tide model directory. +For example, using the eo_tides.model.model_tides function:

+
import pandas as pd
+from eo_tides.model import model_tides
+
+model_tides(
+        x=155,
+        y=-35,
+        time=pd.date_range("2022-01-01", "2022-01-04", freq="1D"),
+        directory="tide_models/"
+)
+
+

2. (Advanced) Setting the EO_TIDES_TIDE_MODELS environmental variable

+

For more advanced usage, you can set the path to your tide model directory by setting the EO_TIDES_TIDE_MODELS environment variable:

+
import os
+os.environ["EO_TIDES_TIDE_MODELS"] = "tide_models/"
+
+

All tide modelling functions from eo-tides will check for the presence of the EO_TIDES_TIDE_MODELS environment variable, and use it as the default directory path if available (the EO_TIDES_TIDE_MODELS environment variable will be overuled by the directory parameter if provided).

+
+

Tip

+

Setting the EO_TIDES_TIDE_MODELS environment variable can be useful when the location of your tide model directory might change between different environments, and you want to avoid hard-coding a single location via the directory parameter.

+
+

Verifying available and supported models

+

You can check what tide models have been correctly set up for use by eo-tides using the eo_tides.utils.list_models function:

+
from eo_tides.utils import list_models
+
+available_models, supported_models = list_models(directory="tide_models/")
+
+

This will print out a useful summary, with available models marked with a ✅:

+
──────────────────────────────────────────────────────────
+ 󠀠🌊  | Model        | Expected path
+──────────────────────────────────────────────────────────
+ ✅  │ EOT20        │ tide_models/EOT20/ocean_tides
+ ❌  │ FES2014      │ tide_models/fes2014/ocean_tide
+ ✅  │ HAMTIDE11    │ tide_models/hamtide
+ ❌  │ TPXO9.1      │ tide_models/TPXO9.1/DATA
+ ...   ...            ...
+──────────────────────────────────────────────────────────
+
+Summary:
+Available models: 2/50
+
+
+

Tip

+

If the models that you saved are not marked as available in this summary, check that they were saved to the expected path as identified in the summary table.

+
+

Clipping model files to improve performance

+
+

Highly recommended

+

Clipping your model files to a smaller spatial extent is highly recommended, unless you are specifically running global-scale analyses.

+
+

Running tide modelling on the default tide modelling data provided by external providers can be slow due to the large size of these files (especially for high-resolution models like FES2022). +To improve performance, it can be extremely useful to clip your model files to a smaller region of interest (e.g. the extents of your country or coastal region). +This can greatly improve run-times: potentially speeding up your tide modelling by over 10 times for large models!

+

Once you have downloaded and verified your tide model data, you can use the eo_tides.utils.clip_models function to automatically clip your data, and export them to a new tide modelling directory:

+
from eo_tides.utils import clip_models
+
+clip_models(
+    input_directory="tide_models/",
+    output_directory="tide_models_clipped/",
+    bbox=(113.3, -43.6, 153.6, -10.7),
+)
+
+

When you run clip_models, the function will automatically identify suitable NetCDF-format models in your input directory, and clip each of them to the extent of your bounding box (specified as (left, bottom, right, top) in EPSG:4326 degree coordinates). +After each model is clipped, the result is exported to your selected output directory and verified to ensure the clipped data is suitable for tide modelling:

+
Preparing to clip suitable NetCDF models: ['HAMTIDE11', 'EOT20']
+
+Clipping HAMTIDE11: 100%|██████████| 9/9 [00:03<00:00,  2.60it/s]
+✅ Clipped model exported and verified
+Clipping EOT20: 100%|██████████| 17/17 [00:07<00:00,  2.36it/s]
+✅ Clipped model exported and verified
+
+Outputs exported to tide_models_clipped/
+
+

You can now pass this new clipped tide model directory to all future eo_tides function calls for improved tide modelling performance, e.g.:

+
model_tides(
+        x=155,
+        y=-35,
+        time=pd.date_range("2022-01-01", "2022-01-04", freq="1D"),
+        directory="tide_models_clipped/"
+)
+
+
+

Example of tide model data for the M2 tidal constituent from EOT20 clipped to Australia:

+
+

Clipped tide model data example

+
+

Tip

+

Because only NetCDF-format tide models can be clipped, we recommend downloading NetCDF versions of your tide models wherever possible.

+
+

Next steps

+

Now that you have installed eo-tides and set up some tide models, you can learn how to use eo-tides for modelling tides and analysing satellite data!

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..8b41e96 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,51 @@ + + + + https://GeoscienceAustralia.github.io/eo-tides/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/api/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/changelog/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/credits/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/install/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/migration/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/setup/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/notebooks/Case_study_intertidal/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/notebooks/Model_tides/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/notebooks/Satellite_data/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/notebooks/Tide_statistics/ + 2024-12-19 + + + https://GeoscienceAustralia.github.io/eo-tides/notebooks/Validating_tides/ + 2024-12-19 + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000..a9808b4 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/stylesheets/extra.css b/stylesheets/extra.css new file mode 100644 index 0000000..a26479c --- /dev/null +++ b/stylesheets/extra.css @@ -0,0 +1,26 @@ +[data-md-color-scheme="eotides"] { + --md-primary-fg-color: #215f9a; + --md-primary-fg-color--light: #4a7dbe; + --md-primary-fg-color--dark: #184878; + --md-primary-bg-color: #ffffff; + --md-primary-bg-color--light: #ffffff; + --md-default-fg-color--lighter: #ffffff; + --md-footer-bg-color: #113457; + --md-footer-bg-color--dark: #113457; +} + +/* Hide dark images in light mode */ +[data-md-color-scheme="eotides"] img[src$="#only-dark"], +[data-md-color-scheme="eotides"] img[src$="#gh-dark-mode-only"] { + display: none; +} + +/* Target the search input placeholder icon */ +.md-search__icon.md-icon svg { + color: #ffffff !important; +} + +/* Add wrapping to code blocks */ +code { + white-space: pre-wrap !important; +}