• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spedas / pyspedas / 17001196665

15 Aug 2025 11:12PM UTC coverage: 89.516% (+0.7%) from 88.849%
17001196665

push

github

web-flow
Merge branch 'pyspedas_2_0_dev' into master

5072 of 6199 new or added lines in 413 files covered. (81.82%)

8 existing lines in 2 files now uncovered.

40061 of 44753 relevant lines covered (89.52%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.79
/pyspedas/tplot_tools/MPLPlotter/specplot.py
1
import numpy as np
1✔
2
from scipy.interpolate import interp1d
1✔
3
import matplotlib as mpl
1✔
4
from datetime import datetime, timezone
1✔
5
from matplotlib.colors import LinearSegmentedColormap
1✔
6
import warnings
1✔
7
import pyspedas
1✔
8
import logging
1✔
9
from copy import copy
1✔
10

11
def get_bin_boundaries(bin_centers:np.ndarray, ylog:bool = False):
1✔
12
    """ Calculate a list of bin boundaries from a 1-D array of bin center values.
13

14
    Parameters
15
    ----------
16
    bin_centers: np.ndarray
17
    Array of Y bin center values
18

19
    ylog: bool
20
    If True, compute the bin boundaries in log space
21

22
    Returns
23
    -------
24
    tuple
25
    bin_boundaries: np.ndarray[float]
26
        Floating point array of bin boundaries computed from the bin centers.  Output array will be one element longer than the input array.
27
    direction: int
28
        Flag increasing or decreasing bin order: +1 increasing, -1 decreasing, 0 indeterminate
29

30
    """
31

32
    nbins = len(bin_centers)
1✔
33
    # Bin boundaries need to be floating point, even if the original bin values are
34
    # integers.  Initialize to all nans. Since the outputs are boundaries, not centers,
35
    # there is an extra element in the output array.
36
    outbins = np.zeros(nbins+1,dtype=np.float64)
1✔
37
    outbins[:] = np.nan
1✔
38

39
    # If we're working in log space, do the transform before filtering for finite values.
40
    # THEMIS ESA can have 0.0 bin centers with log scaling!
41

42

43
    if ylog:
1✔
44
        # There might be bin centers equal to 0.0 (e.g. THEMIS ESA).  Replace them with half the next larger
45
        # bin center.  Any bin centers less than zero will get turned to NaNs when we take logs
46
        # (and the corresponding data bins effectively removed).
47
        clean_bins = copy(bin_centers)
1✔
48
        zero_idx = np.where(bin_centers == 0.0)
1✔
49
        if len(zero_idx[0]) > 0:
1✔
50
            clean_bins[zero_idx] = np.nan
1✔
51
            clean_bins[zero_idx] = np.nanmin(clean_bins)/2.0
1✔
52
        working_bins = np.log10(clean_bins)
1✔
53
    else:
54
        working_bins = bin_centers
1✔
55

56
    # Check for all nans, or only one finite value
57
    idx_finite = np.where(np.isfinite(working_bins))
1✔
58

59
    if type(idx_finite) is tuple:
1✔
60
        idx_finite = idx_finite[0]
1✔
61

62
    if len(idx_finite) == 0:
1✔
63
        # Return all nans, indeterminate direction
64
        return outbins, 0
1✔
65
    elif len(idx_finite) == 1:
1✔
NEW
66
        idx = idx_finite[0]
×
67
        # Only a single bin, so we have to make up some bin boundaries
NEW
68
        if ylog:
×
NEW
69
            outbins[idx] = bin_centers[idx]/2.0
×
NEW
70
            outbins[idx+1] = bin_centers[idx]*2.0
×
71
        else:
NEW
72
            outbins[idx] = bin_centers[idx] - 1.0
×
NEW
73
            outbins[idx+1] = bin_centers[idx] + 1.0
×
NEW
74
        logging.warning("get_bin_boundaries: only one finite bin detected at index %d", idx)
×
NEW
75
        logging.warning("bin center: %f   bin boundaries: [%f, %f]",bin_centers[idx],outbins[idx], outbins[idx+1])
×
76
        # Return boundaries around the single finite bin, direction is increasing
NEW
77
        return outbins, 1
×
78

79
    # The usual case: we have at least two finite bins
80
    # Are they in increasing or decreasing order?
81

82
    if working_bins[idx_finite[0]] > working_bins[idx_finite[-1]]:
1✔
83
        direction = -1
1✔
84
    elif working_bins[idx_finite[0]] < working_bins[idx_finite[-1]]:
1✔
85
        direction = 1
1✔
86
    else:
87
        # All finite bins are the same?  Or nonmonotonic?  I guess it could happen.
NEW
88
        direction = 0
×
NEW
89
        logging.warning("get_bin_boundaries: First and last finite bin values are identical, may be nonmonotonic or all the same?")
×
90

91

92
    # We need to make sure that no NaNs are sandwiched between finite values
93
    good_bin_count = len(idx_finite)
1✔
94
    leading_nan_count = idx_finite[0]
1✔
95
    trailing_nan_count = nbins - idx_finite[-1] - 1
1✔
96

97
    if good_bin_count + leading_nan_count + trailing_nan_count != nbins:
1✔
NEW
98
        logging.warning("get_bin_boundaries: may contain nans between finite values. Total bin count: %d,  leading nans: %d, trailing_nans: %d, finite vals: %d",
×
99
                        nbins, leading_nan_count, trailing_nan_count, good_bin_count)
100

101
    # Now compute bin boundaries from all the finite bin centers
102
    finite_bins = np.copy(working_bins[idx_finite])
1✔
103
    edge_count = good_bin_count+1
1✔
104
    goodbins = np.zeros(edge_count, dtype=np.float64)
1✔
105

106
    goodbins[0] = finite_bins[0] - (finite_bins[1] - finite_bins[0]) / 2.0
1✔
107
    goodbins[1:edge_count-1] = (finite_bins[:-1] + finite_bins[1:]) / 2.0
1✔
108
    goodbins[edge_count-1] = finite_bins[-1] + (finite_bins[-1] - finite_bins[-2]) / 2.0
1✔
109

110
    # Deal with any possible leading or trailing nans
111
    if leading_nan_count > 0:
1✔
NEW
112
        outbins[0:leading_nan_count] = np.nan
×
113
    outbins[leading_nan_count:leading_nan_count+edge_count] = goodbins[:]
1✔
114
    if trailing_nan_count > 0:
1✔
115
        outbins[leading_nan_count+edge_count:nbins+2] = np.nan
1✔
116

117
    # Go back to linear space
118
    if ylog:
1✔
119
        outbins = 10.0**outbins
1✔
120

121
    return outbins, direction
1✔
122

123

124

125
def specplot_make_1d_ybins(values: np.ndarray, vdata:np.ndarray, ylog:bool, min_ratio:float = 0.001, no_regrid=False):
1✔
126
    """ Convert 2-D Y-bin arrays of bin center values to a 1-D array and rebin the data array
127

128
    Parameters
129
    ----------
130
    values : np.ndarray
131
    A 2-D array of values to be plotted as a spectrogram
132

133
    vdata: np.ndarray
134
    A 1-d or 2-D array of values representing center values of Y axis bins.
135
    If 1-d, bin centers are constant, otherwise they are assumed to be time-varying.
136

137
    ylog: bool
138
    If True, compute the bin boundaries in log space
139

140
    min_ratio: float
141
    Specifies a threshold for determining whether adjacent bins should be combined in a
142
    "thinning" process.  The default value of .001 represents a 1-pixel difference in
143
    where the bin boundaries are rendered if the Y axis is 1000 pixels high.
144

145
    no_regrid: bool
146
    If True, skip rebinning the data array and only return the bin boundaries.
147

148
    Returns
149
    -------
150
    tuple
151
    regridded_zdata:np.ndarray
152
        The result of regridding the values array with the new, potentially differwnt bin boundaries
153
    bin_boundaries_1d:np.ndarray
154
        The new bin boundaries
155

156
    Notes
157
    -----
158

159
    We find the union of all the bin boundaries for time-varying bins, and use that
160
    instead of an arbitrary high-resolution grid as the resampling target y-values.
161
    Any NaN values found in the input bin centers (1D or 2D) are dealt with.
162

163
    Allows for monotonically increasing or decreasing bin values, and bins that change over time.
164
    2D bin center arrays with some times having ascending values and other times having descending values
165
    are allowed.
166

167
    """
168
    #logging.info("Starting 1D vbins processing")
169
    ntimes = values.shape[0]
1✔
170
    bins_1d = False
1✔
171

172
    # Determine bin boundaries at each time step (or for all time, with 1-d bin center arrays),
173
    # weeding out NaNs in the bin center values. Form the union of all the individual bin
174
    # boundary sets (keeping the time-specific boundary sets, to use when rebinning later
175
    # Also determine the direction of increase at each time step, and flag any time steps
176
    # where a direction cannot be determined.
177

178
    if len(vdata.shape) == 1:
1✔
179
        #logging.info("Starting 1D vbins boundary processing")
180
        bins_1d = True
1✔
181
        result = get_bin_boundaries(vdata, ylog=ylog)
1✔
182
        vdata_bins = result[0]
1✔
183
        vdata_direction = result[1]
1✔
184
        bin_boundaries_set = set(vdata_bins)
1✔
185
        input_bin_center_count = len(vdata)
1✔
186
    else:  # 2-d V
187
        #logging.info("Starting 2D vbins boundary processing")
188
        bins_1d = False
1✔
189
        input_bin_center_count = vdata.shape[1]
1✔
190
        vdata_bins = np.zeros((ntimes, input_bin_center_count + 1), dtype=np.float64)
1✔
191
        vdata_direction = np.zeros(ntimes, dtype=np.int64)
1✔
192

193
        # The sentinel values are inserted because arrays with nans will not compare equal,
194
        # even if the nans are in same places
195
        sentinel = 1e31
1✔
196
        result = get_bin_boundaries(vdata[0, :], ylog=ylog)
1✔
197
        vdata_bins[0,:] = result[0]
1✔
198
        vdatadir_thistime = result[1]
1✔
199
        vdata_direction[0] = vdatadir_thistime
1✔
200
        pbins = vdata_bins[0,:]
1✔
201
        bin_boundaries_set = set(pbins)
1✔
202
        p = np.copy(vdata[0,:])
1✔
203
        p[np.where(~np.isfinite(p))] = sentinel
1✔
204
        # Now that everything is initialized, go through each time index
205
        # and maintain a set of all the bin boundaries seen so far.
206
        for i in range(ntimes-1):
1✔
207
            k = i+1
1✔
208
            t=np.copy(vdata[k,:])
1✔
209
            t[np.where(~np.isfinite(t))] = sentinel
1✔
210
            diff=t-p
1✔
211
            if np.any(diff):
1✔
212
                # bin values have changed, recalculate boundaries and add to running set
213
                #print(k)
214
                result = get_bin_boundaries(vdata[k,:], ylog=ylog)
1✔
215
                u = result[0]
1✔
216
                vdatadir_thistime = result[1]
1✔
217
                pbins = u
1✔
218
                vdata_bins[k,:] = u
1✔
219
                vdata_direction[k] = vdatadir_thistime
1✔
220
                uset = set(u)
1✔
221
                bin_boundaries_set = bin_boundaries_set | uset
1✔
222
                p = t
1✔
223
            else:
224
                # bin values have not channged at this time step, use previously computed values
225
                vdata_bins[k,:] = pbins
1✔
226
                vdata_direction[k] = vdatadir_thistime
1✔
227

228
    if bins_1d:
1✔
229
        if vdata_direction == 1:
1✔
230
            #print("1d bins are increasing")
231
            pass
1✔
232
        elif vdata_direction == -1:
1✔
233
            #print("1d bins are decreasing")
234
            pass
1✔
235
        else:
NEW
236
            logging.warning("specplot_make_1d_ybins: Direction of increase of 1-D input bin centers are indeterminate (all-nan, all-same, or nonmonotonic)")
×
NEW
237
            pass
×
238
    else:
239
        inc_count = (vdata_direction == 1).sum()
1✔
240
        dec_count = (vdata_direction == -1).sum()
1✔
241
        ind_count = (vdata_direction == 0).sum()
1✔
242
        #print("2d bins increasing: ", str(inc_count))
243
        #print("2d bins decreasing: ", str(dec_count))
244
        #print("2d bins indeterminate: ", str(ind_count))
245
        if ind_count > 0:
1✔
246
            logging.warning("specplot_make_1d_ybins: Direction of increase of input bin centers was indeterminate (all-nan, all-same, or non-monotonic) at %d of %d time indices.", ind_count, ntimes)
1✔
247

248
    # Convert the bin boundary set back to an array
249
    #logging.info("Done finding bin boundaries, sorting")
250
    vdata_unsorted = np.array(list(bin_boundaries_set))
1✔
251

252
    # Clean nans and sentinel values (e.g. may be present in FAST y bin values)
253
    vdata_finite = [val for val in vdata_unsorted if np.isfinite(val)]
1✔
254

255
    # Sort in ascending order
256
    output_bin_boundaries = np.sort(vdata_finite)
1✔
257

258
    output_bin_boundary_len = len(output_bin_boundaries)
1✔
259

260
    # It is possible (e.g. ELFIN) that some bin boundaries are very close, but not equal
261
    # (less than a pixel high).  We might want to thin out any bin boundaries within
262
    # "epsilon" of the previous bin.
263

264
    ymax=output_bin_boundaries[output_bin_boundary_len-1]
1✔
265
    ymin=output_bin_boundaries[0]
1✔
266
    yrange = ymax-ymin
1✔
267
    #logging.info("Thinning bin boundaries")
268
    # With the default min_ratio, epsilon is about one pixel in the y direction for a typical plot size and dpi
269
    # If min_ratio is 0, the effect is that no bin boundaries will be discarded.
270

271
    if ylog:
1✔
272
        epsilon = (np.log10(ymax)-np.log10(ymin)) * min_ratio
1✔
273
    else:
274
        epsilon = (ymax-ymin)*min_ratio
1✔
275
    diff=output_bin_boundaries[1:]-output_bin_boundaries[:output_bin_boundary_len-1]
1✔
276

277
    # logging.info("Specplot 1-D y bins before thinning: array size %d, yrange %f, smallest difference %f, epsilon %f,  ratio %f",len(output_bin_boundaries),yrange,np.min(diff),epsilon, (ymax-ymin)/np.min(diff))
278

279
    last_val = output_bin_boundaries[0]
1✔
280
    thinned_list = [output_bin_boundaries[0]]
1✔
281
    for i in range(output_bin_boundary_len):
1✔
282
        val = output_bin_boundaries[i]
1✔
283
        if ylog:
1✔
284
            diff = np.log10(val) - np.log10(last_val)
1✔
285
        else:
286
            diff = val-last_val
1✔
287
        if abs(diff) > epsilon:
1✔
288
            thinned_list.append(val)
1✔
289
            last_val = val
1✔
290

291
    output_bin_boundaries = np.array(thinned_list)
1✔
292

293
    #logging.info("Done thinning bin boundaries")
294
    output_bin_boundary_len = len(output_bin_boundaries)
1✔
295
    # There could be NaNs (e.g. FAST)
296
    ymax=output_bin_boundaries[output_bin_boundary_len-1]
1✔
297
    ymin=output_bin_boundaries[0]
1✔
298
    yrange = ymax-ymin
1✔
299
    diff=output_bin_boundaries[1:]-output_bin_boundaries[:output_bin_boundary_len-1]
1✔
300

301
    # logging.info("Specplot 1-D y bins after thinning: array size %d, yrange %f, smallest difference %f, epsilon %f,  ratio %f",len(output_bin_boundaries),yrange,np.min(diff),epsilon, (ymax-ymin)/np.min(diff))
302

303
    if no_regrid:
1✔
NEW
304
        return output_bin_boundaries
×
305
    #logging.info("Started rebinning (new style)")
306
    if values.dtype.kind == 'f':
1✔
307
        fill = np.nan
1✔
308
    else:
309
        fill = 0
1✔
310

311
    # Now we rebin the input data array into the output array, using both the original
312
    # and combined bin boundaries.
313

314
    # The output value array should have a y dimension one less than the bin boundary count
315
    out_values = np.zeros((ntimes, output_bin_boundary_len - 1), dtype=values.dtype)
1✔
316
    out_values[:,:] = fill
1✔
317

318
    # Note that output_bin_boundaries is always monotonically increasing, but
319
    # vdata_bins (the original inputs) can be monotonically decreasing
320

321
    for time_index in range(ntimes):
1✔
322
        if len(vdata.shape) == 1:
1✔
323
            input_bin_boundaries = vdata_bins
1✔
324
            input_bin_centers = vdata
1✔
325
            direction = vdata_direction
1✔
326
        else:
327
            input_bin_boundaries = vdata_bins[time_index, :]
1✔
328
            input_bin_centers = vdata[time_index,:]
1✔
329
            direction = vdata_direction[time_index]
1✔
330

331
        #print("Time index: " + str(time_index))
332
        #print("Input bin boundaries:")
333
        #print(input_bin_boundaries)
334
        #print("Input bin centers")
335
        #print(input_bin_centers)
336
        #print("Output bin boundaries")
337
        #print(output_bin_boundaries)
338
        #print("Data values at time step:")
339
        #print(values[time_index,:])
340
        if direction == 1:
1✔
341
            # Increasing bin values
342
            lower_bound_indices = np.searchsorted(output_bin_boundaries, input_bin_boundaries[0:-1],side="left")
1✔
343
            upper_bound_indices = np.searchsorted(output_bin_boundaries, input_bin_boundaries[1:],side="left")
1✔
344
        elif direction == -1:
1✔
345
            lower_bound_indices = np.searchsorted(output_bin_boundaries, input_bin_boundaries[1:],side="left")
1✔
346
            upper_bound_indices = np.searchsorted(output_bin_boundaries, input_bin_boundaries[0:-1],side="left")
1✔
347
        else:
348
            continue
1✔
349
        #print("Lower bound indices:")
350
        #print(lower_bound_indices)
351
        #print("Upper bound indices")
352
        #print(upper_bound_indices)
353

354
        for i in range(input_bin_center_count):
1✔
355
            if not np.isfinite(input_bin_centers[i]):
1✔
356
                pass
1✔
357
            else:
358
                #print("input bin index: " + str(i))
359
                #print("input bin center: " + str(input_bin_centers[i]))
360
                #print("output lower bound index: " + str(lower_bound_indices[i]))
361
                #print("output upper bound index: " + str(upper_bound_indices[i]))
362
                #print("output bin value range: " + str(output_bin_boundaries[lower_bound_indices[i]:upper_bound_indices[i]+1]))
363
                out_values[time_index,lower_bound_indices[i]:upper_bound_indices[i]] = values[time_index, i]
1✔
364
                #print(out_values)
365
    #logging.info("Done making 1D Y bins")
366

367
    return out_values, output_bin_boundaries
1✔
368

369
def specplot(
1✔
370
    var_data,
371
    var_times,
372
    this_axis,
373
    yaxis_options,
374
    zaxis_options,
375
    plot_extras,
376
    colorbars,
377
    axis_font_size,
378
    fig,
379
    variable,
380
    time_idxs=None,
381
    style=None,
382
):
383
    """
384
    Plot a tplot variable as a spectrogram
385

386
    Parameters
387
    ----------
388
    var_data: dict
389
        A tplot dictionary containing the data to be plotted
390
    var_times: array of datetime objects
391
        An array of datetime objects specifying the time axis
392
    this_axis
393
        The matplotlib object for the panel currently being plotted
394
    yaxis_options: dict
395
        A dictionary containing the Y axis options to be used for this variable
396
    zaxis_options: dict
397
        A dictionary containing the Z axis (spectrogram values) options to be used for this variable
398
    plot_extras: dict
399
        A dictionary containing additional plot options for this variable
400
    colorbars: dict
401
        The data structure that contains information (for all variables) for creating colorbars.
402
    axis_font_size: int
403
        The font size in effect for this axis (used to create colorbars)
404
    fig: matplotlib.figure.Figure
405
        A matplotlib figure object to be used for this plot
406
    variable: str
407
        The name of the tplxxot variable to be plotted (used for log messages)
408
    time_idxs: array of int
409
        The indices of the subset of times to use for this plot. Defaults to None (plot all timestamps).
410
    style
411
        A matplotlib style object to be used for this plot. Defaults to None.
412

413
    Returns
414
    -------
415
        True
416
    """
417
    alpha = plot_extras.get("alpha")
1✔
418
    spec_options = {"shading": "auto", "alpha": alpha}
1✔
419
    ztitle = zaxis_options["axis_label"]
1✔
420

421
    zlog_str = zaxis_options["z_axis_type"]
1✔
422
    ylog_str = yaxis_options["y_axis_type"]
1✔
423
    # Convert zlog_str and ylog_str to bool
424
    ylog = False
1✔
425
    zlog = False
1✔
426
    if "log" in ylog_str.lower():
1✔
427
        ylog = True
1✔
428
    if "log" in zlog_str.lower():
1✔
429
        zlog = True
1✔
430
    #logging.info("ylog_str is " + str(ylog_str))
431
    #logging.info("zlog_str is " + str(zlog_str))
432

433
    yrange = yaxis_options["y_range"]
1✔
434
    if yrange[0] is None or not np.isfinite(yrange[0]):
1✔
NEW
435
        yrange[0] = None
×
436
    if yrange[1] is None or not np.isfinite(yrange[1]):
1✔
NEW
437
        yrange[1] = None
×
438

439
    if zaxis_options.get("z_range") is not None:
1✔
440
        zrange = zaxis_options["z_range"]
1✔
441
    else:
442
        zrange = [None, None]
1✔
443

444
    if zaxis_options.get("axis_subtitle") is not None:
1✔
445
        zsubtitle = zaxis_options["axis_subtitle"]
1✔
446
    else:
447
        zsubtitle = ""
1✔
448

449
    # Clean up any fill values in data array
450
    #set -1.e31 fill values to NaN, jmjm, 2024-02-29
451
    ytp = np.where(var_data.y==-1.e31,np.nan,var_data.y)
1✔
452
    var_data.y[:,:] = ytp[:,:]
1✔
453

454
    if zlog:
1✔
455
        zmin = np.nanmin(var_data.y)
1✔
456
        zmax = np.nanmax(var_data.y)
1✔
457
        # gracefully handle the case of all NaNs in the data, but log scale set
458
        # all 0 is also a problem, causes a crash later when creating the colorbar
459
        if np.isnan(var_data.y).all():
1✔
460
            # no need to set a log scale if all the data values are NaNs, or all zeroes
461
            spec_options["norm"] = None
1✔
462
            spec_options["vmin"] = zrange[0]
1✔
463
            spec_options["vmax"] = zrange[1]
1✔
464
            logging.info("Variable %s contains all-NaN data", variable)
1✔
465
        elif not np.any(var_data.y):
1✔
466
            # properly handle all 0s in the data
467
            spec_options["norm"] = None
1✔
468
            spec_options["vmin"] = zrange[0]
1✔
469
            spec_options["vmax"] = zrange[1]
1✔
470
            logging.info("Variable %s contains all-zero data", variable)
1✔
471
        else:
472
            spec_options["norm"] = mpl.colors.LogNorm(vmin=zrange[0], vmax=zrange[1])
1✔
473
    else:
474
        spec_options["norm"] = None
1✔
475
        spec_options["vmin"] = zrange[0]
1✔
476
        spec_options["vmax"] = zrange[1]
1✔
477

478
    cmap = None
1✔
479

480
    if plot_extras.get("colormap") is not None:
1✔
481
        cmap = plot_extras["colormap"][0]
1✔
482
    else:
483
        # default to the SPEDAS color map if the user doesn't have a MPL style set
484
        if style is None:
1✔
485
            cmap = "spedas"
1✔
486

487
    # kludge to add support for the 'spedas' color bar
488
    if cmap == "spedas":
1✔
489
        _colors = pyspedas.tplot_tools.spedas_colorbar
1✔
490
        spd_map = [
1✔
491
            (np.array([r, g, b])).astype(np.float64) / 256
492
            for r, g, b in zip(_colors.r, _colors.g, _colors.b)
493
        ]
494
        cmap = LinearSegmentedColormap.from_list("spedas", spd_map)
1✔
495

496
    spec_options["cmap"] = cmap
1✔
497

498
    input_zdata = var_data.y[time_idxs, :]
1✔
499
    input_times = var_data.times[time_idxs]
1✔
500

501
    # Figure out which attribute to use for Y bin centers
502
    #allow use of v1, v2, jmm, 2024-03-20
503
    if len(var_data) == 3:
1✔
504
        if hasattr(var_data,'v'):
1✔
505
            input_bin_centers = var_data.v
1✔
506
        elif hasattr(var_data,'v1'):
1✔
507
            input_bin_centers = var_data.v1
1✔
508
        else:
NEW
509
            logging.warning("Multidimensional variable %s has no v or v1 attribute",variable)
×
NEW
510
    elif len(var_data) == 4:
×
NEW
511
        if hasattr(var_data, 'v1'):
×
NEW
512
            if 'spec_dim_to_plot' in plot_extras:
×
NEW
513
                if plot_extras['spec_dim_to_plot'] == 'v1':
×
NEW
514
                    input_bin_centers = var_data.v1
×
NEW
515
        if hasattr(var_data, 'v2'):
×
NEW
516
            if 'spec_dim_to_plot' in plot_extras:
×
NEW
517
                if plot_extras['spec_dim_to_plot'] == 'v2':
×
NEW
518
                    input_bin_centers = var_data.v2
×
519
    else:
NEW
520
        logging.warning("Too many dimensions on the variable: " + variable)
×
NEW
521
        return
×
522

523
    # Clean up any fill values in bin center array
524
    vtp = np.where(input_bin_centers == -1.e31, np.nan, input_bin_centers)
1✔
525
    if len(vtp.shape) == 1:
1✔
526
        input_bin_centers[:] = vtp[:]
1✔
527
    else:
528
        input_bin_centers[:, :] = vtp[:, :]
1✔
529

530
    if len(input_bin_centers.shape) > 1:
1✔
531
        # time varying 'v', need to limit the values to those within the requested time range
532
        input_bin_centers = input_bin_centers[time_idxs, :]
1✔
533

534
    # This call flattens any time-varying bin boundaries into a 1-d list (out_vdata)
535
    # and regrids the data array to the new y bin count (regridded_zdata)
536

537
    #logging.info("Starting specplot processing")
538

539
    regridded_zdata, bin_boundaries_1d = specplot_make_1d_ybins(input_zdata, input_bin_centers, ylog)
1✔
540

541
    # At this point, bin_boundaries_1d, the array of bin boundaries, is guaranteed to be
542
    # 1-dimensional, in ascending order, with all finite values. It has one more element
543
    # than the Y dimension of regridded_zdata.  The Y dimension of regridded_zdata may have changed
544
    # as a result of flattening a 2-D out_vdata input.
545
    # If ylog==True, all values in bin_boundaries_1d will be strictly positive.
546

547
    assert(len(bin_boundaries_1d.shape) == 1) # bin boundaries are 1-D
1✔
548
    assert(len(regridded_zdata.shape) == 2) # output array is 2-D
1✔
549
    assert(bin_boundaries_1d.shape[0] == regridded_zdata.shape[1]+1) # bin boundaries have one more element in Y dimension
1✔
550
    assert(np.isfinite(bin_boundaries_1d.all())) # no nans in bin boundaries
1✔
551
    assert(bin_boundaries_1d[-1] > bin_boundaries_1d[0]) # bin boundaries in ascending order
1✔
552
    if ylog:
1✔
553
        assert(np.all(bin_boundaries_1d > 0.0)) # bin boundaries all positive if log scaling
1✔
554

555
    # Get min and max bin boundaries
556
    vmin = np.min(bin_boundaries_1d)
1✔
557
    vmax = np.max(bin_boundaries_1d)
1✔
558

559
    #could also have a fill in yrange
560
    #    if yrange[0] == -1e31: #This does not work sometimes?
561
    if yrange[0] < -0.9e31:
1✔
NEW
562
        yrange[0] = vmin
×
563
    if yrange[1] < -0.9e31:
1✔
NEW
564
        yrange[1] = vmax
×
565

566

567
    #logging.info("Starting specplot time boundary processing")
568
    input_unix_times = np.int64(input_times) / 1e9
1✔
569
    result = get_bin_boundaries(input_unix_times)
1✔
570
    # For pcolormesh, we also want bin boundaries (not center values) on the time axis
571
    time_boundaries_dbl = result[0]
1✔
572
    time_boundaries_ns = np.int64(time_boundaries_dbl*1e9)
1✔
573
    # Now back to numpy datetime64
574
    time_boundaries = np.array(time_boundaries_ns, dtype='datetime64[ns]')
1✔
575
    #logging.info("Done with specplot initial processing")
576
    # If the user set a yrange with the 'options' command, nothing is needed here
577
    # since tplot takes care of it.   If not, set it here to the min/max finite bin
578
    # boundaries.  If left unspecified, pcolormesh might do something weird to the y limits.
579
    if yaxis_options.get('y_range_user') is None:
1✔
580
        this_axis.set_ylim([vmin, vmax])
1✔
581

582

583
    # automatic interpolation options
584
    if yaxis_options.get("x_interp") is not None:
1✔
585
        x_interp = yaxis_options["x_interp"]
1✔
586

587
        # interpolate along the x-axis
588
        if x_interp:
1✔
589
            if yaxis_options.get("x_interp_points") is not None:
1✔
590
                nx = yaxis_options["x_interp_points"]
1✔
591
            else:
592
                fig_size = fig.get_size_inches() * fig.dpi
1✔
593
                nx = fig_size[0]
1✔
594

595
            if zlog:
1✔
596
                zdata = np.log10(regridded_zdata)
1✔
597
            else:
NEW
598
                zdata = regridded_zdata
×
599

600
            zdata[zdata < 0.0] = 0.0
1✔
601
            zdata[zdata == np.nan] = 0.0
1✔
602

603
            # convert to floats for the interpolation
604
            spec_unix_times = np.int64(var_data.times[time_idxs]) / 1e9
1✔
605

606
            # interpolate in the x-direction
607
            interp_func = interp1d(
1✔
608
                spec_unix_times, zdata, axis=0, bounds_error=False, kind="linear"
609
            )
610
            out_times = (
1✔
611
                np.arange(0, nx, dtype=np.float64)
612
                * (spec_unix_times[-1] - spec_unix_times[0])
613
                / (nx - 1)
614
                + spec_unix_times[0]
615
            )
616
            regridded_zdata = interp_func(out_times)
1✔
617

618
            if zlog:
1✔
619
                regridded_zdata = 10**regridded_zdata
1✔
620

621
            # Convert time bin centers to bin boundaries
622
            result = get_bin_boundaries(out_times, ylog=False)
1✔
623
            # Convert unix times back to np.datetime64[ns] objects
624
            unix_time_boundaries_int64 = np.int64(result[0]*1e9)
1✔
625
            time_boundaries = np.array(unix_time_boundaries_int64, dtype="datetime64[ns]")
1✔
626

627
    if yaxis_options.get("y_interp") is not None:
1✔
628
        y_interp = yaxis_options["y_interp"]
1✔
629

630
        if y_interp:
1✔
631
            if yaxis_options.get("y_interp_points") is not None:
1✔
632
                ny = yaxis_options["y_interp_points"]
1✔
633
            else:
634
                fig_size = fig.get_size_inches() * fig.dpi
1✔
635
                ny = fig_size[1]
1✔
636

637
            if zlog:
1✔
638
                zdata = np.log10(regridded_zdata)
1✔
639
            else:
NEW
640
                zdata = regridded_zdata
×
641

642
            if ylog:
1✔
643
                vdata = np.log10(bin_boundaries_1d)
1✔
644
                ycrange = np.log10(yrange)
1✔
645
            else:
NEW
646
                vdata = bin_boundaries_1d
×
NEW
647
                ycrange = yrange
×
648

649
            if not np.isfinite(ycrange[0]):
1✔
NEW
650
                ycrange = [np.min(vdata), yrange[1]]
×
651

652
            zdata[zdata < 0.0] = 0.0
1✔
653
            zdata[zdata == np.nan] = 0.0  # does not work
1✔
654

655
            # vdata was calculated from 1-D bin boundaries, not bin centers.
656
            # We need to go to bin centers for interpolation
657
            uppers = vdata[1:]
1✔
658
            lowers = vdata[0:-1]
1✔
659
            centers = (uppers+lowers)/2.0
1✔
660

661
            interp_func = interp1d(centers, zdata, axis=1, bounds_error=False)
1✔
662
            out_vdata_centers = (
1✔
663
                np.arange(0, ny, dtype=np.float64)
664
                * (ycrange[1] - ycrange[0])
665
                / (ny - 1)
666
                + ycrange[0]
667
            )
668
            regridded_zdata = interp_func(out_vdata_centers)
1✔
669

670
            # Now we'll convert from bin centers back to bin boundaries for pcolormesh
671
            # We're still in linear space at this point
672
            result = get_bin_boundaries(out_vdata_centers, ylog=False)
1✔
673
            rebinned_boundaries = result[0]
1✔
674

675
            if zlog:
1✔
676
                regridded_zdata = 10**regridded_zdata
1✔
677

678
            if ylog:
1✔
679
                bin_boundaries_1d = 10**rebinned_boundaries
1✔
680

681
    # check for negatives if zlog is requested
682
    if zlog:
1✔
683
        regridded_zdata[regridded_zdata < 0.0] = 0.0
1✔
684

685
    ylim_before = this_axis.get_ylim()
1✔
686
    # create the spectrogram (ignoring warnings)
687
    with warnings.catch_warnings():
1✔
688
        warnings.simplefilter("ignore")
1✔
689
        #logging.info("Starting pcolormesh")
690
        im = this_axis.pcolormesh(time_boundaries, bin_boundaries_1d.T, regridded_zdata.T, **spec_options)
1✔
691
        #logging.info("Done with pcolormesh")
692
    ylim_after = this_axis.get_ylim()
1✔
693

694
    #logging.info("ylim before pcolormesh: %s ",str(ylim_before))
695
    #logging.info("ylim after pcolormesh: %s", str(ylim_after))
696

697
    # store everything needed to create the colorbars
698
    colorbars[variable] = {}
1✔
699
    colorbars[variable]["im"] = im
1✔
700
    colorbars[variable]["axis_font_size"] = axis_font_size
1✔
701
    colorbars[variable]["ztitle"] = ztitle
1✔
702
    colorbars[variable]["zsubtitle"] = zsubtitle
1✔
703
    return True
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc