22 Extract chi2 and degrees of freedom values logged by one or more jointcal runs,
23 print warnings about oddities, and make plots.
35 import matplotlib.pyplot
as plt
37 sns.set_style(
"ticks", {
"legend.frameon":
True})
38 sns.set_context(
"talk")
41 @dataclasses.dataclass
43 """Store the chi2 values read in from a jointcal log file.
49 init_count: int = dataclasses.field(init=
False)
53 self.
rawraw = np.array(self.
rawraw, dtype=np.float64)
54 self.
ndofndof = np.array(self.
ndofndof, dtype=np.int)
59 """Return the index of the first "fit step", after initialization.
63 There are never more than ~25 items in the list, so search optimization
64 is not worth the trouble.
67 if "Fit prepared" in self.kind:
68 return self.kind.index(
"Fit prepared") + 1
70 elif "Fit iteration 0" in self.kind:
71 return self.kind.index(
"Fit iteration 0")
73 raise RuntimeError(f
"Cannot find end of initialization sequence in {self.kind}")
77 """Parse a jointcal logfile to extract chi2 values and plot them.
79 Call the instance with the path to a file to check it for anamolous chi2
80 and output plots to your current directory.
85 Make plots for each file (saved to the current working directory)?
87 Print extra updates during processing?
94 chi2_re =
"jointcal INFO: (?P<kind>.+) chi2/ndof : (?P<chi2>.+)/(?P<ndof>.+)=(?P<reduced_chi2>.+)"
100 self.
figfig = plt.figure(figsize=(15, 8))
104 self.
section_startsection_start = {
"astrometry":
"Starting astrometric fitting...",
105 "photometry":
"Starting photometric fitting..."}
106 self.
section_endsection_end = {
"astrometry":
"Updating WCS for visit:",
107 "photometry":
"Updating PhotoCalib for visit:"}
110 """Parse logfile to extract chi2 values and generate and save plots.
112 The plot output is written to the current directory, with the name
113 derived from the basename of ``logfile``.
118 The filename of the jointcal log to process.
120 title = os.path.basename(logfile)
122 print(
"Processing:", title)
124 with open(logfile)
as opened_log:
127 astrometry = self.
_extract_chi2_extract_chi2(opened_log,
"astrometry")
129 photometry = self.
_extract_chi2_extract_chi2(opened_log,
"photometry")
132 if astrometry
is None and photometry
is None and self.
verboseverbose:
133 print(f
"WARNING: No chi2 values found in {logfile}.")
135 if increased
or self.
plotplot:
136 self.
_plot_plot(astrometry, photometry, title)
137 plotfile = f
"{os.path.splitext(title)[0]}.png"
138 plt.savefig(plotfile, bbox_inches=
"tight")
139 print(
"Saved plot:", plotfile)
141 def _find_chi2_increase(self, chi2Data, title, label, threshold=1):
142 """Return True and print a message if the raw chi2 increases
147 diff = np.diff(chi2Data.raw)
148 ratio = diff/chi2Data.raw[:-1]
149 if np.any(ratio > threshold):
150 increased = np.where(ratio > threshold)[0]
151 print(f
"{title} has increasing {label} chi2:")
152 for x
in zip(chi2Data.raw[increased], chi2Data.raw[increased + 1],
153 ratio[increased], diff[increased]):
154 print(f
"{x[0]:.6} -> {x[1]:.6} (ratio: {x[2]:.6}, diff: {x[3]:.6})")
158 def _extract_chi2(self, opened_log, section):
159 """Return the values extracted from the chi2 statements in the logfile.
168 for line
in opened_log:
172 for line
in opened_log:
177 match = self.
matchermatcher.search(line)
178 if match
is not None:
179 kind.append(match.group(
"kind"))
180 chi2.append(match.group(
"chi2"))
181 ndof.append(match.group(
"ndof"))
182 reduced.append(match.group(
"reduced_chi2"))
188 return Chi2Data(kind, np.array(chi2, dtype=np.float64),
189 np.array(ndof, dtype=int), np.array(reduced, dtype=np.float64))
191 def _plot(self, astrometry, photometry, title):
192 """Generate plots of chi2 values.
196 astrometry : `Chi2Data` or None
197 The as-read astrometry data, or None if there is none to plot.
198 photometry : `Chi2Data` or None
199 The as-read photometry data, or None if there is none to plot.
201 Title for the whole plot.
203 palette = itertools.cycle(sns.color_palette())
206 ax0, ax1 = self.
figfig.subplots(ncols=2, gridspec_kw={
"wspace": 0.05})
208 self.
figfig.suptitle(title)
210 if max(getattr(astrometry,
"raw", [0])) > 100
or max(getattr(photometry,
"raw", [0])) > 100:
211 ax0.set_yscale(
"log")
212 ax1.yaxis.set_label_position(
"right")
213 ax1.yaxis.tick_right()
215 if astrometry
is not None:
216 patch1, patch2 = self.
_plot_axes_plot_axes(ax0, ax1, astrometry, palette, label=
"astrometry")
218 if photometry
is not None:
219 patch3, patch4 = self.
_plot_axes_plot_axes(ax0, ax1, photometry, palette, label=
"photometry")
223 handles, labels = ax0.get_legend_handles_labels()
224 ax1.legend(handles, labels)
226 def _plot_axes(self, ax0, ax1, chi2Data, palette, label=""):
227 """Make the chi2 and degrees of freedom subplots."""
228 xrange = np.arange(0, len(chi2Data.raw), dtype=float)
231 ax0.axhline(1, color=
'grey', ls=
'--')
233 ax0.axvline(chi2Data.init_count-0.5, color=
'grey', lw=0.9)
234 color =
next(palette)
235 patch1 = ax0.plot(xrange[:chi2Data.init_count], chi2Data.raw[:chi2Data.init_count],
'*', ms=10,
236 label=f
"{label} pre-init", color=color)
237 patch2 = ax0.plot(xrange[chi2Data.init_count:], chi2Data.raw[chi2Data.init_count:],
'o', ms=10,
238 label=f
"{label} post-init", color=color)
239 patch1 = ax0.plot(xrange[:chi2Data.init_count], chi2Data.reduced[:chi2Data.init_count],
'*',
240 markerfacecolor=
"none", ms=10, color=color)
241 patch2 = ax0.plot(xrange[chi2Data.init_count:], chi2Data.reduced[chi2Data.init_count:],
'o',
242 markerfacecolor=
"none", ms=10, label=f
"{label} reduced", color=color)
244 ax0.set_xlabel(
"Iteration #", fontsize=20)
245 ax0.set_ylabel(
r"$\chi ^2$", fontsize=20)
248 ax1.axvline(chi2Data.init_count-0.5, color=
'grey', lw=0.9)
249 ax1.plot(xrange[:chi2Data.init_count], chi2Data.ndof[:chi2Data.init_count],
'*', ms=10,
250 label=
"pre-init", color=color)
251 ax1.plot(xrange[chi2Data.init_count:], chi2Data.ndof[chi2Data.init_count:],
'o', ms=10,
252 label=
"post-init", color=color)
254 ax1.set_xlabel(
"Iteration #", fontsize=20)
255 ax1.set_ylabel(
"# degrees of freedom", fontsize=20)
257 return patch1[0], patch2[0]
261 parser = argparse.ArgumentParser(description=__doc__,
262 formatter_class=argparse.RawDescriptionHelpFormatter)
263 parser.add_argument(
"files", metavar=
"files", nargs=
"+",
264 help=
"Log file(s) to extract chi2 values from.")
265 parser.add_argument(
"--plot", action=
"store_true",
266 help=
"Generate a plot PNG for each log file, otherwise just for questionable ones.")
267 parser.add_argument(
"-v",
"--verbose", action=
"store_true",
268 help=
"Print extra information during processing.")
269 return parser.parse_args()
274 log_parser =
LogParser(plot=args.plot, verbose=args.verbose)
275 for file
in args.files:
def _plot(self, astrometry, photometry, title)
def _extract_chi2(self, opened_log, section)
def __init__(self, plot=True, verbose=True)
def _find_chi2_increase(self, chi2Data, title, label, threshold=1)
def _plot_axes(self, ax0, ax1, chi2Data, palette, label="")
def __call__(self, logfile)