diff --git a/geomagio/Controller.py b/geomagio/Controller.py index d05f45ae4e8d4848da8e062adddb8540f839e3ef..4185cdea0ea7531721b0d3d7b2825809273c7333 100644 --- a/geomagio/Controller.py +++ b/geomagio/Controller.py @@ -513,7 +513,7 @@ def get_input_factory(args): host=args.input_host, port=args.input_port, locationCode=args.locationcode, - **input_factory_args + **input_factory_args, ) elif input_type == "miniseed": input_factory = edge.MiniSeedFactory( @@ -521,7 +521,7 @@ def get_input_factory(args): port=args.input_port, locationCode=args.locationcode, convert_channels=args.convert_voltbin, - **input_factory_args + **input_factory_args, ) elif input_type == "goes": # TODO: deal with other goes arguments @@ -531,7 +531,15 @@ def get_input_factory(args): password=args.input_goes_password, server=args.input_goes_server, user=args.input_goes_user, - **input_factory_args + **input_factory_args, + ) + elif input_type == "iris": + input_factory = edge.IRISFactory( + base_url=args.iris_url, + network=args.iris_network, + locationCode=args.locationcode, + convert_channels=args.convert_voltbin, + **input_factory_args, ) else: # stream compatible factories @@ -600,7 +608,7 @@ def get_output_factory(args): locationCode=locationcode, tag=args.output_edge_tag, forceout=args.output_edge_forceout, - **output_factory_args + **output_factory_args, ) elif output_type == "miniseed": # TODO: deal with other miniseed arguments @@ -610,7 +618,7 @@ def get_output_factory(args): port=args.output_read_port, write_port=args.output_port, locationCode=locationcode, - **output_factory_args + **output_factory_args, ) elif output_type == "plot": output_factory = PlotTimeseriesFactory() @@ -763,7 +771,16 @@ def parse_args(args): input_type_group = input_group.add_mutually_exclusive_group(required=True) input_type_group.add_argument( "--input", - choices=("edge", "goes", "iaga2002", "imfv122", "imfv283", "miniseed", "pcdcp"), + choices=( + "edge", + "goes", + "iaga2002", + "imfv122", + "imfv283", + "iris", + "miniseed", + "pcdcp", + ), default="edge", help='Input format (Default "edge")', ) @@ -828,6 +845,16 @@ def parse_args(args): help='Data interval, default "minute"', metavar="INTERVAL", ) + input_group.add_argument( + "--iris-network", + default="NT", + help="data network", + ) + input_group.add_argument( + "--iris-url", + default="http://service.iris.edu/irisws", + help="IRIS web service url", + ) input_group.add_argument( "--locationcode", help=""" diff --git a/geomagio/api/ws/Observatory.py b/geomagio/api/ws/Observatory.py index fe857c9acfed34b9fbe96da7b2ad1b85fc9b3589..ecb38c91037b81cb4d822137316c786536b09261 100644 --- a/geomagio/api/ws/Observatory.py +++ b/geomagio/api/ws/Observatory.py @@ -424,4 +424,90 @@ OBSERVATORIES = [ ), ] +ASL_OBSERVATORIES = [ + Observatory( + id="ANMO", + elevation=1820, + latitude=34.946, + longitude=-106.457, + name="Albuquerque", + agency="USGS", + declination_base=0, + ), + Observatory( + id="CASY", + elevation=10, + latitude=-66.279, + longitude=110.535, + name="Casey", + agency="USGS", + declination_base=0, + ), + Observatory( + id="COLA", + elevation=200, + latitude=64.874, + longitude=-147.862, + name="College Outpost", + agency="USGS", + declination_base=0, + ), + Observatory( + id="COR", + elevation=110, + latitude=44.586, + longitude=-123.305, + name="Corvallis", + agency="USGS", + declination_base=0, + ), + Observatory( + id="QSPA", + elevation=2850, + latitude=-89.929, + longitude=144.438, + name="South Pole Remote Earth Science Observatory", + agency="USGS", + declination_base=0, + ), + Observatory( + id="RSSD", + elevation=2090, + latitude=44.121, + longitude=-104.036, + name="Black Hills", + agency="USGS", + declination_base=0, + ), + Observatory( + id="SBA", + elevation=50, + latitude=-77.849, + longitude=166.757, + name="Scott Base", + agency="USGS", + declination_base=0, + ), + Observatory( + id="SFJD", + elevation=330, + latitude=66.996, + longitude=-50.621, + name="Sondre Stromfjord", + agency="USGS", + declination_base=0, + ), + Observatory( + id="SPA", + elevation=2927, + latitude=-90.0, + longitude=0.0, + name="South Pole", + agency="USGS", + declination_base=0, + ), +] + +ASL_OBSERVATORY_INDEX = {o.id: o for o in ASL_OBSERVATORIES} OBSERVATORY_INDEX = {o.id: o for o in OBSERVATORIES} +OBSERVATORY_INDEX.update(ASL_OBSERVATORY_INDEX) diff --git a/geomagio/api/ws/data.py b/geomagio/api/ws/data.py index db204eee3f5243986e7e2dce7eb8ed271005b80d..961c6fae990057502ee332a65dd1595f81a8713d 100644 --- a/geomagio/api/ws/data.py +++ b/geomagio/api/ws/data.py @@ -6,9 +6,10 @@ from obspy import UTCDateTime, Stream from starlette.responses import Response from ... import DerivedTimeseriesFactory, TimeseriesFactory, TimeseriesUtility -from ...edge import EdgeFactory, MiniSeedFactory +from ...edge import EdgeFactory, IRISFactory, MiniSeedFactory from ...iaga2002 import IAGA2002Writer from ...imfjson import IMFJSONWriter +from .Observatory import ASL_OBSERVATORY_INDEX from .DataApiQuery import ( DEFAULT_ELEMENTS, DataApiQuery, @@ -30,7 +31,9 @@ def get_data_factory( """ host = os.getenv("DATA_HOST", "cwbpub.cr.usgs.gov") sampling_period = query.sampling_period - if sampling_period in [ + if query.id in ASL_OBSERVATORY_INDEX: + factory = IRISFactory(network="IU", locationCode="40") + elif sampling_period in [ SamplingPeriod.TEN_HERTZ, SamplingPeriod.HOUR, SamplingPeriod.DAY, diff --git a/geomagio/api/ws/observatories.py b/geomagio/api/ws/observatories.py index 1a8c797c258952ba8793f12a903ffb70b93b6abf..4297fe9b825d82bdd08819ef4057b75eba2f936c 100644 --- a/geomagio/api/ws/observatories.py +++ b/geomagio/api/ws/observatories.py @@ -2,7 +2,7 @@ from typing import Dict from fastapi import APIRouter, Response -from .Observatory import OBSERVATORIES, OBSERVATORY_INDEX +from .Observatory import OBSERVATORY_INDEX router = APIRouter() @@ -15,7 +15,9 @@ router = APIRouter() def get_observatories() -> Dict: return { "type": "FeatureCollection", - "features": [o.geojson() for o in OBSERVATORIES], + "features": [ + OBSERVATORY_INDEX[id].geojson() for id in OBSERVATORY_INDEX.keys() + ], } diff --git a/geomagio/edge/IRISFactory.py b/geomagio/edge/IRISFactory.py new file mode 100644 index 0000000000000000000000000000000000000000..950fb61f58db636edeb33b39c13a6488182bcd39 --- /dev/null +++ b/geomagio/edge/IRISFactory.py @@ -0,0 +1,154 @@ +from typing import List, Optional + +from obspy import Trace, UTCDateTime +from obspy.clients.iris.client import Client + +from ..geomag_types import DataInterval, DataType +from ..ObservatoryMetadata import ObservatoryMetadata +from .. import TimeseriesUtility +from .IRISSNCL import IRISSNCL +from .MiniSeedFactory import MiniSeedFactory + + +class IRISFactory(MiniSeedFactory): + """MiniSeedFactory for IRIS related data. + + Parameters + ---------- + base_url: str + iris web service url + host: str + a string representing the IP number of the host to connect to + write_port: int + the port number the miniseed client writes data + network: str + data network + observatory: str + observatory id + channels: array + list of channels + type: {"adjusted", "definitive", "quasi-definitive", "variation"} + data type + interval: {"tenhertz", "second", "minute", "hour", "day"} + data interval + observatoryMetadata: ObservatoryMetadata + observatory metadata to replace the default ObservatoryMetadata + locationCode: str + the location code for the given edge server + convert_channels: array + list of channels to convert from volt/bin to nT + + See Also + -------- + MiniSeedFactory + + Notes + ----- + This factory is designed to receive data from IRIS and write to edge. + convert_channels should only be used with tenhertz NT data (U, V, W). + Non-NT data conversions are automatically handled by the IRIS web service. + + + """ + + def __init__( + self, + base_url: str = "http://service.iris.edu/irisws", + host: str = "cwbpub.cr.usgs.gov", + write_port: int = 7974, + network: str = "NT", + observatory: Optional[str] = None, + channels: Optional[List[str]] = None, + type: Optional[DataType] = None, + interval: Optional[DataInterval] = None, + observatoryMetadata: Optional[ObservatoryMetadata] = None, + locationCode: Optional[str] = None, + convert_channels: Optional[List[str]] = None, + ): + super().__init__( + host=host, + write_port=write_port, + observatory=observatory, + channels=channels, + type=type, + interval=interval, + observatoryMetadata=observatoryMetadata or ObservatoryMetadata(), + locationCode=locationCode, + convert_channels=convert_channels, + ) + self.base_url = base_url + self.network = network + self.client = Client(base_url=base_url) + + def _get_timeseries( + self, + starttime: UTCDateTime, + endtime: UTCDateTime, + observatory: str, + channel: str, + type: DataType, + interval: DataInterval, + add_empty_channels: bool = True, + ) -> Trace: + """get timeseries data for a single channel + + Parameters + ---------- + starttime: UTCDateTime + the starttime of requested data + endtime: UTCDateTime + the endtime of requested data + observatory: str + observatory id + channel: str + channel name + type: {"adjusted", "definitive", "quasi-definitive", "variation"} + data type + interval: {"tenhertz", "second", "minute", "hour", "day"} + data interval + add_empty_channels: bool + if True, returns channels without data as empty traces + + Returns + ------- + data: Trace + timeseries trace of the requested channel data + """ + sncl = IRISSNCL.get_sncl( + data_type=type, + element=channel, + interval=interval, + station=observatory, + network=self.network, + location=self.locationCode, + ) + filter = ["correct"] + if sncl.channel[1] in ["E", "Y"]: + filter = [] + data = self.client.timeseries( + network=sncl.network, + station=sncl.station, + location=sncl.location, + channel=sncl.channel, + starttime=starttime, + endtime=endtime, + filter=filter, + ) + data.merge() + if data.count() == 0 and add_empty_channels: + data += self._get_empty_trace( + starttime=starttime, + endtime=endtime, + observatory=observatory, + channel=channel, + data_type=type, + interval=interval, + network=sncl.network, + location=sncl.location, + ) + if data.count() != 0: + TimeseriesUtility.pad_and_trim_trace( + trace=data[0], starttime=starttime, endtime=endtime + ) + self._set_metadata(data, observatory, channel, type, interval) + return data diff --git a/geomagio/edge/IRISSNCL.py b/geomagio/edge/IRISSNCL.py new file mode 100644 index 0000000000000000000000000000000000000000..0fdcc417047fe8906bd1f01a8e3789fccdf60085 --- /dev/null +++ b/geomagio/edge/IRISSNCL.py @@ -0,0 +1,83 @@ +from typing import Optional + +from ..geomag_types import DataInterval, DataType +from .SNCL import SNCL, get_channel, get_location, _get_channel_start + + +class IRISSNCL(SNCL): + @classmethod + def get_sncl( + cls, + data_type: DataType, + element: str, + interval: DataInterval, + station: str, + network: str = "NT", + location: Optional[str] = None, + ) -> "IRISSNCL": + if ( + network == "IU" + and location == "40" + and data_type not in ["40", "variation"] # ASL only supports variation data + ): + raise ValueError(f"Unsupported data type: {data_type}") + location = location or get_location(element=element, data_type=data_type) + return IRISSNCL( + station=station, + network=network, + channel=get_iris_channel( + element=element, + data_type=data_type, + interval=interval, + network=network, + location=location, + ), + location=location, + ) + + @property + def data_type(self) -> str: + if self.location == "40" and self.network == "IU": + return "variation" + return super().data_type + + @property + def element(self) -> str: + if self.location == "40" and self.network == "IU": + return _get_iris_element(channel=self.channel) + return super().element + + +def _get_iris_element(channel: str) -> str: + channel_end = channel[1:] + if channel_end == "F1": + return "U" + elif channel_end == "F2": + return "V" + elif channel_end == "FZ": + return "W" + raise ValueError(f"Unsupported channel: {channel}") + + +def get_iris_channel( + element: str, + data_type: DataType, + interval: DataInterval, + network: Optional[str] = None, + location: Optional[str] = None, +) -> str: + if location == "40" and network == "IU": + return _get_channel_start(interval=interval) + _get_channel_end(element=element) + return get_channel(element=element, interval=interval, data_type=data_type) + + +def _get_channel_end(element: str) -> str: + if element in ["H", "U"]: + return "F1" + elif element in ["E", "V"]: + return "F2" + elif element in ["Z", "W"]: + return "FZ" + elif element[0:2] == "LF": # predefined element + return element[1:] + raise ValueError(f"Unsupported element: {element}") diff --git a/geomagio/edge/__init__.py b/geomagio/edge/__init__.py index 45b3d49c3ac29b3dd213d0f83cf1017ea5d95549..ebb15ef67872a2a22773695a22f15c9e9f94151c 100644 --- a/geomagio/edge/__init__.py +++ b/geomagio/edge/__init__.py @@ -3,19 +3,23 @@ from __future__ import absolute_import from .EdgeFactory import EdgeFactory +from .IRISFactory import IRISFactory from .LocationCode import LocationCode from .MiniSeedFactory import MiniSeedFactory from .MiniSeedInputClient import MiniSeedInputClient from .RawInputClient import RawInputClient +from .IRISSNCL import IRISSNCL from .SNCL import SNCL from .LegacySNCL import LegacySNCL __all__ = [ "EdgeFactory", + "IRISFactory", "LocationCode", "MiniSeedFactory", "MiniSeedInputClient", "RawInputClient", - "LegacySNCL", + "IRISSNCL", "SNCL", + "LegacySNCL", ] diff --git a/test/edge_test/IRISSNCL_test.py b/test/edge_test/IRISSNCL_test.py new file mode 100644 index 0000000000000000000000000000000000000000..b67cc6400b5954c1f4020e791fb55775913520a1 --- /dev/null +++ b/test/edge_test/IRISSNCL_test.py @@ -0,0 +1,165 @@ +import pytest + +from geomagio.edge import IRISSNCL +from geomagio.edge.IRISSNCL import get_iris_channel, get_location + + +def test_data_type(): + """edge_test.IRISSNCL_test.test_data_type()""" + assert ( + IRISSNCL(station="ANMO", network="IU", channel="LF1", location="40").data_type + == "variation" + ) + + +def test_element(): + """edge_test.IRISSNCL_test.test_element()""" + assert ( + IRISSNCL(station="ANMO", network="IU", channel="LF1", location="40").element + == "U" + ) + assert ( + IRISSNCL(station="ANMO", network="IU", channel="LF2", location="40").element + == "V" + ) + assert ( + IRISSNCL(station="ANMO", network="IU", channel="LFZ", location="40").element + == "W" + ) + with pytest.raises(ValueError) as error: + IRISSNCL(station="ANMO", network="IU", channel="LFH", location="40").element + assert error.value == "Unsupported channel LFH" + + +def test_get_iris_channel(): + """edge_test.IRISSNCL_test.test_get_iris_channel()""" + assert ( + get_iris_channel( + element="H", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + == "LF1" + ) + assert ( + get_iris_channel( + element="U", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + == "LF1" + ) + assert ( + get_iris_channel( + element="E", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + == "LF2" + ) + assert ( + get_iris_channel( + element="V", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + == "LF2" + ) + assert ( + get_iris_channel( + element="W", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + == "LFZ" + ) + # predefined channel + assert ( + get_iris_channel( + element="LFZ", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + == "LFZ" + ) + with pytest.raises(ValueError) as error: + get_iris_channel( + element="D", + interval="second", + data_type="variation", + network="IU", + location="40", + ) + assert error.value == "Unsupported element: D" + + +def test_get_sncl(): + """edge_test.IRISSNCL_test.test_get_sncl()""" + assert ( + IRISSNCL.get_sncl( + data_type="variation", + element="H", + interval="second", + station="ANMO", + network="IU", + location="40", + ) + == IRISSNCL(station="ANMO", network="IU", channel="LF1", location="40") + ) + with pytest.raises(ValueError) as error: + IRISSNCL.get_sncl( + data_type="adjusted", + element="H", + interval="second", + station="ANMO", + network="IU", + location="40", + ) + assert error.value == "Unsupported data type: adjusted" + assert ( + IRISSNCL.get_sncl( + data_type="adjusted", + element="H", + interval="second", + station="BOU", + ) + == IRISSNCL(station="BOU", network="NT", channel="LFH", location="A0") + ) + + +def test_interval(): + """edge_test.IRISSNCL_test.test_interval()""" + assert ( + IRISSNCL( + station="ANMO", + network="IU", + channel="LF1", + location="40", + ).interval + == "second" + ) + + +def test_parse_sncl(): + """edge_test.IRISSNCL_test.test_parse_sncl()""" + assert IRISSNCL( + station="ANMO", network="IU", channel="LF1", location="40" + ).parse_sncl() == { + "station": "ANMO", + "network": "IU", + "data_type": "variation", + "element": "U", + "interval": "second", + }