Repository: obspy/obspy

Issue:
Station magnitude residual of zero not read from QuakeML
Using ObsPy 1.02 and Python 3.5 installed via Anaconda on Windows.

Try the following  with [test.zip](https://github.com/obspy/obspy/files/646936/test.zip) unzipped to the working directory:

```python
from obspy import read_events
catalog = read_events('test.xml')
print(catalog.events[0].magnitudes[0].station_magnitude_contributions[2].residual)
catalog.write('test2.xml', 'QuakeML')
```

The problem is that station magnitude residual of 0.0 is read and printed, but not written to test2.xml.
It seems that a residual of 0.0 in the third StationMagnitudeContribution is explicitly ignored on writing; you can change the residual to another value and it will write fine.

I expect a residual of 0.0 to be just as valid as any other value. I would expect a value of None to indicate that the residual is not known or not recorded.


Discussion:
I don't have the time right now but if someone want to fix it - this line here as to check `is not None`:

https://github.com/obspy/obspy/blob/9fb860d9868ae5c595ad88e55f317bf84a2d1256/obspy/io/quakeml/core.py#L1164
There might be a couple of other places where this might have to be done.
I think Event.region may need the same treatment.
I've verified that your proposed fix does what we want it to @krischer, by crudely editing the file in my site-packages. Ugly. For want of a C++ compiler on the Windows box I normally develop on I would have forked, fixed and pushed the fix. I hope to be able to soon.

Resolving patch:
diff --git a/obspy/io/quakeml/core.py b/obspy/io/quakeml/core.py
--- a/obspy/io/quakeml/core.py
+++ b/obspy/io/quakeml/core.py
@@ -563,6 +563,7 @@ def _origin(self, element, arrivals):
         obj.composite_times = self._composite_times(element)
         obj.quality = self._origin_quality(element)
         obj.origin_type = self._xpath2obj('type', element)
+        obj.region = self._xpath2obj('region', element)
         obj.evaluation_mode = self._xpath2obj('evaluationMode', element)
         obj.evaluation_status = self._xpath2obj('evaluationStatus', element)
         obj.creation_info = self._creation_info(element)
@@ -1152,14 +1153,10 @@ def _creation_info(self, creation_info, element):
     def _station_magnitude_contributions(self, stat_contrib, element):
         for contrib in stat_contrib:
             contrib_el = etree.Element('stationMagnitudeContribution')
-            etree.SubElement(contrib_el, 'stationMagnitudeID').text = \
-                contrib.station_magnitude_id.id
-            if contrib.weight:
-                etree.SubElement(contrib_el, 'weight').text = \
-                    str(contrib.weight)
-            if contrib.residual:
-                etree.SubElement(contrib_el, 'residual').text = \
-                    str(contrib.residual)
+            self._str(contrib.station_magnitude_id.id, contrib_el,
+                      'stationMagnitudeID')
+            self._str(contrib.weight, contrib_el, 'weight')
+            self._str(contrib.residual, contrib_el, 'residual')
             self._extra(contrib, contrib_el)
             element.append(contrib_el)
 
@@ -1395,6 +1392,7 @@ def _origin(self, origin):
             if len(qu_el) > 0:
                 element.append(qu_el)
         self._str(origin.origin_type, element, 'type')
+        self._str(origin.region, element, 'region')
         self._str(origin.evaluation_mode, element, 'evaluationMode')
         self._str(origin.evaluation_status, element, 'evaluationStatus')
         self._comments(origin.comments, element)


Test patch:
diff --git a/obspy/io/quakeml/tests/data/quakeml_1.2_origin.xml b/obspy/io/quakeml/tests/data/quakeml_1.2_origin.xml
--- a/obspy/io/quakeml/tests/data/quakeml_1.2_origin.xml
+++ b/obspy/io/quakeml/tests/data/quakeml_1.2_origin.xml
@@ -50,6 +50,7 @@
           <maximumDistance>53.03</maximumDistance>
         </quality>
         <type>hypocenter</type>
+        <region>Kalamazoo</region>
         <evaluationMode>manual</evaluationMode>
         <evaluationStatus>preliminary</evaluationStatus>
         <comment id="smi:some/comment/reference">
diff --git a/obspy/io/quakeml/tests/data/quakeml_1.2_stationmagnitudecontributions.xml b/obspy/io/quakeml/tests/data/quakeml_1.2_stationmagnitudecontributions.xml
--- a/obspy/io/quakeml/tests/data/quakeml_1.2_stationmagnitudecontributions.xml
+++ b/obspy/io/quakeml/tests/data/quakeml_1.2_stationmagnitudecontributions.xml
@@ -15,8 +15,8 @@
         </stationMagnitudeContribution>
         <stationMagnitudeContribution>
             <stationMagnitudeID>smi:ch.ethz.sed/magnitude/station/881334</stationMagnitudeID>
-            <weight>0.55</weight>
-            <residual>0.11</residual>
+            <weight>0.0</weight>
+            <residual>0.0</residual>
         </stationMagnitudeContribution>
       </magnitude>
     </event>
diff --git a/obspy/io/quakeml/tests/test_quakeml.py b/obspy/io/quakeml/tests/test_quakeml.py
--- a/obspy/io/quakeml/tests/test_quakeml.py
+++ b/obspy/io/quakeml/tests/test_quakeml.py
@@ -159,6 +159,7 @@ def test_origin(self):
         self.assertEqual(
             origin.earth_model_id,
             ResourceIdentifier(id="smi:same/model/maeh"))
+        self.assertEqual(origin.region, 'Kalamazoo')
         self.assertEqual(origin.evaluation_mode, "manual")
         self.assertEqual(origin.evaluation_status, "preliminary")
         self.assertEqual(origin.origin_type, "hypocenter")
@@ -298,8 +299,8 @@ def test_station_magnitude_contribution(self):
         self.assertEqual(
             stat_contrib.station_magnitude_id.id,
             "smi:ch.ethz.sed/magnitude/station/881334")
-        self.assertEqual(stat_contrib.weight, 0.55)
-        self.assertEqual(stat_contrib.residual, 0.11)
+        self.assertEqual(stat_contrib.weight, 0.)
+        self.assertEqual(stat_contrib.residual, 0.)
 
         # exporting back to XML should result in the same document
         with open(filename, "rt") as fp: