tsp 1.7.3__py3-none-any.whl → 1.7.7__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 tsp might be problematic. Click here for more details.

Files changed (91) hide show
  1. tsp/__init__.py +11 -11
  2. tsp/__meta__.py +1 -1
  3. tsp/core.py +1035 -1010
  4. tsp/data/2023-01-06_755-test-Dataset_2031-Constant_Over_Interval-Hourly-Ground_Temperature-Thermistor_Automated.timeserie.csv +4 -4
  5. tsp/data/2023-01-06_755-test.metadata.txt +208 -208
  6. tsp/data/NTGS_example_csv.csv +6 -0
  7. tsp/data/NTGS_example_slash_dates.csv +6 -0
  8. tsp/data/example_geotop.csv +5240 -5240
  9. tsp/data/example_gtnp.csv +1298 -1298
  10. tsp/data/example_permos.csv +8 -0
  11. tsp/data/test_geotop_has_space.txt +5 -5
  12. tsp/dataloggers/AbstractReader.py +43 -43
  13. tsp/dataloggers/FG2.py +110 -110
  14. tsp/dataloggers/GP5W.py +114 -114
  15. tsp/dataloggers/Geoprecision.py +34 -34
  16. tsp/dataloggers/HOBO.py +914 -914
  17. tsp/dataloggers/RBRXL800.py +190 -190
  18. tsp/dataloggers/RBRXR420.py +308 -308
  19. tsp/dataloggers/__init__.py +15 -15
  20. tsp/dataloggers/logr.py +115 -115
  21. tsp/dataloggers/test_files/004448.DAT +2543 -2543
  22. tsp/dataloggers/test_files/004531.DAT +17106 -17106
  23. tsp/dataloggers/test_files/004531.HEX +3587 -3587
  24. tsp/dataloggers/test_files/004534.HEX +3587 -3587
  25. tsp/dataloggers/test_files/010252.dat +1731 -1731
  26. tsp/dataloggers/test_files/010252.hex +1739 -1739
  27. tsp/dataloggers/test_files/010274.hex +1291 -1291
  28. tsp/dataloggers/test_files/010278.hex +3544 -3544
  29. tsp/dataloggers/test_files/012064.dat +1286 -1286
  30. tsp/dataloggers/test_files/012064.hex +1294 -1294
  31. tsp/dataloggers/test_files/012081.hex +3532 -3532
  32. tsp/dataloggers/test_files/07B1592.DAT +1483 -1483
  33. tsp/dataloggers/test_files/07B1592.HEX +1806 -1806
  34. tsp/dataloggers/test_files/07B4450.DAT +2234 -2234
  35. tsp/dataloggers/test_files/07B4450.HEX +2559 -2559
  36. tsp/dataloggers/test_files/CSc_CR1000_1.dat +295 -0
  37. tsp/dataloggers/test_files/FG2_399.csv +9881 -9881
  38. tsp/dataloggers/test_files/GP5W.csv +1121 -1121
  39. tsp/dataloggers/test_files/GP5W_260.csv +1884 -1884
  40. tsp/dataloggers/test_files/GP5W_270.csv +2210 -2210
  41. tsp/dataloggers/test_files/H08-030-08_HOBOware.csv +998 -998
  42. tsp/dataloggers/test_files/RBR_01.dat +1046 -1046
  43. tsp/dataloggers/test_files/RBR_02.dat +2426 -2426
  44. tsp/dataloggers/test_files/RSTDT2055.csv +2152 -2152
  45. tsp/dataloggers/test_files/U23-001_HOBOware.csv +1001 -1001
  46. tsp/dataloggers/test_files/hobo-negative-2.txt +6396 -6396
  47. tsp/dataloggers/test_files/hobo-negative-3.txt +5593 -5593
  48. tsp/dataloggers/test_files/hobo-positive-number-1.txt +1000 -1000
  49. tsp/dataloggers/test_files/hobo-positive-number-2.csv +1003 -1003
  50. tsp/dataloggers/test_files/hobo-positive-number-3.csv +1133 -1133
  51. tsp/dataloggers/test_files/hobo-positive-number-4.csv +1209 -1209
  52. tsp/dataloggers/test_files/hobo2.csv +8702 -8702
  53. tsp/dataloggers/test_files/hobo_1_AB.csv +21732 -21732
  54. tsp/dataloggers/test_files/hobo_1_AB_Details.txt +133 -133
  55. tsp/dataloggers/test_files/hobo_1_AB_classic.csv +4373 -4373
  56. tsp/dataloggers/test_files/hobo_1_AB_defaults.csv +21732 -21732
  57. tsp/dataloggers/test_files/hobo_1_AB_minimal.txt +1358 -1358
  58. tsp/dataloggers/test_files/hobo_1_AB_var2.csv +3189 -3189
  59. tsp/dataloggers/test_files/hobo_1_AB_var3.csv +2458 -2458
  60. tsp/dataloggers/test_files/logR_ULogC16-32_1.csv +106 -106
  61. tsp/dataloggers/test_files/logR_ULogC16-32_2.csv +100 -100
  62. tsp/dataloggers/test_files/mon_3_Ta_2010-08-18_2013-02-08.txt +21724 -21724
  63. tsp/dataloggers/test_files/rbr_001.dat +1133 -1133
  64. tsp/dataloggers/test_files/rbr_001.hex +1139 -1139
  65. tsp/dataloggers/test_files/rbr_001_no_comment.dat +1132 -1132
  66. tsp/dataloggers/test_files/rbr_001_no_comment.hex +1138 -1138
  67. tsp/dataloggers/test_files/rbr_002.dat +1179 -1179
  68. tsp/dataloggers/test_files/rbr_002.hex +1185 -1185
  69. tsp/dataloggers/test_files/rbr_003.hex +1292 -1292
  70. tsp/dataloggers/test_files/rbr_003.xls +0 -0
  71. tsp/dataloggers/test_files/rbr_xl_001.DAT +1105 -1105
  72. tsp/dataloggers/test_files/rbr_xl_002.DAT +1126 -1126
  73. tsp/dataloggers/test_files/rbr_xl_003.DAT +4622 -4622
  74. tsp/dataloggers/test_files/rbr_xl_003.HEX +3587 -3587
  75. tsp/gtnp.py +148 -141
  76. tsp/labels.py +3 -3
  77. tsp/misc.py +90 -90
  78. tsp/physics.py +101 -101
  79. tsp/plots/static.py +374 -305
  80. tsp/readers.py +548 -536
  81. tsp/scratch.py +6 -0
  82. tsp/time.py +45 -45
  83. tsp/tspwarnings.py +15 -0
  84. tsp/utils.py +101 -101
  85. tsp/version.py +1 -1
  86. {tsp-1.7.3.dist-info → tsp-1.7.7.dist-info}/LICENSE +674 -674
  87. {tsp-1.7.3.dist-info → tsp-1.7.7.dist-info}/METADATA +9 -5
  88. tsp-1.7.7.dist-info/RECORD +95 -0
  89. {tsp-1.7.3.dist-info → tsp-1.7.7.dist-info}/WHEEL +5 -5
  90. tsp-1.7.3.dist-info/RECORD +0 -89
  91. {tsp-1.7.3.dist-info → tsp-1.7.7.dist-info}/top_level.txt +0 -0
tsp/plots/static.py CHANGED
@@ -1,305 +1,374 @@
1
- import numpy as np
2
- import warnings
3
-
4
- import matplotlib.dates as mdates
5
- from matplotlib import pyplot as plt
6
- from matplotlib.figure import Figure
7
- from typing import Optional
8
-
9
- try:
10
- from scipy.interpolate import griddata
11
- except ModuleNotFoundError:
12
- warnings.warn("Missing scipy module. Some functionality will be limited.")
13
-
14
- from typing import Union
15
-
16
- import tsp
17
-
18
-
19
- def trumpet_curve(depth, t_max, t_min, t_mean,
20
- title:str="", max_depth:Optional[float]=None,
21
- t_units:str=u'\N{DEGREE SIGN} C', d_units:str="m",
22
- data_completeness=None,
23
- min_completeness:Optional[float]=None) -> Figure:
24
- """Plot a trumpet curve
25
-
26
- The function returns a matplotlib Figure object. To show the figure, you must call the `show()` method.
27
-
28
- Parameters
29
- ----------
30
- depth : numpy.ndarray
31
- A d-length array of depths at which temperature values are
32
- t_max : numpy.ndarray
33
- A d-length array of temperature values representing the maximum temperatures over the period at each of the depths.
34
- t_min : numpy.ndarray
35
- A d-length array of temperature values representing the minimum temperatures over the period at each of the depths.
36
- t_mean : str
37
- A d-length array of temperature values representing the mean temperatures over the period at each of the depths.
38
- title : str, optional
39
- A title for the figure, by default ""
40
- max_depth : float, optional
41
- If provided, limits the maximum y-axis extent of the plot, by default None
42
- t_units : unicode, optional
43
- Units for the x-axis (assumed to be temperature), by default u'\N{DEGREE SIGN} C'
44
- d_units : str, optional
45
- Units for the y axis (depth), by default "m"
46
- data_completeness : numpy.ndarray
47
- A d-length array of representing data completeness as a fraction (e.g. 0 to 1) for each of the averaging periods ()
48
- min_completeness : float
49
- Minimum data completeness to be included in the temperature envelope
50
-
51
- Returns
52
- -------
53
- Figure
54
- A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
55
-
56
- Raises
57
- ------
58
- ValueError
59
- _description_
60
- """
61
- ## Sanity checks and data
62
- if data_completeness is None:
63
- data_completeness = np.ones_like(depth)
64
- if min_completeness is None:
65
- min_completeness = 0.001
66
-
67
- if not len(depth) == len(t_max) == len(t_min) == len(t_mean) == len(data_completeness):
68
- raise ValueError("Length of input arrays must be equal")
69
-
70
- depth = - np.abs(depth)
71
-
72
- ## Create figure
73
- fig, ax1 = plt.subplots()
74
-
75
- ## Create artists
76
- # TODO: https://stackoverflow.com/questions/45176584/dotted-lines-instead-of-a-missing-value-in-matplotlib
77
- m = np.where(data_completeness >= min_completeness, True, False)
78
- if m.any():
79
- line_max = ax1.plot(t_max[m], depth[m], color='red', gid="ln-max-temperature")
80
- line_min = ax1.plot(t_min[m], depth[m], color='blue', gid="ln-min-temperature")
81
- line_mean = ax1.plot(t_mean[m], depth[m], color='black', gid="ln-mean-temperature")
82
-
83
- alphas = np.where((data_completeness / min_completeness) < 1, 0.1 + (0.7 * data_completeness / min_completeness), 1)
84
-
85
- marker_max = ax1.scatter(t_max, depth, marker='.', c=alpha([1,0,0], alphas), gid="pt-max-temperature")
86
- marker_min = ax1.scatter(t_min, depth, marker='.', c=alpha([0,0,1], alphas), gid="pt-min-temperature")
87
- marker_mean = ax1.scatter(t_mean, depth, marker='.', c=alpha([0,0,0], alphas), gid="pt-mean-temperature")
88
-
89
- surface = ax1.hlines(y=0.0, xmin=-100, xmax=100, linewidth=0.5, linestyles='dotted', color='grey')
90
- zero = ax1.vlines(x=0.0, ymin=-100, ymax=100, linewidth=0.5, linestyles='dotted', color='grey')
91
-
92
- ## Set axis properties
93
- ax1.set_ybound(upper=1, lower=min(depth) - 3)
94
-
95
- if max_depth:
96
- ax1.set_ybound(lower=-abs(max_depth))
97
-
98
- ax1.set_xbound(lower=min(t_min) - 3, upper=max(t_max) + 3)
99
-
100
- ## Set axis labels
101
- ax1.set_xlabel(f"Temperature [{t_units}]")
102
- ax1.set_ylabel(f"Depth [{d_units}]")
103
- ax1.set_title(title)
104
-
105
- return fig
106
-
107
-
108
- def colour_contour(depths, times, values, title="", colours: "Union[str, list]"='symmetric', contour:list=[], label_contour=False, max_depth=None, gap_fill=False,
109
- d_units="m", **kwargs) -> Figure:
110
- """Create a colour-contour plot.
111
-
112
- The x-axis is time and the y-axis is depth. Data values are interpolated and coloured.
113
-
114
- Parameters
115
- ----------
116
- depths : numpy.ndarray
117
- A d-length array of depths at which measurements are collected.
118
- times : numpy.ndarray
119
- A t-length array of python datetimes at which measurements are collected
120
- values : numpy.ndarray
121
- An array with shape (t,d) of values at each depth-time coordinate
122
- title : str, optional
123
- A title for the figure, by default ""
124
- colours : Union[str, list], optional
125
- Either a list of colours to be used for the colour bar, or one of:
126
- * **symmetric**:
127
- * **dynamic**:
128
- ,by default 'symmetric'
129
- contour : list, optional
130
- A list of float values. If provided, draw contours at each of those values, by default []
131
- label_contour : bool, optional
132
- Whether or not to label contour lines. Ignored if `contour` is empty, by default False
133
- max_depth : float, optional
134
- If provided, limits the maximum y-axis extent of the plot, by default None
135
- gap_fill : bool, optional
136
- _description_, by default False
137
- d_units : str, optional
138
- Units for the y axis (depth), by default "m"
139
-
140
- Returns
141
- -------
142
- Figure
143
- A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
144
- """
145
- t = tsp.TSP(times, depths, values)
146
-
147
- # Extract x, y and z (array) values
148
- X = t.times
149
- Y = -abs(t.depths)
150
-
151
- if gap_fill:
152
- try:
153
- smoothed = griddata(points = np.stack([t.long.dropna()['time'].values.astype(float),
154
- t.long.dropna()['depth'].values]).transpose(),
155
- values = t.long.dropna()['temperature_in_ground'].values,
156
- xi = np.stack([t.long['time'].values.astype(float),
157
- t.long['depth'].values]).transpose(),
158
- rescale=True, method='linear')
159
- except NameError:
160
- warnings.warn("Missing scipy library. Could not do gap filling.")
161
- gap_fill = False
162
- Z = np.array(t.wide.drop('time', axis=1)).transpose()
163
-
164
- Z = smoothed.reshape(len(depths), len(values))
165
-
166
- else:
167
- Z = np.array(t.wide.drop('time', axis=1)).transpose()
168
-
169
- # Set up plot
170
- fig, ax1 = plt.subplots()
171
-
172
- clev = contour_levels(Z, colours, step=1)
173
-
174
- # Add data
175
- cs = ax1.contourf(X, Y, Z, levels=clev, cmap=plt.cm.coolwarm)
176
- fig.colorbar(cs, ticks = np.arange(-25,25,5))
177
-
178
- if len(contour) > 0:
179
- cs2 = ax1.contour(X, Y, Z, levels = contour, colors='k', linewidths = 1)
180
- if label_contour:
181
- plt.clabel(cs2, fontsize=8, inline=1, fmt="%1.0f")
182
-
183
- # Set axis properties
184
- if max_depth:
185
- ax1.set_ybound(lower=-abs(max_depth))
186
-
187
- ax1.xaxis.set_major_formatter(mdates.ConciseDateFormatter(mdates.AutoDateLocator()))
188
- fig.autofmt_xdate()
189
- plt.subplots_adjust(bottom = 0.2, top = 0.95, left = 0.2, right = 0.95)
190
-
191
- # Set axis labels
192
- ax1.set_xlabel('Time')
193
- ax1.set_ylabel(f"Depth [{d_units}]")
194
- ax1.set_title(title)
195
-
196
-
197
- return fig
198
-
199
-
200
- def time_series(depths, times, values, title='', d_units='m', t_units=u'\N{DEGREE SIGN} C', legend=True) -> Figure:
201
- """Create a time-series plot
202
-
203
- Using time as the X axis and data values as the y axis. Depths are plotted as their own lines.
204
-
205
- Parameters
206
- ----------
207
- depths : numpy.ndarray
208
- 1-d list or array of datetimes with length d.
209
- times : numpy.ndarray
210
- 1-d list or array of datetimes with length t.
211
- values : array
212
- An array of data values with shape (t,d).
213
- title : str, optional
214
- A title for the plot, by default ''
215
- d_units : str, optional
216
- Units of the depths variable, by default 'm'
217
- t_units : str, optional
218
- Units of the temperature variable, by default u'\N{DEGREE SIGN} C'
219
-
220
- Returns
221
- -------
222
- Figure
223
- A matplotlib Figure. Note that to show the figure you must call the `show()` method or `matplotlib.pyplot.show()`.
224
- """
225
-
226
- # Set up plot
227
- fig, ax = plt.subplots()
228
-
229
- # Add data elements
230
- lines = []
231
- for i, d in enumerate(depths):
232
- line_i, = ax.plot(times, values[:,i], lw=1, label=f'{d} {d_units}')
233
- lines.append(line_i)
234
-
235
- if legend:
236
- box = ax.get_position()
237
- ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
238
- fig.subplots_adjust(right=0.8) # shrink plot to make space
239
-
240
- leg = ax.legend(loc='center left', bbox_to_anchor=(1.04, 0.5), fancybox=True, shadow=True)
241
-
242
- lined = {} # Will map legend lines to original lines.
243
- for legline, origline in zip(leg.get_lines(), lines):
244
- legline.set_picker(True) # Enable picking on the legend line.
245
- lined[legline] = origline
246
-
247
- on_pick = create_legend_picker(fig, lined)
248
- fig.canvas.mpl_connect('pick_event', on_pick)
249
-
250
- zero = ax.hlines(y=0.0, xmin=min(times), xmax=max(times), linewidth=0.5, linestyles='dotted', color='grey')
251
- # Set axis properties
252
-
253
- # Set axis labels
254
- ax.set_xlabel('Time')
255
- ax.set_ylabel(f"Temperature [{t_units}]")
256
- ax.set_title(title)
257
-
258
- return fig
259
-
260
-
261
-
262
-
263
- def alpha(rgb, alpha):
264
- rgb = np.atleast_1d(rgb)
265
- alpha = np.atleast_1d(alpha)
266
- rgba = np.zeros((len(alpha), 4))
267
- rgba[:,3] = alpha
268
- rgba[:,0:3] = rgb
269
- return rgba
270
-
271
-
272
- def contour_levels(data, levels: "Union[str,list]", step=1) -> "np.ndarray":
273
- if levels == "dynamic":
274
- return np.arange(np.nanmin(data), np.nanmax(data), step)
275
-
276
- elif levels == "symmetric":
277
- return np.arange(min(np.nanmin(data), -np.nanmax(data) + 1),
278
- max(-np.nanmin(data) - 1, np.nanmax(data)), step)
279
- else:
280
- try:
281
- lev = np.array(levels, dtype='float')
282
- return lev
283
- except Exception:
284
- raise TypeError("Contour levels not properly specified")
285
-
286
-
287
- def create_legend_picker(fig, lined) -> object:
288
-
289
- def on_pick(event):
290
- # On the pick event, find the original line corresponding to the legend
291
- # proxy line, and toggle its visibility.
292
- legline = event.artist
293
- origline = lined[legline]
294
- visible = not origline.get_visible()
295
- origline.set_visible(visible)
296
- # Change the alpha on the line in the legend so we can see what lines
297
- # have been toggled.
298
- legline.set_alpha(1.0 if visible else 0.2)
299
- fig.canvas.draw()
300
-
301
- return on_pick
302
-
303
-
304
-
305
-
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