Skip to content
Snippets Groups Projects
Flag.py 5.75 KiB
Newer Older
Hobbs, Alexandra (Contractor)'s avatar
Hobbs, Alexandra (Contractor) committed
from typing import Dict, Union, List, Optional
from datetime import timedelta

from obspy import UTCDateTime
from pydantic import BaseModel, Field, field_validator
from enum import Enum

from ...pydantic_utcdatetime import CustomUTCDateTimeType

class FlagCategory(str, Enum):
    ARTIFICIAL_DISTURBANCE = "ARTIFICIAL_DISTURBANCE"
    GAP = "GAP"
    EVENT = "EVENT"
class Flag(BaseModel):
    """
        Base class for flagging features in magnetic timeseries data.

        Flag example:
        ```
        automatic_flag = Metadata(
            created_by='ex_algorithm',
            start_time=UTCDateTime('2023-01-01T03:05:10'),
            end_time=UTCDateTime('2023-01-01T03:10:11'),
            network='NT',
            station='BOU',
            channel='BEH',
            category=MetadataCategory.FLAG,
            comment="spike detected",
            priority=1,
            metadata= ArtificialDisturbance{
                "description": "Spikes in magnetic field strength",
                "artificial_disturbance_type": ArtificialDisturbanceType.SPIKE,
                "deviation": None,
                "spikes": ['2023-01-01T03:05:10','2023-01-01T03:07:20','2023-01-01T03:10:11']
        }
    )
        ```
    """

    description: str = Field(..., description="Description of the flag")


class ArtificialDisturbanceType(str, Enum):
    SPIKES = "SPIKES"
    OFFSET = "OFFSET"
    ARTIFICIAL_DISTURBANCES = "ARTIFICIAL_DISTURBANCES"


class ArtificialDisturbance(Flag):
    """
    This class is used to flag artificial disturbances.

    Artificial disturbances consist of the following types:

    SPIKES = Single data points that are outliers in the timeseries.
    OFFSET = A relatively constant shift or deviation in the baseline magnetic field.
    ARTIFICIAL_DISTURBANCES = A catch-all for a continuous period of unwanted variations, may include multiple spikes, offsets and/or gaps.

    Attributes
    ----------
    artificial_disturbance_type:ArtificialDisturbanceType
        The type of artificial disturbance(s).
    deviation: float
       Deviation of an offset in nt.
    spikes: List[CustomUTCDateTimeType]
        Array of timestamps as UTCDateTime. Can be a single spike or many spikes.

    """

    artificial_disturbance_type: ArtificialDisturbanceType
Hobbs, Alexandra (Contractor)'s avatar
Hobbs, Alexandra (Contractor) committed
    deviation: Optional[float] = None
    spikes: Optional[List[CustomUTCDateTimeType]] = None
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.flag_category = FlagCategory.ARTIFICIAL_DISTURBANCE
    @field_validator("spikes")
    def check_spikes_duration(cls, spikes):
        if spikes is None or len(spikes) < 2:
            return spikes

        duration = spikes[-1] - spikes[0]
        if duration > timedelta(days=1).total_seconds():
            raise ValueError(
                f"The duration between the first and last spike timestamp must not exceed 1 day. Duration: {duration} seconds"
            )

        return spikes

    @classmethod
    def check_spikes_match_times(cls, spikes, values):
        metadata_starttime = values.get("starttime")
        metadata_endtime = values.get("endtime")

        if spikes[0] != metadata_starttime:
            raise ValueError(
                f"The first spike timestamp {spikes[0]} does not match the starttime {metadata_starttime}."
            )

        if spikes[-1] != metadata_endtime:
            raise ValueError(
                f"The last spike timestamp {spikes[-1]} does not match the endtime {metadata_endtime}."
            )


class Gap(Flag):
    """
    This class is used to flag gaps in data.

    A gap is a period where data is missing or not recorded.

    Attributes
    ----------
    cause: str
        Cause of gap, e.g., network outage.
    handling: str
        How the gap is being handled, e.g., backfilled.
    """

    cause: Optional[str] = None
    handling: Optional[str] = None
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.flag_category = FlagCategory.GAP

class EventType(str, Enum):
    GEOMAGNETIC_STORM = "GEOMAGNETIC_STORM"
    GEOMAGNETIC_SUBSTORM = "GEOMAGNETIC_SUBSTORM"
    EARTHQUAKE = "EARTHQUAKE"
    OTHER = "OTHER"


class Event(Flag):
    """
    This class is used to flag an event of interest such as a geomagnetic storm or earthquake.

    Attributes
    ----------
    event_type : EventType
        The type of event.
    scale : str
        Geomagnetic storm scale or Richter scale magnitude.
    index : int
        Planetary K-index, DST index or some other index.
    url : str
        A url related to the event. Could be NOAA SWPC, USGS Earthquakes page or another site.
    """

    event_type: EventType
    index: Optional[int] = None
    scale: Optional[str] = None
    url: Optional[str] = None
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.flag_category = FlagCategory.EVENT

# More example usage:
timestamps_array = [
    UTCDateTime("2023-11-16T12:00:0"),
    UTCDateTime("2023-11-16T12:01:10"),
    UTCDateTime("2023-11-16T12:02:30"),
]

spikes_data = {
    "starttime": "2023-11-16 12:00:00",
    "endtime": "2023-11-16 12:02:30",
    "description": "Spikes description",
    "disturbance_type": ArtificialDisturbanceType.SPIKES,
    "spikes": timestamps_array,
}

offset_data = {
    "description": "Offset description",
    "disturbance_type": ArtificialDisturbanceType.OFFSET,
    "deviation": 10.0,
}
geomagnetic_storm_data = {
    "description": "Geomagnetic storm",
    "event_type": EventType.GEOMAGNETIC_STORM,
    "scale": "G3",
    "index": 7,
    "url": "https://www.swpc.noaa.gov/products/planetary-k-index",
}