source: SHX/trunk/SeismicHandler/modules/stations.py @ 925

Revision 925, 12.7 KB checked in by marcus, 6 years ago (diff)
  • introduced clear keyword
  • Property svn:eol-style set to native
Line 
1# -*- coding: utf-8 -*-
2
3#    This file is part of Seismic Handler eXtended (SHX). For terms of use and
4#    license information please see license.txt and visit
5#    http://www.seismic-handler.org/portal/wiki/Shx/LicenseTerms
6
7import sqlalchemy as sa
8import sqlalchemy.orm as orm
9from sqlalchemy.ext.declarative import declarative_base
10from sqlalchemy.exc import OperationalError
11
12from obspy.core import UTCDateTime
13from SeismicHandler.config import Settings
14from SeismicHandler.basics import Singleton
15from SeismicHandler.basics.messages import log_message
16
17FUTURE_DATE = UTCDateTime(3e10)
18
19
20Base = declarative_base()
21class ChannelMeta(Base):
22    """
23    Table holding stations inventory for single channels (e.g. GR.GRA1..BHZ).
24
25    All floating point data is stored as string in order to prevent floating
26    point issues.
27    """
28    __tablename__ = "meta"
29
30    # internal use only
31    id = sa.Column(sa.Integer, primary_key=True)
32
33    # GrÀfenberg A1
34    description = sa.Column(sa.Unicode(length=100), default=u"")
35    # GR.GRA1..BHZ
36    channel = sa.Column(sa.String(length=15))
37    # GR
38    network = sa.Column(sa.String(length=2), index=True)
39    # GRA1X
40    station = sa.Column(sa.String(length=5), index=True)
41    # 01
42    location = sa.Column(sa.String(length=2), index=True)
43    # BH
44    stream = sa.Column(sa.String(length=2), index=True)
45    # Z
46    component = sa.Column(sa.String(length=1), index=True)
47    # GRF
48    arraycode = sa.Column(sa.String(length=10), index=True, default=None)
49    # SQL does not always support datetime with microseconds
50    ondate = sa.Column(sa.DateTime, index=True)
51    ondatems = sa.Column(sa.Integer, index=True)
52    offdate = sa.Column(sa.DateTime, index=True)
53    offdatems = sa.Column(sa.Integer, index=True)
54    latitude = sa.Column(sa.String(length=20))
55    longitude = sa.Column(sa.String(length=20))
56    elevation = sa.Column(sa.String(length=20))
57    depth = sa.Column(sa.String(length=20), default="0.")
58    # array coordinate offset
59    offsetx = sa.Column(sa.String(length=20), default="")
60    offsety = sa.Column(sa.String(length=20), default="")
61    gain = sa.Column(sa.String(length=20), default=None)
62    zeros = sa.Column(sa.Text)
63    poles = sa.Column(sa.Text)
64
65    # channel + start must be unique
66    __table_args__ = (
67        sa.UniqueConstraint("channel", "ondate"), {}
68    )
69
70    def __init__(self, network, station, location, stream, component, \
71                       latitude, longitude, gain, poles, zeros, start, \
72                       elevation="0.", end=None, description=u"", depth="0.", \
73                       arraycode='', offsetx='', offsety=''):
74
75        self.channel = ".".join([network, station, location, \
76                                                  "".join([stream, component])])
77
78        keys = locals().keys()
79        del keys[keys.index("self")]
80
81        # must be handled separately
82        del keys[keys.index("start")]
83        del keys[keys.index("end")]
84
85        # save all other information into object
86        for i in keys:
87            setattr(self, i, locals()[i])
88
89        # handle start & end
90        # always cut off ms part since most systems do not support it
91        self.ondate = (start - start.microsecond/1e6).datetime
92        self.ondatems = start.microsecond
93
94        if end is not None and end == FUTURE_DATE:
95            end = None
96        if end:
97            self.offdate = (end - end.microsecond/1e6).datetime
98            self.offdatems = end.microsecond
99
100    def __repr__(self):
101        if self.offdate:
102            return "<Channel:%s %s to %s>" % (
103                self.channel,
104                self.ondate.strftime("%Y-%m-%d"),
105                self.offdate.strftime("%Y-%m-%d")
106            )
107        else:
108            return "<Channel:%s %s to NOW>" % (
109                self.channel,
110                self.ondate.strftime("%Y-%m-%d")
111            )
112
113    def __getattr__(self, name):
114        """
115        Special handling for start and end date, combination of network,
116        station and location (name) location, stream and component (canal),
117        poles and zeros.
118
119        For data like latitude, longitude, depth, elevation and gain a floating
120        point representation is returned.
121
122        This works only if an underscore prefix is used (e.g. _latitude). If
123        the plain column name is used, the raw db content is returned
124        (actually this method is not called at all, if an apropriate db column
125        exists).
126        """
127        if not name.startswith('_'):
128            return self.__getattribute__(name)
129
130        name = name.lower()[1:]
131        if name not in ["start", "end", "name", "canal"]:
132            value = self.__getattribute__(name)
133            if name in ["latitude", "longitude", "depth", "elevation", "gain"]:
134                return float(value)
135
136            if name in ["poles", "zeros"]:
137                # remove surrounding brackets []
138                value = value.strip()[1:-1]
139                chain = []
140                # parse values
141                for _i in value.split(','):
142                    chain.append(complex(_i))
143                return chain
144
145            return value
146
147        if name == "name":
148            return ".".join([
149                self.__dict__["network"],
150                self.__dict__["station"],
151                self.__dict__["location"],
152            ])
153
154        if name == "canal":
155            return "".join([
156                self.__dict__["stream"],
157                self.__dict__["component"]
158            ])
159
160        # build UTCDateTime from o[n|ff]date[ms]
161        if name == "start":
162            return UTCDateTime(self.__dict__["ondate"]) + \
163                                                 self.__dict__["ondatems"] / 1e6
164       
165        # "end"
166        if not self.__dict__["offdate"]:
167            return None
168
169        return UTCDateTime(self.__dict__["offdate"]) + \
170                                                self.__dict__["offdatems"] / 1e6
171
172    def __compare(self, other):
173        """
174        Compare station information. All fields except for id and those starting
175        with underscore are compared.
176
177        FIR information is optional and not included!
178        """
179
180        for i in self.__dict__:
181            if i.startswith("_") or i == "id":
182                continue
183
184            if getattr(self, i) != getattr(other, i):
185                return False
186
187        # fir information is optional XXX
188           
189        return True
190
191    def __eq__(self, other):
192        return self.__compare(other)
193
194    def __ne__(self, other):
195        return not self.__compare(other)
196
197
198class FiR(Base):
199    """
200    Table holding finite impulse responses.
201    """
202    __tablename__ = "fir"
203
204    # internal use only
205    id = sa.Column(sa.Integer, primary_key=True)
206    channel_id = sa.Column(sa.Integer, sa.ForeignKey('meta.id'))
207
208    stage = sa.Column(sa.Integer, index=True)
209    coefficients = sa.Column(sa.Text)
210
211    channel = orm.relation(ChannelMeta, backref=orm.backref('fir', order_by=stage))
212
213    # ChannelMeta_id + stage must be unique
214    __table_args__ = (
215        sa.UniqueConstraint("channel_id", "stage"), {}
216    )
217
218    def __init__(self, stage, coefficients):
219        self.stage = stage
220        self.coefficients = coefficients
221
222    def __repr__(self):
223        return "<FIR:%s>" % self.channel
224
225
226class Stations(object):
227    """
228    Supply station read/write access.
229    """
230    __metaclass__ = Singleton
231   
232    stations = {}
233    channels = {}
234
235    dbsessions = {}
236
237    def __init__(self):
238        self.read()
239
240    def __setitem__(self, name, data):
241        """
242        Save or update channel information.
243        """
244        if not isinstance(data, ChannelMeta):
245            raise Exception("Only channel specific information can be updated "
246                            "or saved!")
247
248    def read(self, clear=False):
249        """
250        Init / refresh station information.
251        """
252        # read only information
253        data = []
254        for db in Settings.config.inventory.readonly:
255            data += self.__readDB(db)
256
257        # data base access r/w
258        self.dbreadwrite = Settings.config.inventory.database[0]
259        data += self.__readDB(create=True)
260       
261        if clear:
262            self.stations = {}
263            self.channels = {}
264
265        for i in data:
266            if not i._name in self.stations:
267                self.stations[i._name] = {}
268
269            try:
270                self.stations[i._name][i._canal].append(i)
271                self.channels[i.channel].append(i)
272            except KeyError:
273                self.stations[i._name][i._canal] = [i]
274                self.channels[i.channel] = [i]
275
276#        print self.stations
277
278    def __readDB(self, db=None, create=False):
279        """
280        Read raw channel data from given database.
281        """
282        if not db:
283            db = self.dbreadwrite
284
285        # init db session only if necessary
286        if self.dbsessions.get(db, None) is None:
287            engine = sa.create_engine(db)
288
289            if create:
290                tabledata = Base.metadata
291                tabledata.create_all(engine)
292                Session = orm.sessionmaker(bind=engine)
293            else:
294                Session = orm.sessionmaker(bind=engine, autoflush=False)
295            s = Session()
296            if not create:
297                # monkey patching *urgs*
298                s.flush = saReadonly
299            self.dbsessions[db] = s
300
301        try:
302            a = self.dbsessions[db].query(ChannelMeta).order_by("ondate").all()
303            return a
304        except KeyError:
305            return []
306        except OperationalError as E:
307            # sqlite sometimes does weird things, we log it for now
308            log_message("debug.stations", "%s: %s" % (db, str(E)))
309            return []
310
311    def __getitem__(self, codedate):
312        """
313        Return channel meta data from channel code and time information. Input
314        parameter "codedate" is supposed to be a string containing the channels
315        code or a list/tuple of channel and UTCDateTime information.
316        """
317        try:
318            code, date = codedate
319        except:
320            code = codedate
321            date = UTCDateTime()
322
323        code = code.upper()
324        meta = self.channels.get(code, None)
325        if meta is None:
326            raise KeyError("no meta data found at all for '%s'" % code)
327
328        match = None
329        for m in meta:
330            if m._start > date:
331                continue
332
333            # None == open end
334            if m._end is not None and m._end <= date:
335                continue
336
337            match = m
338            break
339
340        if match:
341            return match
342
343        raise KeyError("no meta data of '%s' found for '%s'" % (code, date))
344
345    def add(self, station, replace=False):
346        """
347        Add station to database.
348        """
349        # XXX check for update -> dirty session
350
351        if not isinstance(station, ChannelMeta):
352            raise ValueError("Wrong data type")
353
354        session = self.dbsessions[self.dbreadwrite]
355
356        # check for conflict
357        try:
358            update = self[station.channel, station._start]
359            if update == station:
360                return
361
362            if not replace:
363                raise ValueError("Concurrent data present! % " % station)
364
365            session.delete(update)
366        except KeyError as E:
367            if E.message.startswith("ond"):
368                print E
369                import pdb; pdb.set_trace()
370        except Exception as E:
371            print "xx", E
372            import pdb; pdb.set_trace()
373
374        session.add(station)
375
376        try:
377            session.commit()
378        except Exception as e:
379            print e
380            import pdb; pdb.set_trace()
381            session.rollback()
382
383    def fetch(self, station):
384        """
385        Fetch information from webdc servers.
386        """
387        pass
388
389
390def resolveStations(stations):
391    """
392    Method for resolving station groups and/or aliases.
393
394    Returns a set of station codes in miniseed dialect.
395    """
396
397    # split string at comma, otherwise assume a list or tuple
398    if isinstance(stations, basestring):
399        stations = [i.strip() for i in stations.split(",")]
400
401    # first: station group
402    intermediate = []
403    for s in stations:
404        try:
405            intermediate += Settings.config["station-groups"][s.lower()]
406        except:
407            intermediate.append(s)
408
409    # lookup station aliases
410    final = []
411    for s in intermediate:
412        rpl = Settings.config["station-aliases"].get(s.lower(), [s,])[0]
413        # handle duplicate stations
414        if rpl not in final:
415            final.append(rpl)
416
417    return final
418
419
420def saReadonly(*args, **kwargs):
421    """
422    Dummy method for supporting read-only access to SQLite DB
423
424    http://writeonly.wordpress.com/2009/07/16/simple-read-only-sqlalchemy-sessions/
425    """
426    return
427
428
429if __name__ == "__main__":
430    stations = Stations()
431    print stations[("GR.GRA1..BHZ", UTCDateTime())]
Note: See TracBrowser for help on using the repository browser.