GrisFitsFile.py 19.6 KB
Newer Older
Carl Schaffer's avatar
Carl Schaffer committed
1
2
3
4
5
6
"""
Created on 2018-04-09
@author Carl Schaffer
@mail   carl.schaffer@leibniz-kis.de
"""

Carl Schaffer's avatar
Carl Schaffer committed
7
import datetime  # date handling
8
import logging
9
10
import os  # path and file manipulations
import sys  # error handling
11
from glob import glob
12
from os.path import basename, exists, join, dirname
13
from pathlib import Path
14

Carl Schaffer's avatar
linting    
Carl Schaffer committed
15
import astropy.io.fits as fitsio  # fits reading and writing
16
import astropy.units as u
17
import matplotlib.pyplot as plt
Carl Schaffer's avatar
Carl Schaffer committed
18
import numpy as np
19
from astropy.coordinates import SkyCoord, Angle
20
from astropy.wcs import WCS
Carl Schaffer's avatar
Carl Schaffer committed
21

Carl Schaffer's avatar
Carl Schaffer committed
22
from kis_tools.generic.fits import FitsFile
23
from kis_tools.gris.coord_correction_ml import get_coords_ml
24
from kis_tools.gris.util import telescope_to_hpc, expand_coords, calculate_slit_translation, calculate_slit_angle
25
from kis_tools.util.calculations import estimate_telescope_drift
26
from kis_tools.util.constants import gris_stepsize_along_slit
Carl Schaffer's avatar
Carl Schaffer committed
27
from kis_tools.util.util import date_from_fn, gris_obs_mode
Carl Schaffer's avatar
Carl Schaffer committed
28

29
30
logger = logging.getLogger(__name__)

Carl Schaffer's avatar
Carl Schaffer committed
31

32
class GrisFitsFile(FitsFile):
Carl Schaffer's avatar
Carl Schaffer committed
33
34
35
36
37
    """Class for handling fits files for GRIS db
    When the constructor is called, the filename is stored and basic
    metadata parameters such as date and runnumber are extracted from
    the filename. Once the class function GrisFitsFile.parse() is
    called, metadata from the fits file is available as a dictionary
Carl Schaffer's avatar
typo    
Carl Schaffer committed
38
    stored at GrisFitsFile.properties. Any errors occurring during
Carl Schaffer's avatar
Carl Schaffer committed
39
40
    opening and parsing of the file are stored as strings in
    GrisFitsFile.errors
Carl Schaffer's avatar
Carl Schaffer committed
41

42
43
44
45
46
    Args:
      filename: filename of target fits file

    Returns:

Carl Schaffer's avatar
Carl Schaffer committed
47
    """
Carl Schaffer's avatar
Carl Schaffer committed
48

Carl Schaffer's avatar
Carl Schaffer committed
49
50
    def __init__(self, filename):
        """Constructor"""
51
52
        if isinstance(filename, Path):
            filename = str(filename.resolve())
53
        super().__init__(filename)
Carl Schaffer's avatar
Carl Schaffer committed
54
        assert exists(filename)
Carl Schaffer's avatar
Carl Schaffer committed
55
56
57
58
        self.properties = {}
        self.errors = []

        self.filename = filename
59
        self.name = basename(filename)
60
        self.path = self.filename
61

62
        self.dirname = dirname(self.path)
Carl Schaffer's avatar
Carl Schaffer committed
63
64
65
66
        self.parse_filename()
        self.is_parsed = False

    def parse_filename(self):
67
        """Extract metadata from filename"""
Carl Schaffer's avatar
Carl Schaffer committed
68
        # Extract time from Filename
69
        name = os.path.splitext(basename(self.filename))[0].split("_")
Carl Schaffer's avatar
linting    
Carl Schaffer committed
70
71
72
        year = int(name[1][:4])
        month = int(name[1][4:6])
        day = int(name[1][6:])
Carl Schaffer's avatar
Carl Schaffer committed
73
74

        # Get time
Carl Schaffer's avatar
linting    
Carl Schaffer committed
75
76
77
        # hh = int(name[2][:2])
        # mm = int(name[2][2:4])
        # outstring = int(name[2][4:6])
Carl Schaffer's avatar
Carl Schaffer committed
78

Carl Schaffer's avatar
linting    
Carl Schaffer committed
79
        # self.date = datetime.datetime(year,month,day,hh,mm,outstring)
Carl Schaffer's avatar
Carl Schaffer committed
80
        self.date = datetime.datetime(year, month, day, 0, 0, 0)
Carl Schaffer's avatar
Carl Schaffer committed
81

82
        # Extract Run-, Map-, and Stepnumber, as well as mode string
83
        self.runnumber = int(name[4])
84
        self.mapnumber = int(name[5])
85
        self.slitposnumber = int(name[6])
86
87
        self.modestring = name[3]

88
    @property
Carl Schaffer's avatar
Carl Schaffer committed
89
    def verbose_runname(self):
90
91
92
93
94
95
        """
        Get verbose run name such as 24apr14.002

        Returns:
            outstring: verbose run name
        """
96
97
98
99
        outstring = self.date.strftime("%d%b%y").lower()
        outstring += f".{int(self.runnumber):03d}"
        return outstring

100
101
102
103
104
105
106
107
108
109
110
111
112
    @property
    def telescope_centering(self):
        """
        Determine state of telescope centering at the time of observation, queries GDBS
        for centering data

        Returns:
            outstring: x_offset, y_offset, x_uncertainty, y_uncertainty all in arcseconds
        """
        # Query centering from GDBS
        try:
            from kis_tools.gdbs.query_gdbs import query_gdbs
            gdbs_usable = True
113
114
            x_telcenter, x_valid, x_age = query_gdbs('xsuncenter', self.obs_time)
            y_telcenter, y_valid, y_age = query_gdbs('ysuncenter', self.obs_time)
115
116
117
118
119
120
121
122
123
124
            # Determine uncertainties for centering values
            centering_age = max(x_age, y_age)
            drift = estimate_telescope_drift(centering_age.seconds / 3600)
            x_std, y_std = drift, drift

            # if the centering is older than 24 hours, discard
            if centering_age > datetime.timedelta(hours=24):
                gdbs_usable = False
        except IOError:
            gdbs_usable = False
125
126
        except ValueError:
            gdbs_usable = False
127

128
        # Catch instances where no numbers but error strings are written to GDBS
Carl Schaffer's avatar
bugfix    
Carl Schaffer committed
129
130
131
132
133
        if gdbs_usable:
            try:
                x_telcenter, y_telcenter = float(x_telcenter), float(y_telcenter)
            except ValueError:
                gdbs_usable = False
134

135
136
137
138
139
140
141
        if not gdbs_usable:
            # Determined from offset between uncentered and cross_correlated data
            # STD-DEV of distances in X and Y again see gris coord study for details
            uncertainty_no_center_x = 117.1
            uncertainty_no_center_y = 182.4
            x_telcenter, y_telcenter, x_std, y_std = 0, 0, uncertainty_no_center_x, uncertainty_no_center_y

142
        return x_telcenter, y_telcenter, x_std, y_std
143

Carl Schaffer's avatar
Carl Schaffer committed
144
    @property
145
    def _coords_from_simple_header(self):
146
147
148
149
150
151
152
153
154
155
        """coordinates for slit, the Keywords 'SLITPOSX' and 'SLITPOSY' are assumed to be the center of the slit
         while the keyword 'SLITORIE' describes the rotation of the slit w.r.t. the helioprojective axes. The
         algorithm further assumes that each pixel represents a square area with side length of 'STEPSIZE'.
         These assumptions are used to calculate the coordinates for the center of each pixel within the data array.

        Returns:
            (X,Y):ndarray(float) x and y coordinates for each pixel of the slit


        WARNING! The code below is not checked for correctness! We do not know what direction SLITORIE references
Carl Schaffer's avatar
Carl Schaffer committed
156
157
        off and coordinates provided by GREGOR are very error-prone. If you need precision better than ~50" this
        function is not sufficient.
158
159
        """

Carl Schaffer's avatar
Carl Schaffer committed
160
        h = self.header
Carl Schaffer's avatar
Carl Schaffer committed
161
        slit_length_pixels = self.NAXIS2 if self.old_header else self.NAXIS1
Carl Schaffer's avatar
Carl Schaffer committed
162

163
        # Slit center in telescope coordinates
Carl Schaffer's avatar
Carl Schaffer committed
164
        x_slitcenter = self.SLITPOSX if self.header['SLITPOSX'] != "" else 0
Carl Schaffer's avatar
Carl Schaffer committed
165
        y_slitcenter = self.SLITPOSY if self.header['SLITPOSY'] != "" else 0
Carl Schaffer's avatar
Carl Schaffer committed
166
167

        coord_ref = slit_length_pixels // 2
Carl Schaffer's avatar
Carl Schaffer committed
168
169
170
171
        if "SLITCNTR" in self.header:
            if self.SLITCNTR.strip() != 'scancntr':
                raise ValueError(
                    f'Cannot determine reference pixel for coordinates! SLITCNTR is {self.SLITCNTR} instead of scancntr')
Carl Schaffer's avatar
Carl Schaffer committed
172

173
174
175
176
177
        # Transform to HPC (shift by telescope centering, rotate by p0 angle)
        delta_x, delta_y, std_delta_x, std_delta_y = self.telescope_centering

        # If uncertainties are too large, use ML model to correct the coordinates
        from kis_tools.gris.coord_correction_ml import coord_model_stdx, coord_model_stdy
178

179
        if std_delta_x > coord_model_stdx or std_delta_y > coord_model_stdy:
180
181
182
183
184
185
            try:
                x_hpc, y_hpc, std_delta_x, std_delta_y = get_coords_ml(self)
            except ValueError as e:
                logger.warning(f"Error in finding ML coords for {self.__repr__()}:{e}")
                # Point to slitpos with 1 solar disk uncertainty if coord retrieval didn't work
                x_hpc, y_hpc, std_delta_x, std_delta_y = x_slitcenter, y_slitcenter, 1000, 1000
186
187
        else:
            x_hpc, y_hpc = telescope_to_hpc(x_slitcenter, y_slitcenter, self.p0_angle, delta_x, delta_y)
Carl Schaffer's avatar
Carl Schaffer committed
188

189
190
191
192
193
        try:
            angle = self.slit_orientation
        except ValueError as e:
            logger.warning(f"Could not determine Slit orientation! \n {e}")
            angle = 0
Carl Schaffer's avatar
Carl Schaffer committed
194

195
        # With the center pixel value given, the coordinates need to be expanded to a full array.
196
197
        X, Y = expand_coords(x_hpc, y_hpc,
                             slit_orientation=angle,
Carl Schaffer's avatar
Carl Schaffer committed
198
                             reference_pixel=coord_ref,
199
200
201
                             stepsize_slit=gris_stepsize_along_slit,
                             slit_length_pixels=slit_length_pixels)

202
203
204
205
206
207
208
209
210
211
212
213
        # Coordinates need to be shifted by the step number of the slit. If the derotator is in
        # or the stepangle is set to 0, each of these factors introduces a flip in the map, canceling
        # each other out if both are true. If a flip is present, we need to start at the maximum slit
        # positions and move slitpos steps towards slitposition 0.
        reversed_scanning = self.step_angle == 0
        derotator_in = self.derotator_in

        slit_number = self.slitposnumber
        if reversed_scanning != derotator_in:
            slit_number = self.number_of_maps - slit_number - 1

        coord_shift = calculate_slit_translation(slit_number=slit_number, stepsize_perp=self.STEPSIZE,
214
215
216
217
                                                 slit_orientation=angle)

        X = X + coord_shift[0]
        Y = Y + coord_shift[1]
218
219
220
221
222

        coord_array = np.array([X, Y]) * u.arcsec
        # reorder axes for consistency with other coordinate retrieval methods
        coord_array = np.swapaxes(coord_array, 0, 1)

223
224
        # track coordinate uncertainty within class
        self.coord_uncertainty = (std_delta_x, std_delta_y)
225
        return coord_array
226

227
228
    @property
    def coords(self):
Carl Schaffer's avatar
Carl Schaffer committed
229
230
231
232
        """
        Returns:array(float) shape (2,len(slit)) helioprojective coordinates in arcsecnd along slit
        """

233
234
235
236
237
238
        try:
            coords = self._coords_from_wcs
        except AssertionError:
            coords = self._coords_from_simple_header
        return coords

239
240
241
242
    @property
    def old_header(self):
        """Check if the header of this file has already been processed"""
        old_header = True
Carl Schaffer's avatar
Carl Schaffer committed
243
        if any([k in self.header for k in ["SOLARNET"]]):
244
245
            old_header = False
        return old_header
Carl Schaffer's avatar
Carl Schaffer committed
246

247
248
249
250
251
    @property
    def zenit(self):
        """
        Get Zenit angle (90° - elevation)
        Returns: zenit float in degrees
252
        Raises Value error if elevation is not contained in the header
253

254
255
256
257
258
259
260
261
262
263
264
        """
        zenit = 90 - self.elevation
        return zenit

    @property
    def elevation(self):
        """
        Get elevation angle
        Returns: in degrees
        Raises Value error if elevation is not contained in the header

265
        """
266
267
268
        elev = self._get_value(['ELEV_ANG', 'ELEVATIO'])
        if elev == '':
            raise ValueError(f'No elevation tracked for {self.__repr__()}')
269
270

        return elev
271

Carl Schaffer's avatar
Carl Schaffer committed
272
273
    @property
    def slit_orientation(self):
Carl Schaffer's avatar
Carl Schaffer committed
274
        """Get slit orientation:
Carl Schaffer's avatar
Carl Schaffer committed
275
         clockwise angle from the solar equator, scan direction is 90° further in clockwise direction.
276

277
        Attempts to correct for derotator and image rotation, usually correct within a couple 10s of degrees.
278
        Returns:
279
            slitorie: float (degrees)
280
        """
281
        # Pull Values from header
282
        p0_angle = self.p0_angle
283
        rotangle = self.ROTANGLE if 'ROTANGLE' in self.header else None
284
        azimut = self._get_value(['AZIMUT'])
285
286
287
288
289
290
291
292
293
294
        elev = self.elevation

        # Calculate Angel
        angle = calculate_slit_angle(
            rotangle=rotangle,
            azimuth=azimut,
            elevation=elev,
            p0_angle=p0_angle,
            date=self.obs_time
        )
295

Carl Schaffer's avatar
Carl Schaffer committed
296
297
298
299
300
301
302
        # *******************************************************************************************
        # **************WARNING!*********************************************************************
        # *******************************************************************************************
        #
        # We currently do not have a validated way to calculate the orientation of the slit. If there is a
        # cross correlation file, we use that, otherwise we return '0' with a warning

303
        return angle
Carl Schaffer's avatar
Carl Schaffer committed
304
305

    @property
306
307
    def number_of_maps(self):
        """
308
309

        Returns:
310
            nmaps: int number of maps in associated observation
311
        """
Carl Schaffer's avatar
Carl Schaffer committed
312
        return self._get_value(["NMAPS", "SERIES"])
Carl Schaffer's avatar
Carl Schaffer committed
313
314
315

    @property
    def map_index(self):
316
317
318
319
320
        """

        Returns:
            i_map: int map index within observation
        """
Carl Schaffer's avatar
Carl Schaffer committed
321
        return self._get_value(["IMAP", "ISERIE"])
Carl Schaffer's avatar
Carl Schaffer committed
322
323
324

    @property
    def n_steps(self):
325
326
327
328
329
330
331
332
333
334
335
336
337
        """
        Determine the number of slit scanning steps in the associated map.

        Returns:
            nsteps: number of steps in the associated map

        """
        # get initial guess from header
        nsteps = self._get_value(["STEPS", "NSTEPS"])

        # check if other files matching the same run are present. If so, check the running step number
        # for the same map to determine the actual number of steps. It is assumed, that all files belonging to the
        # same map are stored in the same directory
Carl Schaffer's avatar
Carl Schaffer committed
338
        pattern = f"gris_*_*_*_{self.runnumber:03d}_{self.mapnumber:03d}_*.fits"
339
340
        matching_files = glob(join(self.dirname, pattern))
        max_steps = int(
Carl Schaffer's avatar
Carl Schaffer committed
341
            max([basename(m).split(".")[0].split("_")[-1] for m in matching_files])
342
343
344
345
346
347
        )

        if max_steps < nsteps:
            nsteps = max_steps

        return nsteps
Carl Schaffer's avatar
Carl Schaffer committed
348

349
350
    @property
    def obs_time(self):
351
352
353
354
355
        """

        Returns:
            obs_time: observation time given in filename
        """
Carl Schaffer's avatar
Carl Schaffer committed
356
        return date_from_fn(self.filename)
357

Carl Schaffer's avatar
Carl Schaffer committed
358
359
    @property
    def step_index(self):
360
361
362
363
364
        """

        Returns:
            step_index: index of scanning step within current map
        """
Carl Schaffer's avatar
Carl Schaffer committed
365
        return self._get_value(["ISTEP"])
Carl Schaffer's avatar
Carl Schaffer committed
366

367
368
369
370
371
372
373
    @property
    def p0_angle(self):
        """

        Returns:
            p0_angle: angle between telescope and solar north
        """
374
375
376
377
        angle = self._get_value(["P0ANGLE", 'SOLAR_P0'])
        if angle == '':
            raise ValueError(f'Header of {self} contains illegal p0_angle:"{angle}"')
        return angle
378

379
380
    @property
    def step_angle(self):
381
382
383
384
385
386
        """

        Returns:
            step_angle: scanning angle
        """

Carl Schaffer's avatar
Carl Schaffer committed
387
        return self._get_value(["STEPANGL"])
388
389
390
391

    @property
    def wavelength_step_size(self):
        """Get step length along wavelength axis. We use the values from the continuum correction fitting"""
Carl Schaffer's avatar
Carl Schaffer committed
392
        return self._kw_mean(["FF1WLDSP", "FF2WLDSP"])
393
394
395
396
397

    @property
    def wavelength_region(self):
        """Get wavelength region covered by file. Returns tuple of (upper, lower)"""
        step_size = self.wavelength_step_size
398

399
        n_steps_wl = self.data.shape[-1] if self.old_header else self.NAXIS3
400
401

        # average for lower border:
Carl Schaffer's avatar
Carl Schaffer committed
402
403
        lower = self._kw_mean(["FF1WLOFF", "FF2WLOFF"])
        # add wl steps to calculate upper border
404
405
406
407
        upper = lower + step_size * n_steps_wl

        return lower, upper

408
409
410
411
412
413
414
415
416
    @property
    def derotator_in(self):
        """
        Determine whether the derotator is inserted

        Returns:
            bool: derotator in

        """
417
418
419
420
421
422
        try:
            rotval = self._get_value(["ROTCODE"])
        except KeyError:
            # if no derotator keywords are scontained, assume it doesn't exist yet
            return False

423
424
425
426
427
        if rotval == 0:
            return True
        else:
            return False

Carl Schaffer's avatar
Carl Schaffer committed
428
429
430
431
432
433
434
435
436
437
438
439
    @property
    def states(self):
        """
        Determine number of polarimetric states

        Returns:
            n_states int: number of states
        """

        # Number of polarimetric states is stored in the last axis. Determine
        # number of axes from NAXIS keyword and query value for last axis.
        naxes = self.query("NAXIS")
440
        keyword = f"NAXIS{naxes}"
Carl Schaffer's avatar
Carl Schaffer committed
441
442
443
444
        states = self.query(keyword)

        return states

445
446
    @property
    def obs_mode(self):
447
        """Get observation mode"""
448
        self.parse()
449
450
451
452
        # extract obs mode keywords
        states = self.states
        n_maps = self.number_of_maps
        n_steps = self.n_steps
453

454
        mode = gris_obs_mode(states, n_maps, n_steps)
455
        return mode
Carl Schaffer's avatar
Carl Schaffer committed
456

Carl Schaffer's avatar
Carl Schaffer committed
457
458
459
    def parse(self):
        """extract data and metadata from file"""
        if self.is_parsed:
Carl Schaffer's avatar
Carl Schaffer committed
460
            return
461
462
463
464
465
466
467
468
469

        def is_valid(value):
            valid = True
            if isinstance(value, fitsio.card.Undefined):
                valid = False
            elif value == "-NAN":
                valid = False

            return valid
Carl Schaffer's avatar
Carl Schaffer committed
470
471

        # Read file
Carl Schaffer's avatar
Carl Schaffer committed
472
        try:
Carl Schaffer's avatar
Carl Schaffer committed
473
            header = self.header
474

Carl Schaffer's avatar
Carl Schaffer committed
475
            # Add header to self.properties, cast strange data types where
476
477
            # necessary, fill invalid values with empty strings

Carl Schaffer's avatar
Carl Schaffer committed
478
            for key in header.keys():
479
480
481
                value = header[key]
                # check validity:
                value = value if is_valid(value) else ""
Carl Schaffer's avatar
Carl Schaffer committed
482
                if key in ["COMMENT", "HISTORY"]:
483
                    self.properties[key] = str(value)
Carl Schaffer's avatar
Carl Schaffer committed
484
                else:
485
486
                    self.properties[key] = value

Carl Schaffer's avatar
Carl Schaffer committed
487
            self.is_parsed = True
Carl Schaffer's avatar
Carl Schaffer committed
488
        except:
Carl Schaffer's avatar
linting    
Carl Schaffer committed
489
            exception = sys.exc_info()[0]
Carl Schaffer's avatar
Carl Schaffer committed
490
            self.errors.append("File %s" % self.filename + str(exception))
Carl Schaffer's avatar
Carl Schaffer committed
491
492

    def make_bson(self):
Carl Schaffer's avatar
Carl Schaffer committed
493
494
        """construct BSON document for mongodb storage"""
        return self.properties
Carl Schaffer's avatar
Carl Schaffer committed
495

496
    @property
497
    def _coords_from_wcs(self):
498
499
500
501
        """
        Retrieve spatial coordinates for each pixel in for slices projected into the spatial dimensions.
        Returns a data cube shaped s the spatial dimensions
        Returns: arcsec, cube of (NAXIS1, NAXIS2, 2) Values are Helioprojective X and Y in arcseconds
502
503
504

        Side-effect: Sets self.coord_uncertainty

505
506
        """

Carl Schaffer's avatar
Carl Schaffer committed
507
508
509
        assert (
            not self.old_header
        ), f"Can't get coordinates from {self.path} ! Run header conversion first!"
510

511
512
513
        wcs = WCS(self.header)
        pixel_shape = wcs.pixel_shape
        n_y = pixel_shape[1]
514
        slit_list = np.zeros((n_y, self["NAXIS"]))
515
516
        slit_list[:, 1] = np.arange(n_y)
        slit_list[:, 0] = 0
517
        converted = np.array(wcs.pixel_to_world_values(slit_list))
518
        degrees = Angle(u.degree * converted[:, :2]).wrap_at(180 * u.degree)
519
        arcsec = degrees.to(u.arcsec)
520

521
522
523
        # Track coord uncertainty within instance
        self.coord_uncertainty = (self.CSYER1, self.CSYER2)

524
525
        return arcsec

526
    def plot_slit(self):
527
        m = self.full_disk_map
528

529
        # retrieve different data sources
530
        coordinates = self.coords
531
532
533
534

        X, Y = coordinates[:, 0], coordinates[:, 1]

        # determine fov limits for plotting
535

536
        plotting_padding = 300 * u.arcsec
537
538
539
540
        x_min = min(X) - plotting_padding
        x_max = max(X) + plotting_padding
        y_min = min(Y) - plotting_padding
        y_max = max(Y) + plotting_padding
541
542

        # Create submap of FOV and plot
Carl Schaffer's avatar
Carl Schaffer committed
543
544
545
546
        submap = m.submap(
            SkyCoord(x_min, y_min, frame=m.coordinate_frame),
            SkyCoord(x_max, y_max, frame=m.coordinate_frame),
        )
547
548
549
550
551
552
        fig = plt.figure()  # noqa F841
        ax = plt.subplot(projection=submap)
        im = submap.plot()  # noqa:F841

        # create SkyCoord objects and overplot
        slit_wcs = SkyCoord(X, Y, frame=submap.coordinate_frame)
Carl Schaffer's avatar
Carl Schaffer committed
553
        ax.plot_coord(slit_wcs, "g-", label="WCS Coordinates")
554
555
556

        # Create Legend and annotations
        title = "Gris Slit {0}_{1:03d}_{2:03d}_{3:03d}"
Carl Schaffer's avatar
Carl Schaffer committed
557
558
559
560
561
562
        title = title.format(
            self.obs_time.strftime("%Y%m%d"),
            self.runnumber,
            self.mapnumber,
            self.step_index,
        )
563
564
        plt.title(title)

Carl Schaffer's avatar
Carl Schaffer committed
565
566
567
568
569
570
571
572
573
        annotation = (
            f"Spectrum taken at \n{self.obs_time.strftime('%Y-%m-%d %H:%M:%S')}"
        )
        annotation += (
            f"\nHMI Reference taken at \n{submap.date.strftime('%Y-%m-%d %H:%M:%S')}"
        )
        plt.text(
            0.05, 0.8, annotation, horizontalalignment="left", transform=ax.transAxes
        )
574
575
        plt.tight_layout(pad=3.5)
        _ = plt.legend()
576
577
578

        return fig, ax

Carl Schaffer's avatar
Carl Schaffer committed
579
    def __str__(self):
Carl Schaffer's avatar
Carl Schaffer committed
580
581
582
        outstring = (
            "GrisFitsFile %s from %s:\n\tRun: %s\n\tMap: %s\n\tSlitposition: %s\n"
        )
Carl Schaffer's avatar
linting    
Carl Schaffer committed
583
        outstring = outstring % (
Carl Schaffer's avatar
Carl Schaffer committed
584
            os.path.basename(self.filename),
Carl Schaffer's avatar
Carl Schaffer committed
585
586
            self.date,
            self.runnumber,
587
            self.map_index,
Carl Schaffer's avatar
Carl Schaffer committed
588
            self.slitposnumber,
Carl Schaffer's avatar
Carl Schaffer committed
589
        )
Carl Schaffer's avatar
linting    
Carl Schaffer committed
590
        return outstring
591
592


Carl Schaffer's avatar
Carl Schaffer committed
593
if __name__ == "__main__":
594
595
596
    gf = GrisFitsFile(sys.argv[1])
    gf.parse()
    print(gf)