Source code for faninsar.NSBAS.tsmodels

"""Time series models for NSBAS inversion."""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal, Sequence

import numpy as np
import pandas as pd

if TYPE_CHECKING:
    from datetime import datetime

    from .freeze_thaw_process import FreezeThawCycle


[docs] class TimeSeriesModels: """Base class for time series models.""" _unit: Literal["year", "day"] _dates: pd.DatetimeIndex _date_spans: np.ndarray # Following attributes should be set in subclasses _G_br: np.ndarray _param_names: list[str] __slots__ = ["_G_br", "_date_spans", "_dates", "_param_names", "_unit"]
[docs] def __init__( self, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize TimeSeriesModels. Parameters ---------- dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ self._unit = None self._dates = None self._date_spans = None self.unit = unit self.dates = dates
def __str__(self) -> str: """Return a string representation of the object.""" return f"{self.__class__.__name__}(dates: {len(self.dates)}, unit: {self.unit})" def __repr__(self) -> str: """Return a string representation of the object.""" return ( f"{self.__class__.__name__}(\n" f" dates: {len(self.dates)}\n" f" unit: {self.unit}\n" f" param_names: {self.param_names}\n" f" G_br shape: {self.G_br.shape})\n" ")" ) @property def unit(self) -> str: """Unit of date_spans in time series model.""" return self._unit @unit.setter def unit( self, unit: Literal["year", "day"], ) -> None: """Update unit.""" if unit not in ["day", "year"]: msg = "unit must be either day or year" raise ValueError(msg) if unit != self._unit and self._unit is not None: self._date_spans = self._date_spans * ( 1 / 365.25 if unit == "year" else 365.25 ) self._unit = unit @property def dates(self) -> pd.DatetimeIndex: """Dates of SAR acquisitions.""" return self._dates @dates.setter def dates(self, dates: pd.DatetimeIndex) -> None: """Update dates.""" if not isinstance(dates, pd.DatetimeIndex): try: dates = pd.to_datetime(dates) except Exception as e: msg = "dates must be either pd.DatetimeIndex or iterable of datetime" raise TypeError(msg) from e self._dates = dates date_spans = (dates - dates[0]).days.values if self.unit == "year": date_spans = date_spans / 365.25 self._date_spans = date_spans @property def date_spans(self) -> np.ndarray: """Date spans of SAR acquisitions in unit of year or day.""" return self._date_spans @property def G_br(self) -> np.ndarray: # noqa: N802 """Bottom right block of the design matrix G in NSBAS inversion.""" return self._G_br @property def param_names(self) -> list[str]: """Parameter names in time series model.""" return self._param_names
[docs] class LinearModel(TimeSeriesModels): """Linear model."""
[docs] def __init__( self, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize LinearModel. Parameters ---------- dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) self._G_br = np.array([self.date_spans, np.ones_like(self.date_spans)]).T self._param_names = ["velocity", "constant"]
[docs] class QuadraticModel(TimeSeriesModels): """Quadratic model."""
[docs] def __init__( self, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize QuadraticModel. Parameters ---------- dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) self._G_br = np.array( [self.date_spans**2, self.date_spans, np.ones_like(self.date_spans)], ).T self._param_names = ["1/2_acceleration", "initial_velocity", "constant"]
[docs] class CubicModel(TimeSeriesModels): """Cubic model."""
[docs] def __init__( self, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize CubicModel. Parameters ---------- dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) self._G_br = np.array( [ self.date_spans**3, self.date_spans**2, self.date_spans, np.ones_like(self.date_spans), ], ).T self._param_names = ["Rate of Change", "acceleration", "velocity", "constant"]
[docs] class AnnualSinusoidalModel(TimeSeriesModels): """A sinusoidal model with annual period."""
[docs] def __init__( self, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize AnnualSinusoidalModel. Parameters ---------- dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) coeff = 2 * np.pi / 365.25 if self.unit == "day" else 2 * np.pi self._G_br = np.array( [ np.sin(self.date_spans * coeff), np.cos(self.date_spans * coeff), self.date_spans, np.ones_like(self.date_spans), ], ).T self._param_names = ["sin(T)", "cos(T)", "velocity", "constant"]
[docs] class AnnualSemiannualSinusoidal(TimeSeriesModels): """A compose sinusoidal model that contains annual and semi-annual periods."""
[docs] def __init__( self, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize AnnualSemiannualSinusoidal. Parameters ---------- dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) coeff = 2 * np.pi / 365.25 if self.unit == "day" else 2 * np.pi self._G_br = np.array( [ np.sin(self.date_spans * coeff), np.cos(self.date_spans * coeff), np.sin(self.date_spans * coeff * 2), np.cos(self.date_spans * coeff * 2), self.date_spans, np.ones_like(self.date_spans), ], ).T self._param_names = [ "sin(T)", "cos(T)", "sin(T/2)", "cos(T/2)", "velocity", "constant", ]
[docs] class FreezeThawCycleModel(TimeSeriesModels): """A pure Freeze-thaw cycle model without velocity."""
[docs] def __init__( self, ftc: FreezeThawCycle, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize FreezeThawCycleModel. Parameters ---------- ftc : FreezeThawCycle Freeze-thaw cycle instance. The dates in ftc should cover the dates of SAR acquisitions. .. warning:: The first date in ftc should be earlier than the thawing onset of the first year in the time series model. dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) df_br = pd.DataFrame( np.full((len(self.dates), 2), np.nan, dtype=np.float32), index=self.dates, ) bias = np.zeros((1, 2), dtype=np.float32) years = self.dates.year.unique() for year in years: start = ftc.get_year_start(year) end = ftc.get_year_end(year) if not start: continue m = np.logical_and(self.dates >= start, self.dates <= end) img_dates = self.dates[m] DDT = ftc.DDT[start:end].copy() # noqa: N806 DDF = ftc.DDF[start:end].copy() # noqa: N806 if year == years[0]: # add missing dates before 07-01 for DDF in the first year if DDF.index[0] > start: dt_missing = pd.date_range(start, DDF.index[0], freq="1D")[:-1] DDF_missing = pd.Series(np.nan, index=dt_missing) # noqa: N806 DDF = pd.concat([DDF_missing, DDF]) # noqa: N806 # set coefficients to zero before the thawing onset for the first year if start > self.dates[0]: df_br.loc[self.dates[0] : start, :] = 0 # set the DDF to 0 during the thawing onset if pd.isna(DDT[0]): DDT[0] = 0 DDF[: f"{year}-07-01"] = 0 DDT = DDT.ffill() # noqa: N806 DDF = DDF.ffill() # noqa: N806 t3 = ftc.t3s[year] if not pd.isna(t3): if t3 in DDT.index: DDT[t3:] = DDT[t3] if t3 in DDF.index: DDF[t3:] = DDF[t3] DDT_A1 = np.sqrt(DDT[img_dates].values) # noqa: N806 DDF_A4 = np.sqrt(DDF[img_dates].values) # noqa: N806 try: df_br[start:end] = np.array([DDT_A1, DDF_A4]).T + bias except Exception as e: msg = f"{bias}\n\n{df_br[start:end]}" raise ValueError(msg) from e # df_br[start:end] = np.array( # [DDT_A1, DDF_A4]).T + bias DDT_A1_end = np.sqrt(DDT[-1]) # noqa: N806 DDF_A4_end = np.sqrt(DDF[-1]) # noqa: N806 bias = bias + np.asarray([[DDT_A1_end, DDF_A4_end]]) df_br.loc[:, "constant"] = 1 self._G_br = df_br.values self._param_names = ["E_t", "E_f", "constant"]
[docs] class FreezeThawCycleModelWithVelocity(TimeSeriesModels): """A Freeze-thaw cycle model with velocity."""
[docs] def __init__( self, ftc: FreezeThawCycle, dates: pd.DatetimeIndex | Sequence[datetime], unit: Literal["year", "day"] = "day", ) -> None: """Initialize FreezeThawCycleModelWithVelocity. Parameters ---------- ftc : FreezeThawCycle Freeze-thaw cycle instance. The dates in ftc should cover the dates of SAR acquisitions. dates : pd.DatetimeIndex | Sequence[datetime] Dates of SAR acquisitions. This can be easily obtained by accessing :attr:`Pairs.dates <faninsar.Pairs.dates>`. unit : Literal["year", "day"], optional Unit of day spans in time series model, by default "day". """ super().__init__(dates, unit=unit) df_br = pd.DataFrame( np.full((len(self.dates), 3), np.nan, dtype=np.float32), index=self.dates, ) bias = 0 years = self.dates.year.unique() for year in years: start = ftc.get_year_start(year) end = ftc.get_year_end(year) if not start: continue m = np.logical_and(self.dates >= start, self.dates <= end) img_dates = self.dates[m] DDT = ftc.DDT[start:end].copy() # noqa: N806 DDF = ftc.DDF[start:end].copy() # noqa: N806 if year == years[0]: # add missing dates before 07-01 for DDF in the first year if DDF.index[0] > start: dt_missing = pd.date_range(start, DDF.index[0], freq="1D")[:-1] DDF_missing = pd.Series(np.nan, index=dt_missing) # noqa: N806 DDF = pd.concat([DDF_missing, DDF]) # noqa: N806 # set coefficients to zero before the thawing onset for the first year if start > self.dates[0]: df_br.loc[self.dates[0] : start, :] = 0 # set the DDF to 0 during the thawing onset if pd.isna(DDT[0]): DDT[0] = 0 DDF[: f"{year}-07-01"] = 0 DDT = DDT.ffill() # noqa: N806 DDF = DDF.ffill() # noqa: N806 t3 = ftc.t3s[year] if not pd.isna(t3): if t3 in DDT.index: DDT[t3:] = DDT[t3] if t3 in DDF.index: DDF[t3:] = DDF[t3] DDT_A1 = np.sqrt(DDT[img_dates].values) # noqa: N806 DDF_A4 = np.sqrt(DDF[img_dates].values) # noqa: N806 df_br[start:end] = np.array([DDT_A1, DDF_A4, np.full_like(DDF_A4, bias)]).T bias = bias + 1 df_br.loc[:, "constant"] = 1 self._G_br = df_br.values self._param_names = ["E_t", "E_f", "V", "constant"]