mt-metadata 0.3.8__py2.py3-none-any.whl → 0.4.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mt-metadata might be problematic. Click here for more details.

Files changed (97) hide show
  1. mt_metadata/__init__.py +1 -1
  2. mt_metadata/base/helpers.py +84 -9
  3. mt_metadata/base/metadata.py +137 -65
  4. mt_metadata/data/transfer_functions/test.edi +28 -28
  5. mt_metadata/features/__init__.py +14 -0
  6. mt_metadata/features/coherence.py +303 -0
  7. mt_metadata/features/cross_powers.py +29 -0
  8. mt_metadata/features/fc_coherence.py +81 -0
  9. mt_metadata/features/feature.py +72 -0
  10. mt_metadata/features/feature_decimation_channel.py +26 -0
  11. mt_metadata/features/feature_fc.py +24 -0
  12. mt_metadata/{transfer_functions/processing/aurora/decimation.py → features/feature_fc_run.py} +9 -4
  13. mt_metadata/features/feature_ts.py +24 -0
  14. mt_metadata/{transfer_functions/processing/aurora/window.py → features/feature_ts_run.py} +11 -18
  15. mt_metadata/features/standards/__init__.py +6 -0
  16. mt_metadata/features/standards/base_feature.json +46 -0
  17. mt_metadata/features/standards/coherence.json +57 -0
  18. mt_metadata/features/standards/fc_coherence.json +57 -0
  19. mt_metadata/features/standards/feature_decimation_channel.json +68 -0
  20. mt_metadata/features/standards/feature_fc_run.json +35 -0
  21. mt_metadata/features/standards/feature_ts_run.json +35 -0
  22. mt_metadata/features/standards/feature_weighting_window.json +46 -0
  23. mt_metadata/features/standards/weight_kernel.json +46 -0
  24. mt_metadata/features/standards/weights.json +101 -0
  25. mt_metadata/features/test_helpers/channel_weight_specs_example.json +156 -0
  26. mt_metadata/features/weights/__init__.py +0 -0
  27. mt_metadata/features/weights/base.py +44 -0
  28. mt_metadata/features/weights/channel_weight_spec.py +209 -0
  29. mt_metadata/features/weights/feature_weight_spec.py +194 -0
  30. mt_metadata/features/weights/monotonic_weight_kernel.py +275 -0
  31. mt_metadata/features/weights/standards/__init__.py +6 -0
  32. mt_metadata/features/weights/standards/activation_monotonic_weight_kernel.json +38 -0
  33. mt_metadata/features/weights/standards/base.json +36 -0
  34. mt_metadata/features/weights/standards/channel_weight_spec.json +35 -0
  35. mt_metadata/features/weights/standards/composite.json +36 -0
  36. mt_metadata/features/weights/standards/feature_weight_spec.json +13 -0
  37. mt_metadata/features/weights/standards/monotonic_weight_kernel.json +49 -0
  38. mt_metadata/features/weights/standards/taper_monotonic_weight_kernel.json +16 -0
  39. mt_metadata/features/weights/taper_weight_kernel.py +60 -0
  40. mt_metadata/helper_functions.py +69 -0
  41. mt_metadata/timeseries/filters/channel_response.py +77 -37
  42. mt_metadata/timeseries/filters/coefficient_filter.py +6 -5
  43. mt_metadata/timeseries/filters/filter_base.py +11 -15
  44. mt_metadata/timeseries/filters/fir_filter.py +8 -1
  45. mt_metadata/timeseries/filters/frequency_response_table_filter.py +26 -11
  46. mt_metadata/timeseries/filters/helper_functions.py +0 -2
  47. mt_metadata/timeseries/filters/obspy_stages.py +4 -1
  48. mt_metadata/timeseries/filters/pole_zero_filter.py +9 -5
  49. mt_metadata/timeseries/filters/time_delay_filter.py +8 -1
  50. mt_metadata/timeseries/location.py +20 -5
  51. mt_metadata/timeseries/person.py +14 -7
  52. mt_metadata/timeseries/standards/person.json +1 -1
  53. mt_metadata/timeseries/standards/run.json +2 -2
  54. mt_metadata/timeseries/station.py +4 -2
  55. mt_metadata/timeseries/stationxml/__init__.py +5 -0
  56. mt_metadata/timeseries/stationxml/xml_channel_mt_channel.py +38 -27
  57. mt_metadata/timeseries/stationxml/xml_inventory_mt_experiment.py +16 -47
  58. mt_metadata/timeseries/stationxml/xml_station_mt_station.py +25 -24
  59. mt_metadata/transfer_functions/__init__.py +3 -0
  60. mt_metadata/transfer_functions/core.py +16 -11
  61. mt_metadata/transfer_functions/io/emtfxml/metadata/location.py +5 -0
  62. mt_metadata/transfer_functions/io/emtfxml/metadata/provenance.py +14 -3
  63. mt_metadata/transfer_functions/io/tools.py +2 -0
  64. mt_metadata/transfer_functions/io/zonge/metadata/header.py +1 -1
  65. mt_metadata/transfer_functions/io/zonge/metadata/standards/header.json +1 -1
  66. mt_metadata/transfer_functions/io/zonge/metadata/standards/job.json +2 -2
  67. mt_metadata/transfer_functions/io/zonge/zonge.py +19 -23
  68. mt_metadata/transfer_functions/processing/__init__.py +2 -1
  69. mt_metadata/transfer_functions/processing/aurora/__init__.py +2 -4
  70. mt_metadata/transfer_functions/processing/aurora/band.py +46 -125
  71. mt_metadata/transfer_functions/processing/aurora/channel_nomenclature.py +27 -20
  72. mt_metadata/transfer_functions/processing/aurora/decimation_level.py +324 -152
  73. mt_metadata/transfer_functions/processing/aurora/frequency_bands.py +230 -0
  74. mt_metadata/transfer_functions/processing/aurora/processing.py +3 -3
  75. mt_metadata/transfer_functions/processing/aurora/run.py +32 -7
  76. mt_metadata/transfer_functions/processing/aurora/standards/decimation_level.json +7 -73
  77. mt_metadata/transfer_functions/processing/aurora/stations.py +33 -4
  78. mt_metadata/transfer_functions/processing/fourier_coefficients/decimation.py +176 -177
  79. mt_metadata/transfer_functions/processing/fourier_coefficients/fc.py +11 -9
  80. mt_metadata/transfer_functions/processing/fourier_coefficients/standards/decimation.json +1 -111
  81. mt_metadata/transfer_functions/processing/short_time_fourier_transform.py +64 -0
  82. mt_metadata/transfer_functions/processing/standards/__init__.py +6 -0
  83. mt_metadata/transfer_functions/processing/standards/short_time_fourier_transform.json +94 -0
  84. mt_metadata/transfer_functions/processing/{aurora/standards/decimation.json → standards/time_series_decimation.json} +17 -6
  85. mt_metadata/transfer_functions/processing/{aurora/standards → standards}/window.json +13 -2
  86. mt_metadata/transfer_functions/processing/time_series_decimation.py +50 -0
  87. mt_metadata/transfer_functions/processing/window.py +118 -0
  88. mt_metadata/transfer_functions/tf/standards/transfer_function.json +1 -1
  89. mt_metadata/transfer_functions/tf/station.py +17 -1
  90. mt_metadata/utils/mttime.py +22 -3
  91. mt_metadata/utils/validators.py +4 -2
  92. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/METADATA +39 -15
  93. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/RECORD +97 -57
  94. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/WHEEL +1 -1
  95. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/AUTHORS.rst +0 -0
  96. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/LICENSE +0 -0
  97. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
1
+ """
2
+ Module containing FrequencyBands class representing a collection of Frequency Band objects.
3
+ """
4
+ from typing import Literal, Optional, Generator, Union, List
5
+ import pandas as pd
6
+ import numpy as np
7
+ import warnings
8
+ from loguru import logger
9
+
10
+ from . import Band
11
+
12
+
13
+ class FrequencyBands:
14
+ """
15
+ Collection of Band objects, typically used at a single decimation level.
16
+
17
+ Attributes
18
+ ----------
19
+ _band_edges : pd.DataFrame
20
+ DataFrame with columns ['lower_bound', 'upper_bound'] containing
21
+ frequency band boundaries
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ band_edges: Optional[Union[np.ndarray, pd.DataFrame]] = None,
27
+ ):
28
+ """
29
+ Parameters
30
+ ----------
31
+ band_edges : np.ndarray or pd.DataFrame, optional
32
+ If numpy array: 2D array with columns [lower_bound, upper_bound]
33
+ If DataFrame: Must have columns ['lower_bound', 'upper_bound']
34
+ """
35
+ if band_edges is not None:
36
+ self.band_edges = band_edges
37
+ else:
38
+ self._band_edges = pd.DataFrame(columns=['lower_bound', 'upper_bound'])
39
+
40
+ def __str__(self) -> str:
41
+ """Returns a Description of frequency bands"""
42
+ intro = "Frequency Bands:"
43
+ return f"{intro} \n{self._band_edges}"
44
+
45
+ def __repr__(self):
46
+ return self.__str__()
47
+
48
+ @property
49
+ def band_edges(self) -> pd.DataFrame:
50
+ """Get band edges as a DataFrame"""
51
+ return self._band_edges
52
+
53
+ @band_edges.setter
54
+ def band_edges(self, value: Union[np.ndarray, pd.DataFrame]) -> None:
55
+ """
56
+ Set band edges from either numpy array or DataFrame
57
+
58
+ Parameters
59
+ ----------
60
+ value : np.ndarray or pd.DataFrame
61
+ Band edge definitions
62
+ """
63
+ if isinstance(value, np.ndarray):
64
+ if value.ndim != 2 or value.shape[1] != 2:
65
+ raise ValueError("band_edges array must be 2D with shape (n_bands, 2)")
66
+ self._band_edges = pd.DataFrame(
67
+ value,
68
+ columns=['lower_bound', 'upper_bound']
69
+ )
70
+ elif isinstance(value, pd.DataFrame):
71
+ required_cols = ['lower_bound', 'upper_bound']
72
+ if not all(col in value.columns for col in required_cols):
73
+ raise ValueError(
74
+ f"DataFrame must contain columns {required_cols}"
75
+ )
76
+ self._band_edges = value[required_cols].copy()
77
+ else:
78
+ raise TypeError(
79
+ "band_edges must be numpy array or DataFrame"
80
+ )
81
+
82
+ # Reset index to ensure 0-based integer indexing
83
+ self._band_edges.reset_index(drop=True, inplace=True)
84
+
85
+ @property
86
+ def number_of_bands(self) -> int:
87
+ """Number of frequency bands"""
88
+ return len(self._band_edges)
89
+
90
+ @property
91
+ def array(self) -> np.ndarray:
92
+ """Get band edges as numpy array"""
93
+ return self._band_edges.values
94
+
95
+ def sort(self, by: str = "center_frequency", ascending: bool = True) -> None:
96
+ """
97
+ Sort bands by specified criterion.
98
+
99
+ Parameters
100
+ ----------
101
+ by : str
102
+ Criterion to sort by:
103
+ - "lower_bound": Sort by lower frequency bound
104
+ - "upper_bound": Sort by upper frequency bound
105
+ - "center_frequency": Sort by geometric center frequency (default)
106
+ ascending : bool
107
+ If True, sort in ascending order, else descending
108
+ """
109
+ if by in ["lower_bound", "upper_bound"]:
110
+ self._band_edges.sort_values(by=by, ascending=ascending, inplace=True)
111
+ elif by == "center_frequency":
112
+ centers = self.band_centers()
113
+ self._band_edges = self._band_edges.iloc[
114
+ np.argsort(centers)[::(-1 if not ascending else 1)]
115
+ ].reset_index(drop=True)
116
+ else:
117
+ raise ValueError(
118
+ f"Invalid sort criterion: {by}. Must be one of: "
119
+ "'lower_bound', 'upper_bound', 'center_frequency'"
120
+ )
121
+
122
+ def bands(
123
+ self,
124
+ direction: str = "increasing_frequency",
125
+ sortby: Optional[str] = None,
126
+ rtype: str = "list"
127
+ ) -> Union[List[Band], Generator[Band, None, None]]:
128
+ """
129
+ Generate Band objects in specified order.
130
+
131
+ Parameters
132
+ ----------
133
+ direction : str
134
+ Order of iteration: "increasing_frequency" or "increasing_period"
135
+ sortby : str, optional
136
+ Sort bands before iteration:
137
+ - "lower_bound": Sort by lower frequency bound
138
+ - "upper_bound": Sort by upper frequency bound
139
+ - "center_frequency": Sort by geometric center frequency
140
+ If None, uses existing order
141
+ rtype : str
142
+ Return type: "list" or "generator". Default is "list" for easier reuse.
143
+ Use "generator" for memory efficiency when bands are only iterated once.
144
+
145
+ Returns
146
+ -------
147
+ Union[List[Band], Generator[Band, None, None]]
148
+ Band objects for each frequency band, either as a list or generator
149
+ depending on rtype parameter.
150
+ """
151
+ if sortby is not None or direction == "increasing_period":
152
+ # Create a copy to avoid modifying original
153
+ temp_bands = FrequencyBands(self._band_edges.copy())
154
+ temp_bands.sort(
155
+ by=sortby or "center_frequency",
156
+ ascending=(direction == "increasing_frequency")
157
+ )
158
+ bands_to_iterate = temp_bands
159
+ else:
160
+ bands_to_iterate = self
161
+
162
+ # Create generator
163
+ def band_generator():
164
+ for idx in range(bands_to_iterate.number_of_bands):
165
+ yield bands_to_iterate.band(idx)
166
+
167
+ # Return as requested type
168
+ if rtype == "generator":
169
+ return band_generator()
170
+ elif rtype == "list":
171
+ return list(band_generator())
172
+ else:
173
+ raise ValueError("rtype must be either 'list' or 'generator'")
174
+
175
+ def band(self, i_band: int) -> Band:
176
+ """
177
+ Get specific frequency band.
178
+
179
+ Parameters
180
+ ----------
181
+ i_band : int
182
+ Index of band to return (zero-based)
183
+
184
+ Returns
185
+ -------
186
+ Band
187
+ Frequency band object
188
+ """
189
+ row = self._band_edges.iloc[i_band]
190
+ return Band(
191
+ frequency_min=row['lower_bound'],
192
+ frequency_max=row['upper_bound']
193
+ )
194
+
195
+ def band_centers(self, frequency_or_period: str = "frequency") -> np.ndarray:
196
+ """
197
+ Calculate center frequencies/periods for all bands.
198
+
199
+ Parameters
200
+ ----------
201
+ frequency_or_period : str
202
+ Return values in "frequency" (Hz) or "period" (s)
203
+
204
+ Returns
205
+ -------
206
+ np.ndarray
207
+ Center frequencies/periods for each band
208
+ """
209
+ band_centers = np.array([
210
+ self.band(i).center_frequency
211
+ for i in range(self.number_of_bands)
212
+ ])
213
+
214
+ if frequency_or_period == "period":
215
+ band_centers = 1.0 / band_centers
216
+
217
+ return band_centers
218
+
219
+ def validate(self) -> None:
220
+ """
221
+ Validate and potentially reorder bands based on center frequencies.
222
+ """
223
+ band_centers = self.band_centers()
224
+
225
+ # Check if band centers are monotonically increasing
226
+ if not np.all(band_centers[1:] > band_centers[:-1]):
227
+ logger.warning(
228
+ "Band centers are not monotonic. Attempting to reorganize bands."
229
+ )
230
+ self.sort(by="center_frequency")
@@ -52,6 +52,7 @@ class Processing(Base):
52
52
  """
53
53
  dictionary of decimations levels
54
54
 
55
+ TODO: replace this convoluted setter with the model used for DecimationLevel.bands setter.
55
56
  :param value: dict of decimation levels
56
57
  :type value: dict
57
58
 
@@ -75,7 +76,6 @@ class Processing(Base):
75
76
  for obj in value:
76
77
  if isinstance(value, DecimationLevel):
77
78
  self._decimations.append(obj)
78
-
79
79
  elif isinstance(obj, dict):
80
80
  level = DecimationLevel()
81
81
  level.from_dict(obj)
@@ -84,7 +84,7 @@ class Processing(Base):
84
84
  raise TypeError(
85
85
  f"List entry must be a DecimationLevel or dict object not {type(obj)}"
86
86
  )
87
-
87
+ # TODO: Add some doc describing the role of this weird check for a long string
88
88
  elif isinstance(value, str):
89
89
  if len(value) > 4:
90
90
  raise TypeError(f"Not sure what to do with {type(value)}")
@@ -216,7 +216,7 @@ class Processing(Base):
216
216
  ) # self.decimations_dict[key]
217
217
  decimation_obj.decimation.factor = d
218
218
  decimation_obj.decimation.sample_rate = sr
219
- decimation_obj.window.num_samples = num_samples_window[i_level]
219
+ decimation_obj.stft.window.num_samples = num_samples_window[i_level]
220
220
  frequencies = decimation_obj.fft_frequencies
221
221
 
222
222
  for low, high in band_edges:
@@ -1,8 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
+ Module for `aurora` Run metadata container with useful built-in methods.
4
+
3
5
  Created on Thu Feb 17 14:15:20 2022
4
6
 
5
7
  @author: jpeacock
8
+
6
9
  """
7
10
  # =============================================================================
8
11
  # Imports
@@ -12,6 +15,7 @@ from mt_metadata.base import get_schema, Base
12
15
  from .standards import SCHEMA_FN_PATHS
13
16
  from mt_metadata.timeseries import TimePeriod
14
17
  from .channel import Channel
18
+ from typing import Union
15
19
 
16
20
  # =============================================================================
17
21
  attr_dict = get_schema("run", SCHEMA_FN_PATHS)
@@ -20,6 +24,10 @@ class Run(Base):
20
24
  __doc__ = write_lines(attr_dict)
21
25
 
22
26
  def __init__(self, **kwargs):
27
+ """
28
+ Constructor.
29
+
30
+ """
23
31
  self._input = []
24
32
  self._output = []
25
33
  self._time_periods = []
@@ -27,16 +35,16 @@ class Run(Base):
27
35
  super().__init__(attr_dict=attr_dict, **kwargs)
28
36
 
29
37
  @property
30
- def input_channel_names(self):
38
+ def input_channel_names(self) -> list:
31
39
  """list of channel names"""
32
40
  return [ch.id for ch in self._input]
33
41
 
34
42
  @property
35
- def input_channels(self):
43
+ def input_channels(self) -> list:
36
44
  return self._input
37
45
 
38
46
  @input_channels.setter
39
- def input_channels(self, values):
47
+ def input_channels(self, values: Union[list, str, Channel, dict]) -> None:
40
48
  self._input = []
41
49
  if not isinstance(values, list):
42
50
  values = [values]
@@ -90,17 +98,34 @@ class Run(Base):
90
98
  return self._time_periods
91
99
 
92
100
  @time_periods.setter
93
- def time_periods(self, values):
101
+ def time_periods(self, values: Union[list, dict, TimePeriod]) -> None:
102
+ """
103
+ Sets self.time_periods
104
+
105
+ Parameters
106
+ ----------
107
+ values: Union[list, dict, TimePeriod]
108
+ If it is a list, the elements of the list must be TimePerid or dictionary representations of TimePeriods
109
+
110
+ """
94
111
  self._time_periods = []
95
112
  if not isinstance(values, list):
96
113
  values = [values]
97
114
 
98
115
  for item in values:
99
- if not isinstance(item, TimePeriod):
116
+ if isinstance(item, TimePeriod):
117
+ self._time_periods.append(item)
118
+ elif isinstance(item, dict):
119
+ try:
120
+ tp = TimePeriod()
121
+ tp.from_dict(item)
122
+ self._time_periods.append(tp)
123
+ except Exception as e:
124
+ msg = f"Could not unpack dict to TimePeriod, got exception {e}"
125
+ raise ValueError(msg)
126
+ else:
100
127
  raise TypeError(f"not sure what to do with type {type(item)}")
101
128
 
102
- self._time_periods.append(item)
103
-
104
129
  @property
105
130
  def channel_scale_factors(self):
106
131
  """
@@ -1,87 +1,21 @@
1
1
  {
2
- "anti_alias_filter": {
3
- "type": "string",
4
- "required": true,
5
- "style": "controlled vocabulary",
6
- "units": null,
7
- "description": "Name of anti alias filter to be applied",
8
- "options": ["deafult", "other"],
9
- "alias": [],
10
- "example": "default",
11
- "default": "default"
12
- },
13
- "recoloring": {
14
- "type": "bool",
15
- "required": true,
16
- "style": "free form",
17
- "units": null,
18
- "description": "Whether the data are recolored [True] or not [False].",
19
- "options": [],
20
- "alias": [],
21
- "example": true,
22
- "default": true
23
- },
24
- "min_num_stft_windows": {
2
+ "bands": {
25
3
  "type": "integer",
26
4
  "required": true,
27
- "style": "number",
5
+ "style": "name list",
28
6
  "units": null,
29
- "description": "How many FFT windows must be available for the time series to valid for STFT.",
7
+ "description": "List of bands",
30
8
  "options": [],
31
9
  "alias": [],
32
- "example": 4,
33
- "default": 2
34
- },
35
- "method": {
36
- "type": "string",
37
- "required": true,
38
- "style": "controlled vocabulary",
39
- "units": null,
40
- "description": "Fourier transform method",
41
- "options": ["fft", "wavelet", "other"],
42
- "alias": [],
43
- "example": "fft",
44
- "default": "fft"
45
- },
46
- "prewhitening_type": {
47
- "type": "string",
48
- "required": true,
49
- "style": "controlled vocabulary",
50
- "units": null,
51
- "description": "Prewhitening method to be applied",
52
- "options": ["first difference", "other"],
53
- "alias": [],
54
- "example": "first difference",
55
- "default": "first difference"
56
- },
57
- "extra_pre_fft_detrend_type": {
58
- "type": "string",
59
- "required": true,
60
- "style": "controlled vocabulary",
61
- "units": null,
62
- "description": "Extra Pre FFT detrend method to be applied",
63
- "options": ["linear", "other"],
64
- "alias": [],
65
- "example": "linear",
66
- "default": "linear"
67
- },
68
- "pre_fft_detrend_type": {
69
- "type": "string",
70
- "required": true,
71
- "style": "controlled vocabulary",
72
- "units": null,
73
- "description": "Pre FFT detrend method to be applied",
74
- "options": ["linear", "other"],
75
- "alias": [],
76
- "example": "linear",
77
- "default": "linear"
10
+ "example": "[]",
11
+ "default": null
78
12
  },
79
- "bands": {
13
+ "channel_weight_specs": {
80
14
  "type": "integer",
81
15
  "required": true,
82
16
  "style": "name list",
83
17
  "units": null,
84
- "description": "List of bands",
18
+ "description": "List of weighting schemes to use for TF processing for each output channel",
85
19
  "options": [],
86
20
  "alias": [],
87
21
  "example": "[]",
@@ -4,6 +4,9 @@ Created on Thu Feb 24 13:58:07 2022
4
4
 
5
5
  @author: jpeacock
6
6
  """
7
+ from loguru import logger
8
+ from typing import Union
9
+
7
10
  import pandas as pd
8
11
 
9
12
  # =============================================================================
@@ -49,15 +52,40 @@ class Stations(Base):
49
52
  return return_list
50
53
 
51
54
  @remote.setter
52
- def remote(self, rr_station):
55
+ def remote(self, rr_station: Union[list, dict]):
56
+ """
57
+ Method for unpacking rr_station info into mt_metadata object.
58
+
59
+ Developmnent Notes:
60
+ This function was raising an exception when trying to populate an aurora.Processing object
61
+ from a json.loads() dict.
62
+ TODO: add a description of input variable and use cases, ... it seems that we may not want
63
+ to support multiple rr stations yet.
64
+
65
+ Parameters
66
+ ----------
67
+ rr_station
68
+
69
+ Returns
70
+ -------
71
+
72
+ """
53
73
  self._remote = []
54
74
  if isinstance(rr_station, list):
55
75
  for item in rr_station:
56
- if not isinstance(item, Station):
76
+ if isinstance(item, Station):
77
+ self._remote.append(item)
78
+ elif isinstance(item, dict):
79
+ try:
80
+ remote = Station()
81
+ remote.from_dict(item)
82
+ self._remote.append(remote)
83
+ except Exception as e:
84
+ raise ValueError("could not unpack dict to a Station object")
85
+ else:
57
86
  raise TypeError(
58
87
  f"list item must be Station object not {type(item)}"
59
88
  )
60
- self._remote.append(item)
61
89
 
62
90
  elif isinstance(rr_station, dict):
63
91
  remote = Station()
@@ -69,9 +97,10 @@ class Stations(Base):
69
97
  rr_station.remote = True
70
98
  self._remote.append(rr_station)
71
99
 
72
- elif isinstance(rr_station, str):
100
+ elif isinstance(rr_station, str): # TODO: Add doc; what is this doing? This does not affect self._remote.
73
101
  if len(rr_station) > 4:
74
102
  raise ValueError(f"not sure to do with {type(rr_station)}")
103
+ # TODO: Add doc explaining what happens when rr_station is str of length 3.
75
104
 
76
105
  else:
77
106
  raise ValueError(f"not sure to do with {type(rr_station)}")