Repository: obspy/obspy

Issue:
A possible rotate error cause by invalid tr.id
### Avoid duplicates

- [X] I searched existing issues

### Bug Summary

There might be a bug when performing the```st.rotate() ``` method.


# Description

## Problem Overview
When using the st.rotate() method in ObsPy, an issue arises when the tr.id of a trace is invalid. This problem occurs when creating a trace using the [Trace class](https://docs.obspy.org/packages/autogen/obspy.core.trace.Trace.id.html#obspy.core.trace.Trace.id) or reading a SAC file from an external source. It appears that ObsPy does not validate the tr.id, leading to unexpected behavior.
```
from obspy.core.trace import Trace

# Create a trace with an invalid tr.id
meta = {'station': 'MANZ.', 'network': 'BW', 'channel': 'EHZ'}
tr = Trace(header=meta)
print(tr.id)  # Outputs 'BW.MANZ...EHZ'

# Performing st.rotate() causes issues when unpacking tr.id
net, sta, loc, cha = tr.id.split(".")
```

## Additional Context 
The problem becomes evident when using the st.rotate() method, which internally involves unpacking the components of tr.id. This can lead to errors and unexpected behavior due to the invalid format of tr.id.

The tricky thing is that, if we just set the tr.id by assignment, obspy will raise the error.

```
>>> tr.id = "invalid.trid"
```

```
Traceback (most recent call last):
  File "/Users/junliu/anaconda3/envs/obspy/lib/python3.11/site-packages/obspy/core/trace.py", line 905, in id
    net, sta, loc, cha = value.split(".")
    ^^^^^^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 4, got 2)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/junliu/anaconda3/envs/obspy/lib/python3.11/site-packages/obspy/core/trace.py", line 486, in __setattr__
    return super(Trace, self).__setattr__(key, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/junliu/anaconda3/envs/obspy/lib/python3.11/site-packages/obspy/core/trace.py", line 912, in id
    raise ValueError(msg)
ValueError: Not a valid SEED ID: 'invaild.trid'
```

And these debugging message is much more clear ( ValueError: Not a valid SEED ID: 'invaild.trid' )

## Proposed Solution

Consider adding validation checks for tr.id when creating instances of the Stream or Trace class. This would help ensure that tr.id follows a valid format, preventing unexpected errors during operations like st.rotate().




### Code to Reproduce

```python
from obspy.core import read
st = read()
st.write("RJOB", format = "SAC")


# Then modify the header of sacfiles by sac
# All those command execute by sac
# r RJOB*
# ch KSTNM RJOB..
# wh

# read those sac file and perform the st.rotate()
st = read("RJOB*")
st.rotate(method = "NE->RT")
```


### Error Traceback

```Python traceback
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/junliu/anaconda3/envs/obspy/lib/python3.11/site-packages/obspy/core/stream.py", line 2756, in rotate
    net, sta, loc, cha = tr.id.split(".")
    ^^^^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 4)
```


### ObsPy Version?

1.4.0

### Operating System?

MacOS(M1)

### Python Version?

3.11

### Installation Method?

conda


Discussion:
You used a dot '.' in your station code which is not allowed character. You can only use uppercase [A-Z] and numeric [0-9] characters in SEED files (see the [SEED Manual](http://www.fdsn.org/pdf/SEEDManual_V2.4.pdf)) or you can also add a dash '-' according to the (already accepted) [FDSN Source Identifier proposal](http://docs.fdsn.org/projects/source-identifiers/en/v1.0/). The question is whether ObsPy should test the correctness of station identifiers.
> You used a dot '.' in your station code which is not allowed character. You can only use uppercase [A-Z] and numeric [0-9] characters in SEED files (see the [SEED Manual](http://www.fdsn.org/pdf/SEEDManual_V2.4.pdf)) or you can also add a dash '-' according to the (already accepted) [FDSN Source Identifier proposal](http://docs.fdsn.org/projects/source-identifiers/en/v1.0/). The question is whether ObsPy should test the correctness of station identifiers.

You are absolutely right. The core idea is that ObsPy does not check the correctness of station identifiers when reading the data from SAC files, or it should raise a warning at least. Otherwise, there should be a try/except sentence before every ```tr.id.split(".")```  sentence.

In fact, I guess that ObsPy wants to check the validity of the identifier, according to the behavior of assigning an invalid identifier. That's the reason why I think it is a bug.
I guess we could show a warning if a NSLC code gets set with a dot in it. But if I understand the report correctly, we kind of gracefully error out with a traceback that should make clear what the problem is (too many dots in the trace id).

But yes, ideally we should change this, probably best solution would be to not rely on `tr.id.split()` but rather use the actual attributes `tr.stats.station` etc. Then it should also work doing the operation with dots in some of the station/... codes.

Resolving patch:
diff --git a/obspy/core/stream.py b/obspy/core/stream.py
--- a/obspy/core/stream.py
+++ b/obspy/core/stream.py
@@ -2755,7 +2755,10 @@ def rotate(self, method, back_azimuth=None, inclination=None,
         # handled separately
         seed_id_groups = {}
         for tr in self:
-            net, sta, loc, cha = tr.id.split(".")
+            net = tr.stats.network
+            sta = tr.stats.station
+            loc = tr.stats.location
+            cha = tr.stats.channel
             if not len(cha):
                 msg = "Channel code must be at least one character long."
                 raise ValueError(msg)


Test patch:
diff --git a/obspy/core/tests/test_stream.py b/obspy/core/tests/test_stream.py
--- a/obspy/core/tests/test_stream.py
+++ b/obspy/core/tests/test_stream.py
@@ -2003,6 +2003,11 @@ def test_rotate(self):
         Testing the rotate method.
         """
         st = read()
+        # test for #3373
+        # add some weird dots in station/location code
+        for tr in st:
+            tr.stats.station = 'RJ.OB'
+            tr.stats.location = '0.'
         st += st.copy()
         st[3:].normalize()
         st2 = st.copy()