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

Revision 907, 12.6 KB checked in by marcus, 6 years ago (diff)
  • some sql dialects are case-sensitive by default
  • 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):
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        for i in data:
262            if not i._name in self.stations:
263                self.stations[i._name] = {}
264
265            try:
266                self.stations[i._name][i._canal].append(i)
267                self.channels[i.channel].append(i)
268            except KeyError:
269                self.stations[i._name][i._canal] = [i]
270                self.channels[i.channel] = [i]
271
272#        print self.stations
273
274    def __readDB(self, db=None, create=False):
275        """
276        Read raw channel data from given database.
277        """
278        if not db:
279            db = self.dbreadwrite
280
281        # init db session only if necessary
282        if self.dbsessions.get(db, None) is None:
283            engine = sa.create_engine(db)
284
285            if create:
286                tabledata = Base.metadata
287                tabledata.create_all(engine)
288                Session = orm.sessionmaker(bind=engine)
289            else:
290                Session = orm.sessionmaker(bind=engine, autoflush=False)
291            s = Session()
292            if not create:
293                # monkey patching *urgs*
294                s.flush = saReadonly
295            self.dbsessions[db] = s
296
297        try:
298            a = self.dbsessions[db].query(ChannelMeta).order_by("ondate").all()
299            return a
300        except KeyError:
301            return []
302        except OperationalError as E:
303            # sqlite sometimes does weird things, we log it for now
304            log_message("debug.stations", "%s: %s" % (db, str(E)))
305            return []
306
307    def __getitem__(self, codedate):
308        """
309        Return channel meta data from channel code and time information. Input
310        parameter "codedate" is supposed to be a string containing the channels
311        code or a list/tuple of channel and UTCDateTime information.
312        """
313        try:
314            code, date = codedate
315        except:
316            code = codedate
317            date = UTCDateTime()
318
319        code = code.upper()
320        meta = self.channels.get(code, None)
321        if meta is None:
322            raise KeyError("no meta data found at all for '%s'" % code)
323
324        match = None
325        for m in meta:
326            if m._start > date:
327                continue
328
329            # None == open end
330            if m._end is not None and m._end <= date:
331                continue
332
333            match = m
334            break
335
336        if match:
337            return match
338
339        raise KeyError("no meta data of '%s' found for '%s'" % (code, date))
340
341    def add(self, station, replace=False):
342        """
343        Add station to database.
344        """
345        # XXX check for update -> dirty session
346
347        if not isinstance(station, ChannelMeta):
348            raise ValueError("Wrong data type")
349
350        session = self.dbsessions[self.dbreadwrite]
351
352        # check for conflict
353        try:
354            update = self[station.channel, station._start]
355            if update == station:
356                return
357
358            if not replace:
359                raise ValueError("Concurrent data present! % " % station)
360
361            session.delete(update)
362        except KeyError as E:
363            if E.message.startswith("ond"):
364                print E
365                import pdb; pdb.set_trace()
366        except Exception as E:
367            print "xx", E
368            import pdb; pdb.set_trace()
369
370        session.add(station)
371
372        try:
373            session.commit()
374        except Exception as e:
375            print e
376            import pdb; pdb.set_trace()
377            session.rollback()
378
379    def fetch(self, station):
380        """
381        Fetch information from webdc servers.
382        """
383        pass
384
385
386def resolveStations(stations):
387    """
388    Method for resolving station groups and/or aliases.
389
390    Returns a set of station codes in miniseed dialect.
391    """
392
393    # split string at comma, otherwise assume a list or tuple
394    if isinstance(stations, basestring):
395        stations = [i.strip() for i in stations.split(",")]
396
397    # first: station group
398    intermediate = []
399    for s in stations:
400        try:
401            intermediate += Settings.config["station-groups"][s.lower()]
402        except:
403            intermediate.append(s)
404
405    # lookup station aliases
406    final = []
407    for s in intermediate:
408        rpl = Settings.config["station-aliases"].get(s.lower(), [s,])[0]
409        # handle duplicate stations
410        if rpl not in final:
411            final.append(rpl)
412
413    return final
414
415
416def saReadonly(*args, **kwargs):
417    """
418    Dummy method for supporting read-only access to SQLite DB
419
420    http://writeonly.wordpress.com/2009/07/16/simple-read-only-sqlalchemy-sessions/
421    """
422    return
423
424
425if __name__ == "__main__":
426    stations = Stations()
427    print stations[("GR.GRA1..BHZ", UTCDateTime())]
Note: See TracBrowser for help on using the repository browser.