tsp 1.8.1__py3-none-any.whl → 1.10.2__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.
Files changed (127) hide show
  1. tsp/__init__.py +11 -11
  2. tsp/__meta__.py +1 -1
  3. tsp/concatenation.py +159 -153
  4. tsp/core.py +1306 -1162
  5. tsp/data/2023-01-06_755-test-Dataset_2031-Constant_Over_Interval-Hourly-Ground_Temperature-Thermistor_Automated.timeserie.csv +4 -4
  6. tsp/data/2023-01-06_755-test.metadata.txt +208 -208
  7. tsp/data/NTGS_example_csv.csv +6 -6
  8. tsp/data/NTGS_example_slash_dates.csv +6 -6
  9. tsp/data/NTGS_gtr_example_excel.xlsx +0 -0
  10. tsp/data/example_geotop.csv +5240 -5240
  11. tsp/data/example_gtnp.csv +1298 -1298
  12. tsp/data/example_permos.csv +7 -7
  13. tsp/data/ntgs-db-multi.txt +3872 -0
  14. tsp/data/ntgs-db-single.txt +2251 -0
  15. tsp/data/test_geotop_has_space.txt +5 -5
  16. tsp/data/tsp_format_long.csv +10 -0
  17. tsp/data/tsp_format_wide_1.csv +7 -0
  18. tsp/data/tsp_format_wide_2.csv +7 -0
  19. tsp/dataloggers/AbstractReader.py +43 -43
  20. tsp/dataloggers/FG2.py +110 -110
  21. tsp/dataloggers/GP5W.py +114 -114
  22. tsp/dataloggers/Geoprecision.py +34 -34
  23. tsp/dataloggers/HOBO.py +930 -914
  24. tsp/dataloggers/RBRXL800.py +190 -190
  25. tsp/dataloggers/RBRXR420.py +371 -308
  26. tsp/dataloggers/Vemco.py +84 -0
  27. tsp/dataloggers/__init__.py +15 -15
  28. tsp/dataloggers/logr.py +196 -115
  29. tsp/dataloggers/test_files/004448.DAT +2543 -2543
  30. tsp/dataloggers/test_files/004531.DAT +17106 -17106
  31. tsp/dataloggers/test_files/004531.HEX +3587 -3587
  32. tsp/dataloggers/test_files/004534.HEX +3587 -3587
  33. tsp/dataloggers/test_files/010252.dat +1731 -1731
  34. tsp/dataloggers/test_files/010252.hex +1739 -1739
  35. tsp/dataloggers/test_files/010274.hex +1291 -1291
  36. tsp/dataloggers/test_files/010278.hex +3544 -3544
  37. tsp/dataloggers/test_files/012064.dat +1286 -1286
  38. tsp/dataloggers/test_files/012064.hex +1294 -1294
  39. tsp/dataloggers/test_files/012064_modified_start.hex +1294 -0
  40. tsp/dataloggers/test_files/012081.hex +3532 -3532
  41. tsp/dataloggers/test_files/013138_recovery_stamp.hex +1123 -0
  42. tsp/dataloggers/test_files/014037-2007.hex +95 -0
  43. tsp/dataloggers/test_files/019360_20160918_1146_SlumpIslandTopofHill.hex +11253 -0
  44. tsp/dataloggers/test_files/019360_20160918_1146_SlumpIslandTopofHill.xls +0 -0
  45. tsp/dataloggers/test_files/07B1592.DAT +1483 -1483
  46. tsp/dataloggers/test_files/07B1592.HEX +1806 -1806
  47. tsp/dataloggers/test_files/07B4450.DAT +2234 -2234
  48. tsp/dataloggers/test_files/07B4450.HEX +2559 -2559
  49. tsp/dataloggers/test_files/2022018_2025-09-18T22-16-16.txt +36 -0
  50. tsp/dataloggers/test_files/2022018_2025-09-18T22-16-16_raw.csv +2074 -0
  51. tsp/dataloggers/test_files/2022018_2025-09-18T22-16-16_temp.csv +2074 -0
  52. tsp/dataloggers/test_files/2025004_2025-12-02T17-07-28_cfg.txt +30 -0
  53. tsp/dataloggers/test_files/2025004_2025-12-02T17-07-28_raw.csv +35 -0
  54. tsp/dataloggers/test_files/2025004_2025-12-02T17-07-28_temp.csv +35 -0
  55. tsp/dataloggers/test_files/204087.xlsx +0 -0
  56. tsp/dataloggers/test_files/Asc-1455As02.000 +2982 -0
  57. tsp/dataloggers/test_files/Asc-1456As02.000 +2992 -0
  58. tsp/dataloggers/test_files/Asc-1457As02.000 +2917 -0
  59. tsp/dataloggers/test_files/BGC_BH15_019362_20140610_1253.hex +1729 -0
  60. tsp/dataloggers/test_files/Bin2944.csv +759 -0
  61. tsp/dataloggers/test_files/Bin5494.csv +2972 -0
  62. tsp/dataloggers/test_files/Bin6786.csv +272 -0
  63. tsp/dataloggers/test_files/FG2_399.csv +9881 -9881
  64. tsp/dataloggers/test_files/GP5W.csv +1121 -1121
  65. tsp/dataloggers/test_files/GP5W_260.csv +1884 -1884
  66. tsp/dataloggers/test_files/GP5W_270.csv +2210 -2210
  67. tsp/dataloggers/test_files/H08-030-08_HOBOware.csv +998 -998
  68. tsp/dataloggers/test_files/Minilog-II-T_350763_20190711_1.csv +2075 -0
  69. tsp/dataloggers/test_files/Minilog-II-T_350769_20190921_1.csv +6384 -0
  70. tsp/dataloggers/test_files/Minilog-II-T_354284_20190921_1.csv +4712 -0
  71. tsp/dataloggers/test_files/Minilog-T_7943_20140920_1.csv +5826 -0
  72. tsp/dataloggers/test_files/Minilog-T_8979_20140806_1.csv +2954 -0
  73. tsp/dataloggers/test_files/Minilog-T_975_20110824_1.csv +4343 -0
  74. tsp/dataloggers/test_files/RBR_01.dat +1046 -1046
  75. tsp/dataloggers/test_files/RBR_02.dat +2426 -2426
  76. tsp/dataloggers/test_files/RI03b_062831_20240905_1801.rsk +0 -0
  77. tsp/dataloggers/test_files/RI03b_062831_20240905_1801.xlsx +0 -0
  78. tsp/dataloggers/test_files/RSTDT2055.csv +2152 -2152
  79. tsp/dataloggers/test_files/U23-001_HOBOware.csv +1001 -1001
  80. tsp/dataloggers/test_files/hobo-negative-2.txt +6396 -6396
  81. tsp/dataloggers/test_files/hobo-negative-3.txt +5593 -5593
  82. tsp/dataloggers/test_files/hobo-positive-number-1.txt +1000 -1000
  83. tsp/dataloggers/test_files/hobo-positive-number-2.csv +1003 -1003
  84. tsp/dataloggers/test_files/hobo-positive-number-3.csv +1133 -1133
  85. tsp/dataloggers/test_files/hobo-positive-number-4.csv +1209 -1209
  86. tsp/dataloggers/test_files/hobo2.csv +8702 -8702
  87. tsp/dataloggers/test_files/hobo_1_AB.csv +21732 -21732
  88. tsp/dataloggers/test_files/hobo_1_AB_Details.txt +133 -133
  89. tsp/dataloggers/test_files/hobo_1_AB_classic.csv +4373 -4373
  90. tsp/dataloggers/test_files/hobo_1_AB_defaults.csv +21732 -21732
  91. tsp/dataloggers/test_files/hobo_1_AB_minimal.txt +1358 -1358
  92. tsp/dataloggers/test_files/hobo_1_AB_var2.csv +3189 -3189
  93. tsp/dataloggers/test_files/hobo_1_AB_var3.csv +2458 -2458
  94. tsp/dataloggers/test_files/logR_ULogC16-32_1.csv +106 -106
  95. tsp/dataloggers/test_files/logR_ULogC16-32_2.csv +100 -100
  96. tsp/dataloggers/test_files/mon_3_Ta_2010-08-18_2013-02-08.txt +21724 -21724
  97. tsp/dataloggers/test_files/rbr_001.dat +1133 -1133
  98. tsp/dataloggers/test_files/rbr_001.hex +1139 -1139
  99. tsp/dataloggers/test_files/rbr_001_no_comment.dat +1132 -1132
  100. tsp/dataloggers/test_files/rbr_001_no_comment.hex +1138 -1138
  101. tsp/dataloggers/test_files/rbr_002.dat +1179 -1179
  102. tsp/dataloggers/test_files/rbr_002.hex +1185 -1185
  103. tsp/dataloggers/test_files/rbr_003.hex +1292 -1292
  104. tsp/dataloggers/test_files/rbr_xl_001.DAT +1105 -1105
  105. tsp/dataloggers/test_files/rbr_xl_002.DAT +1126 -1126
  106. tsp/dataloggers/test_files/rbr_xl_003.DAT +4622 -4622
  107. tsp/dataloggers/test_files/rbr_xl_003.HEX +3587 -3587
  108. tsp/gtnp.py +148 -148
  109. tsp/labels.py +3 -3
  110. tsp/misc.py +90 -90
  111. tsp/physics.py +101 -101
  112. tsp/plots/static.py +388 -374
  113. tsp/readers.py +829 -548
  114. tsp/standardization/__init__.py +0 -0
  115. tsp/standardization/metadata.py +95 -0
  116. tsp/standardization/metadata_ref.py +0 -0
  117. tsp/standardization/validator.py +535 -0
  118. tsp/time.py +45 -45
  119. tsp/tspwarnings.py +27 -15
  120. tsp/utils.py +131 -101
  121. tsp/version.py +1 -1
  122. {tsp-1.8.1.dist-info → tsp-1.10.2.dist-info}/METADATA +95 -86
  123. tsp-1.10.2.dist-info/RECORD +132 -0
  124. {tsp-1.8.1.dist-info → tsp-1.10.2.dist-info}/licenses/LICENSE +674 -674
  125. {tsp-1.8.1.dist-info → tsp-1.10.2.dist-info}/top_level.txt +1 -0
  126. tsp-1.8.1.dist-info/RECORD +0 -94
  127. {tsp-1.8.1.dist-info → tsp-1.10.2.dist-info}/WHEEL +0 -0
tsp/plots/static.py CHANGED
@@ -1,374 +1,388 @@
1
- import numpy as np
2
- import warnings
3
-
4
- import matplotlib.dates as mdates
5
- import matplotlib.cm as cm
6
- from matplotlib import pyplot as plt
7
- from matplotlib.figure import Figure
8
- from matplotlib.colors import ListedColormap
9
- from typing import Optional
10
-
11
- try:
12
- from scipy.interpolate import griddata
13
- except ModuleNotFoundError:
14
- warnings.warn("Missing scipy module. Some functionality will be limited.")
15
-
16
- from typing import Union
17
-
18
- import tsp
19
-
20
-
21
- def trumpet_curve(depth, t_max, t_min, t_mean,
22
- title:str="", max_depth:Optional[float]=None,
23
- t_units:str=u'\N{DEGREE SIGN} C', d_units:str="m",
24
- data_completeness=None,
25
- min_completeness:Optional[float]=None) -> Figure:
26
- """Plot a trumpet curve
27
-
28
- The function returns a matplotlib Figure object. To show the figure, you must call the `show()` method.
29
-
30
- Parameters
31
- ----------
32
- depth : numpy.ndarray
33
- A d-length array of depths at which temperature values are
34
- t_max : numpy.ndarray
35
- A d-length array of temperature values representing the maximum temperatures over the period at each of the depths.
36
- t_min : numpy.ndarray
37
- A d-length array of temperature values representing the minimum temperatures over the period at each of the depths.
38
- t_mean : str
39
- A d-length array of temperature values representing the mean temperatures over the period at each of the depths.
40
- title : str, optional
41
- A title for the figure, by default ""
42
- max_depth : float, optional
43
- If provided, limits the maximum y-axis extent of the plot, by default None
44
- t_units : unicode, optional
45
- Units for the x-axis (assumed to be temperature), by default u'\N{DEGREE SIGN} C'
46
- d_units : str, optional
47
- Units for the y axis (depth), by default "m"
48
- data_completeness : numpy.ndarray
49
- A d-length array of representing data completeness as a fraction (e.g. 0 to 1) for each of the averaging periods ()
50
- min_completeness : float
51
- Minimum data completeness to be included in the temperature envelope
52
-
53
- Returns
54
- -------
55
- Figure
56
- A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
57
-
58
- Raises
59
- ------
60
- ValueError
61
- _description_
62
- """
63
- ## Sanity checks and data
64
- if data_completeness is None:
65
- data_completeness = np.ones_like(depth)
66
- if min_completeness is None:
67
- min_completeness = 0.001
68
-
69
- if not len(depth) == len(t_max) == len(t_min) == len(t_mean) == len(data_completeness):
70
- raise ValueError("Length of input arrays must be equal")
71
-
72
- depth = - np.abs(depth)
73
-
74
- ## Create figure
75
- fig, ax1 = plt.subplots()
76
-
77
- ## Create artists
78
- # TODO: https://stackoverflow.com/questions/45176584/dotted-lines-instead-of-a-missing-value-in-matplotlib
79
- m = np.where(data_completeness >= min_completeness, True, False)
80
- if m.any():
81
- line_max = ax1.plot(t_max[m], depth[m], color='red', gid="ln-max-temperature")
82
- line_min = ax1.plot(t_min[m], depth[m], color='blue', gid="ln-min-temperature")
83
- line_mean = ax1.plot(t_mean[m], depth[m], color='black', gid="ln-mean-temperature")
84
-
85
- alphas = np.where((data_completeness / min_completeness) < 1, 0.1 + (0.7 * data_completeness / min_completeness), 1)
86
-
87
- marker_max = ax1.scatter(t_max, depth, marker='.', c=alpha([1,0,0], alphas), gid="pt-max-temperature")
88
- marker_min = ax1.scatter(t_min, depth, marker='.', c=alpha([0,0,1], alphas), gid="pt-min-temperature")
89
- marker_mean = ax1.scatter(t_mean, depth, marker='.', c=alpha([0,0,0], alphas), gid="pt-mean-temperature")
90
-
91
- surface = ax1.hlines(y=0.0, xmin=-100, xmax=100, linewidth=0.5, linestyles='dotted', color='grey')
92
- zero = ax1.vlines(x=0.0, ymin=-100, ymax=100, linewidth=0.5, linestyles='dotted', color='grey')
93
-
94
- ## Set axis properties
95
- ax1.set_ybound(upper=1, lower=min(depth) - 3)
96
-
97
- if max_depth:
98
- ax1.set_ybound(lower=-abs(max_depth))
99
-
100
- ax1.set_xbound(lower=min(t_min) - 3, upper=max(t_max) + 3)
101
-
102
- ## Set axis labels
103
- ax1.set_xlabel(f"Temperature [{t_units}]")
104
- ax1.set_ylabel(f"Depth [{d_units}]")
105
- ax1.set_title(title)
106
-
107
- return fig
108
-
109
-
110
- def colour_contour(depths, times, values, title="", colours: "Union[str, list]"='symmetric', contour:list=[], label_contour=False, max_depth=None, gap_fill=False,
111
- d_units="m", **kwargs) -> Figure:
112
- """Create a colour-contour plot.
113
-
114
- The x-axis is time and the y-axis is depth. Data values are interpolated and coloured.
115
-
116
- Parameters
117
- ----------
118
- depths : numpy.ndarray
119
- A d-length array of depths at which measurements are collected.
120
- times : numpy.ndarray
121
- A t-length array of python datetimes at which measurements are collected
122
- values : numpy.ndarray
123
- An array with shape (t,d) of values at each depth-time coordinate
124
- title : str, optional
125
- A title for the figure, by default ""
126
- colours : Union[str, list], optional
127
- Either a list of colours to be used for the colour bar, or one of:
128
- * **symmetric**: ensure colour switch is centered at 0C
129
- * **dynamic**: Maximize dynamic range
130
- * **basic**: distinguish unfrozen, warm (>-2C) and
131
- ,by default 'symmetric'
132
- contour : list, optional
133
- A list of float values. If provided, draw contours at each of those values, by default []
134
- label_contour : bool, optional
135
- Whether or not to label contour lines. Ignored if `contour` is empty, by default False
136
- max_depth : float, optional
137
- If provided, limits the maximum y-axis extent of the plot, by default None
138
- gap_fill : bool, optional
139
- _description_, by default False
140
- d_units : str, optional
141
- Units for the y axis (depth), by default "m"
142
-
143
- Returns
144
- -------
145
- Figure
146
- A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
147
- """
148
- t = tsp.TSP(times, depths, values)
149
-
150
- # Extract x, y and z (array) values
151
- X = t.times
152
- Y = -abs(t.depths)
153
-
154
- if gap_fill:
155
- try:
156
- smoothed = griddata(points = np.stack([t.long.dropna()['time'].values.astype(float),
157
- t.long.dropna()['depth'].values]).transpose(),
158
- values = t.long.dropna()['temperature_in_ground'].values,
159
- xi = np.stack([t.long['time'].values.astype(float),
160
- t.long['depth'].values]).transpose(),
161
- rescale=True, method='linear')
162
- except NameError:
163
- warnings.warn("Missing scipy library. Could not do gap filling.")
164
- gap_fill = False
165
- Z = np.array(t.wide.drop('time', axis=1)).transpose()
166
-
167
- Z = smoothed.reshape(len(depths), len(values))
168
-
169
- else:
170
- Z = np.array(t.wide.drop('time', axis=1)).transpose()
171
-
172
- # Set up plot
173
- fig, ax1 = plt.subplots()
174
-
175
- clev = contour_levels(Z, colours, step=1)
176
-
177
- # Add data
178
- if colours == 'basic':
179
- co = ["darkblue", "lightblue", "lightgreen", "lightred"]
180
- cmap = None
181
- else:
182
- co = None
183
- cmap = plt.cm.coolwarm
184
-
185
- cs = ax1.contourf(X, Y, Z, levels=clev, cmap=cmap, colors=co)
186
- fig.colorbar(cs, ticks = np.arange(-25,25,5))
187
-
188
- if len(contour) > 0:
189
- cs2 = ax1.contour(X, Y, Z, levels = contour, colors='k', linewidths = 1)
190
- if label_contour:
191
- plt.clabel(cs2, fontsize=8, inline=1, fmt="%1.0f")
192
-
193
- # Set axis properties
194
- if max_depth:
195
- ax1.set_ybound(lower=-abs(max_depth))
196
-
197
- ax1.xaxis.set_major_formatter(mdates.ConciseDateFormatter(mdates.AutoDateLocator()))
198
- fig.autofmt_xdate()
199
- plt.subplots_adjust(bottom = 0.2, top = 0.95, left = 0.2, right = 0.95)
200
-
201
- # Set axis labels
202
- ax1.set_xlabel('Time')
203
- ax1.set_ylabel(f"Depth [{d_units}]")
204
- ax1.set_title(title)
205
-
206
-
207
- return fig
208
-
209
-
210
- def time_series(depths, times, values, title='', d_units='m', t_units=u'\N{DEGREE SIGN} C', legend=True) -> Figure:
211
- """Create a time-series plot
212
-
213
- Using time as the X axis and data values as the y axis. Depths are plotted as their own lines.
214
-
215
- Parameters
216
- ----------
217
- depths : numpy.ndarray
218
- 1-d list or array of datetimes with length d.
219
- times : numpy.ndarray
220
- 1-d list or array of datetimes with length t.
221
- values : array
222
- An array of data values with shape (t,d).
223
- title : str, optional
224
- A title for the plot, by default ''
225
- d_units : str, optional
226
- Units of the depths variable, by default 'm'
227
- t_units : str, optional
228
- Units of the temperature variable, by default u'\N{DEGREE SIGN} C'
229
-
230
- Returns
231
- -------
232
- Figure
233
- A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
234
- """
235
-
236
- # Set up plot
237
- fig, ax = plt.subplots()
238
-
239
- # Add data elements
240
- lines = []
241
- for i, d in enumerate(depths):
242
- line_i, = ax.plot(times, values[:,i], lw=1, label=f'{d} {d_units}')
243
- lines.append(line_i)
244
-
245
- if legend:
246
- box = ax.get_position()
247
- ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
248
- fig.subplots_adjust(right=0.8) # shrink plot to make space
249
-
250
- leg = ax.legend(loc='center left', bbox_to_anchor=(1.04, 0.5), fancybox=True, shadow=True)
251
-
252
- lined = {} # Will map legend lines to original lines.
253
- for legline, label, origline in zip(leg.get_lines(), leg.get_texts(), lines):
254
- legline.set_picker(True) # Enable picking on the legend line.
255
- label.set_picker(True) # Enable picking on the legend label.
256
- lined[legline] = [origline, [label]]
257
- lined[label] = [origline, [legline]]
258
-
259
- on_pick = create_legend_picker(fig, lined)
260
- on_tilde = create_tilde_toggle(fig, lined)
261
- fig.canvas.mpl_connect('pick_event', on_pick)
262
- fig.canvas.mpl_connect('key_press_event', on_tilde)
263
-
264
- zero = ax.hlines(y=0.0, xmin=min(times), xmax=max(times), linewidth=0.5, linestyles='dotted', color='grey')
265
- # Set axis properties
266
-
267
- # Set axis labels
268
- ax.set_xlabel('Time')
269
- ax.set_ylabel(f"Temperature [{t_units}]")
270
- ax.set_title(title)
271
-
272
- return fig
273
-
274
-
275
- def profile_evolution(depths, times, values, P:int=100, n:int=10):
276
- """ Plot sample of temperature profiles over time
277
-
278
- Parameters
279
- ----------
280
- depths : array-like
281
- Depths of the temperature profile
282
- times : array-like
283
- Times of the temperature profile
284
- values : array-like
285
- Temperature values of the temperature profile
286
- P : int, optional
287
- Percentage of the time series to plot, starting from the end, by default 100
288
- n : int, optional
289
- Number of profiles to plot, evenly spaced over time period to plot, by default 10
290
- """
291
- cmap = cm.get_cmap('winter')
292
- clist = cmap(np.arange(0,1,1/10))
293
-
294
- fig, ax = plt.subplots()
295
-
296
- p = 100 - P
297
- lastP = (p*(len(times) // 100))
298
-
299
- true_depths = -np.abs(depths)
300
- plot_times = times[lastP:][::len(times[lastP:]) // n][:n]
301
- plot_temps = values[lastP:,][::len(times[lastP:]) // n, :][:n,]
302
-
303
- for i in range(n):
304
- ax.plot(plot_temps[i,:], true_depths, color=clist[i],
305
- alpha=0.5, label=f"{plot_times[i].year}")
306
-
307
- ax.legend(fontsize="8")
308
- ax.vlines(0, ymin=min(true_depths), ymax=max(true_depths), linewidth=0.5, color='black')
309
-
310
- return fig
311
-
312
-
313
- def alpha(rgb, alpha):
314
- rgb = np.atleast_1d(rgb)
315
- alpha = np.atleast_1d(alpha)
316
- rgba = np.zeros((len(alpha), 4))
317
- rgba[:,3] = alpha
318
- rgba[:,0:3] = rgb
319
- return rgba
320
-
321
-
322
- def contour_levels(data, levels: "Union[str,list]", step=1) -> np.ndarray:
323
- if levels == "dynamic":
324
- return np.arange(np.nanmin(data), np.nanmax(data), step)
325
-
326
- elif levels == "symmetric":
327
- return np.arange(min(np.nanmin(data), -np.nanmax(data) + 1),
328
- max(-np.nanmin(data) - 1, np.nanmax(data)), step)
329
- elif levels == 'basic':
330
- return np.array([min(-5, np.nanmin(data)), -2, 0, max(1, np.nanmax(data))])
331
- else:
332
- try:
333
- lev = np.array(levels, dtype='float')
334
- return lev
335
- except Exception:
336
- raise TypeError("Contour levels not properly specified")
337
-
338
-
339
- def create_legend_picker(fig, lined) -> object:
340
-
341
- def on_pick(event):
342
- # On the pick event, find the original line corresponding to the legend
343
- # proxy line, and toggle its visibility.
344
- legline = event.artist
345
- origline = lined[legline][0]
346
- other_toggles = lined[legline][1]
347
- visible = not origline.get_visible()
348
- origline.set_visible(visible)
349
- # Change the alpha on the line in the legend so we can see what lines
350
- # have been toggled.
351
- legline.set_alpha(1.0 if visible else 0.2)
352
- for t in other_toggles:
353
- t.set_alpha(1.0 if visible else 0.2)
354
- fig.canvas.draw()
355
-
356
- return on_pick
357
-
358
- def create_tilde_toggle(fig, lined) -> object:
359
-
360
- def on_click(event):
361
- # on the "`" keypress, toggle lines off if any are on.
362
- # on the "`" keypress, toggle lines on if all are off.
363
- if event.key == '`':
364
- visible = False
365
- for togglable, [origline, other_toggles] in lined.items():
366
- visible = visible or origline.get_visible()
367
- for togglable, [origline, other_toggles] in lined.items():
368
- origline.set_visible(not visible)
369
- togglable.set_alpha(1.0 if not visible else 0.2)
370
- for t in other_toggles:
371
- t.set_alpha(1.0 if not visible else 0.2)
372
- fig.canvas.draw()
373
-
374
- return on_click
1
+ import numpy as np
2
+ import warnings
3
+
4
+ import matplotlib.dates as mdates
5
+ import matplotlib.cm as cm
6
+ from matplotlib import pyplot as plt
7
+ from matplotlib.figure import Figure
8
+ from matplotlib.colors import ListedColormap
9
+ from typing import Optional
10
+
11
+ try:
12
+ from scipy.interpolate import griddata
13
+ except ModuleNotFoundError:
14
+ warnings.warn("Missing scipy module. Some functionality will be limited.")
15
+
16
+ from typing import Union
17
+
18
+ import tsp
19
+
20
+
21
+ def trumpet_curve(depth, t_max, t_min, t_mean,
22
+ title:str="", max_depth:Optional[float]=None,
23
+ t_units:str=u'\N{DEGREE SIGN} C', d_units:str="m",
24
+ data_completeness=None,
25
+ min_completeness:Optional[float]=None) -> Figure:
26
+ """Plot a trumpet curve
27
+
28
+ The function returns a matplotlib Figure object. To show the figure, you must call the `show()` method.
29
+
30
+ Parameters
31
+ ----------
32
+ depth : numpy.ndarray
33
+ A d-length array of depths at which temperature values are
34
+ t_max : numpy.ndarray
35
+ A d-length array of temperature values representing the maximum temperatures over the period at each of the depths.
36
+ t_min : numpy.ndarray
37
+ A d-length array of temperature values representing the minimum temperatures over the period at each of the depths.
38
+ t_mean : str
39
+ A d-length array of temperature values representing the mean temperatures over the period at each of the depths.
40
+ title : str, optional
41
+ A title for the figure, by default ""
42
+ max_depth : float, optional
43
+ If provided, limits the maximum y-axis extent of the plot, by default None
44
+ t_units : unicode, optional
45
+ Units for the x-axis (assumed to be temperature), by default u'\N{DEGREE SIGN} C'
46
+ d_units : str, optional
47
+ Units for the y axis (depth), by default "m"
48
+ data_completeness : numpy.ndarray
49
+ A d-length array of representing data completeness as a fraction (e.g. 0 to 1) for each of the averaging periods ()
50
+ min_completeness : float
51
+ Minimum data completeness to be included in the temperature envelope
52
+
53
+ Returns
54
+ -------
55
+ Figure
56
+ A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ _description_
62
+ """
63
+ ## Sanity checks and data
64
+ if data_completeness is None:
65
+ data_completeness = np.ones_like(depth)
66
+ if min_completeness is None:
67
+ min_completeness = 0.001
68
+
69
+ if not len(depth) == len(t_max) == len(t_min) == len(t_mean) == len(data_completeness):
70
+ raise ValueError("Length of input arrays must be equal")
71
+
72
+ depth = - np.abs(depth)
73
+
74
+ ## Create figure
75
+ fig, ax1 = plt.subplots()
76
+
77
+ ## Create artists
78
+ # TODO: https://stackoverflow.com/questions/45176584/dotted-lines-instead-of-a-missing-value-in-matplotlib
79
+ m = np.where(data_completeness >= min_completeness, True, False)
80
+ if m.any():
81
+ line_max = ax1.plot(t_max[m], depth[m], color='red', gid="ln-max-temperature")
82
+ line_min = ax1.plot(t_min[m], depth[m], color='blue', gid="ln-min-temperature")
83
+ line_mean = ax1.plot(t_mean[m], depth[m], color='black', gid="ln-mean-temperature")
84
+
85
+ alphas = np.where((data_completeness / min_completeness) < 1, 0.1 + (0.7 * data_completeness / min_completeness), 1)
86
+
87
+ marker_max = ax1.scatter(t_max, depth, marker='.', c=alpha([1,0,0], alphas), gid="pt-max-temperature")
88
+ marker_min = ax1.scatter(t_min, depth, marker='.', c=alpha([0,0,1], alphas), gid="pt-min-temperature")
89
+ marker_mean = ax1.scatter(t_mean, depth, marker='.', c=alpha([0,0,0], alphas), gid="pt-mean-temperature")
90
+
91
+ surface = ax1.hlines(y=0.0, xmin=-100, xmax=100, linewidth=0.5, linestyles='dotted', color='grey')
92
+ zero = ax1.vlines(x=0.0, ymin=-100, ymax=100, linewidth=0.5, linestyles='dotted', color='grey')
93
+
94
+ ## Set axis properties
95
+ ax1.set_ybound(upper=1, lower=min(depth) - 3)
96
+
97
+ if max_depth:
98
+ ax1.set_ybound(lower=-abs(max_depth))
99
+
100
+ ax1.set_xbound(lower=min(t_min) - 3, upper=max(t_max) + 3)
101
+
102
+ ## Set axis labels
103
+ ax1.set_xlabel(f"Temperature [{t_units}]")
104
+ ax1.set_ylabel(f"Depth [{d_units}]")
105
+ ax1.set_title(title)
106
+
107
+ return fig
108
+
109
+
110
+ def colour_contour(depths, times, values, title="", colours: "Union[str, list]"='symmetric', contour:list=[], label_contour=False, max_depth=None, gap_fill=False,
111
+ d_units="m", **kwargs) -> Figure:
112
+ """Create a colour-contour plot.
113
+
114
+ The x-axis is time and the y-axis is depth. Data values are interpolated and coloured.
115
+
116
+ Parameters
117
+ ----------
118
+ depths : numpy.ndarray
119
+ A d-length array of depths at which measurements are collected.
120
+ times : numpy.ndarray
121
+ A t-length array of python datetimes at which measurements are collected
122
+ values : numpy.ndarray
123
+ An array with shape (t,d) of values at each depth-time coordinate
124
+ title : str, optional
125
+ A title for the figure, by default ""
126
+ colours : Union[str, list], optional
127
+ Either a list of colours to be used for the colour bar, or one of:
128
+ * **symmetric**: ensure colour switch is centered at 0C
129
+ * **dynamic**: Maximize dynamic range
130
+ * **basic**: distinguish unfrozen, warm (>-2C) and
131
+ ,by default 'symmetric'
132
+ contour : list, optional
133
+ A list of float values. If provided, draw contours at each of those values, by default []
134
+ label_contour : bool, optional
135
+ Whether or not to label contour lines. Ignored if `contour` is empty, by default False
136
+ max_depth : float, optional
137
+ If provided, limits the maximum y-axis extent of the plot, by default None
138
+ gap_fill : bool, optional
139
+ _description_, by default False
140
+ d_units : str, optional
141
+ Units for the y axis (depth), by default "m"
142
+
143
+ Returns
144
+ -------
145
+ Figure
146
+ A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
147
+ """
148
+ t = tsp.TSP(times, depths, values)
149
+
150
+ # Extract x, y and z (array) values
151
+ X = t.times
152
+ Y = -abs(t.depths)
153
+
154
+ if gap_fill:
155
+ try:
156
+ smoothed = griddata(points = np.stack([t.long.dropna()['time'].values.astype(float),
157
+ t.long.dropna()['depth'].values]).transpose(),
158
+ values = t.long.dropna()['temperature_in_ground'].values,
159
+ xi = np.stack([t.long['time'].values.astype(float),
160
+ t.long['depth'].values]).transpose(),
161
+ rescale=True, method='linear')
162
+ except NameError:
163
+ warnings.warn("Missing scipy library. Could not do gap filling.")
164
+ gap_fill = False
165
+ Z = np.array(t.wide.drop('time', axis=1)).transpose()
166
+
167
+ Z = smoothed.reshape(len(depths), len(values))
168
+
169
+ else:
170
+ Z = np.array(t.wide.drop('time', axis=1)).transpose()
171
+
172
+ # Set up plot
173
+ fig, ax1 = plt.subplots()
174
+
175
+ clev = contour_levels(Z, colours, step=1)
176
+
177
+ # Add data
178
+ if colours == 'basic':
179
+ co = ["darkblue", "lightblue", "lightgreen", "lightred"]
180
+ cmap = None
181
+ else:
182
+ co = None
183
+ cmap = plt.cm.coolwarm
184
+
185
+ cs = ax1.contourf(X, Y, Z, levels=clev, cmap=cmap, colors=co)
186
+ fig.colorbar(cs, ticks = np.arange(-25,25,5))
187
+
188
+ if len(contour) > 0:
189
+ cs2 = ax1.contour(X, Y, Z, levels = contour, colors='k', linewidths = 1)
190
+ if label_contour:
191
+ plt.clabel(cs2, fontsize=8, inline=1, fmt="%1.0f")
192
+
193
+ # Set axis properties
194
+ if max_depth:
195
+ ax1.set_ybound(lower=-abs(max_depth))
196
+
197
+ ax1.xaxis.set_major_formatter(mdates.ConciseDateFormatter(mdates.AutoDateLocator()))
198
+ fig.autofmt_xdate()
199
+ plt.subplots_adjust(bottom = 0.2, top = 0.95, left = 0.2, right = 0.95)
200
+
201
+ # Set axis labels
202
+ ax1.set_xlabel('Time')
203
+ ax1.set_ylabel(f"Depth [{d_units}]")
204
+ ax1.set_title(title)
205
+
206
+
207
+ return fig
208
+
209
+
210
+ def time_series(depths, times, values, title='', d_units='m', t_units=u'\N{DEGREE SIGN} C', legend=True) -> Figure:
211
+ """Create a time-series plot
212
+
213
+ Using time as the X axis and data values as the y axis. Depths are plotted as their own lines.
214
+
215
+ Parameters
216
+ ----------
217
+ depths : numpy.ndarray
218
+ 1-d list or array of datetimes with length d.
219
+ times : numpy.ndarray
220
+ 1-d list or array of datetimes with length t.
221
+ values : array
222
+ An array of data values with shape (t,d).
223
+ title : str, optional
224
+ A title for the plot, by default ''
225
+ d_units : str, optional
226
+ Units of the depths variable, by default 'm'
227
+ t_units : str, optional
228
+ Units of the temperature variable, by default u'\N{DEGREE SIGN} C'
229
+
230
+ Returns
231
+ -------
232
+ Figure
233
+ A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
234
+ """
235
+
236
+ # Set up plot
237
+ fig, ax = plt.subplots()
238
+
239
+ # Add data elements
240
+ lines = []
241
+ for i, d in enumerate(depths):
242
+ line_i, = ax.plot(times, values[:,i], lw=1, label=f'{d} {d_units}')
243
+ lines.append(line_i)
244
+
245
+ if legend:
246
+ box = ax.get_position()
247
+ ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
248
+ fig.subplots_adjust(right=0.8) # shrink plot to make space
249
+
250
+ leg = ax.legend(loc='center left', bbox_to_anchor=(1.04, 0.5), fancybox=True, shadow=True)
251
+
252
+ lined = {} # Will map legend lines to original lines.
253
+ for legline, label, origline in zip(leg.get_lines(), leg.get_texts(), lines):
254
+ legline.set_picker(True) # Enable picking on the legend line.
255
+ label.set_picker(True) # Enable picking on the legend label.
256
+ lined[legline] = [origline, [label]]
257
+ lined[label] = [origline, [legline]]
258
+
259
+ on_pick = create_legend_picker(fig, lined)
260
+ on_tilde = create_tilde_toggle(fig, lined)
261
+ fig.canvas.mpl_connect('pick_event', on_pick)
262
+ fig.canvas.mpl_connect('key_press_event', on_tilde)
263
+
264
+ zero = ax.hlines(y=0.0, xmin=min(times), xmax=max(times), linewidth=0.5, linestyles='dotted', color='grey')
265
+ # Set axis properties
266
+
267
+ # Set axis labels
268
+ ax.set_xlabel('Time')
269
+ ax.set_ylabel(f"Temperature [{t_units}]")
270
+ ax.set_title(title)
271
+
272
+ return fig
273
+
274
+
275
+ def profile_evolution(depths, times, values, P:int=100, n:int=10):
276
+ """ Plot sample of temperature profiles over time
277
+
278
+ Parameters
279
+ ----------
280
+ depths : array-like
281
+ Depths of the temperature profile
282
+ times : array-like
283
+ Times of the temperature profile
284
+ values : array-like
285
+ Temperature values of the temperature profile
286
+ P : int, optional
287
+ Percentage of the time series to plot, starting from the end, by default 100
288
+ n : int, optional
289
+ Number of profiles to plot, evenly spaced over time period to plot, by default 10
290
+ """
291
+ cmap = cm.get_cmap('winter')
292
+ clist = cmap(np.arange(0,1,1/10))
293
+
294
+ fig, ax = plt.subplots()
295
+
296
+ p = 100 - P
297
+ lastP = (p*(len(times) // 100))
298
+
299
+ true_depths = -np.abs(depths)
300
+ plot_times = times[lastP:][::len(times[lastP:]) // n][:n]
301
+ plot_temps = values[lastP:,][::len(times[lastP:]) // n, :][:n,]
302
+
303
+ for i in range(n):
304
+ ax.plot(plot_temps[i,:], true_depths, color=clist[i],
305
+ alpha=0.5, label=f"{plot_times[i].year}")
306
+
307
+ ax.legend(fontsize="8")
308
+ ax.vlines(0, ymin=min(true_depths), ymax=max(true_depths), linewidth=0.5, color='black')
309
+
310
+ return fig
311
+
312
+
313
+ def alpha(rgb, alpha):
314
+ rgb = np.atleast_1d(rgb)
315
+ alpha = np.atleast_1d(alpha)
316
+ rgba = np.zeros((len(alpha), 4))
317
+ rgba[:,3] = alpha
318
+ rgba[:,0:3] = rgb
319
+ return rgba
320
+
321
+
322
+ def contour_levels(data, levels: "Union[str,list]", step=1) -> np.ndarray:
323
+ if levels == "dynamic":
324
+ return np.arange(np.nanmin(data), np.nanmax(data), step)
325
+
326
+ elif levels == "symmetric":
327
+ return np.arange(min(np.nanmin(data), -np.nanmax(data) + 1),
328
+ max(-np.nanmin(data) - 1, np.nanmax(data)), step)
329
+ elif levels == 'basic':
330
+ return np.array([min(-5, np.nanmin(data)), -2, 0, max(1, np.nanmax(data))])
331
+ else:
332
+ try:
333
+ lev = np.array(levels, dtype='float')
334
+ return lev
335
+ except Exception:
336
+ raise TypeError("Contour levels not properly specified")
337
+
338
+
339
+ def create_legend_picker(fig, lined) -> object:
340
+
341
+ def on_pick(event):
342
+ # On the pick event, find the original line corresponding to the legend
343
+ # proxy line, and toggle its visibility.
344
+ legline = event.artist
345
+ origline = lined[legline][0]
346
+ other_toggles = lined[legline][1]
347
+ visible = not origline.get_visible()
348
+ origline.set_visible(visible)
349
+ # Change the alpha on the line in the legend so we can see what lines
350
+ # have been toggled.
351
+ legline.set_alpha(1.0 if visible else 0.2)
352
+ for t in other_toggles:
353
+ t.set_alpha(1.0 if visible else 0.2)
354
+ fig.canvas.draw()
355
+
356
+ return on_pick
357
+
358
+ def create_tilde_toggle(fig, lined) -> object:
359
+
360
+ def on_click(event):
361
+ # on the "`" keypress, toggle lines off if any are on.
362
+ # on the "`" keypress, toggle lines on if all are off.
363
+ if event.key == '`':
364
+ visible = False
365
+ for togglable, [origline, other_toggles] in lined.items():
366
+ visible = visible or origline.get_visible()
367
+ for togglable, [origline, other_toggles] in lined.items():
368
+ origline.set_visible(not visible)
369
+ togglable.set_alpha(1.0 if not visible else 0.2)
370
+ for t in other_toggles:
371
+ t.set_alpha(1.0 if not visible else 0.2)
372
+ fig.canvas.draw()
373
+
374
+ return on_click
375
+
376
+ def _plot_overlay(fig, t):
377
+ version = tsp.__version__
378
+ site_id = t.site_id if t.site_id else ""
379
+ latitude = f"{t.latitude:.4f}" if t.latitude else ""
380
+ longitude = f"{t.longitude:.4f}" if t.longitude else ""
381
+ file = t.metadata.get('_source_file', '') if t.metadata else ""
382
+ SITE = f"Site: {site_id}" if site_id else None
383
+ COORDS = f"Coords: ({latitude}, {longitude})" if latitude and longitude else None
384
+ info = [s for s in [SITE, COORDS] if s]
385
+ FILE = f"File: {file}" if file else ""
386
+ overlay_text = f'TSP v{version} \n {" | ".join(info)} \n {FILE}'
387
+ fig.text(0.95, 0.01, overlay_text, ha='right', va='bottom', fontsize=8, color='gray', alpha=0.5)
388
+ return fig