Repository: obspy/obspy

Issue:
SAC output does not use obspy headers
io.sac should propagate relevant obspy headers (similar to #1317).
For example: Now (obspy 1.0.1), reading SAC files, rotating them and writing to SAC gives the wrong seed ids.

```
from obspy import read, Stream
from obspy.io.sac.sactrace import SACTrace

def simulate_SAC_write_and_read(stream):
    return Stream([SACTrace.from_obspy_trace(tr).to_obspy_trace() for tr in stream])

stream = simulate_SAC_write_and_read(read())
print stream[0].id
stream.rotate('ZNE->LQT', 10, 10)
print stream[0].id
stream = simulate_SAC_write_and_read(stream)
print stream[0].id
```

```
BW.RJOB..EHZ
BW.RJOB..EHL
BW.RJOB..EHZ
```



Discussion:

Resolving patch:
diff --git a/obspy/io/sac/util.py b/obspy/io/sac/util.py
--- a/obspy/io/sac/util.py
+++ b/obspy/io/sac/util.py
@@ -300,10 +300,11 @@ def obspy_to_sac_header(stats, keep_sac_header=True):
           and "b" is not set, the reftime will be set from stats.starttime
           (with micro/milliseconds precision adjustments) and "b" and "e" are
           set accordingly.
-        * If 'kstnm', 'knetwk', 'kcmpnm', or 'khole' are not set, they are
-          taken from 'station', 'network', 'channel', and 'location' in stats.
+        * If 'kstnm', 'knetwk', 'kcmpnm', or 'khole' are not set or differ
+          from Stats values 'station', 'network', 'channel', or 'location',
+          they are taken from the Stats values.
         If keep_sac_header is False, a new SAC header is constructed from only
-        information found in the stats dictionary, with some other default
+        information found in the Stats dictionary, with some other default
         values introduced.  It will be an iztype 9 ("ib") file, with small
         reference time adjustments for micro/milliseconds precision issues.
         SAC headers nvhdr, level, lovrok, and iftype are always produced.
@@ -313,9 +314,6 @@ def obspy_to_sac_header(stats, keep_sac_header=True):
     header = {}
     oldsac = stats.get('sac', {})
 
-    header['npts'] = stats['npts']
-    header['delta'] = stats['delta']
-
     if keep_sac_header and oldsac:
         # start with the old header
         header.update(oldsac)
@@ -371,20 +369,20 @@ def obspy_to_sac_header(stats, keep_sac_header=True):
             warnings.warn(msg)
 
         # merge some values from stats if they're missing in the SAC header
-        # ObsPy issue 1204
-        if header.get('kstnm') in (None, HD.SNULL):
-            header['kstnm'] = stats['station'] or HD.SNULL
-        if header.get('knetwk') in (None, HD.SNULL):
-            header['knetwk'] = stats['network'] or HD.SNULL
-        if header.get('kcmpnm') in (None, HD.SNULL):
-            header['kcmpnm'] = stats['channel'] or HD.SNULL
-        if header.get('khole') in (None, HD.SNULL):
-            header['khole'] = stats['location'] or HD.SNULL
+        # ObsPy issues 1204, 1457
+        # XXX: If Stats values are empty/"" and SAC header values are real,
+        #   this will replace the real SAC values with SAC null values.
+        # TODO: make this operation into a private helper function
+        for sachdr, statshdr in [('kstnm', 'station'), ('knetwk', 'network'),
+                                 ('kcmpnm', 'channel'), ('khole', 'location')]:
+            if (header.get(sachdr) in (None, HD.SNULL)) or \
+               (header.get(sachdr).strip() != stats[statshdr]):
+                header[sachdr] = stats[statshdr] or HD.SNULL
 
     else:
-        # SAC header from scratch.  Just use stats.
+        # SAC header from scratch.  Just use Stats.
 
-        # Here, set headers from stats that would otherwise depend on the old
+        # Here, set headers from Stats that would otherwise depend on the old
         # SAC header
         header['iztype'] = 9
         starttime = stats['starttime']


Test patch:
diff --git a/obspy/io/sac/tests/test_core.py b/obspy/io/sac/tests/test_core.py
--- a/obspy/io/sac/tests/test_core.py
+++ b/obspy/io/sac/tests/test_core.py
@@ -33,6 +33,7 @@ def setUp(self):
         self.file = os.path.join(self.path, 'data', 'test.sac')
         self.filexy = os.path.join(self.path, 'data', 'testxy.sac')
         self.filebe = os.path.join(self.path, 'data', 'test.sac.swap')
+        self.fileseis = os.path.join(self.path, "data", "seism.sac")
         self.testdata = np.array(
             [-8.74227766e-08, -3.09016973e-01,
              -5.87785363e-01, -8.09017122e-01, -9.51056600e-01,
@@ -276,8 +277,7 @@ def test_reference_time(self):
         starttime of the seismogram is calculated by adding the B header
         (in seconds) to the SAC reference time.
         """
-        file = os.path.join(self.path, "data", "seism.sac")
-        tr = read(file)[0]
+        tr = read(self.fileseis)[0]
         # see that starttime is set correctly (#107)
         self.assertAlmostEqual(tr.stats.sac.iztype, 9)
         self.assertAlmostEqual(tr.stats.sac.b, 9.4599991)
diff --git a/obspy/io/sac/tests/test_sactrace.py b/obspy/io/sac/tests/test_sactrace.py
--- a/obspy/io/sac/tests/test_sactrace.py
+++ b/obspy/io/sac/tests/test_sactrace.py
@@ -9,7 +9,7 @@
 
 import numpy as np
 
-from obspy import UTCDateTime
+from obspy import UTCDateTime, read
 from obspy.core.util import NamedTemporaryFile
 from obspy.geodetics import gps2dist_azimuth, kilometer2degrees
 
@@ -212,6 +212,22 @@ def test_lcalda(self):
         sac = SACTrace(lcalda=True, stla=stla)
         self.assertRaises(SacHeaderError, sac._set_distances, force=True)
 
+    def test_propagate_modified_stats_strings_to_sactrace(self):
+        """
+        If you build a SACTrace from an ObsPy Trace that has certain string
+        headers mismatched between the Stats header and an existing Stats.sac
+        header, channel and kcmpnm for example, the resulting SACTrace values
+        should come from Stats. Addresses GitHub issue #1457.
+        """
+        tr = read(self.fileseis)[0]
+        # modify the header values by adding a single meaningless character
+        for sachdr, statshdr in [('kstnm', 'station'), ('knetwk', 'network'),
+                                 ('kcmpnm', 'channel'), ('khole', 'location')]:
+            modified_value = tr.stats[statshdr] + '1'
+            tr.stats[statshdr] = modified_value
+            sac = SACTrace.from_obspy_trace(tr)
+            self.assertEqual(getattr(sac, sachdr), modified_value)
+
 
 def suite():
     return unittest.makeSuite(SACTraceTestCase, 'test')