Exemplo n.º 1
0
class TimeSeriesMetaData:
    """
    Used to store metadata for `~sunpy.timeseries.TimeSeries` that enables
    multiple `sunpy.timeseries.TimeSeries` metadata to be concatenated in an
    organized fashion.

    Possible signatures::

        TimeSeriesMetaData(meta=dict, timerange=TimeRange, colnames=list)
        TimeSeriesMetaData(meta=tuple)
        TimeSeriesMetaData(meta=list(tuples))
        TimeSeriesMetaData(timerange=TimeRange)
        TimeSeriesMetaData(timerange=TimeRange, colnames=list)

    Parameters
    ----------
    meta : `dict`, `MetaDict`, `tuple`, `list`
        The metadata giving details about the time series data/instrument.
        Defaults to `None`.
    timerange : `~sunpy.time.TimeRange`
        A `~sunpy.time.TimeRange` representing the timespan of the data. Defaults to `None`.
    colnames : `list`
        A mapping from column names in ``data`` to the physical units of that column.
        Defaults to `None`.

    Attributes
    ----------
    metadata : `list` of `tuple`
        The list of 3-tuples which each represent a source files metadata.
        The tuples consist of: ``(TimeRange, [colnames], MetaDict(metadata))``.

    Examples
    --------
    >>> from sunpy.timeseries import TimeSeriesMetaData
    >>> from sunpy.time import TimeRange, parse_time
    >>> from sunpy.util import MetaDict
    >>> tr = TimeRange('2012-06-01 00:00','2012-06-02 00:00')
    >>> md = TimeSeriesMetaData(timerange=tr, colnames=['GOES'],
    ...                         meta=MetaDict([('goes_key','goes_val')]))
    >>> tr2 = TimeRange('2012-06-01 12:00','2012-06-02 12:00')
    >>> md.append(tr2, ['EVE'], MetaDict([('eve_key','eve_val')]))
    >>> md.find(parse_time('2012-06-01T21:08:12'))
    |-------------------------------------------------------------------------------------------------|
    |TimeRange                  | Columns         | Meta                                              |
    |-------------------------------------------------------------------------------------------------|
    |2012-06-01T00:00:00.000    | GOES            | goes_key: goes_val                                |
    |            to             |                 |                                                   |
    |2012-06-02T00:00:00.000    |                 |                                                   |
    |-------------------------------------------------------------------------------------------------|
    |2012-06-01T12:00:00.000    | EVE             | eve_key: eve_val                                  |
    |            to             |                 |                                                   |
    |2012-06-02T12:00:00.000    |                 |                                                   |
    |-------------------------------------------------------------------------------------------------|
    <BLANKLINE>
    >>> md.find(parse_time('2012-06-01T21:08:12')).columns
    ['EVE', 'GOES']
    >>> md.find(parse_time('2012-06-01T21:08:12')).values()
    ['eve_val', 'goes_val']
    >>> md.find(parse_time('2012-06-01T21:08:12')).metas
    [MetaDict([('goes_key', 'goes_val')]), MetaDict([('eve_key', 'eve_val')])]
    >>> md.find(parse_time('2012-06-01T21:08:12'), 'GOES')
    |-------------------------------------------------------------------------------------------------|
    |TimeRange                  | Columns         | Meta                                              |
    |-------------------------------------------------------------------------------------------------|
    |2012-06-01T00:00:00.000    | GOES            | goes_key: goes_val                                |
    |            to             |                 |                                                   |
    |2012-06-02T00:00:00.000    |                 |                                                   |
    |-------------------------------------------------------------------------------------------------|
    """
    def __init__(self, meta=None, timerange=None, colnames=None):
        self.metadata = []
        # Parse in arguments
        if meta is not None:
            if (isinstance(meta, (dict, MetaDict))
                    and isinstance(timerange, TimeRange)
                    and isinstance(colnames, list)):
                # Given a single metadata entry as a dictionary with additional timerange and colnames.
                self.metadata.append((timerange, colnames, meta))
            elif isinstance(meta, tuple):
                # Given a single metadata entry as a tuple.
                if isinstance(meta[0], TimeRange) and isinstance(
                        meta[1], list) and isinstance(meta[2],
                                                      (dict, MetaDict)):
                    self.metadata.append(meta)
                else:
                    raise ValueError("Invalid parameters passed in the meta")
            elif isinstance(meta, list):
                # Given a complex metadata list (of tuples)
                for meta_tuple in meta:
                    if isinstance(meta_tuple[0], TimeRange) and isinstance(
                            meta_tuple[1], list) and isinstance(
                                meta_tuple[2], (dict, MetaDict)):
                        self.metadata.append(meta_tuple)
                    else:
                        raise ValueError(
                            "Invalid parameters passed in the meta")
        else:
            # In the event no metadata dictionary is sent we default to something usable
            if isinstance(timerange, TimeRange):
                if isinstance(colnames, list):
                    self.metadata.append((timerange, colnames, MetaDict()))
                else:
                    self.metadata.append((timerange, [], MetaDict()))
                    warnings.warn(
                        "No time range given for metadata. "
                        "This will mean the metadata can't be linked "
                        "to columns in data.", SunpyUserWarning)
            else:
                raise ValueError("You cannot create a TimeSeriesMetaData "
                                 "object without specifying a TimeRange")

    def __eq__(self, other):
        """
        Checks to see if two `~sunpy.timeseries.TimeSeriesMetaData` are the
        same, they have the same entries in the same order.

        Parameters
        ----------
        other : `~sunpy.timeseries.metadata.TimeSeriesMetaData`
            The second `~sunpy.timeseries.metadata.TimeSeriesMetaData` to compare with.

        Returns
        -------
        `bool`
        """
        match = True
        if len(self.metadata) == len(other.metadata):
            for i in range(0, len(self.metadata)):
                if self.metadata[i] != other.metadata[i]:
                    match = False
        else:
            match = False
        return match

    def __ne__(self, other):
        """
        Checks to see if two `~sunpy.timeseries.TimeSeriesMetaData` are the not
        the same, they don't have same entries in the same order.

        Parameters
        ----------
        other : `~sunpy.timeseries.TimeSeriesMetaData`
            The second `~sunpy.timeseries.TimeSeriesMetaData` to compare with.

        Returns
        -------
        `bool`
        """
        return not self == other

    def append(self, timerange, columns, metadata, **kwargs):
        """
        Add the given metadata into the current metadata.

        Will add the new entry so the list is in chronological order.

        Parameters
        ----------
        timerange : `~sunpy.time.TimeRange`
            The timerange for which a given metadict is relevant. This will
            generally initially be the full range of the original file, but if
            the TimeSeries gets truncated this may change appropriately.
        columns : `list`
            A list of the column name strings that the metadata is relevant for.
        metadata : `~sunpy.util.metadata.MetaDict` or `collections.OrderedDict` or `dict`
            The object holding the metadata.
        """
        # Parameters
        metadata = MetaDict(metadata)

        # Check the types are correct.
        pos = 0
        if isinstance(timerange, TimeRange):
            for i, meta in enumerate(self.metadata):
                if timerange.start > meta[0].start:
                    pos = i + 1
        else:
            raise ValueError(
                'Incorrect datatime or data for append to TimeSeriesMetaData.')

        # Prepare tuple to append.
        new_metadata = (timerange, columns, metadata)

        # Check this isn't a duplicate entry (same TR and comnames)
        duplicate = False
        if pos < len(self.metadata):
            old_metadata = self.metadata[pos]
            if (new_metadata[0] == old_metadata[0]) and (new_metadata[1]
                                                         == old_metadata[1]):
                duplicate = True

        # Insert into the given position
        if not duplicate:
            self.metadata.insert(pos, new_metadata)

    @add_common_docstring(**_variables_for_parse_time_docstring())
    def find_indices(self, time=None, colname=None):
        """
        Find the indices for all the metadata entries matching the given
        filters.

        Will return all metadata entry indices if no filters are given.

        Parameters
        ----------
        time : {parse_time_types}, optional
            A `~sunpy.time.parse_time` parsable string that you need metadata for.
            Defaults to `None`.
        colname : `str`, optional
            A string that can be used to narrow results to specific columns.
            Defaults to `None`.

        Returns
        -------
        `list`
            A list of integers that contain all matching metadata.
        """
        # Parameters
        dt = time
        if not dt:
            dt = False
        elif isinstance(dt, str):
            dt = parse_time(dt)

        # Find all results with suitable timerange.
        results_indices = []
        for i, meta in enumerate(self.metadata):
            if (not dt) or dt in meta[0]:
                results_indices.append(i)

        # Filter out only those with the correct column.
        results = []
        for i in results_indices:
            if (colname in self.metadata[i][1]) or (not colname):
                results.append(i)

        return results

    @add_common_docstring(**_variables_for_parse_time_docstring())
    def find(self, time=None, colname=None):
        """
        Find all metadata matching the given filters.

        Will return all metadata entries if no filters are given.

        Parameters
        ----------
        time : {parse_time_types}, optional
            A `~sunpy.time.parse_time` parsable string that you need metadata for.
            Defaults to `None`.
        colname : `str`, optional
            A string that can be used to narrow results to specific columns.
            Defaults to `None`.

        Returns
        -------
        `~sunpy.timeseries.metadata.TimeSeriesMetaData`
            A `~sunpy.timeseries.metadata.TimeSeriesMetaData` that contain all
            matching metadata entries.
        """
        # Get the indices
        indices = self.find_indices(time=time, colname=colname)

        # Extract the relevant metadata entries
        metadata = []
        for i in indices:
            metadata.append(copy.copy(self.metadata[i]))

        # Return a TimeSeriesMetaData object
        return TimeSeriesMetaData(meta=metadata)

    def get_index(self, index):
        """
        Return the dictionary entry at the given index.

        Parameters
        ----------
        index : `int`
            The integer index of the metadata entry in the list.

        Returns
        -------
        `~sunpy.util.metadata.MetaDict`
            An ordered Dictionary containing the metadata at the given index.
        """
        return self.metadata[index][2]

    @add_common_docstring(**_variables_for_parse_time_docstring())
    def get(self, keys, time=None, colname=None):
        """
        Return a `~sunpy.timeseries.metadata.TimeSeriesMetaData` with all
        entries matching the filters which also contain the given input key.

        Parameters
        ----------
        keys : `str`
            The Key/s to be searched in the dictionary.
        time : {parse_time_types}, optional
            A `~sunpy.time.parse_time` parsable string that you need metadata for.
            Defaults to `None`.
        colname : `str`, optional
            A string that can be used to narrow results to specific columns.

        Returns
        -------
        metadata : `~sunpy.timeseries.metadata.TimeSeriesMetaData`
            A TimeSeriesMetaData that contain all matching metadata entries but
            with only the requested key/value pairs in the MetaDict objects.
        """
        # Make a list of keys if only one is given
        if isinstance(keys, str):
            keys = [keys]

        # Find all metadata entries for the given time/colname filters
        full_metadata = self.find(time=time, colname=colname)
        metadata = []

        # Append to metadata only key:value pairs with requested keys
        for i, entry in enumerate(full_metadata.metadata):
            metadict = MetaDict()
            for curkey, value in entry[2].items():
                for key in keys:
                    if curkey.lower() == key.lower():
                        metadict.update({key: value})
            metadata.append((entry[0], entry[1], metadict))

        # Return a TimeSeriesMetaData object
        return TimeSeriesMetaData(meta=metadata)

    def concatenate(self, others):
        """
        Combine the metadata from a `~sunpy.timeseries.TimeSeriesMetaData` or an
        iterable containing multiple `~sunpy.timeseries.TimeSeriesMetaData`
        with the current `~sunpy.timeseries.TimeSeriesMetaData` and return it as
        a new `~sunpy.timeseries.TimeSeriesMetaData`.

        Parameters
        ----------
        others : `~sunpy.timeseries.TimeSeriesMetaData` or `collections.abc.Iterable`
            The second `~sunpy.timeseries.metadata.TimeSeriesMetaData` object or an iterable
            containing multiple `~sunpy.timeseries.metadata.TimeSeriesMetaData` objects.
        """
        # If an individual TimeSeriesMetaData object is to be concatenated, wrap it in a list
        # Else if it is an iterable, check if all items within it are valid
        # Else, data provided is invalid
        if isinstance(others, self.__class__):
            others = [others]
        elif isinstance(others, Iterable):
            if not all(
                    isinstance(series, self.__class__) for series in others):
                raise TypeError(
                    "Invalid type within iterable. Iterable must only contain "
                    "TimeSeriesMetaData objects.")
        else:
            raise TypeError(
                f"Invalid type provided: {type(others)}. "
                "Please provide a TimeSeriesMetaData object or "
                "an iterable containing TimeSeriesMetaData objects.")

        # Create a copy of the metadata
        meta = TimeSeriesMetaData(copy.copy(self.metadata))

        # Append each metadata entry of each TimeSeriesMetaData object from the iterable
        # to the original TimeSeriesMetaData object.
        for series in others:
            for entry in series.metadata:
                meta.append(entry[0], entry[1], entry[2])

        return meta

    @add_common_docstring(**_variables_for_parse_time_docstring())
    def update(self, dictionary, time=None, colname=None, overwrite=False):
        """
        Updates the `~sunpy.timeseries.TimeSeriesMetaData` for all matching
        metadata entries.

        Parameters
        ----------
        dictionary : `dict`, `collections.OrderedDict`, `~sunpy.util.metadata.MetaDict`
            The second `~sunpy.timeseries.TimeSeriesMetaData` object.
        time : {parse_time_types}, optional
            A `~sunpy.time.parse_time` parsable string that you need metadata for.
            Defaults to `None`.
        colname : `str`, optional
            A string that can be used to narrow results to specific columns.
            Defaults to `None`.
        overwrite : `bool`, optional
            Allows the user to overwrite already present keys.
            Defaults to `False`
        """
        # Find all matching metadata entries
        indices = self.find_indices(time=time, colname=colname)

        # Now update each matching entries
        for i in indices:
            # Seperate keys for new and current pairs
            old_keys = set(dictionary.keys())
            old_keys.intersection_update(set(self.metadata[i][2].keys()))
            new_keys = set(dictionary.keys())
            new_keys.difference_update(old_keys)

            # Old keys only overwritten if allowed
            for key in (self.metadata[i][2].keys()):
                if key in old_keys and overwrite:
                    self.metadata[i][2][key] = dictionary[key]
            for key in dictionary:
                if key in new_keys:
                    self.metadata[i][2][key] = dictionary[key]

    def _truncate(self, timerange):
        """
        Removes metadata entries outside of the new (truncated)
        `sunpy.time.TimeRange`. Also adjusts start and end times of time ranges
        going outside of the truncated time range.

        Parameters
        ----------
        timerange : `sunpy.time.TimeRange`
            The time range to truncate to.
        """
        truncated = []
        for metatuple in self.metadata:
            # Get metadata time range parameters
            start = metatuple[0].start
            end = metatuple[0].end
            out_of_range = False

            # Find truncations
            if start < timerange.start and end > timerange.start:
                # Truncate the start
                start = timerange.start
            elif start > timerange.end:
                # Metadata time range starts after truncated data ends.
                out_of_range = True
            if end > timerange.end and start < timerange.end:
                # Truncate the end
                end = timerange.end
            elif end < timerange.start:
                # Metadata time range finishes before truncated data starts.
                out_of_range = True

            # Add the values if applicable
            if not out_of_range:
                truncated.append((TimeRange(start,
                                            end), metatuple[1], metatuple[2]))

        # Update the original list
        self.metadata = truncated

    @property
    def columns(self):
        """
        Returns a list of all the names of the columns in the metadata.
        """
        all_cols = set()
        for metatuple in self.metadata:
            all_cols.update(metatuple[1])
        all_cols = list(all_cols)
        all_cols.sort()
        return all_cols

    @property
    def metas(self):
        """
        Returns a list of all the metadict objects in the
        `~sunpy.timeseries.TimeSeriesMetaData`.
        """
        all_metas = []
        for metatuple in self.metadata:
            all_metas.append(metatuple[2])
        return all_metas

    @property
    def timeranges(self):
        """
        Returns a list of all the `~sunpy.time.TimeRange` in the
        `~sunpy.timeseries.TimeSeriesMetaData`.
        """
        all_tr = []
        for metatuple in self.metadata:
            all_tr.append(metatuple[0])
        return all_tr

    def values(self):
        """
        Returns a list of all the values from the metadict objects in each
        entry in the `~sunpy.timeseries.TimeSeriesMetaData`.
        """
        all_vals = set()
        for metatuple in self.metadata:
            for key, value in metatuple[2].items():
                all_vals.add(str(value))
        all_vals = list(all_vals)
        all_vals.sort()
        return all_vals

    @property
    def time_range(self):
        """
        Returns the `~sunpy.time.TimeRange` of the entire timeseries metadata.
        """
        start = self.metadata[0][0].start
        end = self.metadata[0][0].end
        for metatuple in self.metadata:
            if end < metatuple[0].end:
                end = metatuple[0].end
        return TimeRange(start, end)

    def _remove_columns(self, colnames):
        """
        Removes the given column(s) from the
        `~sunpy.timeseries.TimeSeriesMetaData`.

        Parameters
        ----------
        colnames : `str`, `list` of `str`
            The name(s) of the columns to be removed.
        """
        # Parameters
        if isinstance(colnames, str):
            colnames = [colnames]

        # Create a new list with all metadata entries without colnames
        reduced = []
        for metatuple in self.metadata:
            # Check each colname
            for colname in colnames:
                if colname in metatuple[1]:
                    # Removed from the list.
                    metatuple[1].remove(colname)
            # Add the column if it still has some columns listed
            if len(metatuple[1]) > 0:
                reduced.append(metatuple)

        # Update the original list
        self.metadata = reduced

    def _rename_column(self, old, new):
        """
        Change the name of a column in the metadata entries.

        Parameters
        ----------
        old : `str`
            The original column name to be changed.
        new : `str`
            The new column name.
        """
        for i in range(0, len(self.metadata)):
            # Update the colnames
            colnames = self.metadata[i][1]
            colnames = [w.replace(old, new) for w in colnames]

            # Replace values
            self.metadata[i] = (self.metadata[i][0], colnames,
                                self.metadata[i][2])

    def _validate_meta(self, meta):
        """
        Validate a metadata argument.
        """
        # Checking for metadata that may overlap.
        indices = range(0, len(self.metadata))
        for i, j in itertools.combinations(indices, 2):
            # Check if the TimeRanges overlap
            if not ((self.metadata[i][0].end <= self.metadata[j][0].start) or
                    (self.metadata[i][0].start >= self.metadata[j][0].end)):
                # Check column headings overlap
                col_overlap = list(
                    set(self.metadata[i][1]) & set(self.metadata[j][1]))
                # If we have an overlap then show a warning
                if col_overlap:
                    warnings.warn(
                        f'Metadata entries {i} and {j} contain interleaved data.',
                        SunpyUserWarning)

        # TODO: Check all entries are in tr.start time order.
        return True

    def to_string(self, depth=10, width=99):
        """
        Print a table-like representation of the
        `~sunpy.timeseries.TimeSeriesMetaData`.

        Parameters
        ----------
        depth : `int`, optional
            The maximum number of lines to show for each entry.
            Metadata dictionaries and column lists will be truncated if this is small.
            Defaults to 10.
        width : `int`, optional
            The number of characters wide to make the entire table. Defaults to 99.
        """
        # Parameters
        colspace = ' | '
        liswidths = (26, 15, width - 2 - 2 * len(colspace) - 26 - 15)
        colheadings = '|' + 'TimeRange'.ljust(100)[:liswidths[0]] + colspace
        colheadings += 'Columns'.ljust(100)[:liswidths[1]] + colspace
        colheadings += 'Meta'.ljust(100)[:liswidths[2]] + '|'
        rowspace = "-" * (liswidths[0] + len(colspace) + liswidths[1] +
                          len(colspace) + liswidths[2])
        rowspace = '|' + rowspace + '|'

        # Headings
        full = rowspace + '\n' + colheadings + '\n' + rowspace + '\n'

        # Add metadata entries
        for entry in self.metadata:
            # Make lists for each of the columns for each metadata entry
            # Padded to the widths given in liswidths
            lis_range = [
                str(entry[0].start), '            to            ',
                str(entry[0].end)
            ]
            # Shorten TimeRange representation if depth of only 2
            if depth == 2:
                lis_range = [str(entry[0].start), str(entry[0].end)]
            liscols = []
            for col in entry[1]:
                liscols.append(col.ljust(100)[:liswidths[1]])
            lismeta = []
            for key in list(entry[2].keys()):
                string = str(key) + ': ' + str(entry[2][key])
                lismeta.append(string.ljust(100)[:liswidths[2]])

            # Add lines of the entry upto the given depth
            for i in range(0, depth):
                # What to do in the event any of the lists have more entries
                # then the current depth
                if len(lis_range) > i or len(entry[1]) > i or len(lismeta) > i:
                    # The start of the line Str is just a vertical bar/pipe
                    line = '|'
                    # Check we have a time range entry to print
                    if len(lis_range) > i:
                        # Simply add that time range entry to the line Str
                        line += lis_range[i].ljust(100)[:liswidths[0]]
                    else:
                        # No entry to add, so just add a blank space
                        line += ''.ljust(100)[:liswidths[0]]
                    # Add a column break vertical bar/pipe
                    line += colspace
                    # Check we have another column name entry to print
                    if len(entry[1]) > i:
                        # Simply add that column name to the line Str
                        line += entry[1][i].ljust(100)[:liswidths[1]]
                    else:
                        # No entry to add, so just add a blank space
                        line += ''.ljust(100)[:liswidths[1]]
                    # Add a column break vertical bar/pipe
                    line += colspace
                    # Check we have another meta key/value pair to print
                    if len(lismeta) > i:
                        # Simply add that key/value pair to the line Str
                        line += lismeta[i].ljust(100)[:liswidths[2]]
                    else:
                        # No entry to add, so just add a blank space
                        line += ''.ljust(100)[:liswidths[2]]
                    # Finish the line Str with vertical bar/pipe and \n
                    full += line + '|\n'
            # Reached the depth limit, add line to show if the columns are truncated
            if len(lis_range) >= depth or len(
                    entry[1]) >= depth or len(lismeta) >= depth:
                # The start of the line Str is just a vertical bar/pipe
                line = '|'
                # Check we have more time range entries to print
                if len(lis_range) > depth:
                    # We have more time range entries, use ellipsis to show this
                    line += '...'.ljust(100)[:liswidths[0]]
                else:
                    # No entry to add, so just add a blank space
                    line += ''.ljust(100)[:liswidths[0]]
                # Add a column break vertical bar/pipe
                line += colspace
                # Check we have more than one column name entry to print
                if len(entry[1]) > depth:
                    # We have more column name entries, use ellipsis
                    line += '...'.ljust(100)[:liswidths[1]]
                else:
                    # No more column name entries, so just add a blank space
                    line += ''.ljust(100)[:liswidths[1]]
                # Add a column break vertical bar/pipe
                line += colspace
                # Check we have more meta key/value pairs to print
                if len(lismeta) > depth:
                    # We have more key/value pairs, use ellipsis to show this
                    line += '...'.ljust(100)[:liswidths[2]]
                else:
                    # No morekey/value pairs, add a blank space
                    line += ''.ljust(100)[:liswidths[2]]
                # Finish the line Str with vertical bar/pipe and \n
                full += line + '|\n'
            # Add a line to close the table
            full += rowspace + '\n'
        return full

    def __repr__(self):
        return self.to_string()

    def __str__(self):
        return self.to_string()
Exemplo n.º 2
0
        if i == 7:
            out.append('Auroral zone')
        if i == 8:
            out.append('Moon in LYRA')
        if i == 9:
            out.append('Moon in SWAP')
        if i == 10:
            out.append('Venus in LYRA')
        if i == 11:
            out.append('Venus in SWAP')

    return out


# TODO: Change this function to only need the amount of channels to be passed in.
@add_common_docstring(**_variables_for_parse_time_docstring())
def _prep_columns(time, channels=None, filecolumns=None):
    """
    Checks and prepares data to be written out to a file.

    Parameters
    ----------
    time: {parse_time_types}
        A list or array of times.
    channels: `list`, optional
        A list of the channels found within the data.
    filecolumns: `list`, optional
        A list of strings that are the column names for the file.
        Defaults to `None`, which means that a filenames list is generated
        equal to ``["time", "channel0", "channel1",..., "channelN"]``
        where ``N`` is the number of arrays in the ``channels`` list (if provided).
Exemplo n.º 3
0
class SUVIClient(GenericClient):
    """
    Provides access to data from the GOES Solar Ultraviolet Imager (SUVI).

    SUVI data are provided by NOAA at the following url
    https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/
    The SUVI instrument was first included on GOES-16. It produces level-1b as
    well as level-2 data products. Level-2 data products are a weighted average
    of level-1b product files and therefore provide higher imaging dynamic
    range than individual images. The exposure time of level 1b images range
    from 1 s to 0.005 s. SUVI supports the following wavelengths;
    94, 131, 171, 195, 284, 304 angstrom. If no wavelength is specified, images
    from all wavelengths are returned.

    Note
    ----
    GOES-16 began providing regular level-1b data on 2018-06-01.  At the time
    of writing, SUVI on GOES-17 is operational but currently does not provide
    Level-2 data.
    """
    @add_common_docstring(**_variables_for_parse_time_docstring())
    def _get_goes_sat_num(self, date):
        """
        Determines the best satellite number for a given date.

        Parameters
        ----------
        date : {parse_time_types}
            The date to determine which satellite is active.

        Note
        ----
        At the time this function was written.
        GOES-17 is operational but currently does not provide Level 2 data therefore it is never returned.
        The GOES-16 start date is based on the availability of regular level 1b data.
        """

        # GOES-17 is operational but currently does not provide Level 2 data
        # GOES-16 start date is based on the availability of regular level 1b data
        suvi_operational = {
            16: TimeRange("2018-06-01", parse_time("now")),
        }

        results = []
        for sat_num in suvi_operational:
            if date in suvi_operational[sat_num]:
                # if true then the satellite with sat_num is available
                results.append(sat_num)

        if results:
            # Return the newest satellite
            return max(results)
        else:
            # if no satellites were found then raise an exception
            raise ValueError(
                f"No operational SUVI instrument on {date.strftime(TIME_FORMAT)}"
            )

    def _get_time_for_url(self, urls):
        these_timeranges = []

        for this_url in urls:
            if this_url.count('/l2/') > 0:  # this is a level 2 data file
                start_time = parse_time(
                    os.path.basename(this_url).split('_s')[2].split('Z')[0])
                end_time = parse_time(
                    os.path.basename(this_url).split('_e')[1].split('Z')[0])
                these_timeranges.append(TimeRange(start_time, end_time))
            if this_url.count('/l1b/') > 0:  # this is a level 1b data file
                start_time = datetime.strptime(
                    os.path.basename(this_url).split('_s')[1].split('_e')[0]
                    [:-1], '%Y%j%H%M%S')
                end_time = datetime.strptime(
                    os.path.basename(this_url).split('_e')[1].split('_c')[0]
                    [:-1], '%Y%j%H%M%S')
                these_timeranges.append(TimeRange(start_time, end_time))
        return these_timeranges

    def _get_url_for_timerange(self, timerange, **kwargs):
        """
        Returns urls to the SUVI data for the specified time range.

        Parameters
        ----------
        timerange: `sunpy.time.TimeRange`
            Time range for which data is to be downloaded.
        level : `str`, optional
            The level of the data. Possible values are 1b and 2 (default).
        wavelength : `astropy.units.Quantity` or `tuple`, optional
            Wavelength band. If not given, all wavelengths are returned.
        satellitenumber : `int`, optional
            GOES satellite number. Must be >= 16. Default is 16.
        """
        base_url = "https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes{goes_number}/"
        supported_waves = [94, 131, 171, 195, 284, 304]
        supported_levels = ("2", "1b")

        # these are optional requirements so if not provided assume defaults
        # if wavelength is not provided assuming all of them
        if "wavelength" in kwargs.keys():
            wavelength_input = kwargs.get("wavelength")
            if isinstance(wavelength_input, u.Quantity):  # not a range
                if int(wavelength_input.to_value(
                        'Angstrom')) not in supported_waves:
                    raise ValueError(
                        f"Wavelength {kwargs.get('wavelength')} not supported."
                    )
                else:
                    wavelength = [kwargs.get("wavelength")]
            else:  #  _Range was provided
                compress_index = [
                    wavelength_input.wavemin <= this_wave <=
                    wavelength_input.wavemax
                    for this_wave in (supported_waves * u.Angstrom)
                ]
                if not any(compress_index):
                    raise ValueError(
                        f"Wavelength {wavelength_input} not supported.")
                else:
                    wavelength = list(compress(supported_waves,
                                               compress_index)) * u.Angstrom
        else:  # no wavelength provided return all of them
            wavelength = supported_waves * u.Angstrom
        # check that the input wavelength can be converted to angstrom
        waves = [
            int(this_wave.to_value('angstrom', equivalencies=u.spectral()))
            for this_wave in wavelength
        ]
        # use the given satellite number or choose the best one
        satellitenumber = int(
            kwargs.get("satellitenumber",
                       self._get_goes_sat_num(timerange.start)))
        if satellitenumber < 16:
            raise ValueError(
                f"Satellite number {satellitenumber} not supported.")
        # default to the highest level of data
        level = str(kwargs.get(
            "level", "2"))  # make string in case the input is a number

        if level not in supported_levels:
            raise ValueError(f"Level {level} is not supported.")

        results = []
        for this_wave in waves:
            if level == "2":
                search_pattern = base_url + 'l{level}/data/suvi-l{level}-ci{wave:03}/%Y/%m/%d/dr_suvi-l{level}-ci{wave:03}_g{goes_number}_s%Y%m%dT%H%M%SZ_.*\.fits'
            elif level == "1b":
                if this_wave in [131, 171, 195, 284]:
                    search_pattern = base_url + 'l{level}/suvi-l{level}-fe{wave:03}/%Y/%m/%d/OR_SUVI-L{level}-Fe{wave:03}_G{goes_number}_s%Y%j%H%M%S.*\.fits.gz'
                elif this_wave == 304:
                    search_pattern = base_url + 'l{level}/suvi-l{level}-he{wave:03}/%Y/%m/%d/OR_SUVI-L{level}-He{wave_minus1:03}_G{goes_number}_s%Y%j%H%M%S.*\.fits.gz'
                elif this_wave == 94:
                    search_pattern = base_url + 'l{level}/suvi-l{level}-fe{wave:03}/%Y/%m/%d/OR_SUVI-L{level}-Fe{wave_minus1:03}_G{goes_number}_s%Y%j%H%M%S.*\.fits.gz'

            if search_pattern.count('wave_minus1'):
                scraper = Scraper(search_pattern,
                                  level=level,
                                  wave=this_wave,
                                  goes_number=satellitenumber,
                                  wave_minus1=this_wave - 1)
            else:
                scraper = Scraper(search_pattern,
                                  level=level,
                                  wave=this_wave,
                                  goes_number=satellitenumber)
            results.extend(scraper.filelist(timerange))
        return results

    def _makeimap(self):
        """
        Helper Function used to hold information about source.
        """
        self.map_['source'] = 'GOES'
        self.map_['provider'] = 'NOAA'
        self.map_['instrument'] = 'SUVI'
        self.map_['physobs'] = 'flux'

    @classmethod
    def _can_handle_query(cls, *query):
        """
        Answers whether client can service the query.

        Parameters
        ----------
        query : `tuple`
            All specified query objects.

        Returns
        -------
        `bool`
            answer as to whether client can service the query.
        """

        required = {a.Time, a.Instrument}
        optional = {a.Wavelength, a.Level, a.goes.SatelliteNumber}
        all_attrs = {type(x) for x in query}

        ops = all_attrs - required
        # check to ensure that all optional requirements are in approved list
        if ops and not all(elem in optional for elem in ops):
            return False

        # if we get this far we have either Instrument and Time
        # or Instrument, Time and Wavelength
        check_var_count = 0
        for x in query:
            if isinstance(x, a.Instrument) and x.value.lower() == 'suvi':
                check_var_count += 1

        if check_var_count == 1:
            return True
        else:
            return False
Exemplo n.º 4
0
class TimeRange:
    """
    A class to create and handle time ranges.

    .. note::

       Regardless of how a `sunpy.time.TimeRange` is constructed it will always
       provide a positive time range where the start time is before the end time.

       `~sunpy.time.TimeRange.__contains__` has been implemented which means you can
       check if a time is within the time range you have created.
       Please see the example section below.

    Parameters
    ----------
    a : {parse_time_types}
        A time (the start time) specified as a parse_time-compatible
        time string, number, or a datetime object.
    b : {parse_time_types}
        Another time (the end time) specified as a
        parse_time-compatible time string, number, or a datetime object.
        May also be the size of the time range specified as a timedelta object,
        or a `~astropy.units.Quantity`.

    Examples
    --------
    >>> from sunpy.time import TimeRange
    >>> time_range = TimeRange('2010/03/04 00:10', '2010/03/04 00:20')
    >>> time_range = TimeRange(('2010/03/04 00:10', '2010/03/04 00:20'))
    >>> import astropy.units as u
    >>> time_range = TimeRange('2010/03/04 00:10', 400 * u.s)
    >>> TimeRange('2010/03/04 00:10', 400 * u.day)
       <sunpy.time.timerange.TimeRange object at ...>
        Start: 2010-03-04 00:10:00
        End:   2011-04-08 00:10:00
        Center:2010-09-20 00:10:00
        Duration:400.0 days or
               9600.0 hours or
               576000.0 minutes or
               34560000.0 seconds
    <BLANKLINE>
    >>> time1 = '2014/5/5 12:11'
    >>> time2 = '2012/5/5 12:11'
    >>> time_range = TimeRange('2014/05/04 13:54', '2018/02/03 12:12')
    >>> time1 in time_range
    True
    >>> time2 in time_range
    False
    """
    def __init__(self, a, b=None, format=None):
        # If a is a TimeRange object, copy attributes to new instance.
        self._t1 = None
        self._t2 = None

        if isinstance(a, TimeRange):
            self.__dict__ = a.__dict__.copy()
            return

        # Normalize different input types
        if b is None:
            x = parse_time(a[0], format=format)
            if len(a) != 2:
                raise ValueError('If b is None a must have two elements')
            else:
                y = a[1]
        else:
            x = parse_time(a, format=format)
            y = b

        if isinstance(y, u.Quantity):
            y = TimeDelta(y)

        if isinstance(y, timedelta):
            y = TimeDelta(y, format='datetime')

        # Timedelta
        if isinstance(y, TimeDelta):
            if y.jd >= 0:
                self._t1 = x
                self._t2 = x + y
            else:
                self._t1 = x + y
                self._t2 = x
            return

        # Otherwise, assume that the second argument is parse_time-compatible
        y = parse_time(y, format=format)

        if isinstance(y, Time):
            if x < y:
                self._t1 = x
                self._t2 = y
            else:
                self._t1 = y
                self._t2 = x

    @property
    def start(self):
        """
        Get the start time.

        Returns
        -------
        `astropy.time.Time`
            The start time.
        """
        return self._t1

    @property
    def end(self):
        """
        Get the end time.

        Returns
        -------
        `astropy.time.Time`
            The end time.
        """
        return self._t2

    @property
    def dt(self):
        """
        Get the length of the time range. Always a positive value.

        Returns
        -------
        `astropy.time.TimeDelta`
            The difference between the start and the end time.
        """
        return self._t2 - self._t1

    @property
    def center(self):
        """
        Gets the center of the time range.

        Returns
        -------
        `astropy.time.Time`
           The center time.
        """
        return self.start + self.dt / 2

    @property
    def hours(self):
        """
        Get the number of hours elapsed.

        Returns
        -------
        `astropy.units.Quantity`
           The amount of hours between the start and end time.
        """
        return self.dt.to('hour')

    @property
    def days(self):
        """
        Gets the number of days elapsed.

        Returns
        -------
        `astropy.units.Quantity`
            The amount of days between the start and end time.
        """
        return self.dt.to('d')

    @property
    def seconds(self):
        """
        Gets the number of seconds elapsed.

        Returns
        -------
        `astropy.units.Quantity`
           The amount of seconds between the start and end time.
        """
        return self.dt.to('s')

    @property
    def minutes(self):
        """
        Gets the number of minutes elapsed.

        Returns
        -------
        `astropy.units.Quantity`
           The amount of minutes between the start and end time.
        """
        return self.dt.to('min')

    def __eq__(self, other):
        """
        Check that two `sunpy.time.TimeRange` have the same start and end
        datetime.

        Parameters
        ----------
        other : `~sunpy.time.timerange.TimeRange`
            The second `sunpy.time.TimeRange` to compare to.

        Returns
        -------
        `bool`
            `True` if equal, `False` otherwise.
        """
        if isinstance(other, TimeRange):
            return is_time_equal(self.start, other.start) and is_time_equal(
                self.end, other.end)

        return NotImplemented

    def __ne__(self, other):
        """
        Check two `sunpy.time.TimeRange` have different start or end datetimes.

        Parameters
        ----------
        other : `~sunpy.time.timerange.TimeRange`
            The second `sunpy.time.TimeRange` to compare to.

        Returns
        -------
        `bool`
            `True` if non-equal, `False` otherwise.
        """
        if isinstance(other, TimeRange):
            return not (is_time_equal(self.start, other.start)
                        and is_time_equal(self.end, other.end))

        return NotImplemented

    def __repr__(self):
        """
        Returns a human-readable representation of `sunpy.time.TimeRange`.
        """

        t1 = self.start.strftime(TIME_FORMAT)
        t2 = self.end.strftime(TIME_FORMAT)
        center = self.center.strftime(TIME_FORMAT)
        fully_qualified_name = f'{self.__class__.__module__}.{self.__class__.__name__}'

        return ('   <{} object at {}>'.format(
            fully_qualified_name, hex(id(self))) + '\n    Start:'.ljust(12) +
                t1 + '\n    End:'.ljust(12) + t2 + '\n    Center:'.ljust(12) +
                center + '\n    Duration:'.ljust(12) + str(self.days.value) +
                ' days or' + '\n    '.ljust(12) + str(self.hours.value) +
                ' hours or' + '\n    '.ljust(12) + str(self.minutes.value) +
                ' minutes or' + '\n    '.ljust(12) + str(self.seconds.value) +
                ' seconds' + '\n')

    def split(self, n=2):
        """
        Splits the time range into multiple equally sized parts.

        Parameters
        ----------
        n : `int`, optional
            The number of times to split the time range (must >= 1).
            Defaults to 2.

        Returns
        -------
        `list`
            A list of equally sized `sunpy.time.TimeRange` between the start and end times.
        """
        if n <= 0:
            raise ValueError('n must be greater than or equal to 1')
        subsections = []
        previous_time = self.start
        next_time = None
        for _ in range(n):
            next_time = previous_time + self.dt / n
            next_range = TimeRange(previous_time, next_time)
            subsections.append(next_range)
            previous_time = next_time
        return subsections

    def window(self, cadence, window):
        """
        Split the time range up into a series of `~sunpy.time.TimeRange` that
        are ``window`` long, with a cadence of ``cadence``.

        Parameters
        ----------
        cadence : `astropy.units.Quantity`, `astropy.time.TimeDelta`
            Cadence.
        window : `astropy.units.quantity`, `astropy.time.TimeDelta`
            The length of window.

        Returns
        -------
        `list`
            A list of `~sunpy.time.TimeRange`, that are ``window`` long
            and separated by ``cadence``.

        Examples
        --------
        >>> import astropy.units as u
        >>> from sunpy.time import TimeRange
        >>> time_range = TimeRange('2010/03/04 00:10', '2010/03/04 01:20')
        >>> time_range.window(60*60*u.s, window=12*u.s)   # doctest:  +SKIP
        [   <sunpy.time.timerange.TimeRange object at 0x7f0214bfc208>
            Start: 2010-03-04 00:10:00
            End:   2010-03-04 00:10:12
            Center:2010-03-04 00:10:06
            Duration:0.0001388888888888889 days or
                    0.003333333333333333 hours or
                    0.2 minutes or
                    12.0 seconds,
            <sunpy.time.timerange.TimeRange object at 0x7f01fe43ac50>
            Start: 2010-03-04 01:10:00
            End:   2010-03-04 01:10:12
            Center:2010-03-04 01:10:06
            Duration:0.0001388888888888889 days or
                    0.003333333333333333 hours or
                    0.2 minutes or
                    12.0 seconds,
            <sunpy.time.timerange.TimeRange object at 0x7f01fb90b898>
            Start: 2010-03-04 02:10:00
            End:   2010-03-04 02:10:12
            Center:2010-03-04 02:10:06
            Duration:0.0001388888888888889 days or
                    0.003333333333333333 hours or
                    0.2 minutes or
                    12.0 seconds]
        """
        # TODO: After astropy 3.1 remove this check
        if isinstance(window, timedelta):
            window = TimeDelta(window, format="datetime")
        if isinstance(cadence, timedelta):
            cadence = TimeDelta(cadence, format="datetime")

        if not isinstance(window, TimeDelta):
            window = TimeDelta(window)
        if not isinstance(cadence, TimeDelta):
            cadence = TimeDelta(cadence)

        n = 1
        times = [TimeRange(self.start, self.start + window)]
        while times[-1].end < self.end:
            times.append(
                TimeRange(self.start + cadence * n,
                          self.start + cadence * n + window))
            n += 1
        return times

    def next(self):
        """
        Shift the time range forward by the amount of time elapsed.
        """
        dt = self.dt
        self._t1 = self._t1 + dt
        self._t2 = self._t2 + dt

        return self

    def previous(self):
        """
        Shift the time range backward by the amount of time elapsed.
        """
        dt = self.dt
        self._t1 = self._t1 - dt
        self._t2 = self._t2 - dt

        return self

    def extend(self, dt_start, dt_end):
        """
        Extend the time range forwards and backwards.

        Parameters
        ----------
        dt_start : `astropy.time.TimeDelta`
            The amount to shift the start time.
        dt_end : `astropy.time.TimeDelta`
            The amount to shift the end time.
        """
        # TODO: Support datetime.timedelta
        self._t1 = self._t1 + dt_start
        self._t2 = self._t2 + dt_end

    def get_dates(self):
        """
        Return all partial days contained within the time range.
        """
        delta = self.end.to_datetime().date() - self.start.to_datetime().date()
        dates = []
        dates = [
            parse_time(self.start.strftime('%Y-%m-%d')) + TimeDelta(i * u.day)
            for i in range(delta.days + 1)
        ]
        return dates

    @add_common_docstring(**_variables_for_parse_time_docstring())
    def __contains__(self, time):
        """
        Checks whether the given time lies within this range. Both limits are
        inclusive (i.e., ``__contains__(t1)`` and ``__contains__(t2)`` always
        return `True).that.

        Parameters
        ----------
        time : {parse_time_types}
            {parse_time_desc}

        Returns
        -------
        `bool`
            `True` if time lies between start and end, `False` otherwise.

        Examples
        --------
        >>> from sunpy.time import TimeRange
        >>> time1 = '2014/5/5 12:11'
        >>> time2 = '2012/5/5 12:11'
        >>> time_range = TimeRange('2014/05/04 13:54', '2018/02/03 12:12')
        >>> time1 in time_range
        True
        >>> time2 in time_range
        False
        """
        this_time = parse_time(time)
        return this_time >= self.start and this_time <= self.end
Exemplo n.º 5
0
from datetime import timedelta

import astropy.units as u
from astropy.time import Time, TimeDelta

from sunpy import config
from sunpy.time import is_time_equal, parse_time
from sunpy.time.time import _variables_for_parse_time_docstring
from sunpy.util.decorators import add_common_docstring

TIME_FORMAT = config.get('general', 'time_format')

__all__ = ['TimeRange']


@add_common_docstring(**_variables_for_parse_time_docstring())
class TimeRange:
    """
    A class to create and handle time ranges.

    .. note::

       Regardless of how a `sunpy.time.TimeRange` is constructed it will always
       provide a positive time range where the start time is before the end time.

    Parameters
    ----------
    a : {parse_time_types}
        A time (the start time) specified as a parse_time-compatible
        time string, number, or a datetime object.
    b : {parse_time_types}