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

California-Planet-Search / radvel / 25467290701

06 May 2026 11:39PM UTC coverage: 90.15% (-0.07%) from 90.224%
25467290701

Pull #427

github

web-flow
UI: taller plots, full plot/table picker, persistent run.log, run- prefix (#434)

Bundle of small follow-ups against the run page based on user feedback:

- Plots panel now scales to ~85vh for the RV multipanel (the tallest
  plot) and ~70vh for the corner, so the embed never inner-scrolls.
  RV gets its own row; corner / autocorr / trend / derived lay out
  2-up as before.
- Plots and Tables steps now open a type-picker modal listing every
  type the API supports (plots: rv, corner, auto, trend, derived;
  tables: params, priors, rv, ic_compare, derived, crit). Types whose
  prerequisite isn't met (e.g. "corner needs MCMC/NS") are shown
  greyed out with the reason inline.
- IC compare and Tables steps were stuck in 'ready' even after
  running because the [ic_compare] / [table] sections don't write
  ``run = True``. Detect by section presence instead.
- Report step now writes a [report] section to .stat on success so
  the step flips to '✓ done'. Failures are translated from the
  driver's ``shutil.copy(<pdfname>, current)`` FileNotFoundError to
  a clearer ``report_pdf_missing`` message that points at the
  ``<run_id>_results.tex`` source for debugging.
- ``_capture`` now appends every step's stdout/stderr to
  ``<rundir>/run.log`` with a step header. The UI's terminal panel
  refetches that file on every render so the log survives page
  reloads (was previously in-memory only).
- New runs use the ``run-`` prefix instead of ``r-``. The validator
  still accepts ``r-`` for backward compatibility so existing run
  directories keep working.

Tests updated for the new prefix; default suite + 29 API tests pass.
Pull Request #427: Release v1.6.0: HTTP API + Docker service

84 of 97 new or added lines in 3 files covered. (86.6%)

314 existing lines in 14 files now uncovered.

4320 of 4792 relevant lines covered (90.15%)

0.9 hits per line

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

88.97
/radvel/plot/orbit_plots.py
1
import warnings
1✔
2

3
import numpy as np
1✔
4
import pandas as pd
1✔
5
from matplotlib import rcParams, gridspec
1✔
6
from matplotlib import pyplot as pl
1✔
7
from matplotlib.ticker import MaxNLocator
1✔
8
from astropy.time import Time
1✔
9
import copy
1✔
10

11
import radvel
1✔
12
from radvel import plot
1✔
13
from radvel.plot import mcmc_plots
1✔
14
from radvel.utils import t_to_phase, fastbin, sigfig
1✔
15

16

17
class MultipanelPlot(object):
1✔
18
    """
19
    Class to handle the creation of RV multipanel plots.
20

21
    Args:
22
        post (radvel.Posterior): radvel.Posterior object. The model
23
            plotted will be generated from `post.params`
24
        epoch (int, optional): epoch to subtract off of all time measurements
25
        yscale_auto (bool, optional): Use matplotlib auto y-axis
26
             scaling (default: False)
27
        yscale_sigma (float, optional): Scale y-axis limits for all panels to be +/-
28
             yscale_sigma*(RMS of data plotted) if yscale_auto==False
29
        phase_nrows (int, optional): number of rows in the phase
30
            folded plots. Default is nplanets.
31
        phase_ncols (int, optional): number of columns in the phase
32
            folded plots. Default is 1.
33
        uparams (dict, optional): parameter uncertainties, must
34
           contain 'per', 'k', and 'e' keys.
35
        telfmts (dict, optional): dictionary of dictionaries mapping
36
            instrument suffix to plotting format code. Example:
37
                telfmts = {
38
                     'hires': dict(fmt='o',label='HIRES'),
39
                     'harps-n' dict(fmt='s')
40
                }
41
        legend (bool, optional): include legend on plot? Default: True.
42
        phase_limits (list, optional): two element list specifying 
43
            pyplot.xlim bounds for phase-folded plots. Useful for
44
            partial orbits.
45
        nobin (bool, optional): If True do not show binned data on
46
            phase plots. Will default to True if total number of
47
            measurements is less then 20.
48
        phasetext_size (string, optional): fontsize for text in phase plots.
49
            Choice of {'xx-small', 'x-small', 'small', 'medium', 'large', 
50
            'x-large', 'xx-large'}. Default: 'x-small'.
51
        rv_phase_space (float, optional): amount of space to leave between orbit/residual plot
52
            and phase plots.
53
        figwidth (float, optional): width of the figures to be produced. 
54
            Default: 7.5 (spans a page with 0.5 in margins)
55
        fit_linewidth (float, optional): linewidth to use for orbit model lines in phase-folded
56
            plots and residuals plots.
57
        set_xlim (list of float): limits to use for x-axes of the timeseries and residuals plots, in
58
            JD - `epoch`. Ex: [7000., 70005.]
59
        text_size (int): set matplotlib.rcParams['font.size'] (default: 9)
60
        highlight_last (bool): make the most recent measurement much larger in all panels
61
        show_rms (bool): show RMS of the residuals by instrument in the legend
62
        legend_kwargs (dict): dict of options to pass to legend (plotted in top panel)
63
        status (ConfigParser): (optional) result of radvel.driver.load_status on the .stat status file
64
    """
65
    def __init__(self, post, saveplot=None, epoch=2450000, yscale_auto=False, yscale_sigma=3.0,
1✔
66
                 phase_nrows=None, phase_ncols=None, uparams=None, telfmts={}, legend=True,
67
                 phase_limits=[], nobin=False, phasetext_size='large', rv_phase_space=0.08,
68
                 figwidth=7.5, fit_linewidth=2.0, set_xlim=None, text_size=9, highlight_last=False,
69
                 show_rms=False, legend_kwargs=dict(loc='best'), status=None):
70

71
        self.post = copy.deepcopy(post)
1✔
72
        self.saveplot = saveplot
1✔
73
        self.epoch = epoch
1✔
74
        self.yscale_auto = yscale_auto
1✔
75
        self.yscale_sigma = yscale_sigma
1✔
76
        if phase_ncols is None:
1✔
77
            self.phase_ncols = 1
1✔
78
        else:
79
            self.phase_ncols = phase_ncols
×
80
        if phase_nrows is None:
1✔
81
            self.phase_nrows = int(np.ceil(self.post.likelihood.model.num_planets / self.phase_ncols))
1✔
82
        else:
83
            self.phase_nrows = phase_nrows
×
84
        self.uparams = uparams
1✔
85
        self.rv_phase_space = rv_phase_space
1✔
86
        self.telfmts = telfmts
1✔
87
        self.legend = legend
1✔
88
        self.phase_limits = phase_limits
1✔
89
        self.nobin = nobin
1✔
90
        self.phasetext_size = phasetext_size
1✔
91
        self.figwidth = figwidth
1✔
92
        self.fit_linewidth = fit_linewidth
1✔
93
        self.set_xlim = set_xlim
1✔
94
        self.highlight_last = highlight_last
1✔
95
        self.show_rms = show_rms
1✔
96
        self.legend_kwargs = legend_kwargs
1✔
97
        rcParams['font.size'] = text_size
1✔
98

99
        if status is not None:
1✔
100
            self.status = status
1✔
101

102
        if isinstance(self.post.likelihood, radvel.likelihood.CompositeLikelihood):
1✔
103
            self.like_list = self.post.likelihood.like_list
1✔
104
        else:
UNCOV
105
            self.like_list = [self.post.likelihood]
×
106

107
        # FIGURE PROVISIONING
108
        self.ax_rv_height = self.figwidth * 0.6
1✔
109
        self.ax_phase_height = self.ax_rv_height / 1.4
1✔
110

111
        # convert params to synth basis
112
        synthparams = self.post.params.basis.to_synth(self.post.params)
1✔
113
        self.post.params = synthparams
1✔
114
        self.post.vector.dict_to_vector()
1✔
115

116
        self.model = self.post.likelihood.model
1✔
117
        self.rvtimes = self.post.likelihood.x
1✔
118
        self.rverr = self.post.likelihood.errorbars()
1✔
119
        self.num_planets = self.model.num_planets
1✔
120

121
        self.rawresid = self.post.likelihood.residuals()
1✔
122

123
        self.resid = (
1✔
124
            self.rawresid + self.post.params['dvdt'].value*(self.rvtimes-self.model.time_base)
125
            + self.post.params['curv'].value*(self.rvtimes-self.model.time_base)**2
126
        )
127

128
        if self.saveplot is not None:
1✔
129
            resolution = 10000
1✔
130
        else: 
131
            resolution = 2000
1✔
132

133
        periods = []
1✔
134
        for i in range(self.num_planets):
1✔
135
            periods.append(synthparams['per%d' % (i+1)].value)            
1✔
136
        if len(periods) > 0:
1✔
137
            longp = max(periods)
1✔
138
        else:
UNCOV
139
            longp = max(self.post.likelihood.x) - min(self.post.likelihood.x)
×
140

141
        if self.set_xlim is not None:
1✔
UNCOV
142
            self.dt = self.set_xlim[1] - self.set_xlim[0]
×
UNCOV
143
            self.rvmodt = np.linspace(
×
144
                (self.set_xlim[0]+self.epoch) - 0.05 * self.dt, (self.set_xlim[1]+self.epoch) + 0.05 * self.dt + longp,
145
                int(resolution)
146
            )
147
        else:
148
            self.dt = max(self.rvtimes) - min(self.rvtimes)
1✔
149
            self.rvmodt = np.linspace(
1✔
150
                min(self.rvtimes) - 0.05 * self.dt, max(self.rvtimes) + 0.05 * self.dt + longp,
151
                int(resolution)
152
            )
153
        
154
        self.orbit_model = self.model(self.rvmodt)
1✔
155
        self.rvmod = self.model(self.rvtimes)
1✔
156

157
        if ((self.rvtimes - self.epoch) < -2.4e6).any():
1✔
158
            self.plttimes = self.rvtimes
1✔
159
            self.mplttimes = self.rvmodt
1✔
160
        elif self.epoch == 0:
1✔
161
            self.epoch = 2450000
1✔
162
            self.plttimes = self.rvtimes - self.epoch
1✔
163
            self.mplttimes = self.rvmodt - self.epoch
1✔
164
        else:
165
            self.plttimes = self.rvtimes - self.epoch
1✔
166
            self.mplttimes = self.rvmodt - self.epoch
1✔
167

168
        self.slope = (
1✔
169
            self.post.params['dvdt'].value * (self.rvmodt-self.model.time_base)
170
            + self.post.params['curv'].value * (self.rvmodt-self.model.time_base)**2
171
        )
172
        self.slope_low = (
1✔
173
            self.post.params['dvdt'].value * (self.rvtimes-self.model.time_base)
174
            + self.post.params['curv'].value * (self.rvtimes-self.model.time_base)**2
175
        )
176

177
        # list for Axes objects
178
        self.ax_list = []
1✔
179

180
    def plot_timeseries(self):
1✔
181
        """
182
        Make a plot of the RV data and model in the current Axes.
183
        """
184

185
        ax = pl.gca()
1✔
186

187
        ax.axhline(0, color='0.5', linestyle='--')
1✔
188

189
        if self.show_rms:
1✔
190
            rms_values = dict()
1✔
191
            for like in self.like_list:
1✔
192
                inst = like.suffix
1✔
193
                rms = np.std(like.residuals())
1✔
194
                rms_values[inst] = rms
1✔
195
        else:
196
            rms_values = False
1✔
197

198
        # plot orbit model
199
        ax.plot(self.mplttimes, self.orbit_model, 'b-', rasterized=False, lw=self.fit_linewidth)
1✔
200

201
        # plot data
202
        vels = self.rawresid+self.rvmod
1✔
203
        plot.mtelplot(
1✔
204
            # data = residuals + model
205
            self.plttimes, vels, self.rverr, self.post.likelihood.telvec, ax, telfmts=self.telfmts,
206
            rms_values=rms_values
207
        )
208

209
        if self.set_xlim is not None:
1✔
UNCOV
210
            ax.set_xlim(self.set_xlim)
×
211
        else:
212
            ax.set_xlim(min(self.plttimes)-0.01*self.dt, max(self.plttimes)+0.01*self.dt)    
1✔
213
        pl.setp(ax.get_xticklabels(), visible=False)
1✔
214

215
        if self.highlight_last:
1✔
216
            ind = np.argmax(self.plttimes)
1✔
217
            pl.plot(self.plttimes[ind], vels[ind], **plot.highlight_format)
1✔
218

219
        # legend
220
        if self.legend:
1✔
221
            ax.legend(numpoints=1, **self.legend_kwargs)
1✔
222

223
        # years on upper axis
224
        axyrs = ax.twiny()
1✔
225
        xl = np.array(list(ax.get_xlim())) + self.epoch
1✔
226
        decimalyear = Time(xl, format='jd', scale='utc').decimalyear
1✔
227
#        axyrs.plot(decimalyear, decimalyear)
228
        axyrs.get_xaxis().get_major_formatter().set_useOffset(False)
1✔
229
        axyrs.set_xlim(*decimalyear)
1✔
230
        axyrs.set_xlabel('Year', fontweight='bold')
1✔
231
        pl.locator_params(axis='x', nbins=5)
1✔
232

233
        if not self.yscale_auto: 
1✔
234
            scale = np.std(self.rawresid+self.rvmod)
1✔
235
            ax.set_ylim(-self.yscale_sigma * scale, self.yscale_sigma * scale)
1✔
236

237
        ax.set_ylabel('RV [{ms:}]'.format(**plot.latex), weight='bold')
1✔
238
        ticks = ax.yaxis.get_majorticklocs()
1✔
239
        ax.yaxis.set_ticks(ticks[1:])
1✔
240

241
    def plot_residuals(self):
1✔
242
        """
243
        Make a plot of residuals in the current Axes.
244
        """
245
        
246
        ax = pl.gca()
1✔
247

248
        ax.plot(self.mplttimes, self.slope-self.slope, 'b-', lw=self.fit_linewidth)
1✔
249

250
        plot.mtelplot(self.plttimes, self.rawresid, self.rverr, self.post.likelihood.telvec, ax, telfmts=self.telfmts)
1✔
251
        if not self.yscale_auto: 
1✔
252
            scale = np.std(self.rawresid)
1✔
253
            ax.set_ylim(-self.yscale_sigma * scale, self.yscale_sigma * scale)
1✔
254

255
        if self.highlight_last:
1✔
256
            ind = np.argmax(self.plttimes)
1✔
257
            pl.plot(self.plttimes[ind], self.rawresid[ind], **plot.highlight_format)
1✔
258

259
        if self.set_xlim is not None:
1✔
UNCOV
260
            ax.set_xlim(self.set_xlim)
×
261
        else:
262
            ax.set_xlim(min(self.plttimes)-0.01*self.dt, max(self.plttimes)+0.01*self.dt)
1✔
263
        ticks = ax.yaxis.get_majorticklocs()
1✔
264
        ax.yaxis.set_ticks([ticks[0], 0.0, ticks[-1]])
1✔
265
        pl.xlabel('JD - {:d}'.format(int(np.round(self.epoch))), weight='bold')
1✔
266
        ax.set_ylabel('Residuals', weight='bold')
1✔
267
        ax.yaxis.set_major_locator(MaxNLocator(5, prune='both'))
1✔
268

269
    def plot_phasefold(self, pltletter, pnum):
1✔
270
        """
271
        Plot phased orbit plots for each planet in the fit.
272

273
        Args:
274
            pltletter (int): integer representation of 
275
                letter to be printed in the corner of the first
276
                phase plot.
277
                Ex: ord("a") gives 97, so the input should be 97.
278
            pnum (int): the number of the planet to be plotted. Must be
279
                the same as the number used to define a planet's 
280
                Parameter objects (e.g. 'per1' is for planet #1)
281

282
        """
283

284
        ax = pl.gca()
1✔
285

286
        if len(self.post.likelihood.x) < 20: 
1✔
287
            self.nobin = True
×
288

289
        bin_fac = 1.75
1✔
290
        bin_markersize = bin_fac * rcParams['lines.markersize']
1✔
291
        bin_markeredgewidth = bin_fac * rcParams['lines.markeredgewidth']
1✔
292

293
        rvmod2 = self.model(self.rvmodt, planet_num=pnum) - self.slope
1✔
294
        modph = t_to_phase(self.post.params, self.rvmodt, pnum, cat=True) - 1
1✔
295
        rvdat = self.rawresid + self.model(self.rvtimes, planet_num=pnum) - self.slope_low
1✔
296
        phase = t_to_phase(self.post.params, self.rvtimes, pnum, cat=True) - 1
1✔
297
        rvdatcat = np.concatenate((rvdat, rvdat))
1✔
298
        rverrcat = np.concatenate((self.rverr, self.rverr))
1✔
299
        rvmod2cat = np.concatenate((rvmod2, rvmod2))
1✔
300
        bint, bindat, binerr = fastbin(phase+1, rvdatcat, nbins=25)
1✔
301
        bint -= 1.0
1✔
302

303
        ax.axhline(0, color='0.5', linestyle='--', )
1✔
304
        ax.plot(sorted(modph), rvmod2cat[np.argsort(modph)], 'b-', linewidth=self.fit_linewidth)
1✔
305
        plot.labelfig(pltletter)
1✔
306

307
        telcat = np.concatenate((self.post.likelihood.telvec, self.post.likelihood.telvec))
1✔
308

309
        if self.highlight_last:
1✔
310
            ind = np.argmax(self.rvtimes)
1✔
311
            hphase = t_to_phase(self.post.params, self.rvtimes[ind], pnum, cat=False)
1✔
312
            if hphase > 0.5:
1✔
313
                hphase -= 1
1✔
314
            pl.plot(hphase, rvdatcat[ind], **plot.highlight_format)
1✔
315

316
        plot.mtelplot(phase, rvdatcat, rverrcat, telcat, ax, telfmts=self.telfmts)
1✔
317
        if not self.nobin and len(rvdat) > 10: 
1✔
318
            ax.errorbar(
1✔
319
                bint, bindat, yerr=binerr, fmt='ro', mec='w', ms=bin_markersize,
320
                mew=bin_markeredgewidth
321
            )
322

323
        if self.phase_limits:
1✔
UNCOV
324
            ax.set_xlim(self.phase_limits[0], self.phase_limits[1])
×
325
        else:
326
            ax.set_xlim(-0.5, 0.5)
1✔
327

328
        if not self.yscale_auto: 
1✔
329
            scale = np.std(rvdatcat)
1✔
330
            ax.set_ylim(-self.yscale_sigma*scale, self.yscale_sigma*scale)
1✔
331
        
332
        keys = [p+str(pnum) for p in ['per', 'k', 'e']]
1✔
333

334
        labels = [self.post.params.tex_labels().get(k, k) for k in keys]
1✔
335
        if pnum < self.num_planets:
1✔
336
            ticks = ax.yaxis.get_majorticklocs()
1✔
337
            ax.yaxis.set_ticks(ticks[1:-1])
1✔
338

339
        ax.set_ylabel('RV [{ms:}]'.format(**plot.latex), weight='bold')
1✔
340
        ax.set_xlabel('Phase', weight='bold')
1✔
341

342
        print_params = ['per', 'k', 'e']
1✔
343
        units = {'per': 'days', 'k': plot.latex['ms'], 'e': ''}
1✔
344

345
        anotext = []
1✔
346
        for l, p in enumerate(print_params):
1✔
347
            val = self.post.params["%s%d" % (print_params[l], pnum)].value
1✔
348
            
349
            if self.uparams is None:
1✔
350
                _anotext = r'$\mathregular{%s}$ = %4.2f %s' % (labels[l].replace("$", ""), val, units[p])
1✔
351
            else:
352
                if hasattr(self.post, 'medparams'):
1✔
353
                    val = self.post.medparams["%s%d" % (print_params[l], pnum)]
1✔
354
                else:
UNCOV
355
                    print("WARNING: medparams attribute not found in " +
×
356
                          "posterior object will annotate with " +
357
                          "max-likelihood values and reported uncertainties " +
358
                          "may not be appropriate.")
359
                err = self.uparams["%s%d" % (print_params[l], pnum)]
1✔
360
                if err > 1e-15:
1✔
361
                    val, err, errlow = sigfig(val, err)
1✔
362
                    _anotext = r'$\mathregular{%s}$ = %s $\mathregular{\pm}$ %s %s' \
1✔
363
                               % (labels[l].replace("$", ""), val, err, units[p])
364
                else:
365
                    _anotext = r'$\mathregular{%s}$ = %4.2f %s' % (labels[l].replace("$", ""), val, units[p])
1✔
366

367
            anotext += [_anotext]
1✔
368

369
        if hasattr(self.post, 'derived'):
1✔
370
            has_status = hasattr(self, 'status') and self.status is not None \
1✔
371
                         and 'derive' in self.status and 'chainfile' in self.status['derive']
372
            if has_status:
1✔
373
                chains = pd.read_csv(self.status['derive']['chainfile'])
1✔
374
                self.post.nplanets = self.num_planets
1✔
375
                dp = mcmc_plots.DerivedPlot(chains, self.post)
1✔
376
                labels = dp.labels
1✔
377
                texlabels = dp.texlabels
1✔
378
                units = dp.units
1✔
379
                derived_params = ['mpsini']
1✔
380
                for l, par in enumerate(derived_params):
1✔
381
                    par_label = par + str(pnum)
1✔
382
                    if par_label in self.post.derived.columns:
1✔
383
                        index = np.where(np.array(labels) == par_label)[0][0]
1✔
384

385
                        unit = units[index]
1✔
386
                        if unit == "M$_{\\rm Jup}$":
1✔
UNCOV
387
                            conversion_fac = 0.00315
×
388
                        elif unit == "M$_{\\odot}$":
1✔
UNCOV
389
                            conversion_fac = 0.000954265748
×
390
                        else:
391
                            conversion_fac = 1
1✔
392

393
                        val = self.post.derived["%s%d" % (derived_params[l], pnum)].loc[0.500] * conversion_fac
1✔
394
                        low = self.post.derived["%s%d" % (derived_params[l], pnum)].loc[0.159] * conversion_fac
1✔
395
                        high = self.post.derived["%s%d" % (derived_params[l], pnum)].loc[0.841] * conversion_fac
1✔
396
                        err_low = val - low
1✔
397
                        err_high = high - val
1✔
398
                        err = np.mean([err_low, err_high])
1✔
399
                        err = radvel.utils.round_sig(err)
1✔
400
                        if err > 1e-15:
1✔
401
                            val, err, errlow = sigfig(val, err)
1✔
402
                            _anotext = r'$\mathregular{%s}$ = %s $\mathregular{\pm}$ %s %s' \
1✔
403
                                       % (texlabels[index].replace("$", ""), val, err, units[index])
404
                        else:
UNCOV
405
                            _anotext = r'$\mathregular{%s}$ = %4.2f %s' % (texlabels[index].replace("$", ""), val, units[index])
×
406

407
                        anotext += [_anotext]
1✔
UNCOV
408
            elif self.uparams is not None:
×
409
                # Fallback: use uparams directly for derived param annotations
410
                # when no status/chain file is available (e.g. API usage)
UNCOV
411
                derived_params = ['mpsini']
×
UNCOV
412
                for par in derived_params:
×
UNCOV
413
                    par_label = par + str(pnum)
×
414
                    if par_label in self.uparams and par_label in self.post.derived.columns:
×
UNCOV
415
                        val = self.post.derived[par_label].loc[0.500]
×
416
                        err = self.uparams[par_label]
×
UNCOV
417
                        err = radvel.utils.round_sig(err)
×
UNCOV
418
                        if err > 1e-15:
×
UNCOV
419
                            val, err, errlow = sigfig(val, err)
×
UNCOV
420
                            _anotext = r'$\mathregular{%s}$ = %s $\mathregular{\pm}$ %s' \
×
421
                                       % (par_label, val, err)
422
                        else:
UNCOV
423
                            _anotext = r'$\mathregular{%s}$ = %4.2f' % (par_label, val)
×
UNCOV
424
                        anotext += [_anotext]
×
425

426
        anotext = '\n'.join(anotext)
1✔
427
        plot.add_anchored(
1✔
428
            anotext, loc=1, frameon=True, prop=dict(size=self.phasetext_size, weight='bold'),
429
            bbox=dict(ec='none', fc='w', alpha=0.8)
430
        )
431

432
    def plot_multipanel(self, nophase=False, letter_labels=True):
1✔
433
        """
434
        Provision and plot an RV multipanel plot
435

436
        Args:
437
            nophase (bool, optional): if True, don't
438
                include phase plots. Default: False.
439
            letter_labels (bool, optional): if True, include 
440
                letter labels on orbit and residual plots.
441
                Default: True.
442

443
        Returns:
444
            tuple containing:
445
                - current matplotlib Figure object
446
                - list of Axes objects
447
        """
448

449
        if nophase:
1✔
450
            scalefactor = 1
×
451
        else:
452
            scalefactor = self.phase_nrows
1✔
453

454
        figheight = self.ax_rv_height + self.ax_phase_height * scalefactor
1✔
455

456
        # provision figure
457
        fig = pl.figure(figsize=(self.figwidth, figheight))
1✔
458
        
459
        fig.subplots_adjust(left=0.12, right=0.95)
1✔
460
        gs_rv = gridspec.GridSpec(2, 1, height_ratios=[1., 0.5])
1✔
461

462
        divide = 1 - self.ax_rv_height / figheight
1✔
463
        gs_rv.update(left=0.12, right=0.93, top=0.93,
1✔
464
                     bottom=divide+self.rv_phase_space*0.5, hspace=0.)
465

466
        # orbit plot
467
        ax_rv = pl.subplot(gs_rv[0, 0])
1✔
468
        self.ax_list += [ax_rv]
1✔
469

470
        pl.sca(ax_rv)
1✔
471
        self.plot_timeseries()
1✔
472
        if letter_labels:
1✔
473
            pltletter = ord('a')
1✔
474
            plot.labelfig(pltletter)
1✔
475
            pltletter += 1
1✔
476

477
        # residuals
478
        ax_resid = pl.subplot(gs_rv[1, 0])
1✔
479
        self.ax_list += [ax_resid]
1✔
480

481
        pl.sca(ax_resid)
1✔
482
        self.plot_residuals()
1✔
483
        if letter_labels:
1✔
484
            plot.labelfig(pltletter)
1✔
485
            pltletter += 1
1✔
486

487
        # phase-folded plots
488
        if not nophase:
1✔
489
            gs_phase = gridspec.GridSpec(max([1,self.phase_nrows]), max([1,self.phase_ncols]))
1✔
490
            
491
            if self.phase_ncols == 1:
1✔
492
                gs_phase.update(left=0.12, right=0.93,
1✔
493
                                top=divide - self.rv_phase_space * 0.5,
494
                                bottom=0.07, hspace=0.003)
495
            else:
UNCOV
496
                gs_phase.update(left=0.12, right=0.93,
×
497
                                top=divide - self.rv_phase_space * 0.5,
498
                                bottom=0.07, hspace=0.25, wspace=0.25)
499

500
            for i in range(self.num_planets):
1✔
501
                i_row = int(i / self.phase_ncols)
1✔
502
                i_col = int(i - i_row * self.phase_ncols)
1✔
503
                ax_phase = pl.subplot(gs_phase[i_row, i_col])
1✔
504
                self.ax_list += [ax_phase]
1✔
505

506
                pl.sca(ax_phase)
1✔
507
                self.plot_phasefold(pltletter, i+1)
1✔
508
                pltletter += 1
1✔
509

510
        with warnings.catch_warnings():
1✔
511
            warnings.filterwarnings(
1✔
512
                "ignore",
513
                message="This figure includes Axes that are not compatible with tight_layout",
514
                category=UserWarning,
515
            )
516
            fig.tight_layout()
1✔
517
        if self.saveplot is not None:
1✔
518
            pl.savefig(self.saveplot, dpi=150)
1✔
519
            print("RV multi-panel plot saved to %s" % self.saveplot)
1✔
520

521
        return fig, self.ax_list
1✔
522

523

524
class GPMultipanelPlot(MultipanelPlot):
1✔
525
    """
526
    Class to handle the creation of RV multipanel plots for posteriors fitted
527
    using Gaussian Processes. 
528

529
    Takes the same args as MultipanelPlot, with a few additional bells and whistles...
530
    
531
    Args:
532
        subtract_gp_mean_model (bool, optional): if True, subtract the Gaussian
533
            process mean max likelihood model from the data and the
534
            model when plotting the results. Default: False.
535
        plot_likelihoods_separately (bool, optional): if True, plot a separate
536
            panel for each Likelihood object. Default: False
537
        subtract_orbit_model (bool, optional): if True, subtract the best-fit
538
            orbit model from the data and the model when plotting 
539
            the results. Useful for seeing the structure of correlated
540
            noise in the data. Default: False.
541
        status (ConfigParser): (optional) result of radvel.driver.load_status on the .stat status file
542

543
    """
544
    def __init__(self, post, saveplot=None, epoch=2450000, yscale_auto=False, yscale_sigma=3.0,
1✔
545
                 phase_nrows=None, phase_ncols=None, uparams=None, rv_phase_space=0.08, telfmts={},
546
                 legend=True,
547
                 phase_limits=[], nobin=False, phasetext_size='large',  figwidth=7.5, fit_linewidth=2.0,
548
                 set_xlim=None, text_size=9, legend_kwargs=dict(loc='best'), subtract_gp_mean_model=False,
549
                 plot_likelihoods_separately=False, subtract_orbit_model=False, status=None, separate_orbit_gp=False):
550

551
        super(GPMultipanelPlot, self).__init__(
1✔
552
            post, saveplot=saveplot, epoch=epoch, yscale_auto=yscale_auto,
553
            yscale_sigma=yscale_sigma, phase_nrows=phase_nrows, phase_ncols=phase_ncols,
554
            uparams=uparams, rv_phase_space=rv_phase_space, telfmts=telfmts, legend=legend,
555
            phase_limits=phase_limits, nobin=nobin, phasetext_size=phasetext_size, 
556
            figwidth=figwidth, fit_linewidth=fit_linewidth, set_xlim=set_xlim, text_size=text_size,
557
            legend_kwargs=legend_kwargs
558
        )
559

560
        self.subtract_gp_mean_model = subtract_gp_mean_model
1✔
561
        self.plot_likelihoods_separately = plot_likelihoods_separately
1✔
562
        self.subtract_orbit_model = subtract_orbit_model
1✔
563
        self.separate_orbit_gp = separate_orbit_gp
1✔
564
        if status is not None:
1✔
565
            self.status = status
1✔
566

567
        is_gp = False
1✔
568
        for like in self.like_list:
1✔
569
            if isinstance(like, radvel.likelihood.GPLikelihood):
1✔
570
                is_gp = True
1✔
571
                break
1✔
572
            else:
573
                pass
×
574
        assert is_gp, "This class requires at least one GPLikelihood object in the Posterior."
1✔
575

576
    def plot_gp_like(self, like, orbit_model4data, ci):
1✔
577
        """
578
        Plot a single Gaussian Process Likleihood object in the current Axes, 
579
        including Gaussian Process uncertainty bands.
580

581
        Args:
582
            like (radvel.GPLikelihood): radvel.GPLikelihood object. The model
583
                plotted will be generated from `like.params`.
584
            orbit_model4data (numpy array): 
585
            ci (int): index to use when choosing a color to plot from 
586
                radvel.plot.default_colors. This is only used if the
587
                Likelihood object being plotted is not in the list of defaults.
588
                Increments by 1 if it is used.
589

590
        Returns: current (possibly changed) value of the input `ci`
591

592
        """
593
        ax = pl.gca()
1✔
594

595
        if isinstance(like, radvel.likelihood.GPLikelihood):
1✔
596

597
            if self.set_xlim is not None:
1✔
UNCOV
598
                xpred = np.linspace(self.set_xlim[0]+self.epoch, self.set_xlim[1]+self.epoch, num=int(3e3))
×
599
            else:
600
                xpred = np.linspace(np.min(like.x), np.max(like.x), num=int(3e3))
1✔
601

602
            gpmu, stddev = like.predict(xpred)
1✔
603
            if self.subtract_orbit_model:
1✔
UNCOV
604
                gp_orbit_model = np.zeros(xpred.shape)
×
605
            else:
606
                gp_orbit_model = self.model(xpred)
1✔
607

608
            if ((xpred - self.epoch) < -2.4e6).any():
1✔
UNCOV
609
                pass
×
610
            elif self.epoch == 0:
1✔
UNCOV
611
                self.epoch = 2450000
×
UNCOV
612
                xpred = xpred - self.epoch
×
613
            else:
614
                xpred = xpred - self.epoch
1✔
615

616
            if self.subtract_gp_mean_model:
1✔
UNCOV
617
                gpmu = 0.
×
618
            else:
619
                gp_mean4data, _ = like.predict(like.x)
1✔
620
                indx = np.where(self.post.likelihood.telvec == like.suffix)
1✔
621
                orbit_model4data[indx] += gp_mean4data
1✔
622

623
            if like.suffix not in self.telfmts and like.suffix in plot.telfmts_default:
1✔
624
                color = plot.telfmts_default[like.suffix]['color']
1✔
625
            if like.suffix in self.telfmts:
1✔
UNCOV
626
                color = self.telfmts[like.suffix]['color']
×
627
            if like.suffix not in self.telfmts and like.suffix not in plot.telfmts_default:
1✔
UNCOV
628
                color = plot.default_colors[ci]
×
UNCOV
629
                ci += 1
×
630

631
            ax.fill_between(xpred, gpmu+gp_orbit_model-stddev, gpmu+gp_orbit_model+stddev, 
1✔
632
                            color=color, alpha=0.5, lw=0
633
                            )
634
            if self.separate_orbit_gp:
1✔
UNCOV
635
                ax.plot(xpred, gpmu, '-', color='orange', rasterized=False, lw=0.2, label='GP')
×
UNCOV
636
                ax.plot(xpred, gp_orbit_model, 'g-', rasterized=False, lw=0.2, label="Orbit")
×
UNCOV
637
                ax.plot(xpred, gpmu+gp_orbit_model, 'b-', rasterized=False, lw=0.4, label="Orbit+GP")
×
638
            else:
639
                ax.plot(xpred, gpmu+gp_orbit_model, 'b-', rasterized=False, lw=0.4)
1✔
640
        else:
641
            # plot orbit model
UNCOV
642
            ax.plot(self.mplttimes, self.orbit_model, 'b-', rasterized=False, lw=0.1)
×
643

644
        if not self.yscale_auto: 
1✔
645
            scale = np.std(self.rawresid+self.rvmod)
1✔
646
            ax.set_ylim(-self.yscale_sigma * scale, self.yscale_sigma * scale)
1✔
647

648
        ax.set_ylabel('RV [{ms:}]'.format(**plot.latex), weight='bold')
1✔
649
        ticks = ax.yaxis.get_majorticklocs()
1✔
650
        ax.yaxis.set_ticks(ticks[1:])
1✔
651
        ax.xaxis.set_ticks([])
1✔
652

653
        return ci
1✔
654

655
    def plot_timeseries(self):
1✔
656
        """
657
        Make a plot of the RV data and Gaussian Process + orbit model in the current Axes.
658
        """
659

660
        ax = pl.gca()
1✔
661

662
        ax.axhline(0, color='0.5', linestyle='--')
1✔
663

664
        if self.subtract_orbit_model:
1✔
665
            orbit_model4data = np.zeros(self.rvmod.shape)
×
666
        else:
667
            orbit_model4data = self.rvmod
1✔
668

669
        ci = 0
1✔
670
        for like in self.like_list:
1✔
671
            ci = self.plot_gp_like(like, orbit_model4data, ci)
1✔
672

673
        # plot data
674
        plot.mtelplot(
1✔
675
            # data = residuals + model
676
            self.plttimes, self.rawresid+orbit_model4data, self.rverr,
677
            self.post.likelihood.telvec, ax, telfmts=self.telfmts
678
        )
679
        if self.set_xlim is not None:
1✔
UNCOV
680
            ax.set_xlim(self.set_xlim)
×
681
        else:
682
            ax.set_xlim(min(self.plttimes)-0.01*self.dt, max(self.plttimes)+0.01*self.dt)    
1✔
683
        pl.setp(ax.get_xticklabels(), visible=False)
1✔
684

685
        # legend
686
        if self.legend:
1✔
687
            ax.legend(numpoints=1, **self.legend_kwargs)
1✔
688

689
        # years on upper axis
690
        axyrs = ax.twiny()
1✔
691
        xl = np.array(list(ax.get_xlim())) + self.epoch
1✔
692
        decimalyear = Time(xl, format='jd', scale='utc').decimalyear
1✔
693
        axyrs.plot(decimalyear, decimalyear)
1✔
694
        axyrs.get_xaxis().get_major_formatter().set_useOffset(False)
1✔
695
        axyrs.set_xlim(*decimalyear)
1✔
696
        pl.locator_params(axis='x', nbins=5)
1✔
697
        axyrs.set_xlabel('Year', fontweight='bold')
1✔
698

699

700
    def plot_multipanel(self, nophase=False):
1✔
701
        """
702
        Provision and plot an RV multipanel plot for a Posterior object containing 
703
        one or more Gaussian Process Likelihood objects. 
704
        
705
        Args:
706
            nophase (bool, optional): if True, don't
707
                include phase plots. Default: False.
708
        Returns:
709
            tuple containing:
710
                - current matplotlib Figure object
711
                - list of Axes objects
712
        """
713

714
        if not self.plot_likelihoods_separately:
1✔
715
            super(GPMultipanelPlot, self).plot_multipanel()
1✔
716
        else:
717

718
            if nophase:
1✔
UNCOV
719
                scalefactor = 1
×
720
            else:
721
                scalefactor = self.phase_nrows
1✔
722

723
            n_likes = len(self.like_list)
1✔
724
            figheight = self.ax_rv_height*(n_likes//self.phase_ncols+1.5) + self.ax_phase_height * scalefactor
1✔
725

726
            # provision figure
727
            fig = pl.figure(figsize=(self.figwidth, figheight))
1✔
728
            
729
            fig.subplots_adjust(left=0.12, right=0.95)
1✔
730

731
            hrs = np.zeros(n_likes+1) + 1.
1✔
732
            hrs[-1] = 0.5
1✔
733
            gs_rv = gridspec.GridSpec(n_likes+1, 1, height_ratios=hrs)
1✔
734

735
            divide = 1 - self.ax_rv_height*len(self.like_list) / figheight
1✔
736
            gs_rv.update(left=0.12, right=0.93, top=0.93,
1✔
737
                         bottom=divide+self.rv_phase_space*0.5, hspace=0.0)
738

739
            # orbit plot for each likelihood
740
            pltletter = ord('a')
1✔
741

742
            i = 0
1✔
743
            ci = 0
1✔
744
            for like in self.like_list:
1✔
745

746
                ax = pl.subplot(gs_rv[i, 0])
1✔
747
                i += 1
1✔
748
                self.ax_list += [ax]
1✔
749
                pl.sca(ax)
1✔
750

751
                ax.axhline(0, color='0.5', linestyle='--')
1✔
752

753
                if self.subtract_orbit_model:
1✔
UNCOV
754
                    orbit_model4data = np.zeros(self.rvmod.shape)
×
755
                else:
756
                    orbit_model4data = self.rvmod
1✔
757

758
                self.plot_gp_like(like, orbit_model4data, ci)
1✔
759

760
                # plot data
761
                plot.mtelplot(
1✔
762
                    # data = residuals + model
763
                    self.plttimes, self.rawresid+orbit_model4data, self.rverr,
764
                    self.post.likelihood.telvec, ax, telfmts=self.telfmts
765
                )
766

767
                ax.set_xlim(min(self.plttimes)-0.01*self.dt, max(self.plttimes)+0.01*self.dt)    
1✔
768
                pl.setp(ax.get_xticklabels(), visible=False)
1✔
769

770
                # legend
771
                if self.legend and i == 1:
1✔
772
                    ax.legend(numpoints=1, **self.legend_kwargs)
1✔
773

774
                # years on upper axis
775
                if i == 1:
1✔
776
                    axyrs = ax.twiny()
1✔
777
                    xl = np.array(list(ax.get_xlim())) + self.epoch
1✔
778
                    decimalyear = Time(xl, format='jd', scale='utc').decimalyear
1✔
779
                    axyrs.plot(decimalyear, decimalyear)
1✔
780
                    axyrs.get_xaxis().get_major_formatter().set_useOffset(False)
1✔
781
                    axyrs.set_xlim(*decimalyear)
1✔
782
                    axyrs.set_xlabel('Year', fontweight='bold')    
1✔
783

784
                plot.labelfig(pltletter)
1✔
785
                pltletter += 1  
1✔
786

787
            # residuals
788
            ax_resid = pl.subplot(gs_rv[-1, 0])
1✔
789
            self.ax_list += [ax_resid]
1✔
790

791
            pl.sca(ax_resid)
1✔
792
            self.plot_residuals()
1✔
793
            plot.labelfig(pltletter)
1✔
794
            pltletter += 1
1✔
795

796
            # phase-folded plots
797
            if not nophase:
1✔
798
                gs_phase = gridspec.GridSpec(self.phase_nrows, self.phase_ncols)
1✔
799

800
                if self.phase_ncols == 1:
1✔
801
                    gs_phase.update(left=0.12, right=0.93,
1✔
802
                                    top=divide - self.rv_phase_space * 0.5,
803
                                    bottom=0.07, hspace=0.003)
804
                else:
UNCOV
805
                    gs_phase.update(left=0.12, right=0.93,
×
806
                                    top=divide - self.rv_phase_space * 0.5,
807
                                    bottom=0.07, hspace=0.25, wspace=0.25)
808

809
                for i in range(self.num_planets):
1✔
810
                    i_row = int(i / self.phase_ncols)
1✔
811
                    i_col = int(i - i_row * self.phase_ncols)
1✔
812
                    ax_phase = pl.subplot(gs_phase[i_row, i_col])
1✔
813
                    self.ax_list += [ax_phase]
1✔
814

815
                    pl.sca(ax_phase)
1✔
816
                    self.plot_phasefold(pltletter, i+1)
1✔
817
                    pltletter += 1
1✔
818

819
            if self.saveplot is not None:
1✔
820
                pl.savefig(self.saveplot, dpi=150)
1✔
821
                print("RV multi-panel plot saved to %s" % self.saveplot)
1✔
822

823
            return fig, self.ax_list
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