diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9c8d6090..66e4844d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 @@ -48,5 +48,3 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos fail_ci_if_error: true # optional (default = false) - - diff --git a/docs/change_history.rst b/docs/change_history.rst index 5e99ade9..64d24de7 100644 --- a/docs/change_history.rst +++ b/docs/change_history.rst @@ -1,6 +1,7 @@ Change History ############## + .. _v1.3.8: V1.3.8 Not Released @@ -112,7 +113,7 @@ V1.3.0 06-03-2020 - Removed bias overscan and trimming correction on master bias creation. - Bugs Fixed: - + `--max-targets` was not being used, missed connection in `MainApp`. + + `--max-targets` was not being used, missed connection in `RedSpec`. - Updated keyword values of all reference lamps in the library according to [#292] - Refactored `wavelength.WavelengthCalibration` class moving several methods to diff --git a/docs/conf.py b/docs/conf.py index 0bbc8496..c5e497d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -186,8 +186,6 @@ 'Miscellaneous'), ] - - # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. @@ -207,5 +205,3 @@ # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] - - diff --git a/environment.yml b/environment.yml index 091a4347..4dc0a12f 100644 --- a/environment.yml +++ b/environment.yml @@ -14,5 +14,6 @@ dependencies: - pydata-sphinx-theme - astroplan - mock + - setuptools - sphinx - sphinxcontrib.napoleon diff --git a/goodman_pipeline/__init__.py b/goodman_pipeline/__init__.py index b5efb05f..dfd0af4b 100644 --- a/goodman_pipeline/__init__.py +++ b/goodman_pipeline/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + from __future__ import absolute_import from .version import __version__ @@ -6,4 +8,4 @@ from . import images from . import core -from .core import setup_logging \ No newline at end of file +from .core import setup_logging diff --git a/goodman_pipeline/core/__init__.py b/goodman_pipeline/core/__init__.py index 32698dc8..eb09d6dc 100644 --- a/goodman_pipeline/core/__init__.py +++ b/goodman_pipeline/core/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + from __future__ import (absolute_import, division, print_function, unicode_literals) @@ -13,7 +15,7 @@ # import of functions in core.py from .core import (astroscrappy_lacosmic, - add_wcs_keys, + add_linear_wcs_keys, add_linear_wavelength_solution, bin_reference_data, call_cosmic_rejection, @@ -52,6 +54,7 @@ recenter_broad_lines, recenter_lines, record_trace_information, + record_wavelength_solution_evaluation, save_extracted, search_comp_group, setup_logging, diff --git a/goodman_pipeline/core/core.py b/goodman_pipeline/core/core.py index c9115669..32f4d8a1 100644 --- a/goodman_pipeline/core/core.py +++ b/goodman_pipeline/core/core.py @@ -29,11 +29,13 @@ from astropy.stats import sigma_clip from astropy.time import Time from astroscrappy import detect_cosmics +from ccdproc import CCDData from matplotlib import pyplot as plt from scipy import signal, interpolate from threading import Timer from . import check_version +from ..wcs import WCS __version__ = __import__('goodman_pipeline').__version__ @@ -61,7 +63,7 @@ def astroscrappy_lacosmic(ccd, red_path=None, save_mask=False): return ccd -def add_wcs_keys(ccd): +def add_linear_wcs_keys(ccd): """Adds generic keyword for linear wavelength solution to the header Linear wavelength solutions require a set of standard fits keywords. Later @@ -146,7 +148,7 @@ def add_linear_wavelength_solution(ccd, x_axis, reference_lamp, crpix=1): astronomical tool. (e.g. IRAF) Args: - ccd (CCDData) Instance of :class:`~astropy.nddata.CCDData` + ccd (CCDData): Instance of :class:`~astropy.nddata.CCDData` x_axis (ndarray): Linearized x-axis in angstrom reference_lamp (str): Name of lamp used to get wavelength solution. crpix (int): reference pixel for defining wavelength solution. @@ -162,6 +164,22 @@ def add_linear_wavelength_solution(ccd, x_axis, reference_lamp, crpix=1): new_crpix = crpix new_crval = x_axis[new_crpix - crpix] new_cdelt = x_axis[new_crpix] - x_axis[new_crpix - crpix] + incoming_header_keys = ccd.header.keys() + linear_wcs_keys = ['BANDID1', + 'WCSDIM', + 'CTYPE1', + 'CRVAL1', + 'CRPIX1', + 'CDELT1', + 'CD1_1', + 'LTM1_1', + 'WAT0_001', + 'WAT1_001', + 'DC-FLAG', + 'DCLOG1'] + + if not all([key in incoming_header_keys for key in linear_wcs_keys]): + ccd = add_linear_wcs_keys(ccd=ccd) ccd.header.set('BANDID1', 'spectrum - background none, weights none, ' 'clean no') @@ -175,7 +193,7 @@ def add_linear_wavelength_solution(ccd, x_axis, reference_lamp, crpix=1): ccd.header.set('WAT0_001', 'system=equispec') ccd.header.set('WAT1_001', 'wtype=linear label=Wavelength units=angstroms') ccd.header.set('DC-FLAG', 0) - ccd.header.set('DCLOG1', 'REFSPEC1 = {:s}'.format(reference_lamp)) + ccd.header.set('DCLOG1', 'REFSPEC1 = {:s}'.format(os.path.basename(reference_lamp))) return ccd @@ -323,7 +341,7 @@ def call_cosmic_rejection(ccd, generate_dcr_parameters_file(instrument=_instrument, binning=_binning, path=red_path) - #out_prefix = prefix + out_prefix #Move line here + # out_prefix = prefix + out_prefix #Move line here full_path = os.path.join(red_path, f"{out_prefix}_{image_name}") @@ -574,6 +592,7 @@ def cross_correlation(reference, compared, slit_size, serial_binning, + selection_bias='none', mode='full', plot=False): """Do cross correlation of two 1D spectra @@ -591,6 +610,8 @@ def cross_correlation(reference, compared (array): Array to be matched. A new reference lamp. slit_size (float): Slit width in arcseconds serial_binning (int): Binning in the spectral axis + selection_bias (str): For arrays expected to be similar therefore a relatively small shift is expected select 'center'. + 'none' (default) or 'center'. mode (str): Correlation mode for `signal.correlate`. plot (bool): Switch debugging plots on or off. @@ -619,19 +640,35 @@ def cross_correlation(reference, ccorr = signal.correlate(cyaxis1, cyaxis2, mode=mode) - max_index = np.argmax(ccorr) - x_ccorr = np.linspace(-int(len(ccorr) / 2.), int(len(ccorr) / 2.), len(ccorr)) + if selection_bias == 'center': + gaussian_model = models.Gaussian1D(amplitude=1, mean=len(ccorr) / 2., stddev=50) + gaussian_weights = gaussian_model(range(len(x_ccorr))) + gaussian_weighted = gaussian_weights * ccorr + + max_index = np.argmax(gaussian_weighted) + elif selection_bias == 'none': + max_index = np.argmax(ccorr) + + else: + raise NotImplementedError(f"'selection_bias' {selection_bias} is not valid. Options are 'none' and 'center'") correlation_value = x_ccorr[max_index] + + log.debug(f"Found correlation value of {correlation_value}") + if plot: # pragma: no cover plt.ion() plt.title('Cross Correlation') plt.xlabel('Lag Value') plt.ylabel('Correlation Value') - plt.plot(x_ccorr, ccorr) + plt.plot(x_ccorr, ccorr, label='Original Cross Correlation') + if selection_bias == 'center': + plt.plot(x_ccorr, gaussian_weights, label="Centered Gaussian") + plt.plot(x_ccorr, gaussian_weighted, label='Gaussian Weighted') + plt.legend(loc='best') plt.draw() plt.pause(2) plt.clf() @@ -742,9 +779,9 @@ def classify_spectroscopic_data(path, search_pattern): for i in pifc.index.tolist(): radeg, decdeg = ra_dec_to_deg(pifc.obsra.iloc[i], pifc.obsdec.iloc[i]) - pifc.iloc[i, pifc.columns.get_loc('radeg')] = '{:.2f}'.format(radeg) + pifc.iloc[i, pifc.columns.get_loc('radeg')] = '{:.6f}'.format(radeg) - pifc.iloc[i, pifc.columns.get_loc('decdeg')] = '{:.2f}'.format(decdeg) + pifc.iloc[i, pifc.columns.get_loc('decdeg')] = '{:.6f}'.format(decdeg) # now we can compare using degrees confs = pifc.groupby(['slit', @@ -973,7 +1010,7 @@ def kill_process(process): # pragma: no cover # wait for dcr to terminate # dcr.wait() - #go back to the original directory. Could be the same. + # go back to the original directory. Could be the same. os.chdir(cwd) # If no error stderr is an empty string @@ -1016,6 +1053,7 @@ def kill_process(process): # pragma: no cover os.unlink(full_path_out) return ccd + def define_trim_section(sample_image, technique): """Get the initial trim section @@ -1047,9 +1085,7 @@ def define_trim_section(sample_image, technique): # serial binning - dispersion binning # parallel binning - spatial binning spatial_length, dispersion_length = ccd.data.shape - serial_binning, \ - parallel_binning = [int(x) for x - in ccd.header['CCDSUM'].split()] + serial_binning, parallel_binning = [int(x) for x in ccd.header['CCDSUM'].split()] # Trim section is valid for Blue and Red Camera Binning 1x1 and # Spectroscopic ROI @@ -1273,20 +1309,20 @@ def extract_fractional_pixel(ccd, target_trace, target_fwhm, extraction_width, background = background_1 if background_info_1 is None: background_info_1 = "{:.2f}:{:.2f} column {:d}".format( - low_1, high_1, i+1) + low_1, high_1, i + 1) elif background_1 is None and background_2 is not None: background = background_2 if background_info_2 is None: background_info_2 = "{:.2f}:{:.2f} column {:d}".format( - low_2, high_2, i+1) + low_2, high_2, i + 1) else: background = np.mean([background_1, background_2]) if background_info_1 is None: background_info_1 = "{:.2f}:{:.2f} column {:d}".format( - low_1, high_1, i+1) + low_1, high_1, i + 1) if background_info_2 is None: background_info_2 = "{:.2f}:{:.2f} column {:d}".format( - low_2, high_2, i+1) + low_2, high_2, i + 1) # actual background subtraction background_subtracted_column_sum = column_sum - background @@ -1552,8 +1588,11 @@ def get_lines_in_lamp(ccd, plots=False): fig, ax = plt.subplots() fig.canvas.manager.set_window_title('Lines Detected') - mng = plt.get_current_fig_manager() - mng.window.showMaximized() + try: + mng = plt.get_current_fig_manager() + mng.window.showMaximized() + except AttributeError as error: + log.debug(error) ax.set_title('Lines detected in Lamp\n' '{:s}'.format(lamp_header['OBJECT'])) @@ -2071,7 +2110,7 @@ def interpolate_spectrum(spectrum, interpolation_size): """Creates an interpolated version of the input spectrum This method creates an interpolated version of the input array, it is - used mainly for a spectrum but it can also be used with any + used mainly for a spectrum, but it can also be used with any unidimensional array. The reason for doing interpolation is that it allows to find the lines and its respective center more precisely. @@ -2103,7 +2142,7 @@ def is_file_saturated(ccd, threshold): """Detects a saturated image It counts the number of pixels above the saturation_threshold level, then finds - which percentage they represents and if it is above the threshold it + which percentage they represent and if it is above the threshold it will return True. The percentage threshold can be set using the command line argument ``--saturation_threshold``. @@ -2483,6 +2522,7 @@ def read_fits(full_path, technique='Unknown'): GSP_EXTR: Extraction window at first column GSP_BKG1: First background extraction zone GSP_BKG2: Second background extraction zone + GSP_LINE: Data has been linearized. GSP_WRMS: Wavelength solution RMS Error. GSP_WPOI: Number of points used to calculate the wavelength solution Error. @@ -2589,6 +2629,11 @@ def read_fits(full_path, technique='Unknown'): value='none', comment='Second background extraction zone') + if 'GSP_LINE' not in all_keys: + ccd.header.set('GSP_LINE', + value='FALSE', + comment='Data has been linearized') + if 'GSP_WRMS' not in all_keys: ccd.header.set('GSP_WRMS', value='none', @@ -2817,6 +2862,26 @@ def record_trace_information(ccd, trace_info): return ccd +def record_wavelength_solution_evaluation(ccd: CCDData, rms_error: float, n_points: int, n_rejections: int): + """Add record of wavalength solution evaluation + + Args: + ccd (CCDData): data to be updated. + rms_error (float): Root Nean Square Error of the wavelength solution. + n_points (int): Number of points used to evaluate the solution. + n_rejections (int): Number of points rejected while evaluating the solution. + + Returns: + ccd modified + + """ + + ccd.header.set('GSP_WRMS', value=rms_error) + ccd.header.set('GSP_WPOI', value=n_points) + ccd.header.set('GSP_WREJ', value=n_rejections) + return ccd + + def save_extracted(ccd, destination, prefix='e', target_number=1): """Save extracted spectrum while adding a prefix. @@ -3745,6 +3810,9 @@ def __init__(self, reference_dir): reference_dir (str): full path to the reference data directory """ self.log = logging.getLogger(__name__) + if not os.path.isdir(reference_dir): + self.log.error(f"Given `reference_dir`: {reference_dir} does not exist.") + self.log.debug(f"Current Directory: {os.getcwd()}") self.reference_dir = reference_dir reference_collection = ccdproc.ImageFileCollection(self.reference_dir) self.ref_lamp_collection = reference_collection.summary.to_pandas() @@ -3764,6 +3832,689 @@ def __init__(self, reference_dir): 'LAMP_DOM', 'LAMP_DPE'] + self.lamps_elements = {'LAMP_HGA': ['Hg', 'Ar'], + 'LAMP_NE': ['Ne'], + 'LAMP_AR': ['Ar'], + 'LAMP_FE': ['Fe', 'He'], + 'LAMP_CU': ['Cu', 'He']} + + self.line_list_files = {'cu': 'Cu_3000A-10000A_clean.csv', + 'he': 'He_3000A-10000A_clean.csv', + 'ne': 'Ne_3000A-10000A_clean.csv', + 'ar': 'Ar_3000A-10000A_clean.csv', + 'hg': 'Hg_3000A-10000A_clean.csv'} + + self.line_list = {'hg': [3125.67, + 3131.70, + 3341.48, + 3650.153, + 3654.84, + 3663.279, + 4046.563, + 4077.831, + 4358.327, + 4916.068, + 5460.750, + 5769.598, + 5790.663], + 'cu': [1999.698, + 2035.854, + 2037.127, + 2043.802, + 2135.981, + 2165.09, + 2178.94, + 2179.410, + 2181.72, + 2189.630, + 2192.268, + 2199.58, + 2210.268, + 2214.58, + 2218.108, + 2225.70, + 2227.78, + 2230.08, + 2242.618, + 2244.26, + 2247.002, + 2263.08, + 2293.84, + 2392.63, + 2406.66, + 2441.64, + 2492.15, + 2618.37, + 2689.300, + 2700.962, + 2703.184, + 2713.508, + 2718.778, + 2766.37, + 2769.669, + 2877.100, + 2961.16, + 3036.10, + 3063.41, + 3108.60, + 3194.10, + 3243.16, + 3247.54, + 3273.96, + 3290.54, + 3307.95, + 3337.84, + 3530.38, + 3686.555, + 4062.64, + 4651.12, + 4909.734, + 4931.698, + 4953.724, + 5051.793, + 5105.54, + 5153.24, + 5218.20, + 5292.52, + 5700.24, + 5782.13, + 6000.120, + 6150.384, + 6154.222, + 6216.939, + 6219.844, + 6273.349, + 6301.009, + 6377.840, + 6423.884, + 6448.559, + 6470.168, + 6481.437, + 6624.292, + 6641.396, + 7404.354, + 7652.333, + 7664.648, + 7778.738, + 7805.184, + 7807.659, + 7825.654, + 7902.553, + 7933.13, + 7988.163, + 8092.63, + 8283.160, + 8511.061, + 9861.280, + 9864.137, + 10054.938], + 'ne': [5852.488, + 5881.895, + 5944.834, + 5975.534, + 6029.997, + 6074.337, + 6096.163, + 6128.450, + 6143.062, + 6163.594, + 6217.281, + 6266.495, + 6304.789, + 6334.428, + 6382.991, + 6402.246, + 6506.528, + 6532.882, + 6598.953, + 6652.092, + 6678.276, + 6717.043, + 6929.467, + 7024.050, + 7032.413, + 7173.938, + 7245.166, + 7438.898, + 7488.871, + 7535.774, + 7544.044, + 7943.180, + 8082.457, + 8136.406, + 8300.324, + 8377.606, + 8418.426, + 8495.359, + 8591.258, + 8634.647, + 8654.384, + 8853.867, + 8865.756, + 8919.499, + 8988.58, + 9148.68, + 9201.76, + 9300.85, + 9313.98, + 9326.52, + 9354.218, + 9425.38, + 9459.21, + 9486.680, + 9534.167, + 9547.40, + 9665.424], + 'he': [3187.745, + 3354.55, + 3613.643, + 3819.6072, + 3888.648, + 3964.7289, + 4009.268, + 4120.815, + 4168.967, + 4437.551, + 4471.479, + 4713.1455, + 4921.931, + 5015.6779, + 5047.738, + 5875.621, + 7281.349, + 10830.337], + 'ar': [3093.4019, + 3139.0176, + 3161.3726, + 3169.6685, + 3181.0376, + 3204.3210, + 3243.6887, + 3249.8003, + 3281.7016, + 3307.2283, + 3350.9243, + 3376.4359, + 3388.5309, + 3454.0952, + 3476.7474, + 3478.2324, + 3480.5055, + 3499.4765, + 3509.7785, + 3514.3877, + 3519.9936, + 3521.2601, + 3521.9781, + 3535.3196, + 3548.5144, + 3554.3058, + 3556.9041, + 3559.5081, + 3561.0304, + 3565.0298, + 3567.6564, + 3576.6156, + 3581.6084, + 3582.3546, + 3588.4407, + 3605.8792, + 3606.5218, + 3622.1375, + 3632.6831, + 3637.0310, + 3639.8329, + 3649.8323, + 3650.8896, + 3655.2782, + 3656.0498, + 3659.5289, + 3660.437, + 3669.6024, + 3671.0048, + 3673.2645, + 3678.2701, + 3680.0609, + 3682.5448, + 3690.8951, + 3706.9302, + 3709.9088, + 3714.7337, + 3717.1713, + 3718.2065, + 3720.4265, + 3724.5165, + 3729.3087, + 3737.889, + 3746.4476, + 3750.4799, + 3753.5177, + 3754.0498, + 3763.5053, + 3765.27, + 3766.1186, + 3770.52, + 3780.8398, + 3786.3824, + 3796.5934, + 3799.382, + 3803.1724, + 3808.5748, + 3809.4561, + 3819.0159, + 3825.6729, + 3826.8072, + 3834.6787, + 3841.5187, + 3844.7311, + 3845.4055, + 3850.5813, + 3856.1382, + 3861.747, + 3868.5284, + 3872.1371, + 3875.2645, + 3880.3332, + 3891.4017, + 3891.9792, + 3894.66, + 3895.2502, + 3900.6266, + 3911.576, + 3914.7675, + 3925.7188, + 3926.0456, + 3928.6233, + 3931.2359, + 3932.5466, + 3944.2717, + 3946.0971, + 3947.5046, + 3948.9789, + 3952.7291, + 3958.38, + 3968.3594, + 3974.4766, + 3974.759, + 3979.3559, + 3979.7155, + 3988.1576, + 3992.0535, + 3994.7918, + 3999.252, + 4001.1379, + 4005.3628, + 4011.2093, + 4013.8566, + 4019.8429, + 4022.629, + 4031.3783, + 4033.8093, + 4035.46, + 4038.8043, + 4042.8937, + 4044.4179, + 4045.6773, + 4045.9654, + 4047.4815, + 4052.9208, + 4054.5258, + 4062.641, + 4063.238, + 4070.7835, + 4072.0047, + 4072.3849, + 4076.6284, + 4076.9432, + 4079.5738, + 4082.3872, + 4099.4563, + 4103.9121, + 4112.8153, + 4116.3753, + 4128.64, + 4129.6823, + 4131.7235, + 4144.2435, + 4156.086, + 4158.5905, + 4164.1795, + 4168.9682, + 4178.3658, + 4179.2973, + 4181.8836, + 4189.6511, + 4190.7129, + 4191.0294, + 4198.317, + 4199.8891, + 4200.6745, + 4201.5549, + 4201.9715, + 4203.4109, + 4217.4308, + 4218.6649, + 4222.6373, + 4226.6089, + 4226.9876, + 4228.158, + 4229.8696, + 4237.2198, + 4248.956, + 4251.1846, + 4255.6034, + 4259.3619, + 4266.2864, + 4266.5271, + 4272.1689, + 4277.5282, + 4282.8976, + 4297.9645, + 4300.1008, + 4300.6495, + 4309.2392, + 4331.1995, + 4332.0297, + 4333.5612, + 4335.3379, + 4337.0708, + 4338.2314, + 4345.168, + 4345.8966, + 4348.064, + 4352.2049, + 4362.0662, + 4363.7945, + 4367.8316, + 4370.7532, + 4371.329, + 4372.49, + 4374.8579, + 4375.9542, + 4379.6668, + 4382.928, + 4383.7535, + 4385.0566, + 4386.9656, + 4397.7971, + 4400.0968, + 4400.9863, + 4401.755, + 4404.9022, + 4406.4704, + 4420.9124, + 4423.9944, + 4426.0011, + 4430.189, + 4430.9963, + 4433.838, + 4438.1175, + 4439.4614, + 4439.8793, + 4440.1216, + 4445.8483, + 4448.4597, + 4448.8792, + 4449.5206, + 4460.5574, + 4474.7594, + 4480.35, + 4481.8107, + 4490.9816, + 4498.5384, + 4502.9268, + 4507.8339, + 4509.374, + 4510.7332, + 4522.323, + 4530.5523, + 4530.785, + 4535.4903, + 4537.6426, + 4543.8692, + 4545.0519, + 4547.7589, + 4561.0128, + 4563.7429, + 4564.4054, + 4579.3495, + 4589.8978, + 4596.0967, + 4598.7627, + 4609.5673, + 4628.4409, + 4637.2328, + 4651.124, + 4657.9012, + 4682.2759, + 4702.3161, + 4721.5910, + 4726.8683, + 4732.0532, + 4735.9058, + 4764.8646, + 4806.0205, + 4847.8095, + 4865.9105, + 4876.2611, + 4879.8635, + 4882.2432, + 4888.2612, + 4889.0422, + 4904.7516, + 4914.3146, + 4933.2091, + 4942.9214, + 4965.0795, + 4972.1597, + 5009.3344, + 5017.1628, + 5062.0371, + 5090.4951, + 5118.2023, + 5125.7654, + 5141.7827, + 5145.3083, + 5151.3907, + 5162.2846, + 5165.7728, + 5176.2292, + 5187.7462, + 5216.8139, + 5218.2020, + 5221.2710, + 5254.4648, + 5421.3517, + 5439.9891, + 5442.2427, + 5443.6893, + 5451.6520, + 5457.4157, + 5467.1608, + 5473.4516, + 5490.1194, + 5495.8738, + 5498.1841, + 5506.1128, + 5524.9570, + 5558.7020, + 5572.5413, + 5577.6845, + 5581.8714, + 5588.7200, + 5597.4756, + 5601.1216, + 5606.7330, + 5641.3751, + 5648.6863, + 5650.7043, + 5659.1272, + 5681.9001, + 5691.6612, + 5700.8730, + 5738.3869, + 5739.5196, + 5772.1143, + 5774.0087, + 5783.5360, + 5802.0798, + 5834.2633, + 5860.3103, + 5882.6242, + 5888.5841, + 5912.0853, + 5916.5992, + 5927.1258, + 5928.8130, + 5942.6686, + 5949.2583, + 5971.6008, + 5998.9987, + 6005.7242, + 6013.6777, + 6025.1500, + 6032.1274, + 6043.2233, + 6046.8977, + 6052.7229, + 6059.3725, + 6064.7508, + 6081.2433, + 6085.8797, + 6090.7848, + 6098.8031, + 6101.1615, + 6103.5390, + 6105.6351, + 6113.4657, + 6114.9234, + 6119.6565, + 6123.3619, + 6127.4160, + 6128.7227, + 6145.4411, + 6155.2385, + 6165.1232, + 6170.1740, + 6173.0964, + 6174.4032, + 6212.5031, + 6215.9383, + 6243.1201, + 6296.8722, + 6307.6570, + 6333.1459, + 6357.0229, + 6364.8937, + 6369.5748, + 6384.7169, + 6396.6097, + 6399.2065, + 6403.0128, + 6416.3071, + 6418.3703, + 6431.5550, + 6437.6003, + 6441.8994, + 6443.8598, + 6466.5526, + 6468.0483, + 6472.4294, + 6481.1453, + 6483.0825, + 6493.9694, + 6499.1061, + 6538.1120, + 6604.8534, + 6620.9665, + 6632.0837, + 6638.2207, + 6639.7403, + 6643.6976, + 6656.9386, + 6660.6761, + 6664.0510, + 6666.3588, + 6677.2817, + 6684.2929, + 6719.2184, + 6752.8335, + 6756.1631, + 6766.6117, + 6827.2488, + 6861.2688, + 6863.5350, + 6871.2891, + 6879.5824, + 6888.1742, + 6937.6642, + 6951.4776, + 6965.4307, + 7030.2514, + 7067.2181, + 7147.0416, + 7206.9804, + 7272.9359, + 7311.7159, + 7353.2930, + 7372.1184, + 7383.9805, + 7503.8691, + 7514.6518, + 7635.1060, + 7723.7611, + 7724.2072, + 7948.1764, + 8006.1567, + 8014.7857, + 8103.6931, + 8115.311, + 8264.5225, + 8408.2096, + 8424.6475, + 8521.4422, + 8667.9442, + 9122.9674, + 9224.4992, + 9354.2198, + 9657.7863, + 9784.5028, + 10470.0535]} + + def get_line_list_by_lamp(self, ccd): + """Get the reference lines for elements in the lamp's name + + Splits the name in chunks of two characters assuming each one of them + represents an element in the comparison lamp, then fetches the list of + line positions available and appends it to a list that will be return + ordered. + + Args: + lamp_name (str): Lamp's name as in the header keyword OBJECT. + + Returns: + line_list(list): Sorted line list. + + """ + + + if ccd.header['OBSTYPE'] in ['ARC', 'COMP']: + elements = [] + for lamp_key in ccd.header['LAMP_*']: + print(lamp_key, ccd.header[lamp_key]) + if ccd.header[lamp_key] == 'TRUE': + elements.extend(self.lamps_elements[lamp_key]) + # elements = [lamp_name[i:i + 2].lower() for i in range(0, + # len(lamp_name), + # 2)] + print(elements) + line_list = [] + for element in elements: + line_list.extend(self.line_list[element.lower()]) + return sorted(line_list) + else: + log.critical(f"Obstype {ccd.header['OBSTYPE']} it's not expected to have lamp signal.") + def get_reference_lamp(self, header): """Finds a suitable template lamp from the catalog @@ -3772,7 +4523,7 @@ def get_reference_lamp(self, header): lamp. Returns: - full path to best matching reference lamp. + full path to best matching reference lamp or None. """ @@ -3786,7 +4537,7 @@ def get_reference_lamp(self, header): (self.ref_lamp_collection['lamp_cu'] == header['LAMP_CU']) & (self.ref_lamp_collection['wavmode'] == header['WAVMODE']))] if filtered_collection.empty: - error_message = "Unable to find a match for: "\ + error_message = "Unable to find a match in the reference library for: "\ "LAMP_HGA = {}, "\ "LAMP_NE = {}, "\ "LAMP_AR = {}, "\ @@ -3799,7 +4550,7 @@ def get_reference_lamp(self, header): header['LAMP_CU'], header['WAVMODE']) self.log.error(error_message) - raise NoMatchFound(error_message) + return None else: filtered_collection = self.ref_lamp_collection[ (self.ref_lamp_collection['object'] == header['object']) & @@ -3812,7 +4563,7 @@ def get_reference_lamp(self, header): "WAVMODE = {}".format(header['OBJECT'], header['WAVMODE']) self.log.error(error_message) - raise NoMatchFound(error_message) + return None if len(filtered_collection) == 1: self.log.info( @@ -3919,6 +4670,54 @@ def check_comp_group(self, comp_group): return comp_group return None + def create_reference_lamp(self, ccd, wavelength_solution, lines_pixel, lines_angstrom): + self.log.info(f"Creating Reference lamp.") + assert len(lines_pixel) == len(lines_angstrom), f"Pixel and Angstrom list must have same length." + self.lines_pixel = lines_pixel + self.lines_angstrom = lines_angstrom + + wcs = WCS() + + ccd = self._record_lines(ccd=ccd) + ccd = wcs.write_gsp_wcs(ccd=ccd, model=wavelength_solution) + + lamp_elements = [] + for keyword in ccd.header['LAMP_*']: + if ccd.header[keyword] == 'TRUE': + lamp_elements.extend(self.lamps_elements[keyword]) + + elements = set(lamp_elements) + + lamp_elements_name = "".join(elements) + + if ccd.header['FILTER2'] in ['NO_FILTER']: + pass + + new_name = f"goodman_comp_{ccd.header['WAVMODE']}_{ccd.header['FILTER2']}_{lamp_elements_name}.fits" + + self.log.info(f"Created new reference lamp with name {new_name}") + + write_fits(ccd=ccd, full_path=new_name, overwrite=True) + + def _record_lines(self, ccd): + self.log.info("Adding pixel and angstrom line record.") + last_added = 'GSP_WREJ' + for i in range(len(self.lines_pixel)): + keyword_name = f"GSP_P{i + 1:03}" + ccd.header.set(keyword_name, + value=self.lines_pixel[i], + comment="Line location in pixel value", + after=last_added) + last_added = keyword_name + for i in range(len(self.lines_angstrom)): + keyword_name = f"GSP_A{i + 1:03}" + ccd.header.set(keyword_name, + value=self.lines_angstrom[i], + comment="Line location in angstrom value", + after=last_added) + last_added = keyword_name + return ccd + def _recover_lines(self): """Read lines from the reference lamp's header.""" self.log.info("Recovering line information from reference Lamp.") @@ -3931,6 +4730,7 @@ def _recover_lines(self): assert pixel_key[-3:] == angstrom_key[-3:] assert angstrom_key in self._ccd.header if int(float(self._ccd.header[angstrom_key])) != 0: + self.log.debug(f"Retrieving Line {re.sub('GSP_P', '', pixel_key)}") self.lines_pixel.append(float(self._ccd.header[pixel_key])) self.lines_angstrom.append( float(self._ccd.header[angstrom_key])) @@ -4715,10 +5515,10 @@ def _fit_gaussian(fitter, profile_model = [] if stddev_min is None: stddev_min = 0 - log.debug(f"Setting STDDEV minimum value to {stddev_min} pixels. Set it with `--profile-min-width`.") + log.debug(f"Setting STDDEV minimum value to {stddev_min} pixels. Set it with `--target-min-width`.") if stddev_max is None: stddev_max = 4 * order - log.debug(f"Setting STDDEV maximum value to {stddev_max} pixels. Set it with `--profile-max-width`.") + log.debug(f"Setting STDDEV maximum value to {stddev_max} pixels. Set it with `--target-max-width`.") log.debug(f"Using minimum STDDEV = {stddev_min} pixels.") log.debug(f"Using maximum STDDEV = {stddev_max} pixels.") for peak in selected_peaks: diff --git a/goodman_pipeline/core/tests/test_core.py b/goodman_pipeline/core/tests/test_core.py index 3185d313..ca905c07 100644 --- a/goodman_pipeline/core/tests/test_core.py +++ b/goodman_pipeline/core/tests/test_core.py @@ -34,7 +34,7 @@ # import of functions in core.py from ..core import (astroscrappy_lacosmic, add_linear_wavelength_solution, - add_wcs_keys, + add_linear_wcs_keys, bias_subtract, bin_reference_data, call_cosmic_rejection, @@ -93,7 +93,7 @@ def setUp(self): self.ccd = CCDData(data=np.random.random_sample(200), meta=fits.Header(), unit='adu') - self.ccd = add_wcs_keys(ccd=self.ccd) + self.ccd = add_linear_wcs_keys(ccd=self.ccd) self.ccd.header.set('SLIT', value='1.0_LONG_SLIT', comment="slit [arcsec]") @@ -145,9 +145,7 @@ def test_add_wcs_keys(self): 'DC-FLAG', 'DCLOG1'] - - - self.test_ccd = add_wcs_keys(ccd=self.test_ccd) + self.test_ccd = add_linear_wcs_keys(ccd=self.test_ccd) for key in wcs_keys: self.assertIn(key, self.test_ccd.header) @@ -208,8 +206,6 @@ def test__bin_reference_data(self): self.assertEqual(len(new_wavelength), np.floor(len(wavelength) / i)) - - class CentralWavelength(TestCase): def setUp(self): @@ -1856,9 +1852,9 @@ def test_get_reference_lamp_no_match_status_keys(self): self.ccd.header.set('WAVMODE', value='400_M2') - self.assertRaises(NoMatchFound, - self.rd.get_reference_lamp, - self.ccd.header) + reference_lamp = self.rd.get_reference_lamp(header=self.ccd.header) + + self.assertIsNone(reference_lamp) def test_get_reference_lamp_exist_with_object_key(self): self.ccd.header.set('OBJECT', value='HgArNe') @@ -1884,9 +1880,9 @@ def test_get_reference_lamp_does_not_exist(self): self.ccd.header.set('OBJECT', value='HgArCu') self.ccd.header.set('WAVMODE', value='400_M5') - self.assertRaises(NoMatchFound, - self.rd.get_reference_lamp, - self.ccd.header) + reference_lamp = self.rd.get_reference_lamp(header=self.ccd.header) + + self.assertIsNone(reference_lamp) def test_lamp_exist(self): self.ccd.header.set('LAMP_HGA', value='TRUE') diff --git a/goodman_pipeline/data/ref_comp/goodman_comp_2100_3900A_NO_FILTER_ArHg.fits b/goodman_pipeline/data/ref_comp/goodman_comp_2100_3900A_NO_FILTER_ArHg.fits new file mode 100644 index 00000000..2ffbe6e3 --- /dev/null +++ b/goodman_pipeline/data/ref_comp/goodman_comp_2100_3900A_NO_FILTER_ArHg.fits @@ -0,0 +1,106 @@ +SIMPLE = T / conforms to FITS standard BITPIX = -64 / array data type NAXIS = 1 / number of array dimensions NAXIS1 = 4060 ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator IRAF-TLM= '2024-05-10T19:58:54' / Time of last modification OBJECT = 'CD-34d241' / Name of the object observed DATE-OBS= '2023-12-15T00:52:56.895' / DATE-OBS Format: YYYY-MM-DDThh:mm:ss.sss DATE = '2024-05-10T19:56:49' / Date Format is YYYY-MM-DD TIME = '00:52:56.895 to 00:52:57.900' / ~ Start & Stop of Exposure N_PRM0 = 5 / Status PG0_0 = -106.2 / CCD 0 CCD Temp., C PG0_1 = 0.000 / Chamber Pressure, Torr PG0_2 = 0 / Shutter Status, (Closed) PG0_3 = 1 / XIRQA Status, (Occured) PG0_4 = 1 / Cooler Status, (On) N_PRM1 = 36 / Factory PG1_0 = 699 / Instrument Model, PG1_1 = 8388607 / Instrument SN, PG1_2 = 8388607 / Hardware Revision, PG1_3 = 1 / Two Serial Registers, (True) PG1_4 = 1 / Installed CCDs, PG1_5 = 4 / Installed Ports, PG1_6 = 0 / Port 0 Connect, (SR0) PG1_7 = 0 / Port 0 Map, (ADC0) PG1_8 = 0 / Port 0 Shift Direction, (Same) PG1_9 = 0 / Port 1 Connect, (SR0) PG1_10 = 1 / Port 1 Map, (ADC1) PG1_11 = 1 / Port 1 Shift Direction, (Reverse) PG1_12 = 1 / Port 2 Connect, (SR1) PG1_13 = 2 / Port 2 Map, (ADC2) PG1_14 = 0 / Port 2 Shift Direction, (Same) PG1_15 = 1 / Port 3 Connect, (SR1) PG1_16 = 3 / Port 3 Map, (ADC3) PG1_17 = 1 / Port 3 Shift Direction, (Reverse) PG1_18 = 16 / Bits Per Pixel, PG1_19 = 1 / CCD Enable Mask, PG1_20 = 1 / Rectangular Grid X, PG1_21 = 1 / Rectangular Grid Y, PG1_22 = 8388607 / Controller SN, PG1_23 = 8388607 / Analog Part #, PG1_24 = 8388607 / Analog Rev. #, PG1_25 = 8388607 / Controller Rev. #, PG1_26 = 16 / Serial Pre-Masked, PG1_27 = 4096 / Serial Active Pix., PG1_28 = 0 / Serial Post-Masked, PG1_29 = 0 / Parallel Pre-Masked, PG1_30 = 4096 / Parallel Active Pix., PG1_31 = 0 / Parallel Post-Masked, PG1_32 = 0 / CCD Type, (Full-Frame) PG1_33 = 'CCD 486' / CCD Model PG1_34 = 4221 / Software Part Number, PG1_35 = 1 / Software Revision, N_PRM2 = 5 / Miscellaneous PG2_0 = 130 / Tested Speeds, (200 KHz) PG2_1 = -110.0 / Low Temp Limit, C PG2_2 = -100.0 / Operational Temp, C PG2_3 = 5.000 / High Pressure Limit, Torr PG2_4 = 0.100 / Operational Pressure, Torr N_PRM3 = 18 / Control PG3_0 = 0 / Serial Origin, Pixels PG3_1 = 4142 / Serial Length, Binned Pixels PG3_2 = 1 / Serial Binning, Pixels PG3_3 = 0 / Serial Post Scan, Pixels PG3_4 = 980 / Parallel Origin, Pixels PG3_5 = 948 / Parallel Length, Binned Pixels PG3_6 = 2 / Parallel Binning, Pixels PG3_7 = 0 / Parallel Post Scan, Pixels PG3_8 = 130 / DSI Sample Time, PG3_9 = 0 / Analog Attenuation, (Low) PG3_10 = 1836 / CCD 0 Port 0 Correlation Bias, PG3_11 = 1836 / CCD 0 Port 1 Correlation Bias, PG3_12 = 1964 / CCD 0 Port 2 Correlation Bias, PG3_13 = 1748 / CCD 0 Port 3 Correlation Bias, PG3_14 = 0 / Parallel Phasing, (SR0) PG3_15 = 0 / Serial Phasing, (Normal) PG3_16 = 2 / Port Select, (Port 1) PG3_17 = 0 / Abort Mode, (Without Readout) N_PRM4 = 6 / Setup PG4_0 = 1.000 / Exposure Time, Sec. PG4_1 = 100 / TDI Delay, us PG4_2 = 4 / Trigger Mode, (Light Exposure) PG4_3 = 250 / Parallel Shift Delay, 100 ns PG4_4 = 0.500 / Shutter Close Delay, Sec. PG4_5 = -107.0 / CCD Temperature Setpoint, C N_PRM5 = 1 / Operational PG5_0 = 11 / Mode, TELESCOP= 'SOAR 4.1m' INSTRUME= 'ghts_blue' NOTES = '' INSTCONF= 'Blue' CAMSWV = 'Beta 29' CONSWV = '8359,10' INSTSWV = 'GSCS 2.0.17.6.6-Alpha' CAMERA = 'Si 600-658 ' RA = '00:41:46.642' / right ascension [hh:mm:ss.sss] DEC = '-33:39:26.740' / declination [dd:mm:ss.sss] AIRMASS = 1.030000 / airmass at approx. start of exposure UT = '00:53:23.947' / time at approx. start of exposure [UTC] FOCUS = -851.000000 / SOAR telescope focus MOUNT_AZ= 252.016746 / SOAR mount azimuth MOUNT_EL= 76.225221 / SOAR mount elevation ROTATOR = 356.800000 / Nasymth cage rotator angle [deg] POSANGLE= 0.000000 / position angle [deg. E of N] SEEING = -1.000000 / DIMM seeing [arcsec] LST = '01:44:12.033' / Local Sidereal Time [hh:mm:ss.sss] OBSRA = '00:41:46.680' / target right ascension [hh:mm:ss.sss] OBSDEC = '-33:39:26.701' / target declination [hh:mm:ss.sss] DOME_AZ = 250.250293 / SOAR Dome Shutter azimuth HA = '01:02:25.391' / Hour Angle [hh:mm:ss.sss] ADCSTAT = 'IN' / ADC Status ADCPOS = 'DONE_82.089' / ADC Position LAMP_HGA= 'TRUE' / Hg(Ar) LAMP_NE = 'FALSE' / Neon LAMP_AR = 'FALSE' / Argon LAMP_FE = 'FALSE' / Iron LAMP_CU = 'FALSE' / Copper LAMP_QUA= 'FALSE' / Quartz LAMP_QPE= 0.000000 / Quartz Percent LAMP_BUL= 'FALSE' / Bulb LAMP_DOM= 'FALSE' / Dome LAMP_DPE= 0.000000 / Dome Percent CAM_ANG = 48.348653 / camera angle [deg] GRT_ANG = 24.173100 / grating angle [deg] CAM_TARG= 48.346900 / camera target [deg] GRT_TARG= 24.173400 / grating target [deg] CAM_FOC = 827 / camera focus COLL_FOC= 1001 / collimator focus FILTER = 'NO_FILTER' / primary filter wheel FILTER2 = 'NO_FILTER' / secondary filter wheel GRATING = '2100_SYZY' / VPH grating [1/mm] SLIT = '0.45_LONG_SLIT' / slit [arcsec] COL_TEMP= 17.578125 / coll ext temp (deg C) CAM_TEMP= 19.906250 / cam ext temp (deg C) WAVMODE = '2100_3900A' / Wavelength EXPTIME = 1.000000 / integration time RDNOISE = 4.740000 / CCD readnoise [e-] GAIN = 1.400000 / CCD gain [e-/ADU] OBSTYPE = 'ARC' / observation type OBSERVER= 'Diego Gomez' PROPOSAL= '' EQUINOX = 2000.000000 / equinox of coordinates ROI = 'CUST_0_4142_980_948_1x2' ENVWIN = 24.919920 / Wind Speed [km/hr] at start of exposure ENVPRE = 735.350000 / Atmospheric Pressure [hPS] at start of exposur ENVDIR = 318.700000 / Wind Direction at start of exposure ENVTEM = 14.675000 / Outside Temperature [C] at start of exposure ENVHUM = 19.450000 / Relative Humidity at start of exposure DISPAXIS= 1 DETSIZE = '[1:4096,1:4096]' TRIMSEC = '[1:4142,1:1896]' CCDSIZE = '[1:4096,1:4096]' CCDSUM = '1 2' BUNIT = 'adu ' OBSID = CONFID = REQNUM = PROPID = 'calibrate' SITEID = 'sor' TELID = '4m0a' RLEVEL = 0 BLKUID = 0 L1PUBDAT= '2023-12-15T00:52:23.078580Z' IMAGEID = '307' HEADERVE= '1.0.1' BLOCK_ID= 1702600267045374 OPENTIME= '00:53:26.1313352' / GPS-Synched Time for Shutter Open (UT) OPENDATE= '2023-12-15' / GPS-Synched Time for Shutter Open (UT) CLOSETIM= '00:53:27.0366172' / This is the GPS-Synched Time for Shutter Closi -- Goodman Spectroscopic Pipeline -- GSP_VERS= '1.3.7 ' / Goodman Spectroscopic Pipeline Version GSP_ONAM= '0307_Calibration-Star_15-12-2023_comp.fits' / Original file name GSP_PNAM= 'cfzst_0307_Calibration-Star_15-12-2023_comp_386.11-415.65.fits' / ParGSP_FNAM= 'goodman_comp_2100_3900A_NO_FILTER_ArHg.fits' / Cu GSP_PATH= '' / Location at mo GSP_TECH= 'Spectroscopy' / Observing technique GSP_DATE= '2024-05-10' / Processing date GSP_OVER= 'none ' / Overscan region GSP_TRIM= '[51:4110,2:948]' / Trim section from TRIMSEC GSP_SLIT= '[1:4060,62:819]' / Slit trim section, slit illuminated area only. GSP_BIAS= 'master_bias_BLUE_SP_1x2_R04.74_G01.40.fits' / Master bias image GSP_FLAT= 'norm_master_flat_2100Custom_3900nm_0.45_dome.fits' / Master flat imagGSP_NORM= 'simple ' / Flat normalization method GSP_COSM= 'none ' / Cosmic ray rejection method GSP_TMOD= 'Polynomial1D' / Model name used to fit trace GSP_TORD= 2 / Degree of the model used to fit target trace GSP_TC00= 400.8807838751269 / Parameter c0 GSP_TC01= -0.00633820903910904 / Parameter c1 GSP_TC02= 2.98343351871721E-09 / Parameter c2 GSP_TERR= 0.3403306007581067 / RMS error of target trace GSP_EXTR= '386.11:415.65' / Extraction window at first column GSP_BKG1= 'none ' / First background extraction zone GSP_BKG2= 'none ' / Second background extraction zone GSP_LINE= 'FALSE ' / Data has been linearized GSP_WRMS= 'none ' / Wavelength solution RMS Error GSP_WPOI= 'none ' / Number of points used to calculate wavelength sGSP_WREJ= 'none ' / Number of points rejected GSP_FUNC= 'Chebyshev1D' / Mathematical model of non-linearized data GSP_ORDR= 2 / Mathematical model order GSP_NPIX= 4060 / Number of Pixels GSP_C000= 3493.517485375643 / Value of parameter c0 GSP_C001= 0.18002656901217812 / Value of parameter c1 GSP_C002= -7.4196419059406E-07 / Value of parameter c2 GSP_P001= 876.4 / Line location in pixel value GSP_P002= 902.8 / Line location in pixel value GSP_P003= 3154.02 / Line location in pixel value GSP_A001= 3650.153 / Line location in angstrom value GSP_A002= 3654.836 / Line location in angstrom value GSP_A003= 4046.563 / Line location in angstrom value GSP_SCTR= 'ecfzst_0308_Calibration-Star_15-12-2023.fits' / Science target file tAPNUM1 = '1 1 386.11 415.65' / Aperture in first column -- GSP END -- COMMENT Triggered, Exp Time= 01 - Image Transform: Flip Y - Image Transform: FlCOMMENT ip Y, Saved as: 0307_Calibration-Star_15-12-2023_comp.fits REFSPEC1= 'ecfzst_0307_Calibration-Star_15-12-2023_comp_386.11-415.65_copy' END @iZV{@Oœ  +RhºÀT•¡%Ñ@IÒ²-"$@LÛÖŠh@Jñ¹dbÚß@PîJžr@S¾Àîñ¨l@RK ÛFÔÀ0Ý{C¤ËÚ@"oîˆ !@Hqû†f„|@9CÔ€{@Qy„ˆ]@@ªÆÞK{„@LæJD]å‚@Q§¦C9yÀ(Î}Õ¶‡®ÀPJÃã½í“À]½ÚqÀ;‘ÕÔ¦å@ +DS›zsÀ(–±eË”À8ŒòN7ÀÀ<ª¸µ1Õ$À>«ýQ‰xÀD`Ô9³œ|@(4çõ²ÊŸÀ9›ƒü¸°ÀIé¾·äj…ÀG#ACœ°@—QF.>ÀE•yÛúB:À$3%®ï¾uÀW¹úfO +ÀN«“„ÀGu5=³±ÿÀF8Ûëo´¯À5­¼s †ÐÀGwSl$ÇýÀA<ÍÜ¡[”ÀJÖ™&ÍcÀGõ OƆíÀ:‘|Ñ^˜À7ÔÒÐ|À0®É*¹ÀV2¶]qÀ=¿¹^–?À(q›šp\ÀK隟]”À10ª¿ô*ÀIˆÚ1ÀHœÂÛ,ì*À*%ÒiTfÀ-:›wD¯ÀI™Y[[n-ÀI»’fäÎ@.®’RV­ÀP6/÷ðî†À@X~HÔÀRwÝÕ˜`´ÀA±¼-{ @0÷oöÓtÀL1 >õÞÀRLÝ¿hÀLŽ´0IIeÀ,íÜ‘ŽŠÀ8€Ý2“à^À8£rÎ{èÀ:’)p•6¾À7®œ—píÞÀB$…DJ#ÏÀI$,•µ5†À:šÆx”«À9ÖÀNRŠÀQBŒ}z¹ÄÀIkƒøeÀF€øsž:¦À0Ï­Ô§é*ÀH*e qÒÀ4`²÷ßk&ÀN‡eD ’EÀ.ìs ªÀ3º%Ýå ‘@pm6ÀPŸªÊ‰ –ÀDÚë]ÒÀPœb* {zÀ>8'ú‚9ÄÀR; Aõ‰^ÀLjR9³ã†ÀAï_Xj7\ÀDJ¼DáGiÀ=¸·f¿B_ÀGH„Ý8ÀK`–²§CàÀT²ðët®ÆÀC%0¹¼ÀBÔ¶ЂÀ@§e¢ÚÀYIí¢È‰À?5HåŸç»âÀG¶:ð–•"@#ÖŠÜÙVDÀDŸm¡8˜^À5™=ö¤ôóÀ1bÒNñ]À6‰*ŸldÀM4 ¼o$;ÀF¹¬ª ;%¿Â÷CGh@®ySô¹êÀN½ƒö>ZÙÀEH-oVàÀ;wE•Y ÀIëzL¨À=׿=_ À9GÂ_i~`À@v,  $À-¯Dn¢CDÀD†èR2ÀJY]–˜RÀB¬% êåÀ7ÒÓãü½ÌÀ3°—ø$äÀ@9øY¹À$ãæo`DÀ@ë„hgÛÀK|.ÂÁ§ÀFqί¸¸ðÀAzFUe§À@¦ß#ñJÀ:ß—8sÀ>%oôÀVÀ;U¸Æ&:ÀB¦/FsÀAcSÞƒýÀ/Ô·•À;<‰Ã6[ÊÀG< • ª@2ÙûåÝ!@7ܹ¾c-ÅÀFj*Q—4›ÀQ›Öªx¯Àñ†k[h@œu›k³ÏÀ8ßÌIzSºÀ5¿A–A!;ÀJ¾SY._À6˜K Ÿ¬À;U__#µ¤ÀM)Ù—yÈmÀ0ùò{š4?èOš[± fÀ7€’î€ÀKC\Ñc ŠÀ@`M¡a"SÀG_]Ó«úÀ>U¯bWÀŠG|9»ŽÀGxG¯åÀ3Ó¡”ÄÓÀB¯³ÊëÊœÀ6ì|:Ñ–rÀ8/—cG]™À8¿Ô>bæÀR;°ÃÆsbÀC ¨vµÀ1UpæÑÀCå$…â?@#6VßÀ3ÅAîÞ±˜ÀC;l‚+ØÀ(ô÷ýÞ²®ÀKÌŒr±ŽÀ4 ®\ì5À4³—sK°@¹²ÍUr@¤@òfÀEbtY‰ cÀ2¼ßJµ7Àr×@”À?ÒR°p‹À1ÂÁé=DÅ@%—öTÅ#ÀÓÖ’*’+¿æ8PƒÁÖÀ,páùæ@*ËbÊRnÀ;(g@ØWÀ1!‰srÈÀù: ãfpÀ@GS(Ÿ¥’À(<k¼yÀR÷_ +À:I›“ôxO@ÚÈÝÑÇ®À8:²¹ð^¿æ2LlëMn@ +6' +@0Dw}äí¿óö‰å”À1Cˆ©-æÀKO½“ÿ­WÀ@ÏÙ¹:"Ï@ÔõàK5Àç!ÎÑ{ìÀ"׺у\@8¹1c] ÀGsIñ¢@-©“qx(À/‰!ž£Æ/À8èäJs¯œÀGfÏ L¹¡Àž¬z[<@¢'"*+À­Á¥Ì&·@ ©m]ü@,¢ºÞœŠ@:˜@Ñ‘ö@!Aö{}±À×ΩȮÀ8ïßpßT«À.˜î2z‚À.½ µ1V¥@'Á 5ns@5V¡ ó³À@Ý jÀÀ7Ú%ˆ¾€žÀ +QzõÕRZÀ4«šçH#ÀDѼ†€ÀMwO ž‰ÀJÆBœÔMIÀ-,FS}ÀŽ8qKnÎÀú +18ždÀìƒb…`¦@Ž'‰+,À#ôÝI¥?@ÛbÄ@º¡ƒ½?ñÆ5,MžÒ@4Ų8õqÀA»ð@ævÀ !¥“´æÀJE"³ÂLÎÀ19ŽÄ DÀ?¿§½$@*ŽyrXE@4‚]ß&ÞaÀ$…-͈œÀ+²‰vøcÎÀ ȯÜK˜ +À UÉÿˆ¡@3˜·B…ž—À„¯~Ë@+†ÛWjJÉ@¾íµ{sÀ7íÉ>Êå®À{ûb©À+TƒÓæ@={Œº@Ú6À/tÔ¾<À>ŒzÑûLhÀ4<2•%?ÜÀ&7‚0W o?ôhuþ›”<À>+ ­±PÀDÛùY^ëh@7u€Ék@6E&¼©Ì@H¼è³‚°@1¨†ÞJc>À {úWbïâÀ(ôg7Á[ +@,––ž(©oÀÀ+¿s£„Ø@ =‘L˜ÜýÀ7éŸ?8À6Ò:«k9PÀ6“ªà³ì×À!Ä~WE¡?Ófô>bT@,bFðînÀ!‘tvïÀ2ÁŸÑ±q@­.A‹€@ òÚ%<*@'T_oœñ\@@siNšÎÀ7ÇÃ2€À@6'ÕÇ z¿þЄqn@“çËýü@;tA•~*@8œU›­í=À4ö‘@™¡@ +S/P¾½@7•Š³ŽÎû@<ä Vh‡À&ê¢ùkæ@!ço^ÈM„@?ƒGÙá¢@DŒ°}i¹ë@2ÛÖ¿ooT@4, Ÿåy @)Ñß È@(Yo÷j½À-ÌÖ±uyâÀ'OT<-ãÀó[-Õ~@G›ÛÔ`FÄ@&?z%圯@·‚¹ºd@7Ë‚ì0ÅÔ@5åM:^óÎÀ K¥ÇwÑ@(NV}ßÖú?Ép'1¥^¨À6¹½ƒ¤!@ ¯¬­;Ò@9C™Â&¦!@FÜ~é<›h@305’Ò@>ÂÈ¡ Ê@5y`÷}Ý@4—ÍyÏ‘»@=8Í¿]@@8uú~ÞI@Fþ|Dq‹É@4p¾øÏw@4XŽûÐÒiÀlºÈÏ@&M“'Uâ@6_Á’`Ǹ@ï£X¸@@ƒkÇ‘©@-œ&—Ä2À'·x´,œ@&2bq+BÀS=©á? @A£Ž²^Å@=µfÜO@>@zçjÎÕ@A—1Nô§@2£Áæcá@G¨Î´ç,@'[Òœ@Gq²Ïžvå@I_¨Wž@8 ¬y°Æ@CTоž8>@&hZc}<Ï@Nr¦@¤z@H4ø3pxñÀ2Š°ùŸð@K²R<ë™­@WˆÔvì!@?¯«*ÊÖâ@A“ÂüBÛ3@H´7®©Dµ@#c6Kp5@)âD²OŒçÀ," +„*sùÀE‡A:@ð¯Æ·ûx@HDmê¥Z@=Ž!Ø@Q$íS@Hý? ܲ,@LlÈÝtA@@ž¼ÃÀ”@0 ’æ¡Áy@/›T;Z@!(kuÕ=Û@?vÕ£;ÇÖ@BW‡˜æyD@H‡×¤W»¿@DòùÔ3/@D + ó"ŸA@>Hšv6®@Bª`ó‡@8$–·––B@=Y ~¤F¿?÷ªóªÁ\¬@0µï lS@$TüÛû-@3ÉË +¼ƒ@Fª·¡ID@-Š³¢¾a@LI±ƒô@@?šg%á6@N¦„Y–@EÆ£+4$@<³1Èå@I|L]F;@B¦v!/1]@ywzå6@H‚ø9aëR@8¨Æí@F‡]ƺ5@&:ûAÐ×@CúlÙߥõ@9/=³†ù]@Hyõ“ )@VüîS¥”@CóXD ‚ @ÿÏÃA©@B¢•”Ä @À$‘P×ì@KbÏÄG£³@!? “h’@Iøe@M‹ð@<.ÉÔ¦ p@:㌃f†2@Nç˜õ9Ú`@¬ÙØšÖ@M_pÜ°Ý @P*{ü,ðù@Uºy\ .þ@Rï»"æSÍ@@{œ‘B@:¶t¬Qt @ Ê.@G}wDJ@CoÊ4™ú—@I¢!!’G@R”žbƒ5 @PÊ ¢yw@DT©v©é{@PÕ#GÜ{ü@MÈÚ%J/¦@U#‚QÝ"|@v¦lH=ƒ@-ÉžßaH @F%SAFá,@9Ç™É'Ë6@W”É´óU+@T¿N[ù@S`—@Oï%ç5µ@P–n¢û@A¨“pË<Æ@DÇE~gb@Lv AÀv@J]nß)9ª@+œØÛ¡„@+àçì.@0u·ïœ@KÚ^64@"@OàJYý¶@Q=;4°G@;c.Q>—@TsKnTxÔ@Q¶]‡’Ú@Wp‘£”æ@Ti(ÌÍ"@O¬_Û²1+@P½|¥Ž@MÊAÚ«ûù@KßÓV„ì@KЕAëHo@P»¦Ô‹@‘@P$'@h)Å@F³þ{ê_¾@Pl–fMÏ@JÈ¡2@˜@Qaf)Âo1@HËÁÝ Ô@QZµ lÐ@UB^}–Ÿ@[úáè}±ž@[Ç{Lµ}½@X>LÌ i@Sƒ)Þ„ê@%c6_·#@PQ"B)@QW{]cJ@WÓd^áä@A&HyNº@SÉcwƒ ×@Uþ8)@UÔ ÉBÃ@\}/^¾ ê@SDC2îý@Væ ”c @UZòìBØ@T$·--@W"ʈSD@\6™)PŒ@Q¤[xÉ#1@IßV¢É4/@Q÷*(Õ/A@T&‡ÿ½Ø@Vk4 0@V2  çp@PAV¾ýÎ@R.}ãÏhG@U†JJ.T(@]Õb¨|ð@SÇhG¿”é@O»àïßzî@Z/ÈÚbø@\ê¤ML À@SnljÊÐ@UáwùAê@3jô¬€”@OóJdI@MüIR•3¡@W*ö±\@^å3¬š–@R¤>kZÔÐ@\n¶myäæ@WxvïtŠ@Z*}#Ü@PDho*ÊU@SÝ\¨3ˆ@aÎmêZ8@Užó~Ú@ZA¸ô Q&@]‰Ö^Æ@]ZŠå³T@[mþCUªV@]‚bž[@THì1pf@\c«¥‡k@[oüN¦®L@ZêùFÌ@Pú•YWu@^Ž©%çB@X ‘~ÛØ@_”³…%Ùz@\ï?Ÿá¨@V19öÏ3Þ@Z¦KPø*•@Y1‹ÜǼÈ@\V0Uø/(@\ Îñ”@Y®Ï’©f{@W ÈDêþz@^æM—bp#@_ÙCS®T@dëÁÔ¨åq@\8˜¨ôî@bHL áÑ@c OlN@a€…{²)¤@]6µ!^%@eÀˆÌ²É @bÖ¶òů—@aÒŒ:ññ@`~FzñL(@dDlÙB’ø@_Mw.Æ;z@fcž‰š@i¤tˆÌý@f/âî­Ç™@bâ³((|Þ@aÐ>½Ç¯4@fÀ’Ÿ\@eñ2N –:@fîÿ1º!ä@e©ƒ&c˜ @hC +wȳ²@fíôaBÏé@b‚˵Zæ]@gà;[t#@e翹ð@gzbü5r@go7ïôì@d +õm¥í@fÅ*£v»‹@jA»oÊý$@j't,Ž@i<²Ì!ñù@i 47iΉ@jõU7@k¯±kV+@rmšˆr—@n¸6L¸@lÞzIt@oSïÛ @rRÏQ@oœkyŽÒ@m'Ñ^¯ õ@jè³ÏA”@o…‘i+~x@nì µTé@ss]Öï@o¡ÍÆÆl@pâ|lÏ¢‘@sö´'¨2@t‡ËØþ¬@rȃ¹ˆí@rhù–CÍw@p§”o! O@qûpq‚¸@rów̨@tMb)£…¬@uJsm7P'@v$¼v’ç@uÚ\ áÖ@u4¶Q®@v§ÇKz˜Û@vãž›”k/@xj‡¼PsU@v£ª–½È@xü UåÉÿ@z#f’C#ù@{BÞ-@| .`ª}@{]5æn‡Ò@z  $ˆ¥@}¨ë|>Q@}Rí†Ø@}K¯>¨®×@~È—–Ôp@€§Qp!ûí@€ŒÁð™þc@D^hÃ@€°ßˆ¥î”@KÁN'›„@‚õ<¦w@‚TÇ&í€@‚¿UCŸ&@„HT‹ÚDx@„7RÍC]’@„jGAþC@…ÿÅ»Dv:@†ÊfÇüZ@†Ö2DZ<@‡{ê<äÙ@†xÌk3^@‰ZÔï@ŠÒ¨-×\@‹YqC-›@@Œ0ÄyJ`¢@Ž]ñŸzö@z>1 @y•ß(Ý@ªÐ + «Ô@‘KŠÓçÇ@’ÏÂD(ø@’›Üp¼ÚØ@’³IæY5@“'ÈxŽe=@”’Äùi@•””©G@•÷Òp¥à@¶”)éŠ@»d37¹ÚÔ@Ãñï1=ì:@Õæ$Ë]¤´@êåÜ¢ó @ô€ÇýÂ'@óŽÐò‚·k@æùrH >@ÒšRP‹|»@Äyx„1w@¾ÞqD뀮@¹ÎýlŠ;@¶*ÌZÍ@´. „ÄÜ&@±Óª… f@°>`N@®¡ý‹Ãâ@­DGõ'Û@«fCœ4@@©ýzàNé@©w ÄÀG|@¨&ÒŸ\… @¦ñÆÐrÔ@§hÍQ[]@§þÒß.@¥é4bGnp@¦¬"ùaû-@§/û9*D•@©¶«%­i@±œT‘E.+@Á˜²{ˆ@Íîê&¶9@зÚáP'Ì@É&`îº@¹/m@«²ºŸ„ò@¦—»e@¢êk“@¡1-€ XA@ vôµ;@ýŠ3IŠ@›ÜmÿP@šjiDƒ¤.@™ñ®Ç´f@—úRìK¿@—®ÞòÅ(@–«ýá˜@–ØO6µÍz@–8`}uc@”™J¡ ,@”;±h@“‘4§>K­@“uÍsк@’ýFütÿ@‘ÃÔ8§"{@‘;VŸK"U@‘lo%Þ@\(;¶Õ@} $Y‹@‘ ;”@:èñJÐ@e- íŽ}@µIÉ,2i@S$¯¹F @Žv¥Ø8 +W@Í–b¾@F{Q@_)–Þ$@OÆ©2J@žèIY±_@‘VÙ§ C@‘ðxÇ1ØI@’ô$Ô÷×@“,‚›µ[–@–dŽÆŒð-@£ïÑD@§ÔUϼŸ@²áØÈ_J@Àj)`OÂ@ÆocוRÄ@Å@0}w@º3˜VÈÜ@§(®«¢ˆy@šµ×Cq#@•=(•)Ý=@’Ê ·1Å@’0Ò³­~@@8ŸØ@Ž=ZøzÇ@‹óÏðÉÌ +@‰ö¢­Kº@‡,F@†´{~ˆ>×@‡Ï$êó~ã@†kt†Ó„@†Ÿ]©E~‡@„wÇèœ9[@ƒßˆuô3@ƒ Ÿx"ã@€\vS±Œ,@€ýÔf>@€0fù¿@k@Ûª«ÆÍè@‚wRšvë@~w0¬­"“@€?ðp¨€@zj„ö”Yw@~Ý_"½`@~DÅ4:±E@zWNœÀº@y‰Eå[t@xª¦hÊ%Ç@v©Ì²ë‡2@wÆ©8`HL@{~¥§æw™@xm›ÚåH@u©•e·J@tv=´©Ö¿@w”@᧵W@tR×þù£@t3»_¡Úÿ@u(fN‹y»@rÈ4Vþž@t¼Î¥•@t(ÅÐÁOÊ@qUËÝ_è@t¸,£r„®@sXþº;ã@qãc-4¾@qÃGQ;ím@jÌ1ÄÁ¤/@o¹ýÔÛF@p\né7C“@qD·++@n²ã~w¾@q§Ž@pÉ­Gœ$L@n—ÊÍ#ª@n¤<EN@qŸˆ# ›@k©³c×@m¨<ç×9+@pzêwRs@kçU}Ìg@jÇXrlØ@gp6ÍŸÓ@nsìë@ažd£ƒ@iw}éª_@j®Ç>¯˜@gÚ¿L}AY@h/N#jB$@j(e×L­r@h¤fàyOž@l©rp’@fÌû¸LKü@lqsqüø«@l"9žN@kkíÅ?W@gyïÒ’+@g*ˆr<)}@grĦõ¯@h—Åü´@h)¬Wÿ@bq˧@gy'¯À@g„´eælŒ@bDÀ™ãQ8@dÀ‚ºL@d¿¬fƸ…@gNd{³@fmµ¢£™@^CÄLÄ-•@^µͯFš@hS~ gÒ@d˜[‹-Ú¦@i ª^;š@f bF @f¯°­¿@d(Ãàc@dH ËG·@cQì6ˆAç@e„¹ÙÕp@fÎä[ñ@gñã Ì+@bú'÷ 3~@`Û[£¸†ˆ@d¨Û½MŽ›@gõG2ê—@cûÏ‹æC@^À¥ïïÑ@gêô™Wëy@`ŒBaÓ œ@c -ó°%@aYfóôù£@_AÆÙz‡k@cFva íi@fÌ”—¤n>@eÙõm­t@bÂ|ò5=@`>¯Œ»4@fdìîR@cžJÊÉ>@`-Ñ,¨ÿÀ@b{O»9´@f¤ÉŒô@agyRÓ*`@`5u°Éÿ@`uû7#@ZÕŪ@[›‰?m›@bé8]@d­°ù ˜‰@dÈe„Sþ@[ŸÐP!ý@b7&WvFz@d)›° @cÈh›6@XÊÛ>‹µ@]ät‹\@Rð.P‰¦@ZP|‰½@_㞆‘Ú¥@eŸ´{ðÔ@@U7¨@¶3@^·¶üZ@]á|f„ü@\?é–ðÌ@cÌEÿûd@KzÄ&äÅf@`ríêòIu@`½®CÕà@R6lþ³÷Z@aIÞ§þa@[«¾i©Í£@\Í—Mïüé@UKÄ» ¤"@`Û•—(Ì@Z…’$zé¯@_-x¼v¯@W g @^"É7†s@\"|¤ÑVä@bHâTg÷z@[¹ç.yn@aB^ß8 @W—×™˜û@aÏ—*åDá@`Ek@]]¨)Ý—@]{ø­…{@[³›¯$@a(¨F,@asxŒr©Ã@SßùÒ+ @Y— Èðý@bð-’*5æ@Xü‡¯? @`r`; @^Zž_j(Ü@U1[0\ý*@U™uû@Vü›ÉìÌ@Qy@šP%@^ƒõ…êò@ZyìÌö]Y@\WºÄ•¦@@_L¯5CÞ‹@]ßÏ?…QZ@YƒØšTç@[&VÈòn¼@[°(@›ðÍ@Z ÄÎPÅ¥@a[ì'—A‚@[ü«Ôwö@]#“õ1 @ZïSÈ™M×@\Â|ˆ@¢@ZžLšû@YÍè V ž@]ÊŒwðFØ@ZÁ¿ +¬,@^smÆìæ@`ÿdZÀ~}@[È«âeds@XU‚ðOÆ.@Sìf~&æ@IXfÌZbc@WhIjÛéŠ@_…éFâÚÞ@T*ˆOµã@Zêˆ` +@U…à¯N0Ì@UÇ®w(rõ@` +ØÄÍôl@\žãàÇ×Ú@_È1ÙéëŸ@_ÐUiîÆ0@V +½] Ð¤@a#TÉÝ@Wö[È0˜@Wù;<²Í½@Y§S`åF@VؘEùÆ@Wæ á£~@S•äwK¾i@\ïDðzsÂ@\RÍÍ9c@XìBý;@\•O„ÿ¡@UÌ÷îZØ“@\HË~ÔÐ@X–3»@Y'Õ|«uô@X5]ø*Ë3@`^—ÇÃû™@]¬ö%‚@Zë5V7#@ZŒƒÀÉâÙ@`7^|@ã@aUZþ<@W\DòÂ=4@]?9» v@W÷àÆ—@Vk œ” @[Õ2F]Y@[u¢Ùt@`³­6Ú6²@VßF:ññ³@VÜ×(Yhs@`Õ䯧pˆ@IJö"bàF@\(@–†@d^\÷*•s@\½˜píè†@`<TR +@[¹‘l,¾@P<üþìÂq@\Ê ìà@dèÁï:ð@c|G)-@Uœ%y78·@Z.SëcSº@a;Ñ̦@`ùïñÒñ€@WZä°nŸm@^›Ûð«²@Z€/bBEç@aÛ•Î!áù@bNûFhѳ@WŸ‡vj7@^|#Ðyú@Xâ—BB3L@cŠO.Þ¯@]y€n@Yr!O×)@YÙÝÑ,ê@Yл¬B²@V€Î/oBº@UœÚ˜Tî5@X¹“½º„@Q6ÚfÒ—@Zô ãy” @_oåè`@X)3†ãÜ@Zl¶$*‡U@YmƒÌäzÁ@\󼜇íÃ@[§&—±@Rîµc2x@cÖîSw"@\Ä»–„TÓ@ZG^÷1ÕO@_ÂOº­/@`•×ûü¹ú@\aÉÑ!G@Z]‡¿‚Wà@aÔ‘m‡o @\Ç”=-,@\hêÇÆW³@b$ƒ# MP@`*ž#ÂÇ @YBaÛxÿY@`nQ×xné@U³àpêÏŠ@W›Ü%õp@`vð Ú_Þ@b§a·&‰@PÍ%c9@Wq…ï";@Uö&¸(ßo@Xœ!eiÕ@Tü‘ÒŠ!Z@b?¥Ùä@M@až®,ÃØC@` 9“L@`Ïu.&@UÕÉdF•@_¢ñô'›W@[+9Ê/N¾@]sï‚Ž?(@Z(!U;p­@_Ý‘œ®®ê@V é €®@`~ÞéeYE@`1–~ +z1@\žˆ×oôi@`µ!ÞqtW@Y²œ¼¶/S@Yçý5@`Zð-éÿ@X‹Åžù4_@P¤_è_Ÿ@ZT ØœóÍ@[¬ñzã2@]ÝŽ©ÎQ@Y­âAYÇ@_ËkyÔÈX@UÌHKÖ&å@Z솸¿Ø@[q>à6:@R´÷};@Xuû<}Na@U÷û!˜@\‹Ž()£#@Y?—r¬@Z¨wŸ@_CÆB‘°–@^ 0 ¤’@S&ƒ!õ³@\ÆZ×k¤Ž@XWI0@]5º^V‚@btI™Â@@_ŒëeMY@_  rúv@`õVdûO@`ÒÔC)‡@[Jò¸ +` +@]J€XkÒ@\•Oz`çŸ@YÞ¿ud“ã@_ ‚„Þ@TFÈé=†@V«qÂ$@bIo+&|@^ZTDÏ]¢@ZŽé·&@Yþ<ÝmÝ@^ŒôO]°›@aJù÷Á¶Í@Tød +ßÆ@[)'À@\rÍf(A@\ ѹ´ª@XÑ—rJnT@`¾FUa@\ÆλØ@YÕ’¢,­0@R‚ëêœ|@a8‹M~@bB:yø«@`-2Û~%‘@Vb‘˜I:–@_Û¶ÒKèí@`2J.«Ã¿@^‹¿«XùN@bŠ¤d£E@U~‘ÇÕ¥@``ÀO ó@\cΩHtH@`M^qøÔ½@_©ÊìFÊn@Y¼öu¯6@aêôÁ,.3@Z©ðÏÀ—@]ûbÙkk@^§ÈüòUƒ@`°#]£1 @VÒ §<ó@ZƒÕAôàù@`&ç^1W@`2Mßu×r@aºq9î²@[ +° œF@`_´ßÖ_„@]1yrÌï@c ÅïLÊ—@_P“@`ÿU†¥(r@`T{0Fœú@cçäÈÄV@[•axç@`…§*jšú@cAF¼Ä@W_` y:@cT¤ã馷@c~õ>îI@_{ȉ3Yò@c€s¢¨ãv@bšE’.À@`-%Òª.@cñÝ{"M@`Ó6ßÒf²@fs¡ö« +ß@e5¥šñ#@bZBÔ¼‰@d¦ öoŠ@cw}±k$Ú@bsƒ¾™@eê4´ýýö@cYwÀ„@^¨tÎ@`ëÕqF +@d)¦Æùg{@diÝòU3&@`mÉnÅõ@d×4, @_hÐÕIT‰@b·UÇÍL@bCÞu…`@d‰}'+@cÄÝ:0@`{•WÁ—Ë@aHKîç;‚@`Žù&+¸@`4­ïüWy@b%aL£ù@[Dsîñ~@d¼c€B@`8´Šàuú@`©ò…F"@a~ª¤=@cÔÎ?Óz@Ukך<Ø*@h©vb²@g¹_@ýÝ@agœËÙ?@aëSæUz§@bX%æz’2@cኊöð@enÞÍK”@dxþ>.@fíÞ÷üó@b±rðˆC‘@fÿ4L'ì¾@hÛ@Ý|@a—ÈÄÓE@afÄ£"@k”IÂõ4‚@fŒÆ!Ëÿ@`çNTúuÇ@ež¡Móð|@f©Å „@`ÁªjZý@a#œ™òö@fý©|È—‚@b^M´k=@f†–S s@a( 5)ªè@hSÛ•­Ê¨@ag±ÛÇ;û@bjª+ê÷@bV6—5˜·@hWSÄG½@fÃÀ$ˆÛ@g¯¥#„@a%0\GîÑ@dοZL÷Ò@f°8üMý6@`gàÛ{@gJëuݤ @SDÛuý}@a5ÔvÙ{@c'ËGKP†@dá|u#ö@a¨@b×ÁòL2þ@a¹(ý:ˆ\@b›øõ-¨@h®Í]ˆ@c³EgÛƒþ@dX­YR'@g¡5Ó!@cÏ tZ»p@cë¥pvfß@hÍG³¡=@dN×N=²s@eW¶:ç@c;¹¥É%P@fÙħì—å@d^ì:®@evæï©=@e†ÅW[Z£@a\ÞA^e•@aÓ|Sæ@b!$é Úë@a÷ÐêG@cÝnçø@cJu·h»"@g¬=FNØ@hf1EV'÷@hù¨ÄßÉ@gRM⑇š@ahBÿ9‚¶@`W +éd\|@g'»€—.@bš¯§ OZ@eò”Õ÷@d‰Mêt1Þ@g‰3Ez‘Á@eGÅÄ9nN@YMb¼8)@eµT{GÌ@k½àæ ï:@aí?$@€ì@g"¸ÎÔ @avIFß¡½@ešuÎc@coX§î£@f#Tuâ9É@g‹Ã*hø@Z2!I 37@`¥à¤F @je…À0.`@d¶–ÃG@g|ƒ$æŒ@bC1ä׳ø@g`¢ÏË`!@h^M‚Pg.@dB1H§Ç@dÞCrçH¾@cœ ±M5@bÜ¿™¡¤@jŠI³º×µ@f󞥇i@a{¢¾@bL5w²]@p ¢¹÷žÁ@g Í—@g T f@i›Ù ¥ž@aUéëíæ@fÉl¹|§@d5£¨»ƒ¯@f,`†LÒ@f«†|ËJÀ@jÆ’y=@k&½t @kc>ý·¥@l©8l w @kVYÛûp@e€Å¹•×ü@l¾ýÑe®/@gFc·Ê@fçµÏ› @iª¿º¦A³@k×ûZ! _@ghž÷(»¼@jÕÖ9zcô@a±iLÍâ@kjÜÿn_@c%´|ö‡&@j,3Ô'íJ@e’pRÎâ@hõª£Ð·@d¸gækÐÁ@gæKtŒ@f´|‡#ü@l~Wgêç@h`VÜ>@f«þûÄ—æ@k`áÿïŒ@h_¤QÌk@lrÚ: ‰K@dœá$§º@háH(iu@hdJGF@hÛ+m.@cI¸4{@b˜RÚŠÂ@g8—p ïC@iÚXóJì@g¬Jo@jØþœl•U@kyÖ#ƒK@hh©\5µ@c ±|Y†Ò@k¥ Ëq `@eàý´&p@föÌJ¢"Ë@jAlF@fgR½°b @ltôòyþÝ@hÃ6%M®@hÖ©È®Ÿl@i$öfgã@kÂ<{í<@jÜvÃL½@e.‡?¦%@khÙ"¢@j¸E6'š@j¯}¿žð@i° §–@hæùá£=ç@kû²!ô@nr}> kº@n!»*zŽí@i1"C~8@hI¡/æþ@g¹”…{@fÆŸ0+¼s@ooÔ£ M@iVAåÜÐ@kÍbÆå@hâÑñ`@pÀL¦ó@f}‹÷Åbô@dø–¶E=@h#+NýZg@gL]I¢U¹@hÕ¿%jq¯@jðiƒ®¥t@k@G/]æb@iLž¯Pß +@k' {°l@jü€b¢'@i j€|¦5@j šsöb@m›QŠÖñ@qZ¼~¤ð?@k9ÝW·à@jIýÐHÖ@jük5„‚Û@ló+.JSò@geùÞ¹=­@iq P¯•Y@fþ´GuÁ@k-ù]V^@gÛ«£”Z@h,#‡Í +@k+ƒø½/§@iÄöf|\Â@jŸÎ!_Q@kkP÷°Êž@i–ƒ$ƒ­W@k¡¹§Û¯5@eÿ`Mr–@p3Äh:H@i}æ—¨@k3U,/L·@f×P’wAg@j5íEZB@mIe’I@mpÆ‹—ñ@j“íÉJ@j<ä)‰Ž@lÚ´’å@n,ûu@oÅ:ü2?4@hhÇ>Š@e\Í£µ@i«Š°0km@f÷Ñ +å@f©"£äŽ@f|×&@fÂ~qâ-²@nG.¢èV¼@hÛdÛ{çq@lÜf%€,&@iÈ èµÇ@nN'¶Šm@n((ƒÖB@k"3¶mW®@gN~Bébâ@kPÔ@i«²’‘´S@d8 $ÅS@gÜNù4Ö@eÛ^õWM˜@lèVÒ§ 8@e’LëêU@l&ü @mT“)3Ç@k»¦]`%@jóÌÓ‡@g°ö@iÖ—þÁ°O@oQÔÅç@kÚ~î1Û@ln[~%»@lYºåad @i"–6=gB@lÑxmõ@k$€ˆ™È@i”uÐñ@mLQúü´@m·µëÕ݆@i~aVCÀ0@lˆñµ…È@i‰êÊ_ÿ @fOâŽö[7@pè±{@n aKxÃ÷@k”ÅTb}@fà¶ÇIðW@j»j>‘@luñº‹Ê@kÍ Tvâ×@n:u+ü ¼@j·7ÐúS@m ê¿Ã@l]!ÜŒ•@layíèÉ@kÜô%©@ljìlç·@iG”8Öd@kðZá9¯È@kpýg;|@jtç\oMd@k®@j²)£ +@k& n˦e@k•2¤¶cœ@jhOIBR@j¿¯âÎÎK@ijˆv8}@kC¨ì&~Œ@i#PP'e-@hÀüâÓí@k‡bý]uÁ@n + +/]|u@qÑ&38@p,œÔ³=N@nÍš'º0@oE“<´.µ@p™žam¾@nQ?ã„9ƒ@gdù'¦@lÀ¡¬Ñj·@nüGyŠ‘G@k¼}'¿>@k† ö°@mÞ£.°!5@j ^P+¤@jCŠs”k@n¶wÜ•G +@pvß6zò@sK?:¢µ&@p+Ï‚C„Ñ@rÑ ØŠÔ@l¯kÓê,D@rÜƆqò@qÃ'ðˆqŒ@nf–þy9@lñÛÂÂ=[@o„ÄjRå®@jLäím+@l rÆòø@q!îw\@o‹7¢ÃÚ@j$a;¾r@ntˆ Û¦@n)/@icµ¦Ÿ)@kh?pŽÜÇ@p%ú‘+Î@r%+´½@pˆ¶Tøyò@oyˆ>Î@pQÅS¾´?@p F-(wò@pläOÔ»@qöe‘î@nû} }Ù—@pO¡{@pþýú|@p^…g@nƯž•@qÈJÏt¥D@mSÙ:Ý>q@m¸å‹XM@k9´]¥@rJ,¬@pjþJ-·@o“2þ1T«@m/d†@pçÃ2Ø@k);ÏÎݵ@o{z€8Öû@pɸÀñ3@s8ûM@qÔUõž-@jóP€n½ò@oY°*Å4@t”S‘Ìã@k¢»´#ž@q§ÂD®†l@qJßÚ@nšÊ¬}‚#@måüCr0@qµɯÑ@nþÿÝ(ô@sÜIÕ3í¼@rB¶À‡ø@r)˜6£L@r74•û'@hía€‰Vž@p–ù” @n¥A:@o¼ºmP ¼@q‰ h`_@l…æh}0a@pá­„O@oX!Ý) '@n=¯=p­Î@j¬˜ÊM@jóòÇzØ@oÿ/Ë5l-@lö3u@A@r¢‚õÇ3@pæ•+oΉ@qÊlÓ +@pSQÉß@rs–«x¯É@hl€¢¡‹x@q!•#å@qµ•Q‚7¶@n¡4WÑ6§@sq ת–¾@nËíÜÚ@mz«Êšþô@mä-f[—@nz{¼/#@n©Hœ*¥@hç^+ùÛ®@mÁ·qEh@rIé3 +#‚@nwœ02@nËG8Æ•ª@p¾$7ùmx@pgID¡ð@r­#``—@n’¨ÐË@r5Ö[KN)@oït¼5^@qìåñq“?@q3”ßš||@lß_Ïu{@o/xTm@rÙ64E@q9ý¸Yd_@qÝ뮓@ldâkZ?]@iϯI]jÖ@lÛ{1…åD@kÁæ:½í@qþ¶·e9@p†ñÓ'Y@m®ÍÞh™@q bîÓa@r†:GT¡@riÐ˘ã,@qäC]¦4@mw•Ý_Í@mÑ +Ns@pËæå,ü!@mü¢CAàÒ@pã×\æU@rBá¡‚áÒ@qú ó,ù†@ré¹Jÿ@mñD½Ý˜@nHù;@oæîÏ䣅@pÛÕ)Ѓ!@s@ vÄHë@pL3I‹@q ÃÍ”;@my;[wÀ“@lL=¬Ê@p® G/ª@rG&w[A@p•„‹$®u@qfâ5œŽ9@oÉxŽØ¦A@stÄg1j«@p;¼Æu +\@j=ÄG‘@l²fÓÎö²@q²Ç»µö@pÝ-42à@q™@²æ1@mðã8U@qÇB]ôÆÔ@paBúp›@pÈ}× W@r\MŸ‚ü@rŽ£«UR@o{x½“•È@s¨}¹ú¯@qo½º‘@q·û%T2Ó@pLsšÖKP@pæ£ãq¹‰@q°€iv@p¥M *\ã@pL‘gAB†@pö>cŒ Ò@q¦jýCµ@p¼îTÐRa@r“ÍõH?@pÍ5Î1ñ]@rEzdÍÞß@r(òtú®‚@páwJÍ¥š@pˆ¯#@pèIåJV@qym${D@p-°brßï@q’k#/oø@rQ0F‹¢@s35„Å(ó@hƒ:õ(ä@r +B©ðÖe@txþäú‚Â@sWÃþK£1@rñÍÊHÒ¥@sþ»û@p˜íÓö@qÿ=Õr²I@q¿êÙè[<@pù±r²ÀL@p:EF° #@qmú­Ê˜@r¿ÿÚjSÎ@r¨1 Xæl@r˜ø½l¤ç@q}«›“@q¬­› ”&@s½ô›ˆ@q«‘b¨)_@pV™( M–@r¬ n¸Ü@tKþF"—@t#ýÔuÚ@s`²°a«@s +Ãû,€@q£VŒ'¿@pøNé]Ç@v§õ™rß‹@q¹ìù±é@uóç?S§G@r"•‰JH@pÖjŸ™­2@p:ÈÒ¢‡@r¦1†€.@q,ÎÍOo@pYÚm¥@ræÙêŸ@qQó<Õ((@qâ%ÆÀæ @qP8$b@r”F¶mG@t$·uÉ€@t’c—DÁ°@q4óá™"@q?Öƒ0Z@t‡-÷A@n`9k)¼Y@qzËËn@t.ØìL¢@sQí<ع @n§½¶š”@s—¢„q&3@tOxÀ.;š@w ñ¨˜ùh@yâ¥\ C@xäè"@vè(ðòº@q/n8ß d@s¨3€,>@v{Ã9÷?@m]þqÜ9­@o9㛋×@svlNÆ¥”@r7))°@rW|lM3è@pÿÌ¡S@t“J„–@rÍA#V@sOLÂxøf@oµv÷ëS\@pÃá¡)f@ràÍ-ë¯9@q«loÌüÀ@p»ÚÆÙÚÍ@pëe*ˆsÂ@p@G¹mY@s? ¦&—@pe¶O®ó@p–*êšK@s¯K˜þ@s>ÆØá“@r×ËUhí@t + $—F@sÅ:Ëß°Â@qy$Tº{@p­ö ]ň@pãŒ#@pº˜qòD@p Ž ÉxÀ@p«ŒL ‹I@sw@qvŸ@s‘Ùè¾@qÏ"Æf˜@qDYk¢&@pCâæ0û@n€'2YÔ"@t—#$Åi:@q…jrV–@mÍWÌLÇW@o£Æšs@q tO°Ã.@tÌ©üÖß‘@q¶HÕQ¤«@reqÉq¤@sª†@æ,Ä@r x½TJ¹@pF÷PÌ<2@nZZ¾i@råÄ™$FC@sÓ9¼4G@u‰;TUs@r„6>k·@p’FæŽz'@p_dE¥›@qþJ8|%ÿ@qL¶|3kÅ@sÃ¥ºÎRÅ@q=Á§¢pý@r¿eZÜ4ÿ@t4æÕ“@q·z'š6%@qŽè(²»@t5’3Aq@tF­ÅÉ @rc“ßVÞ‚@nw} ü¸»@r¤)ÎËÇ@sm¯­¬ù’@r¹8û`;=@r]*þº@pÕ õdÐ@s¾câ«íw@t\Œ£á@piÃ&³Wš@qê¡ ,å‡@r¸}*@oà±ÿéàš@s»™š/4Ú@q‘$1Ô@rÊã Ïõ*@t|e‡fp@t[­Æ–j¹@usDpF_v@t& é÷Á”@s¶Gˆ$ +@s[cnƳV@rJÑ×x#²@s$5Þ>²@rWþ¥qrË@qˆU}Æ.@q¥l$~x@r4u-™ö@t³_¼›µ@sðóóí@uÔA{Ÿ@s®1䀈@rŸ³5$Á@pô{¼<Y@rß~åÈ50@t]p篣²@sHVHši>@t?¥~ý˜@v²K§¤ÛÕ@sqLù;¨ @u2T‡Øht@tŠ¿ìqú@võ‰£ØZj@u˸˜:¶@s(Ëܸ=@sA©ï¼ß@tFêiøËr@r¤¶ƒ×zÏ@rGIJ ?@s+F[÷$X@u—ÒaÉ»@s7UuÐ#@s_˜€ô@uC¯# N@tÕ2²é­@v.»ò»@uª…£ëå@u8TÙ¨•\@r¥ß­ +"µ@s±è ‰%†@u~œ›(Âè@rJ$Ç›• @uy†Øé»ô@u,ãÿ>[–@uZÀ>5«@räô’ò@w‡qÿX Å@téû9rp@pÅŽu}~þ@tDù*n@s$ ºâÍ@v.nÁÌÐ0@rᕇ:§@piãx¿ð-@t¬§Y,@sÁ·šŸ2¤@rt÷%pÖ@s_tõk(ÿ@jš ×”ØH@r«9Ý" +P@r‘e "@súöQÀëD@s`r°£ §@uZ©³VÝ@tµ ¤@uŸmb@s…Þó ÅE@såôRýW@qã²3Pj*@sµ¦^\@qÀ>¤Þ:Ö@sWÚÆ^q+@qwÎ"*åC@qÒÅñ–äº@rGÄp5@t†°ª”ü–@v =)þg@s×›š( +@rbö3Š3Q@sÇU0J @u7x6<Ç2@sÙµÄ:³g@uÑwkî‰@r2'oc̯@utˆÐ ²ý@r-Ü%ö©6@r¾“ +ìÆÀ@pA½æÒV@u|‡Ø¦hr@uUH'~,@sÖAñsÓ@p\¾eú`@súr!¸\C@ub Êty@t{Ãa9êG@r‹ï&Àáƒ@tya§™Ÿ'@uO‘è&=X@q˜{™`Ô@qÏ×P œ`@sì&gn0@sœIT@sž‚˜,_@s’®ía¤@t½rÁ´ {@q7ž"!@tšÐM×]@s\&Î…Y@s€z;U»b@tW½xR@qä”1µ’Å@pù0‘ÍŸ±@s8k®ƒôÃ@uÄÎH$ãK@t<²¸È@uòïx¢-@s€qaèp@t¥h +T±#@u)‘èûèÙ@sÌ­N"@røedíf@ulJ?†x@v#B#¨ä@uïÊ^êrx@txœ`«ø@püsœœ@uVŠS«)@tGŠ,Á8Ü@q37`œ†C@s­ÜÌòD@uuù`@ @tU‡©»ö@táq^˜æ@w +¢ÒÈ“I@rš¾·ˆ@s Ó‹\*@uœGs10¡@qѼNàjÈ@td Öƒ“@@s‚/MX@qj%Èë*@t3ÉXpJ,@v•ù…»~u@rqÔPZò@v”b+zO@s×6O– +A@w.÷x®@s˜›>èoU@r—¿r¦¡@sÑC§Ò[k@tªü·ÂÖ@tBí¯Ò–@uúˆÌgò¡@t‘]€Ê4@uK [ä[;@t +ãƒ7°;@pt r"DÁ@sÉbSÈìµ@r% óä²\@tkm…±e@sÓ9Tæ´É@rSRà\…@tèÅ£" †@uXüµJi“@u("Ȭr@uiQ:ͼ@r¯ùqÔ@u÷uìÉÌ@rq/!ÚnC@sø¬ÁqyÛ@u]PC³j@r¿Ý$FO£@rÒ´Vl~W@sªÄËw@v4Þv @u@ª€ßx@qX¿T@täâ¤X*@tŸ×£«>$@tÔôtwö4@t­… ¯—ù@uÁ¬fÇÏq@v 3ÒÌÑ@vºÔ(@vmÜ«m¶D@u,¢…G0/@vxDú ñ@tS&h5šÐ@uÆ{·,/@tËÅ9/±B@sóØzoí¹@s­Ÿ‡;ÐÊ@tÊ\_ûÝ@w7‹–N‰r@v +µŒþx@säkû×@sE3eI{Á@u) •ã÷‡@s¡¿˜öTé@v“žË,Wô@vpƯej@rî\l‹@t£Ä_{ „@tkf§dÌ@r¢.¨Ú ï@sOñ[Nq@w®bðÅÅ@tÌüŠÚŸ@u|ï‚Må@sv`fÝí@wßhIL +Í@whõÀ¦$ï@qÏÿ ç@rµ¾k>K@x`É4c@ur4îr|ò@v3­Bá @t|`v1í@u°hÖ(@t†3¨po@sc$ $Å@pøÊhâg¸@xáüêXI@wÆ=‰ÃW@täËäïN@t£$ø¢è³@v,æ&п@tYGb=«–@töÿÒfëì@t"iåþ»@t¯ä ¤µ @w$Jq|„b@vÇ4Õˆù@sXQ?lÆÅ@vwqüC‡@v˜ÎKãò@xN×£Å@p£­õ ¤@s¼Ž:‡û@uÝU`2S @vëj0²‰@sÆçg(@w±ŠíµaÈ@x2Šeƒ@rhy˜, ‰@sÁZã¸? @vSÙ<@u[LzÕ@s¦nÉ=@,@w“} ”+@v€E_Mª@w·Qa˜Ç@rô#¶ò@7@qÄ=B‚Ä@v}'‹%¸@v­ÒÒO¥U@r +›2‰²@t©-'¥¹@v}_ˆú¼@s÷xÃ@u55ŠCîä@u Ô(b¥@v4âyÛù@tX¤xâd@u!fŽ¼4@u‹›\ßÓå@t;2öÛ@rþðk¾@tí™à%ó@r£ q @uÕ[vƒ=H@s‰p:& @t¿?^©@q[z€@v!t•s @tcì…7!‚@sŸIO@sþ~o'…¸@wùG/E@uH2,‡˜@sµ—UìU@vf¡’ª™@uÑaÇögì@wãѯX¿@u¸êöYF/@t?ãØ#@vÇ'LõV@vÞy&y­é@z!MDÄ÷@z.>¬'Þ·@xº…å]@vpé‰bª1@u¢F>@y×W wÂ@w·S?[@w¢7‚„,˜@w©W “a@s¾¸|åÑ @uV^„üÁ`@v§>ºQUš@v]Ù“Ÿ´@tøª.õ{@w‘üXû ¢@vÿ¾Üû@xl„£.@vÿ>=ß‚@sKœ&íoÑ@vRß–“¡§@v@€g"ª@u Fá†@5@vHŽ:¹@sV¬dØ{Ž@täÊ“þV…@v9‰à‰@vÍ°>’¶@w¡(ôG@vuÿ¥hÊ@yðãŠ)q@t~PT¬RC@v^å¨Í$Ü@v­‚²^2@u´((¼<@tYFW̃@wTÜ%ÇJ%@xòj4ˆÓ¦@wâúéä+)@sÑH°,ÿw@t‰Ä¹l¥Ï@v–ýu•p@ví¸@l;È@w€òR :@w×G ÎÝ@w—ô÷ÿq@x‡Â³r?@y@†hÔØ@uæXmU?-@w0ÁH²M@wÙ¦ªÙÒo@q…ƒwøþä@yÑßÿÎ¥-@v™aëXó@x4®äMã@vùqNç@u;uj@uÅ…8Ã@tc êÀpÁ@v%‹[›Fi@to y´Z@vÐX0"H +@t<;mST@v `ñ¯©—@wzðÜÙß"@vWqݲ÷ê@wAóYmŠR@xÑ«2õð@wÕP“„¸.@wÐ’µ4¸@uCCá:Êj@võ¶Ì¸´F@wþe‹y>d@våعK@xœ,ÖÎT@v4þ3mJâ@t7IþÛÎk@v‘ñ|Ãøs@vd Õ +"@x¤].ê@v„u¾}b@{ÝK^½þ±@wÈ_‘QÓ@v‰„eo2@týL¯¬¿‹@wÎrUÙ³@|CN®¥H8@u|„tÅ}Ý@y¹ÆW†@xw\Œ ‘k@yc­¯ÑÒ@wÈ¢È_M@wç¼µÕG@wî“ÎfQý@yƒZÙÕé@uß•á@w%ÉQD@@u:ž9¡½@v+Rxû c@uí9J¶œ<@uu›·*¦@yS5ªv@{‡ð}Ó\›@yúÛ¢ åj@}M4%_|@x©Îû„[@yƒSjªÎE@yÙ˜÷Ï‘ï@{˜âs?B@yšzmSé@x$°©â=ž@tIôîÚ`Ã@{ ãŠ0^@zò|¸žT@xV„s£Jø@xÕRœÀOÝ@z)+Gr+@y;Úî@zûY!æÒJ@wPZ/UŽ@{‚EÒ©‡@y¤ñlZÌe@zW0ßþ9 @y'QÑã-@v•:È—@yáûõ>:@zí]b……@|cúnRïÄ@yu`O#@yvÙ[&ò@@z]ìÚ5ß@zÅïùDB@yŒä4á‡o@|›Å@z•dÆêï;@wôäI+”Å@xBº0åE@y9€§'±@{†„¸OÒ@|ö4éW$@{¢CB ÄT@{‘¸+Èè)@{V ú?@{$—ž,ÔX@z>&X˜@zF§He™‹@}Šß|Þw@|·¢óŒ8@}vîy&£a@|2–„@~YÕC;ˆ@xhýY¯@}=Ö…Ñ»@{¤jZˆÔ@| í!~M@z51„‰J@y` 8¶c¦@€H”4š@$.V“|B@|Nc Ã¬—@z°ò¹N@Ï`Œ +Ê@yVô‡ªGo@~'^ož%C@|± +N>@~ëá¾þæ@|ð{D;S|@}€  y/È@}$Zèáe@zžß« ÷¶@~[žµCË@,AâËi@éÖèƒ,*@{Tp™ïÅ¢@~K DÑ@}øYXbO@{t\FÈ@€×ù­²èê@€Ÿ@<\“h@~rÇì­œn@8YÓí7@| Ü¸ÈÁ@žw ì@€‚’g}K@RÓƒ”@€b€0ÂþÜ@€Á?Œ‘@€}PêB¡>@;¯ +N@À^‹ä9@€™úÿUŠ@]ÐbSðÁ@E³•¬ @€|ÝMuí@ছ3:Ž@€=6Y¸‘´@Cý®õ¿­@8‹ì‡@€Æy‹–¬@€ þÎ û@~Ö~jbe@‚ƒÞ•²>­@*H3÷e@ƒ$Vé‹v@‚×´9LQ@°8ŠŠì˜@‚ùï2fh@€úkQ»}@‚2h¾@#ÆØfXß@‚ÕoÆQÍM@ƒ“ë×ýd“@ƒ’CÑÖSÍ@ƒ¹2ò9 Û@‚ÈÆ?ºäZ@ƒLç·@ƒ:µœ{¢Å@„£Çú@‚pâ2ûk@‚=NçØú@ƒ¯íO,ìk@„Z¼(E¿@„F¢…žº@ƒãg•b@ƒPÒP6kÒ@…BISÕõí@…5»ÁþÁ‘@ƒ@âXàÏû@…}œ“…@ƒÜLƒÎJê@…­üÕÏ–@„ͬf×@†êc”.tÚ@†zFbæv‡@…M:œæøô@„æ1F†\@‡ö‡ã¿~@ˆ–˜2©q*@‡EFÞÉ‘¼@…˜vZ›‚×@ˆŠW¸êí@ˆu(ÙŒvå@‰ªNnÇöV@‰jÔ·NP@‡ ¯´åÈ @‰g³K^@ˆê¥rÕ@ˆT'Í’à@ŠVWב½a@Œ>#ÛÙÕ@Šâ‰O@Ô@‹€a8zï@ŠA„Rþ·L@«þöÜÞ@ŒÉ³¦"§@ŒKB5¼k@Žòç.A@“‚¶ú@ñVmÀÛà@ŽÖ¶öTå¼@æ¥SE9}@Qý‹E(¸@ìcTÉð@Z~XÔjM@ñï†Þ @‘fÂA œ@’2ø PÞ@’0£þ@’A¦/.ì{@’Ľ¡Q©+@’ˆvÉÈÙ@“¥‰ÔB@”…€ž@ú@“ +’áß‘@”¶©HŽjW@”Iüü½-0@•âAƸ@•Ë +‰ÐŒ@—¥MÏÃô@–Ø~æ`@—Äô“‡Ì@™0kUô{®@˜®hÑCSØ@š‹žÛ¾¥È@šZäãèø@œ$ÈÒýæñ@šÍ!Ó^V@ð,ÉïìÙ@žtî1¹¥@žPÕÔ¶@Ÿñ@益 @ ãû…¿Ã@ ñu½`R¤@ Ø_:\%@¢AÖç@¢qóbèø’@£‰!¬ƒuÒ@¤WòÓl@@¤J÷-Ïøº@¥“:Í?@¦öïBÐs§@§ç¦fq›Þ@¨A8W«_•@©œ*£5@«"ðÕF=@«ÄЭÃ@«õ +ºå1¼@®s6ƒÝßË@¯þ²ÈPÀ@°U´7Š~@°ý趧?@² ½M¶&ì@²Å©ÄŒê#@³€1Üga@´gƒòÑ»@¶6½×–ü@·v˜‰i@¹$†Îˆüð@»–ŸGcd@¾5íÊ°åÈ@ÀŠÓ¹]à·@ÂŽ÷t`6ß@ŧøhÊì@É4YRô@ÐVwà%J!@ÝàH™Í@ò}ÜnÒA€TR² +A¿½”Üý´Ag›ëQU‡@óF}b!§]@ÝhÜD @РŠ!F @É[ŒcUª`@Å™¿ŠOò¦@ÂËŒ.¤o@Àß(§Ðx@¾è9n{l@¼8`cËzã@ºCxr´&"@¸bŸ€Gô÷@¶Ë?ò®lÙ@µ¾‚w—K@´:zÛó±@²üaŽ`Ž@²`Õܼþ@±)Àò16@°fGÑ"@®/}ï½Çƒ@¬ëD³7»*@«‰ÆJº‘>@ªl7Óq˜A@¨ÝW‡ã@§ÌSFч@§û;÷oFz@¦Xÿ¿fG@¦v¢Ò@¥5 30“¬@£Í•,ôû@¢ËŽB`Ò@£`¿1ð-@¡|3ìÒ©t@¡‘„­M|@¡€–C´U@ ,µ‘$çB@ ä@ž‚‰i½ @žbÓ‹R­³@œ‚TÞ;|@›Ï¨Þ‰â:@šüX¡üw@šE”Þ¯ìÖ@™€’avW@™Šî¿&f@—ïI¨Úþ@—ßèÏŽù@–~›o¨çq@–„KoŒ@–såùw= @”-ÖÐB„o@”ï#f~û@“üˆ,{^!@”ohglú@“>6¶~×@“ž¥Ü>%V@’68Ñž'º@’]*¿Os@‘ZvNÝ×?@‘Ýþ»Fqš@‘^ô„³ÅC@‘Ö«nãÀ@‘+€¢ÇóÕ@‘>îÛa@> Ÿ‚ŒH@˜‘Ë9ï@Ž[+ÞC@ŽÍÕ<{@ÉpØ™@˜fUnû+@Ž0ôì|ôË@Œ¢È³#‚È@‹XŒ[õ.@‹~soÆã@‹À ì¤r!@‰öv[U²@ŠOi‘+p@Š$#&W ®@ˆÐ)á~ªº@ŠÇÅô—¤Ì@ˆî}¬óB@ˆéWl"[Ø@†}oØóZë@ˆï'"Q8@‰ée¥÷Q@‡g +¨!«@‡yê ºš£@‡w¡K{´ª@ˆêBÒŒ@‡¯Ø>3B@@‡¥´@‡ÄÙ#>@†J +<˜ }@†%{t¸@‡iGýÝ@…óûÕ'¬M@†,ý( XU@„­·áe:ä@…42®ÂS@†{vPº“Ù@„cUn@…SϾ3‚@ƒ )ή—*@…²ÉlIº@…1v¹H@„ßl„É{@ƒ¢‘Öæ(@ƒ+U~CÝ@ƒS>ó¦î@…4•ŸÐ™@ƒA§| K{@ƒW³jJ^ü@„‘ +Ô£æ¹@‚×öfÖ`Œ@„>–D^?@‚è :‘Aþ@„‰Ó¨@‚1+,”Bì@€^óDç€Í@‚† ­Ï@‚Œxti€@‚ñ³ñy(d@‚+8ˉ`ô@‚q „cŒ(@„Ý–Gg°@€¼_Æc@'®$@ŠÐøf„ÌÝ@Œ ëXÓ©@D);-×@göÝuߥ@Á?ºF@”Üo-4©Ÿ@Ã÷—‰Ç@¯?öî–Tü@À¼M\ðÄ¿@Æó–¼Í—@Ä-®éM»@·×‹ñl@£cBºš¹@–ˆZ9<„@“ 00LÄ@‘psz6‰@DÄJ§À»@Œ'T]¶@‹_!ÃÇT™@Š°M³¯|@ˆ¶Ï5ÕFË@‰‰K0v'@‡÷Z9%r@…D8fkìN@†ôÝÚÞ V@ƒÅ —Bæ’@…„Ì"bŒ@„‰À,é¿„@ƒ³t[+P@„ Š_ õ²@ƒ\ó°÷¬÷@ƒÿ! a@„ÖÍV€B>@‚&ið“@@ƒl1»K¶ +@‚Ä8zõÿ@(=‡áËJ@|1a˜@€p=/ckO@ÌGÿXŒ@‚B`ï .K@W]Æœ¥@‚^,Êž@€3Ïo¦þ9@„}Ú÷@€& †ƒ§@~‰×*˜@~!Ä´¯Ÿ@€–üxèÀ@Nj€é“@~pq¸Ì›Q@~’àäsÍÎ@|K¢—†@ôE´oÔ­@€wj˜Î÷£@~B{¡ÜBN@Kü«…±@|BúU«²@{ÿrÔñò@}4ÍÞž-@{”Œðd»S@~÷ì{.ô¹@zÎ…†¦@|-æR»–W@{?JiUÌ@~äm™ ±@@|cQ©8q@z<Ð ¹ÑV@}ÜÔ#a´ @~WFŒçœ@}á·zš ‘@}Ý£š}Š"@{pzd®óë@|8ódÏ@{¢h‚RbJ@z€æ†*_²@{¤L¬Óù@{ÃÜ'Š¥@{‹«×\¼@z 9·÷ê{@yù×ïHU€@w4±»ì¸/@z 6Kv‡ @zMªÔÚ!5@y‘>¶m6@yTOé|‚ê@zæ3tþ"â@{â%oÝ +@y—Švð-Ç@|_q¹~ð@xf«â6@zÐ=@{ÇËú5´|@|5ü5*’Ž@}6B€³@xÉ(`WºM@{oóÓ9ù@vÍVZ>Î@z©…™b@z.Þ3>µ@yt~áAQë@xT¿U³]•@w%þWóšÙ@yS^¡hó@yh¯ ÍUì@yJ6ÆíW@v¨ i*¾@w´ªÿõÎ@zÉ ÍÏšI@z—?Ú¹[@}7›‰,·@{“Q»@y,9×fT/@{Xv|¥U@zÈíÁrg@x†{:7â¶@yzN/“XÜ@yAwâaF@v¨%86Z3@y‘U¶è`^@x×÷@yí"E[™š@z#= @v>ù‘@zw“ðØ@ycÖQîtH@w¸©ôò{@xÓÙ‹yG@z—‚Ø|@y8µ#æâ@x*m<ˆˆR@wòŒÇÁÒ@xaf5›X³@v\>x³Ì§@vjbˆè† @{$3½­ñ&@y·|8Ç!à@u÷ÎA;6@y¦%_¿þ@y +JsË@uÎà%aâ§@vs*€áOÒ@x('àÖM@u½|RÀ@v Ó¶‰2I@uˆL,|@yáˆìM¿@w(®é mj@w©!b%‹›@wGb$8ø‘@{•ˆôéâk@v]žÛKw¥@xE»T5cÙ@xëÏzuÕ}@y“)Ç?‹€@xý÷CnG@x ³ûX²‚@x qGŽŸÅ@v´låÞˆ@xBò521@v‚âöˆÑ€@w›o3vx@vO¶ZŒ@xë¿Å«§@xpºµ“p@xȆUÚ@w%_רr@wõ»°ÿ&F@{=x’ù@y*B‚"RZ@y¤†) ê@tð=‘fú!@uÿ¾)×f?@|ó€KŒô’@zçõJ¼ÞÀ@wvCta@z+/Ëï-õ@w¯¹À0Z@ttWF@u~vF3¨@|§Tûñ–@uƒÁµ ú@y³#щ€ý@yãßîîò@wUdtJ@xþ™¼£4!@z6Ô¬vS@xfÞ {ð0@zG#q½ð@z%<=ü@zêíxÇß@yŸWú Ë@vôÙ5¹ñ@u’¹ØÄï@wL"NKgp@z[ƒêŒr@vÿÅj¯¸¢@yô\xüøN@y4ç¶|‰ @x–…åw7@u½ßEÁ@wBöd¢^f@vCøüA?@w>%¯øA%@w×ø&0¿·@z_ +sÉl@z£V»¥FÕ@vÈÝjL@´@x9 ‡`Í@u(N‰„%Û@w `y+,Í@xàÔq¶@w*ñ|Î%x@s™³UÆI@v@¤Ÿš$ö@wÐê1N@uw1›á#Ó@vêÿz9‡¼@u)­^‘Àe@z :|©õ@uêuM“J@u‘G¤Û@u~)êës@uøôÃþFª@wê£r ñ5@u~¶sY@v³2KÝYñ@v®©› NÉ@t×CŒHuª@wéÍ_¯v¡@wÃïþ?ê@vnäYÔÉ@x$bk|ç@vÀ:ÇOt@w³V+³­ì@yÏ([UØ@u~†$ô|C@v'Ñ],A¼@vé\ 8ÀÔ@w¥3CG@xA ƒó¸@v;´±Ð?x@tNÞŠDôÝ@tó-žÛ©³@uø=íÖµ@wζ¦úq@v“ALç@x!nô¥Ã@y—[K“dD@w €g/x@xA‘^s1Y@w±BϬéK@vù€X'ƒ@w=•da=`@xxšQ–Ic@tÿ·èWcì@uõGvip@uÊS–ýe@uʄЂ•»@xuC¶m>¡@tý:Ê3{@w=lÉúâ@v家r@w¡í×°ãÈ@z ÑûTn]@x9Ñ\»@tJm¥@u$ÜÛy½³@t~m¤™2@v"´#Äåã@xÛâH˜Yå@yÈý*a¿)@wN…»…×@xðŸEV>ê@uR… ·ç@vÌŒaÒºß@wÉþYq@u+å +Å‹@vÑÕHSG@x ª…f4Å@u͵ælg@wÿ.vWø@v˜Dx³â@uN–„¼ôy@w¬žv' @r‘Ý?£øØ@wW‡<8Æu@v|ë¥û»µ@t."„¯R@wsÝÌ(@wi;lŽ5@tû +ü1@tÍس eÁ@tJÌB³¤X@vÍ3ðD_@vßÉ+÷®í@u^t€@uXw¹Z®@wU~ÙŽ@v³rÊ•r;@xË‘ùvç@xÖFê +Q²@v‰Zú[·J@vhÍÃÝc@wj?¹&ƒ¨@vGx=ÿ0Ó@wCÉ’‡Pù@xjŸå­Úâ@vO úCZ@yWÙÅ|Îù@vó¹ÀÜ÷@vê\J=€T@w87à`@=@tw,ÿf„S@u¶ôP¾ Î@tc»rg‰Ì@s@wö9âÁôV@vêrãçŒ>@xf{,C‘©@w^±¹Ý@wAÌc+ªù@u¤JÍ€¿ @wùñ=ç +@u-¿=`ø@vâIš)W@u¦™gÍ@w©ECóò:@urdíTÄ@vˆ|AŒ:@v²¤ÔfS†@u‰Mèä@uÙº@oÌÆ@wÏ0Ò¹@tÜm 0@x)ýMÅ@w*qvfy@u–ú-34˜@xHÔäæT@x¥7@CÁ2@vmPZ¡J9@v‚É0Zë@wcmãf³@yµ|CqåÃ@v°ûá:fæ@wè.>œË@ta(T#ã@wÅS¬ÈÓ@wô÷ÉQ@vÓBûJÕ@wBJsÿßµ@x_±\"õ@už¨÷hi}@yé·†@v´ò‚êÎ@w|3Òk'ª@u°Ëei@v+¢äS6@v&¶ûÀâ.@vnåw>[@uœàY„ï@væ¦IB˜@z6‘ž‹.@x:4Ì'âÜ@uïËÐY\@t÷àá@â%@z…î‰Þþ&@vkI Qø›@wx“H@wìÑ®X(@y¶[ª5ÿð@vúúªÌÛ@uéIíÇl@zŒ/f@t×Qž·‹l@zŽÔº¦ØŽ@sa{HŽ³¯@u˜téÌ)@v\‘| +Õ@u¤œMër?@vÙŠ÷êgÿ@v‘%­±@vÎLS @vIi{ç @u¯Iÿ­eó@w`h§H@vàd4°…×@yokéS 0@uð±^ôi@s-bÖöò¥@u ,žVý@wŽ6Ñ6Q@wM‹/HÏ/@vëíÏ @x+Òt™m¼@x}j»Ñ®,@v/" f=@xDÎeîH@yÈ:Ð×$€@w]»qõÈ"@vìQñG@y;k³ SØ@v›J¡v¢@zNÖšh£@w­½Øvr@x±t_£@t¦‚ßSoõ@wȇ‹³òÑ@wÆb?3'£@tËÆ„m@x˜ktµ³@v{ž#@wCƒU¸¹@uƒE€Æ@wêîêŠÿ@xUx‘ÿ@t³ O0|@wÿ !…F?@y©„S˜”t@w$5PŒ°†@v*½?t"@t[œ8ÅÜ@uƒ dúþ@uõQ•Ùy¦@zÌçNé@w¿±“ikÑ@wíu”æŒ@tv'/Ž@x6¾¸yä@u™²Îj+@wqò¢Þöƒ@vdª&íÕ³@qÑ£Úˆ @u*þÒW*@u”‘\^»@v”É,? @wd<…ÁÖ@yqLô”rs@ulî3©½@}jûDy@yf=JÆEÖ@yÑýÌG@x8 eœ¼@uæf¯L @x(Z +bã@z}V/ +@wýøÊÉ@vaNzÿ@u›E‚—7²@u<|Õ·þŸ@y2$„…Þ@v{Ùdˆ–@sªíÝç»@x‚]=``$@wÒ®h F@uÔmÑ•T‹@wûQ°^~@x<–7°§+@uá2úã!m@yótÊ@zpB°4“@t0r rü@z¤âïf÷ @wW¨‡ˆÕQ@{ƒIžúª@{röA@y§¹®x’ã@y㺽_ë@~gLÓô…@~f`>2†É@zn±7³Ôâ@usútã~@x°¹\/@vÕ%è@wRÌì‡B4@w Ç{63æ@}!ãÙ5ä@zm––ÙCß@wúOÈ16Ý@vÌ›Yõõn@w"e~[ª±@uºÔv-¿@yMrÊ‚›@x¢>mÀ~@x¹ʬU8@xd>vt£@u¸@º¯i|@zÅ-&6WŠ@vï žw¢@uÍ#ó6@x;;·‚ž@wàš×<ê@y|Ńö« @y¶†•Ê5@x¤ÿ) ›f@y<Šg²ºõ@vùþ@zuùIÖ:'@y¥ñ³¤ã8@zÜ„ mîú@y2Œ*íœQ@x6ØPv@z{Þz–ýy@v­"cAÞ @u¿’ÍuIB@xª€ ôE@x5ÏtÖž@z3‰¦HŽ@yuÒåA4O@vb>5ç<@|‹lIËà#@x` ’YK®@vpˆ”¦ü@ycÂäŒÇ@}I#ê[é@y +µÜ—~Î@x§sÏ:&@wVbdVˆ¯@x°=åu†@z‰JÃ+lÉ@wìËÕÏ=ƒ@z y†h-e@v¢º¯ÜY@zóäôðs@zq‚”Ä7@z¡%›ü%U@wN2Á…@yñÄKP­@zž¾9Â@y1$žªà5@{)™}ÂsÛ@yé¦ Pð@u¢`°é|@y§ ¢pë@~¥Àâ/ê^@{4é`™t9@|¯U ¼¥@z>dp•«@xqèÈ(@zÿŸ1¨d@yQ¡»ÿaØ@| xæ\/i@y·õ +g¯‘@wε +Èšš@zúAïC@zeeÕv@|HƵ!ÖÎ@|ùL_ô$@yk1 sFd@}°`ñÎæÀ@y…ÔÒY„@yzÛÁç8@}êIXÑ@{z˜ëÄ@yy›@ŠIØ@|Í4Öî~@{jceÝÅ4@|¿fÍ@{§ åq±@z§åAVú(@yËÝË‚|<@zy«úÎ9Û@{] s¯!V@~û2—’-Ø@|k5'Í@z˜Lc¹'Ž@|ªhyl +V@y[¨ä^B1@y9ÝÌøÛ@~fN“þw§@{¤Ê“îC@z­vÕ.¿q@xþnžv@zS/¬§w@}²˜²»«@z86oÍœ†@y{¸Àïq×@}M±,y4U@|%ÉJaõ@}eŠ3IO@xÏRBqêG@{õËý–èh@{Ã?^ð.²@|tºBã@{ï pÍÖ@z|[ÇËÚ@z +‘(ÅÚS@yCÊwÄ3@|³˜ªšó@|tóøPÙ@z”<AH@z ÎÃK@xv„Wš@x MR³#@zö4¨%HŒ@ lùÏ@{½|±Å¥¯@}‹iÌq@|xsµZ„@zÁ×WìÎ@|Ó"ÑÂH±@%¥xcŠæ@‡07÷@{2³¢xé@xtÍ€ÀC@}ÌMêp„ÿ@yÀT§„öä@{odìu@~´=81÷n@|\@"ã›ê@}(kºî@~~Üš5í@|tA"¼ ô@{†¹˜¾C@{#ÉØF@~ãs´zŠ@€•8§o¿@€ÚÃØ+X@yRá¡v@€¯0»žÿ@~ã P‡@€ Ÿ½m@[Cí:ô@Býo,®­@€6³“+‡@€µ&Ó†+@Ö +·'¯@ Ù˜),é@€Úû×Rû@}€…Ý„@€Y¥ØPó!@€‘$‚X@€øg³@\°Ï±^à@€Ñ»Üš@‚‰ÛÆìN@}üÞ¿ÌR@€ÿFø å@€ÜûåcŒm@~.¸Å˜µø@¨«µéX@€>¬d'd@‚°U=¡ •@yçŠ[pÞ@€a-Jöµ²@¡èƒ°_@½P­Òz@ѹïIù@ªÃÜÚ{@€´Q‚°g@„MŽ#“Å8@~÷Ó¨”+Ò@d¿S"¨¡@€azý*wG@€Ê±6õ²@¥‡Us¸@}¾“UõD±@z‹¿‚%@€ÏÏߌxX@‚4<–—Á@‚S¨{ø æ@‚¤q Îÿß@*¥ â@œbà%¡p@|ëpï@‚|:ÍnF@€ÂM+Ù¶@=I ;@;¨4î~â@ƒ ™i B@€¤¥˜'i @`te„H@ƒ£…#·Ü@‚Â'KÑu@‚ #Ö`Ó@ƒ1Vç„@‚(:^°@1@‚µ©-½—‡@ãÉ’B˜Ø@‚ñQHµ–@„SrÑhÅ°@„*Gº¢¡œ@ƒL0kb@„†j¡üñ@‚ pˆV³‚ \ No newline at end of file diff --git a/goodman_pipeline/data/ref_comp/goodman_comp_2100_650_GG455_HgNeAr.fits b/goodman_pipeline/data/ref_comp/goodman_comp_2100_650_GG455_HgNeAr.fits new file mode 100644 index 00000000..f18d15ab --- /dev/null +++ b/goodman_pipeline/data/ref_comp/goodman_comp_2100_650_GG455_HgNeAr.fits @@ -0,0 +1,100 @@ +SIMPLE = T / conforms to FITS standard BITPIX = -64 / array data type NAXIS = 1 / number of array dimensions NAXIS1 = 4060 ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator IRAF-TLM= '2024-05-08T18:56:30' / Time of last modification OBJECT = 'Haro_5-2_A' / Name of the object observed DATE-OBS= '2021-10-21T06:06:55.840' / ISO-8601 time of observation DATE = '2024-05-08T18:53:49' / Date Format is YYYY-MM-DD TIME = '06:06:55.840 to 06:06:56.349' / ~ Start & Stop of Exposure N_PRM0 = 59 / Status PG0_0 = 24624 / Camera Flags, PG0_1 = 895078.436 / Camera Up Time, HH:MM:SS.sss PG0_2 = 0 / Continuous Clear Mode, PG0_3 = 4.476 / Frame Clear Time, HH:MM:SS.sss PG0_4 = 4.476 / CCD Read Time, HH:MM:SS.sss PG0_5 = 5.976 / Cryo Current, Amps PG0_6 = 0.000 / Cryo Delay, HH:MM:SS.sss PG0_7 = 19071 / PS Flags, PG0_8 = 39 / PS Pressure 1, PSI PG0_9 = 252 / PS Pressure 2, PSI PG0_10 = 27.6 / PS CPU Temp, C PG0_11 = 39 / PS Fan 0 Speed, Hz PG0_12 = 40 / PS Fan 1 Speed, Hz PG0_13 = 40 / PS Fan 2 Speed, Hz PG0_14 = 41 / PS Fan 3 Speed, Hz PG0_15 = 33.9 / PS Case Temp, C PG0_16 = 3.292 / PS 3.3V Supply, Volts PG0_17 = 11.983 / PS 12V Supply, Volts PG0_18 = 11.767 / PS Sensor Supply, Volts PG0_19 = 23.997 / PS 24V Supply, Volts PG0_20 = 49.587 / PS 48V Supply, Volts PG0_21 = -13.168 / Camera Switched -13V, Volts PG0_22 = -6.624 / Camera Switched -6V, Volts PG0_23 = 6.615 / Camera Switched 5V, Volts PG0_24 = 5.717 / Camera Switched 6V, Volts PG0_25 = 13.416 / Camera Switched 13V, Volts PG0_26 = 24.570 / Camera Switched 24V, Volts PG0_27 = 31.494 / Camera Switched 28V, Volts PG0_28 = 2147483647 / PS Software Rel., PG0_29 = 10.174 / SW+, Volts PG0_30 = -0.002 / SW-, Volts PG0_31 = 12.045 / R+, Volts PG0_32 = 0.020 / R-, Volts PG0_33 = 29.522 / VDD, Volts PG0_34 = 17.015 / VRD, Volts PG0_35 = 2.267 / VLG1, Volts PG0_36 = 0.994 / VLG2, Volts PG0_37 = -13.093 / -13V, Volts PG0_38 = -6.307 / -6V, Volts PG0_39 = -4.870 / -5V Pre., Volts PG0_40 = 1.212 / 1.2V, Volts PG0_41 = 3.275 / 3.3V, Volts PG0_42 = 3.290 / VREF_P, Volts PG0_43 = 5.157 / +5V, Volts PG0_44 = 5.029 / +5V Pre., Volts PG0_45 = 6.166 / +6V, Volts PG0_46 = 13.368 / +13V, Volts PG0_47 = 23.856 / +24V, Volts PG0_48 = 31.493 / +28V, Volts PG0_49 = 0.000 / Chamber Pressure, Torr PG0_50 = 40.5 / CPU Temperature, C PG0_51 = -125.0 / Cold End Temperature, C PG0_52 = -100.0 / CCD 0 CCD Temp., C PG0_53 = 23.702 / CCD Heater+, Volts PG0_54 = 0.282 / CCD Heater Current, Amps PG0_55 = 23.648 / Cold End Heater+, Volts PG0_56 = 0.194 / Cold End Heater Current, Amps PG0_57 = 23.916 / Window Heater+, Volts PG0_58 = 0 / HKS Flags, N_PRM1 = 10 / Miscellaneous PG1_0 = 750 / Tested Speeds, (Not Defined) PG1_1 = 'e2V CCD231-84-1-F21' / CCD Model PG1_2 = '10282-14-01' / CCD 0 SN PG1_3 = 8811 / Hardware Part Number, PG1_4 = 0 / Hardware Revision, PG1_5 = -69.8 / Operational Temp, C PG1_6 = 0.150 / Operational Pressure, Torr PG1_7 = 41.120000 / Image Area Size X, mm PG1_8 = 40.960000 / Image Area Size Y, mm PG1_9 = 2.000 / Frame Clear Time, Sec. N_PRM2 = 25 / Setup PG2_0 = 0.500 / Exposure Time, Sec. PG2_1 = 1000 / TDI Delay, us PG2_2 = 0 / Acquisition mode, (Video) PG2_3 = 2 / Trigger Mode, (Timed Exposure After Trig.) PG2_4 = 0 / Initial Clear Mode, (Enable) PG2_5 = 0 / Initial Cooler State, (Off) PG2_6 = 0 / Initial Window Heat, PG2_7 = -100.0 / CCD Temperature Setpoint, C PG2_8 = -125.0 / Cold End Temperature Setpoint, C PG2_9 = -125.0 / Low Flow Begins, C PG2_10 = -124.0 / Low Flow Ends, C PG2_11 = 3 / Trigger 1 In Setup, (CMOS (Pull-Down Inverted))PG2_12 = 1 / Trigger 1 Out Setup, (CMOS) PG2_13 = 0 / Trigger 1 Out Selector, (Shutter) PG2_14 = 3 / Trigger 2 In Setup, (CMOS (Pull-Down Inverted))PG2_15 = 0 / Trigger 2 Out Setup, (Opto) PG2_16 = 2 / Trigger 2 Out Selector, (Frame) PG2_17 = 0 / Trigger Input Select, (1) PG2_18 = 0.000 / Shutter Open Delay, Sec. PG2_19 = 0.500 / Shutter Close Delay, Sec. PG2_20 = 10 / Parallel Shift Delay, us PG2_21 = 1 / Parallel Clear Count, PG2_22 = 2048 / Serial Clear Count, PG2_23 = 0 / Pre-exposure pulse select, (Short) PG2_24 = 1 / LED Operation, (Enable) N_PRM3 = 1 / Mode Mask PG3_0 = 127 / Mode Mask, N_PRM4 = 59 / Factory PG4_0 = 1110 / Instrument Model, PG4_1 = 111 / Instrument SN, PG4_2 = 50 / Line Strobe Fall Delay, 10 ns PG4_3 = 0 / Pixel Pipeline, PG4_4 = 0 / VDD off, (Disable) PG4_5 = 0 / Clear During Exposure, (Disable) PG4_6 = 0.100 / Long Pre-exposure pulse duration, Sec. PG4_7 = 0.001 / Pre-exposure pulse duration, Sec. PG4_8 = 0 / Pre-exposure pulse enable, (Disable) PG4_9 = 0 / Camera De-interlace, (Software) PG4_10 = 0 / SREG 0 De-interlace Position, PG4_11 = 1 / SREG 1 De-interlace Position, PG4_12 = 0 / CCD Type, (Full-Frame) PG4_13 = 4 / Parallel Clock Regions, PG4_14 = 1 / ADC Clock Divisor, (1) PG4_15 = 0 / Analog Chain Delay, ADC Clocks PG4_16 = 0 / Parallel Pre-Masked, PG4_17 = 4112 / Parallel Active Pix., PG4_18 = 0 / Parallel Post-Masked, PG4_19 = 0 / Parallel Region 0 Masked, (False) PG4_20 = 2 / Parallel Region 0 Shift Cap., (Both) PG4_21 = 0 / Parallel Region 1 Masked, (False) PG4_22 = 2 / Parallel Region 1 Shift Cap., (Both) PG4_23 = 0 / Parallel Region 2 Masked, (False) PG4_24 = 2 / Parallel Region 2 Shift Cap., (Both) PG4_25 = 0 / Parallel Region 3 Masked, (False) PG4_26 = 2 / Parallel Region 3 Shift Cap., (Both) PG4_27 = 1 / Two Serial Registers, (True) PG4_28 = 50 / Serial Pre-Masked, PG4_29 = 4096 / Serial Active Pix., PG4_30 = 0 / Serial Post-Masked, PG4_31 = 4 / Installed Ports, (4) PG4_32 = 5.100 / PS +5V Setting, Volts PG4_33 = 6.000 / PS +6V Setting, Volts PG4_34 = -6.300 / PS -6V Setting, Volts PG4_35 = 13.200 / PS +13V Setting, Volts PG4_36 = 24.500 / PS +24V Setting, Volts PG4_37 = 31.500 / PS +28V Setting, Volts PG4_38 = -13.000 / PS -13V Setting, Volts PG4_39 = 0 / Port 0 Connect, (SR0) PG4_40 = 0 / Port 0 Map, (ADC0) PG4_41 = 0 / Port 0 Shift Direction, (Same) PG4_42 = 1018 / CCD 0 Port 0 Gain Correction, PG4_43 = 1024 / CCD 0 Port 0 HG Correction, PG4_44 = 0 / Port 1 Connect, (SR0) PG4_45 = 1 / Port 1 Map, (ADC1) PG4_46 = 1 / Port 1 Shift Direction, (Reverse) PG4_47 = 1024 / CCD 0 Port 1 Gain Correction, PG4_48 = 1024 / CCD 0 Port 1 HG Correction, PG4_49 = 1 / Port 2 Connect, (SR1) PG4_50 = 2 / Port 2 Map, (ADC2) PG4_51 = 0 / Port 2 Shift Direction, (Same) PG4_52 = 1055 / CCD 0 Port 2 Gain Correction, PG4_53 = 1055 / CCD 0 Port 2 HG Correction, PG4_54 = 1 / Port 3 Connect, (SR1) PG4_55 = 3 / Port 3 Map, (ADC3) PG4_56 = 1 / Port 3 Shift Direction, (Reverse) PG4_57 = 1038 / CCD 0 Port 3 Gain Correction, PG4_58 = 1024 / CCD 0 Port 3 HG Correction, N_PRM5 = 22 / Control PG5_0 = 16 / Bits Per Pixel, (16) PG5_1 = 113 / DSI Sample Time, PG5_2 = 1 / Gain, (High) PG5_3 = 1 / Port Select, (Port 0) PG5_4 = 2 / Parallel Binning, PG5_5 = 948 / Parallel Length, PG5_6 = 980 / Parallel Origin, PG5_7 = 0 / Parallel Phasing, (SR0) PG5_8 = 0 / Parallel Post Scan, PG5_9 = 1 / Serial Binning, PG5_10 = 4142 / Serial Length, PG5_11 = 0 / Serial Origin, PG5_12 = 0 / Serial Phasing, (Normal) PG5_13 = 0 / Serial Post Scan, PG5_14 = -1.691 / CCD 0 Port 0 ADC Offset, Volts PG5_15 = 1993 / CCD 0 Port 0 Correlation Bias, PG5_16 = -1.709 / CCD 0 Port 1 ADC Offset, Volts PG5_17 = 2015 / CCD 0 Port 1 Correlation Bias, PG5_18 = -1.709 / CCD 0 Port 2 ADC Offset, Volts PG5_19 = 2017 / CCD 0 Port 2 Correlation Bias, PG5_20 = -1.702 / CCD 0 Port 3 ADC Offset, Volts PG5_21 = 1984 / CCD 0 Port 3 Correlation Bias, N_PRM6 = 1 / Operational PG6_0 = 10 / Mode, TELESCOP= 'SOAR 4.1m' INSTRUME= 'ghts_red' NOTES = '' INSTCONF= 'Red' CAMSWV = 'Beta 29' CONSWV = '8359,10' INSTSWV = 'GSCS 2.0.17.6.6-Alpha' CAMERA = 'Si 1110-111' RA = '05:35:06.841' / right ascension [hh:mm:ss.sss] DEC = '-02:48:33.492' / declination [dd:mm:ss.sss] AIRMASS = 1.350000 / airmass at approx. start of exposure UT = '06:06:53.873' / time at approx. start of exposure [UTC] FOCUS = -1279.700000 / SOAR telescope focus MOUNT_AZ= 55.468354 / SOAR mount azimuth MOUNT_EL= 47.953000 / SOAR mount elevation ROTATOR = 38.000000 / Nasymth cage rotator angle [deg] POSANGLE= 214.700000 / position angle [deg. E of N] SEEING = 0.954000 / DIMM seeing [arcsec] LST = '03:23:37.876' / Local Sidereal Time [hh:mm:ss.sss] OBSRA = '05:35:06.835' / target right ascension [hh:mm:ss.sss] OBSDEC = '-2:48:33.638' / target declination [hh:mm:ss.sss] DOME_AZ = 56.249099 / SOAR Dome Shutter azimuth HA = '-02:11:29.967' / Hour Angle [hh:mm:ss.sss] ADCSTAT = 'IN' / ADC Status ADCPOS = 'DONE_42.441' / ADC Position LAMP_HGA= 'TRUE' / Hg(Ar) LAMP_NE = 'TRUE' / Neon LAMP_AR = 'FALSE' / Argon LAMP_FE = 'FALSE' / Iron LAMP_CU = 'FALSE' / Copper LAMP_QUA= 'FALSE' / Quartz LAMP_QPE= 0.000000 / Quartz Percent LAMP_BUL= 'FALSE' / Bulb LAMP_DOM= 'FALSE' / Dome LAMP_DPE= 0.000000 / Dome Percent CAM_ANG = 86.080156 / camera angle [deg] GRT_ANG = 43.039500 / grating angle [deg] CAM_TARG= 86.080000 / camera target [deg] GRT_TARG= 43.040000 / grating target [deg] CAM_FOC = -943 / camera focus COLL_FOC= 1001 / collimator focus FILTER = 'NO_FILTER' / primary filter wheel FILTER2 = 'GG455' / secondary filter wheel GRATING = '2100_SYZY' / VPH grating [1/mm] SLIT = '0.45_LONG_SLIT' / slit [arcsec] COL_TEMP= 9.281250 / coll ext temp (deg C) CAM_TEMP= 12.906250 / cam ext temp (deg C) WAVMODE = '2100_650' / Wavelength EXPTIME = 0.500000 / integration time RDNOISE = 3.890000 / CCD readnoise [e-] GAIN = 1.480000 / CCD gain [e-/ADU] OBSTYPE = 'ARC' / observation type OBSERVER= 'Cesar Briceno' PROPOSAL= '' EQUINOX = 2000.000000 / equinox of coordinates ROI = 'CUST_0_4142_980_948_1x2' ENVWIN = 26.721360 / Wind Speed [km/hr] at start of exposure ENVPRE = 736.850000 / Atmospheric Pressure [hPS] at start of exposur ENVDIR = 318.300000 / Wind Direction at start of exposure ENVTEM = 7.725000 / Outside Temperature [C] at start of exposure ENVHUM = 22.800000 / Relative Humidity at start of exposure DISPAXIS= 1 DETSIZE = '[1:4096,1:4096]' TRIMSEC = '[1:4142,1:1896]' CCDSIZE = '[1:4096,1:4096]' CCDSUM = '1 2' BUNIT = 'adu ' OBSID = CONFID = REQNUM = PROPID = 'GHTS_ENGINEERING' SITEID = 'sor' TELID = '4m0a' RLEVEL = 0 BLKUID = 1634792943495716 L1PUBDAT= '2023-04-21T06:06:55.295219Z' IMAGEID = '81108' HEADERVE= '1.0.1' OPENTIME= '06:06:55.9242486' / GPS-Synched Time for Shutter Open (UT) OPENDATE= '2021-10-21' / GPS-Synched Time for Shutter Open (UT) CLOSETIM= '06:06:56.7576303' / This is the GPS-Synched Time for Shutter Closi -- Goodman Spectroscopic Pipeline -- GSP_VERS= '1.3.7 ' / Goodman Spectroscopic Pipeline Version GSP_ONAM= '0382_Haro_5-2_componet_A_21-10-2021_comp.fits' / Original file name GSP_PNAM= 'cfzst_0382_Haro_5-2_componet_A_21-10-2021_comp_390.16-406.97.fits' / GSP_FNAM= 'goodman_comp_2100_650_GG455_NeHgAr.fits' GSP_PATH= '' / Location at moment of reduce GSP_TECH= 'Spectroscopy' / Observing technique GSP_DATE= '2024-05-08' / Processing date GSP_OVER= 'none ' / Overscan region GSP_TRIM= '[51:4110,2:948]' / Trim section from TRIMSEC GSP_SLIT= '[1:4060,67:826]' / Slit trim section, slit illuminated area only. GSP_BIAS= 'master_bias_RED_SP_1x2_R03.89_G01.48.fits' / Master bias image GSP_FLAT= 'norm_master_flat_2100Custom_6500nm_GG455_0.45_dome.fits' / Master flaGSP_NORM= 'simple ' / Flat normalization method GSP_COSM= 'none ' / Cosmic ray rejection method GSP_TMOD= 'Polynomial1D' / Model name used to fit trace GSP_TORD= 2 / Degree of the model used to fit target trace GSP_TC00= 398.56581606865825 / Parameter c0 GSP_TC01= -0.00191583338475523 / Parameter c1 GSP_TC02= -3.1950842919327E-08 / Parameter c2 GSP_TERR= 0.3496584714239631 / RMS error of target trace GSP_EXTR= '390.16:406.97' / Extraction window at first column GSP_BKG1= 'none ' / First background extraction zone GSP_BKG2= 'none ' / Second background extraction zone GSP_WRMS= 'none ' / Wavelength solution RMS Error GSP_WPOI= 'none ' / Number of points used to calculate wavelength sGSP_WREJ= 'none ' / Number of points rejected GSP_FUNC= 'Chebyshev1D' / Mathematical model of non-linearized data GSP_ORDR= 3 / Mathematical model order GSP_NPIX= 4060 / Number of Pixels GSP_C000= 6134.757573933042 / Value of parameter c0 GSP_C001= 0.14942319843612395 / Value of parameter c1 GSP_C002= -9.1903523823550E-07 / Value of parameter c2 GSP_C003= -2.7530156141553E-11 / Value of parameter c3 GSP_P001= 55.54 / Line location in pixel value GSP_P002= 193.47 / Line location in pixel value GSP_P003= 556.31 / Line location in pixel value GSP_P004= 892.0 / Line location in pixel value GSP_P005= 1155.49 / Line location in pixel value GSP_P006= 1360.89 / Line location in pixel value GSP_P007= 1700.4 / Line location in pixel value GSP_P008= 1836.11 / Line location in pixel value GSP_P009= 2582.82 / Line location in pixel value GSP_P010= 2774.92 / Line location in pixel value GSP_P011= 3263.22 / Line location in pixel value GSP_P012= 3863.53 / Line location in pixel value GSP_A001= 6143.0626 / Line location in angstrom value GSP_A002= 6163.5939 / Line location in angstrom value GSP_A003= 6217.2812 / Line location in angstrom value GSP_A004= 6266.495 / Line location in angstrom value GSP_A005= 6304.7889 / Line location in angstrom value GSP_A006= 6334.4278 / Line location in angstrom value GSP_A007= 6382.9917 / Line location in angstrom value GSP_A008= 6402.248 / Line location in angstrom value GSP_A009= 6506.5281 / Line location in angstrom value GSP_A010= 6532.8822 / Line location in angstrom value GSP_A011= 6598.9529 / Line location in angstrom value GSP_A012= 6678.2762 / Line location in angstrom value BANDID1 = 'spectrum - background none, weights none, clean no' APNUM1 = '1 1 390.16 406.97' / Aperture in first column WCSDIM = 1 CD1_1 = 1 LTM1_1 = 1 WAT0_001= 'system=equispec' WAT1_001= 'wtype=linear label=Wavelength units=angstroms' DC-FLAG = 0 DCLOG1 = 'REFSPEC1 = non set' GSP_SCTR= 'ecfzst_0381_Haro_5-2_componet_A_21-10-2021.fits' / Science target fil -- GSP END -- COMMENT Light, Exp Time= 00.500, Saved as: 0382_Haro_5-2_componet_A_21-10-2021_COMMENT comp.fits REFSPEC1= 'ecfzst_0382_Haro_5-2_componet_A_21-10-2021_comp_390.16-406.97_copy' WCSAXES = 1 / Number of coordinate axes CRPIX1 = 1.0 / Pixel coordinate of reference point CDELT1 = 1.0 / Coordinate increment at reference point CTYPE1 = 'LINEAR' / Coordinate type code CRVAL1 = 1.0 / Coordinate value at reference point LATPOLE = 90.0 / [deg] Native latitude of celestial pole MJDREF = 0.0 / [d] MJD of fiducial time MJD-OBS = 59508.254812963 / [d] MJD of observation END @šõ‹®à[X@›·¿7tó¸@›æÜ%›@Ÿ0°aŸ@ž¬5‰Ðá³@Ÿ~û‘ó¯ƒ@ < ¹ãÀ°@ ª +³ê°…@ À½õV3]@¡i`³à@¢J&Ã[@¢²:òï‘?@¢Î}+VKË@£w:¨q<@¥+•ç‡)@¦ uø†±@¦C6Ökì@¥À®Æ­*™@¦ùÒ72)¯@§—þó{¤@¨·­ÎùcV@©¬ÞW+;@«pes;ž@«‡•§˜@­i™N±J@®‚nÎp¡@¯¼ïš½ú@°Aä2ù@±ß~Ð@²puCñ ^@³Øç5;=@´ÿç«t £@µ#ü xs@µ²AÅk™f@¶ð®ßÃv@·Ïkò @¸È*AM¬²@ºµ™iôØ@ºYú¸ØÊ;@»×êÁåmô@¾$¬tùÕß@¾t»¼DÐÏ@¾x9Ô1@½gˆmÒeÖ@¾À²ûmØ@¿ùFÃÈLH@À0{Ÿ±©@Á)»*)=ˆ@§FFÎ@Ä8ò”qï@Æ (^4¡(@Έµ«t7@åœ×´Å~Aó'ÝØA¢\€†`NAÁM8—¢A­lyfÅ@âþ-XªT@ËÆ|tÕÎõ@ÄÎ'hÙ/z@ÁñbQA@¿K:Í«@½Õu°5¯@»Pw|C€@º-b1E/ž@¹| »ã3@·Ò^‡ññý@·71¢þ©@¶•²ÿb×¢@¶$y¢ û@´þ»¢þ *@´H{GÄ +?@³V +iOî…@²§¿¤"å\@²Eï+úñ‹@±]dìÃÄ8@±W&IN@°†jÌOJë@°Y~´“W"@®¦gt±„½@­Qì·&&É@­.ûÞž5@¬ŒŒf]«¯@¬{.zo:;@ªzm†•Ôr@¨˜m†IŒ@¨¿¼¹Š¦¦@¦·m‡l;û@§Y¨Øâ~Ë@¦«‘jO@¥ºç1€ @¥‡F Úû@¤·žlb¹@£hýH@¢ê]õQ˜@¢°@! Ö—@¢´3ó%@¢'d·,/á@¡oÝÛ ¼@ øz'Ëe@ 1e tV¸@Ÿ×ã):@ x(¸‹G@ O¨ÍF°¸@žZ»ç²×f@žE»ñë‡E@œsY–¶uÚ@œ:ÿ]c}$@ Œ—U`ÿ@œ )ÁcÆ@š„>ÝÜ&@™™üCÜ@™TÆ™Jã@šõøͧ\@™)V`‰V@˜Œ%~ý„“@™ˆýÚ \@˜Åä¯v@˜˜°¶“@˜œ2·°@—a¬þ @˜$ÎÉ5¢@–©²çF°T@–ÞTÖ}«@–H%Õ%^@”ðŽ~$yÑ@–êË +ÿöf@•ð²zÂ7@–M‡ù{@”;÷¿ZE\@”ÎƳÆÀÚ@•kyˆ¾ð @“˪M +Á?@”7A,q…@–&€ò¢¹$@•E§æW^—@•¶\–Ÿ‘‰@•4uò@«%@•/@e;@•ê†}§‹@•‡>!ãP@”‚¬¸8Ñ@•êÑ{ Rw@•JJoÿÕ—@•Óÿõ8™@•ÛqðKf@•åzÖ$@•þS½Ïº²@•„Õ»Ó@—JÛûuâ@–TŠÄ»a@—¦šÑMY@—L΄l@—^UkŸhØ@˜OùǶo@˜>•­w@˜ÿÚ´Ò@˜˜lGÜ@™<ß)y@™n}\¨å@šÙýÇ›¦@œF +Ë@œ®þ“«@œïzµÁ£Š@žoûع‡@žHGÖ¥³b@žÓÊõ«V@ ¥ðVcZë@¡7û™–\@¡bÝë²'ã@¢‹¸(Ô@¢<–ü™àŠ@£æÁÿã+@£Ç¤,…@¤›‡s Ô-@¥.$ç’I@¥°ç¡®»«@§›n´øj@§½§YÚ@¨xØ„hÙS@¦üíš=tj@§¯øåEIq@¨D.bÜ @¨‡G™Šz@©ÙB¶#·,@ª»Çœ 9@¬·Z;qF@­Ñ*WØg]@°–󸽊_@· 1~ƒ@нLã´ÑŠ@îC›:è@üö”È‹'@ü0g*ƒŒ–@íª@—Ï´ŸŠ@•Ö AF¿C@”fŠ÷’Wk@“Òÿ][åÅ@“uáë-'„@’Û™õÖ“@’{DkÀÞ @‘£SŒ£@‘«‹.טÖ@‘U "ÂÚ@ )f®Çå@gP7v€<@Ž9'BiØx@Žã_¡’®¼@ ÃÆJž@ŽÊßí³ú(@Ž¥%àäº@û³)£Š@Šþöndm~@Œ c¯"A@Š™ÑBžÏú@Š¯?ŸÙK@‰w˜h•Æ•@ˆkY=5Ì@ˆ×[Öâž@ˆv<^b5j@Š7‚dx@ˆÄBPlÏ£@‡×DÏûm@…ó?1sÓ@†å½Äß@†Éššüt@…Û™ðeà@†ÂŽK@‡??ÖãoÞ@†Y<¸þ[@†$ø%ñò@„¾°’n£@…J}ÍlÏ@„$î c@‚$#ãdzœ@‚‰>Ôí#@˜<·à5V@ƒ§÷Z%¯ó@ƒ¯ep{.@…MÑ{ƒv@‡$„ 8iª@…gWÈÚZ°@„\YÅ¡“Y@„+ê×C@„«_=ï@Äб½@€EÌ2nó@€_=Ô—!ù@€áDaÒMO@€#uötQ@€ëä}1£@§È•tóî@Okŵ@©×Y n@€"_TR@~³¸±eÁ¸@¬#q²¤@~«tÅ\b@~ÇPÃ˸É@~ËâFö;…@l'€Dê®@{ØÎóˆÓ@|ml€ÝcÀ@z#õt/¢@}pŠ;ß²@} Çu ö·@|TPÏ}A@}ÖJñv}î@{pWÂU@yX‹ Ж@{ÈÚ§ïÄ@{P©{¤@zIú´)¯@y¦hºÃ@zöÕAJ°@zp4±¹ªÊ@xÉlzäîS@zHÇ5ì‰@z,œ“Dv^@xßÜÓºó @yÜ07@xEâø}™@wŸ`îè³Í@y²zà7@zx÷´¦@yÛ¼}'x@{yóëv +Ô@}{|DïK@…E>4á@‹*4„>ú@Œ)®ÄÍÌ@ƒcT–>y_@~7šTËjá@z!^b®@v˜CJçò@v¶óСB@uåóá+.@wR¾2Ró@vËŽÄ<¢@yM`”ç=è@x±¯š9‰Â@wkç)¤®@wEŒáÿ@v¢#€Ñ @wËÔ!×…@ué;2-@v/åÖÒ¯œ@wRyÕ2iº@ub÷Ñ@vþòðM@w u6á?@vÌ+ÞCsK@vö 4^!@vH»¢X$Ë@wLÑËá£6@vPŸÿK@u?{¨l.ê@tÞÕ-?¦¢@u=÷{ÛÖª@t˚çÅ@wÒ%Ž!@siÜ^[#U@w&ÁZI9C@uAˆ^Nç@w:LõGEe@te‡âñØ@uEb ï@vZs0¼"–@u¥!’Y¿è@t$÷“ù_@uÓ¤0¿@s¾âñĹ@u\½‹@¼¼@uéÉŠÁ@uÿ0ùEé@weõÙù ¥@{éS.h'^@x-‚ajÌž@tÏÙwò °@t=䂾²„@v9Õ5)Ì@tU ?Y³ÿ@t… Ž³Ý3@vºU¬Ï¸a@qm™®õ¶-@tÎâ´Óu²@sý=˜ëf{@t1P €mÉ@uúº,²@vhØÃúú@sì¯ò'i@uëÙÿá¦w@tOµ&ö}g@t1ß³/{–@s@=6@tâ®*™@sÒ!R½UŽ@u"¸)yg¿@s¦[ÿ<Á7@ti¦ùZn@s´GD›n@uI¬1ªÑ!@t£Ïr[Ñ@uÑ|þ¾=…@s២a\@voìDGÊ@uªÝ\ø¨+@sû|¸@sÒ-ËJ@sš|Ͼt@r ®«º^@tœý3¹!Ì@sg×L +æ™@t¤«z‡qU@rÇ¥Ì@twË­ fâ@tÒ¢ §Q@uG°£šà.@rônOÁƒù@tTan¢í@r‚cÒ +™À@u–³Ð.»o@u! &è—™@tÏûa•Aƒ@rÌ:á@|Òç‡@|³¬“¹ïç@|š*öÎ: @|HïûbŽ¸@|¦`KßÄ4@£îÁÌ@X@€ôu{Y½þ@‚ÜK„Ê;é@€øÖ¹ý¢‡@KH?µe+@×]g‡¨Æ@}ÜýLð&‰@|ácCÐ ð@€pV°ÙŽ.@€yþsv‚—@€Ü5¶Ïsd@;Ça#“H@€ú]IcŸk@úZÖ=~@ÈŒ@‚”b7¾ã@‚&i},8@‚€_å¶í@Õ«øcÿ@„¹2êsB@‚ä“ÝXK@ƒåö‹¾Ú@„¶œaw„ò@„Í&e9@„x›áÅ\H@…%®eóÓ@†Ì: +]¤Í@†\Æn»l@‡˜˜ÕȱŠ@ˆI´2¡D@‡ W´z×@‰SÈØÎù@ˆ¸˜3Ø÷·@‰ñW"Üä@Š>Ú³@Ð @‹ÁL…I£@ŒÖ,Àq¦@ßüv÷‚s@ŽÎyÍ|Ù@ŽEç°3@âêÙbij@,«pÄ@JjÛn@‘½ÚÒ¸@’RøN͹W@‘ýŠþO' @”{»ÿ?u}@”òµíØ°è@•˜ìT‘Æ#@–$|Ö¢æ@–QôHÊlÒ@—p/Ç>§ß@—¨;H‡Òq@™s “3}p@™áüRªù@œ¶îy@ hq{Œ@¡Xâ'Šy@¡`Pd5@ ª!Çl “@ 6´Bé9ç@¡]…,ø:…@¢OÍiE @¢…YÛm“E@£¶,ñBãÜ@¤‡õoÛã3@¤øQØs .@¥n(˜5­à@¤4ÕVèy¦@¤M°ÑÒ@¥KF€mï@¦`+jœ-@¨QG~—Åð@§²Ðò¢ì@©±ÒWœ§/@«µn!…@¯é ~j%@¸K2ÎpF=@Ô01(¡ Î@ñDÆsÏ@ý0꣄~ê@ú8 û GP@çÒÎä€fK@Âß1œ*òÄ@±2_@«nÈ8Ɔ @¨¦6L­@¥”ì/Ö4Ê@¤MÚ‰Êë¼@£{_„k<®@¢‡m±¬q^@¡g¤âô,Õ@ ÐCý|_Z@Ÿ†» š¿*@ŸSBÃÅ @Ÿ)²lð@ž-‹gf³@ùD6S)‡@œ*Îoü¯è@›¥u¶ï@™–úó´×S@™“€&Ü@˜!>Ï5ئ@—í?®nl>@—Ÿá D@–ý/$Ï@”ÙÖçyÝ.@•ÞPì 8Q@•¢Ó)ø¦@•QÑR*@”ÇAš'…ð@“É,tÇY@’j^Ò’@‘!âNT/@‘¾Ô7½ä @å¿Üb@¾]Ëuó@ƒQ8Ø¥@ó÷ö'@Ž$Þ˲Î@ÉÎ~J©@@ë2'– @‹¶ä¨Öµê@‹Ü©É[ä@Š‚¹*‰ @‰=PYÔÝB@Š•§ºN:@ˆ±Ô«Ëe@ˆõÔžÏç@ˆãù$E3@‡ õ«Ó†×@…ív²I)›@…L:Ѹ´æ@„éÅÇñAÌ@…·…òµa/@†X¿¿ïMo@„lú§‚@†©Ô]×…}@ƒÇpC›Rh@ƒÜù% C@ƒ..[Ù½@ƒã.\˜Ÿ­@„—Œv4“@ƒ›|E£@ƒAüŠ1Ñ@‚À¡å§çY@‚¸€{¤Æ@‚®†•…ªå@€‘î |…@}uŽÖlþ@€c›.V¸¦@~Ðö?oü @ik¬ïì×@1R¥W@€_ -¤Å@9ƒ6.a@±%#ð²@}¶~»œô@~ ü<§&@}œä„"6Z@~ \ôv.[@}ˆ„Ó +ÁF@|:.å¤c@Óø2½’Y@|%ú.iœ)@}eÏ1}ø/@|ë]¬bú1@|ÂvA°º@|t’>£ @{7 žžÖ@y(\/ýÇ@|0­K“u@z!ýšØè‚@{–†—4„@|‘|bq8Ä@zƉš)¶Ý@x©kK#4@wì‘Û·Ø@{¯á;N§@wää"ÇÔ6@x½‡Žìß@yk ôˆ@xÍo|¤@y’3>çn@x¶ÜÚŸH@wšŒà ©@x2$ŒÛˆ@xµG}¤k@x S=ò‚Á@wƒ$þ@u–ä“c!@vj ëÞ ³@wœ ÛÅé*@wMÜ>OÁ@v£êW–&@v”°ã;7Ñ@u¡ÝŽc@v°§8øÀv@y_gcEì@y€ìß“@v~ †ý¾@wÃÓN”¼@xмò”w@tðÓðEh@t`4Xû9@vqBÿ ‚@t´EáæïG@uËk÷ý^ê@vÙæ—Zn@v&E r;¬@uoIpQÇ@v‰…@v‡@K#`!@týû Þô6@xñú]Š@u|ÒLU@vb/òæM@u¢³‡$Ø(@tÎí¢Š @w6ݤA¨™@usãL`]X@wïÒ^j¨ß@vkYCÖ¡2@už¯ÑcŽ@uÌwÓP&@u{²6ƒ>@vÀ»¤Ô½Ø@w4¶ûŽý@u(¶F‰ˆ@w×Ñ¢ú@wƒa)=Û@vŸá,ç@wÖ „N@uL’é«.@t(5í—ý[@u²ƒÖF¥@x‚ü-»Æ)@v©£ú"«„@vjƒ’¦Ûü@tNê1„à¯@v©->3#”@xlW\ù7@t?„ããx@wæ[yFŽ+@vß‚ÐF"@u=!ŽÐ—@v|(LÕÏ@xÑ âM@xoä9¼Ë@xÆ<“°Û@wb&ÿ C@vÚ ÅZ[@w8×Äo¨@xpƒ¾é}×@yôÛ ¦Øf@wÔ‡Êú@wÈ ›«Ž§@u7êž ñU@wq=ÏH1›@x§é½ý²@x:/4Š +¿@xƒó¸áÑç@y6a=Ótñ@y  ¨Bf@w¶‰G,&@zjsÍqY@x{Mxû«U@{íÍú°É?@vt´Ù×@zoÆIœ7f@€3öäŠåS@€§à}2¬Q@~úH-^T@|éÔ< @x ¬ì¼²&@{ë¹ÒMá@{˜Ö@|£VþLi@z~¤¢a#Ö@yF“OÐpd@|;M¨tç@z†OwRè´@{hæl@|­‰aå°@|1¾éÕ@}z+¥¶æ@~ŠS‰úüF@€ /;ˆª@| jREw @{ÝI‚vË@|i ÓEN@}“Ϻ‰@ZÞÅHlÉ@<éð@Ïæläd@}þišŽ1_@}/[+qF@}'MKQIÔ@|à2¶è@3¬,'«÷@€3ÃÃë@€›ÉJ¨èu@åsÆð0Ï@~ÅêE‚j@€xÂVgý@€Ø^Øü—…@Äq5BƤ@ü¤)¼íN@‚‰æÞ)©±@‚N.Ù@‚—³‘é@ƒ,wAÚAû@€öÀТ .@‚Ý_"Ki@ƒ{‚ö´@ƒýÙ2Œ@‚ÔùõÍ@ƒ˜ˆ´Ýô;@ƒ¯)C¬h@„ây稵X@„´:õ{è@ƒß›ƒé¥@ƒäŒ{“Pí@…†å‰=ƒÊ@„¬‹atKÑ@…U"À(>b@†š.8¼Ï@@‡c ï m1@†²2£·{@ˆ‹ó{%P@‡Mj¹òt@‡]&ÊS×@‰Á(Ð’E@‰ÞÞ@£s÷@‹ÕeF?q¹@ˆéêŽ|8@‹R÷œ¢l@‹òd¹?9’@Œ-¿ÂÞ÷ä@Žü +Ó«@Ž4¢K÷@0¥gnV8@Ž½ÍŸ×†@þógwÔÁ@YŸ¨~·ç@8Ú†¦âF@‘!)ßd6‡@ÖjÞÚ<Œ@ÆêÒG @’AòÉæB–@’jÖHðc@“5ÿlkƒå@”T3*ÜL@•4º¢ù$@”šNâo”@•Ï 7ù¿@•õ&.)þ@–L®§Œ¸@–ÿ¨”Î @–§ Í Î@—·a£pÂ~@™E4 m@™€1Oõâ@™ñ9NÇùí@›†.â9°@›ãQ›•9@œ/P£j@ @œª´û#1@žµycb @Ÿ:OŒUeÓ@ åÃ1¬Î@¡\“Lá’~@¡¼NÓ)@¢Ä VA%}@£¶Ëµ=už@¤T©)Õ¨@¥zÃ×hQ@¥µùµ»_@¦±þü ß@¨Ÿ@€” @©µl¾æ¡‚@¬)9±^äñ@¬êžðý·¦@¬á²øâxÒ@®ËŒk¨F@¯à^ Ú2’@°+k¼æ0W@±4hçNï@±­ é’^´@²lÂ{dìß@³;_2wT@´%£¨á[‚@´hµQ šÁ@´f†i +@´&e»¤@³í{ Á¢@´÷ÒýTÚú@µöýØó@¶ªPõúbó@¸oaÚ€>@º`Œ)€:@¼]þý¶¾@ÁBCR°O°@Ñ ÀKýÒC@ðeVŽAíA×ÓíA…j VfuAi9ÑÅ î@ï/Bé¢õÎ@Ëã‰PÎ÷ð@¿ž©b7Æ[@ºÚø¦i†@·\ùeÜUù@´µP#©g@³•7(TUˆ@²h'ÌS(¤@±[€Äú@°ª^öÁ}Ž@°!ÚÅÍC@®NoáXv@®sMC¤×a@­Õ º3¹Ò@­ÄÂUæž¿@«lOâ—Ð@«§]ù@©Q†"4@¨¢ú˜rýâ@§“ÑIö¶î@§øçh›‘@¦RõÚÃêh@¦OwûÔ+@¤÷½‹z@¤/©ÿî*@£X)Uš)@¤9ØüíñÎ@£L˵î%@¢=F²Ro@¡”oœ­@ ³y£Ì,@Ÿ-å$Åå@žü¸[«i€@˜ç@R¸Ï÷î`@œwœÇÙÅT@œ÷‹ÿ^¤@š o‡V@™œu½@™dö‚k@Õ@™Ÿã³¬@™iµÆg@—¿”>rzø@–ÞüÈZ¾œ@• ((ƒ•@–0Sú\°@”y2?gÅV@•Õ{ë›@”0¾× + y@”[$1ºl}@“Rì6û @’2Üj$'R@’m„ÒâŒÓ@‘ðC+OÐÔ@’€£í¿w¬@‘»Íç@‘Ðþââ«£@—çê9è»@‘ÞvBÅ_@=g.…¬’@X˜OV¶@€YÔ²@Ž§Ó"_"­@ŽèŠ¡cu@Ží5óqÎ7@Ž¨}s÷Ê@ŒMÇŽõ¿@‹4Væ@ŒQÂ9~‹‚@Œ}@¥™S@ˆ„YBw@Šw 8ò@‰‡Š¤kDg@ˆÌš×\oë@Š ù (‚@ˆÜó)-ø@Š1 +Ù³e@†~Õñ&t†@†aE–sS@‡ØcÆ*æF@†$b$ û@‡•²Š›Z@…œ`Xyfã@†¿ì°¤ F@…½õ>$b@†ð/•¶@„í1Me—Š@„ÍøÄ^ÙÓ@…ÝrgËå@ƒµï‡AQQ@„$T4|XY@„ ï†Ähý@“p¶£3@“\Ÿõäv7@’…”˜=[þ@’u:oHRM@’NaÊ öd@‘5…í)@’Z’oµ¬Ï@Ÿd±>úæ@t=…öY@#XãEÏd@1HV~<Ÿ@ ¾JL@ðøJÒ@Žk¹ôwM@Œ²fÞZ7@Ÿ?ï c@‹E£K¹ì@‹z²P¯s‰@‹èvÝpcU@ŒÂmDØ=@ŠíWnž@ŠtÓ¼ØÀ7@ŠPÑûXì@‰Dð’§I@Šå%¡ÆË@ˆƒ Rµk@‰9“qo}@‰±GoeÊæ@ˆR4g]“h@‡çE^F)Ò@‡2­ 7Ç@‡w3hÇ @‡É+ïrˆÊ@‡Òf¨u@‡‰–v%ƒÐ@‰V×m=Vœ@Œ “ÅÈÔ@Œuo|Ge@‰è¥ÉH-@ˆÀ`ƒƒ6~@‡Q!Ú’kö@„‰‚DUÈ@† ͸‘ÅÝ@…Í7ס3@†V¨´G1@…£ avôÙ@…-†8pÖN@…øY_@‡Jú»!x‡@…=«,Ym@† wÉ_@…!šÛ¼±a@„ÒJ×rh@… çXØ=@„¢—§Ümã@†±»ÙD2Œ@†J¼9ø!@†J¥µ¾îÄ@…·ÖbpR@…Ž§àðb@†ŽŒ¹\ý€@…KÐLNK@†a#BÞ»@…ksõu@‡Lm—Ä@†Òé`l ]@†I× g~@…wYýõ²@…QB…=@…›Ê[Œþ¨@…ú¨ñSQæ@‡ižrâ½-@†el–´?@†ÐM +MwÒ@‡¶óG÷*@†Žx¯eß_@†!ø(Ëæh@†0¹Á¤Ë™@ˆbBÈY@‡¾¼Åj£@ˆ$zTÚhª@ˆ¿°T•cÑ@ŠvO +L­Œ@ˆsvâCq@‡ßÉy¿Œ@‰¶í9H@‰,£Š§}@‰¦ö’Í @‰rLÂó=@‰{hPÚœ@‹×,(@Šùø‘ì&@Š*ñôî¦@Œ =V3@‹ƒ^‚$Ù¥@‹Ì&ÑÒ@%óSø‰)@ŒÚDïƒå@š¬Ù€hš@B`ÔSZ@kœBôz@¨/ ¸nˆ@kÉT¦é¯@60tæš@æþ&«Šž@í@š·dœ@]Šw_Š§@‘G¢!Î@ä=‚U÷@ÈÝw¸@‘ìsÌv›/@‘r½·ŠK?@‘þ¤âsW°@’ýáL6¨ @’»gæ@Rd@“>ø¤Þ¯@“qì‚Q³W@“ûS“é¤@•-ÇqUïñ@•\¸ÎÞî@•€p¹[´û@”νa( É@—´ » 1@–Ày,ÿŽ@—«{À''É@˜À ±ØR@™ÃrW÷ú@™FèüÀŠ˜@›q3ñ€@›ÊÆç¦@*r5…rÂ@›/à€½J@ÄèX@ž‹û¼hS@Ÿ…,‡o¡ù@ –£¯¸{"@¢‡UkT=&@¤)Y$D@¤X¦8=Î@£H Bð›@¢an¿»>@¢Ñ×(®à @£1ÿ• p@¤BÆŒíU@¥&]®NÊõ@¥ #~p¤ @¦Ï.ïi@§s“·@¨±Ó«¾E”@©Ë4;«ë@ª³9äîŠâ@«kíZ°@«úZz¡¦@­tº² D-@¯k²Æ™ìç@±d÷çu{@²©3ÕœŽR@²n5—;jÅ@²òˆsõ@²ð `èxs@´1¼l,«ø@µ‡æ“-¤ @¶pV#u=8@¶Ú)Úg?@·Ñ®ò|Õ~@¹bË°íö@@ºRàÙµ@¹¸Íª´†ž@¹^ÚbçŽ@¹,á¨EÔ@¹–ö)tÙ@»Ê]vAO@»ãSã=V@½PuÒûº@¿"nÝ]ø@À­¸ ò@¤DUZUX@ÆÏY-Î#ÿ@Øÿjq­@øPNã&Í"A¹¨]÷Ayç\Ù”A Më²t&R@ñ/C³ãÛ@Ðø\‘Ãr@Ä:mÿ(Ì@ÀÈf0¶"@¾ÑÌe~@ºŸè¼µÀb@¸˜òr¸*@·—n{Î`‰@¶e% ™Š@´÷ˆ€ Ú¹@´•º¡nàã@³_óNþ1@³9»¶{Û@²ªf`þ°@±µ‹N|ÅÃ@±pÁ¸¶ª=@±Uµ¿ö¶@°·ŠÂ‰\@¯”ïÞдÓ@®‡’–ÀÄ@­¹1NùóŒ@¬/„ZÐê@¬ö¢q@«nùÃ'Ó@©ñýõØ@©– bI@¨`jâûƒ×@§ömLÆ…&@¨£8Œ&¾@§ÇPlíþ;@¥ÓBœ>ù@¤Ý\9*Sí@£Åçk™¯@£¼Êoºœ@¢å©ã¹b@¢íÊ*3ý@¢M`«/(ð@ Ï½E‡ @¡ÈDŽ;ì@ _•ˆl÷:@ q&¯Ù‰@Ÿ°¨RϾs@ ‰7Ô‡n@´Ý™‚ìî@œ)ò"™@œ4‡p?@šÀþg{ @š¼À–0¨á@š[ëþö^þ@™öGUáA~@™™Ö†ébè@˜Ïýv3U^@—×ÿ\‡Ø@•Õ¼Ç$xw@—h½¤®j@–[+4|±@–[«I©Tå@”ûátåþÛ@”ÑË5!@“ü—ÿ7Ð@”wåŽ,ÉË@”W´dr@”G À|b@“ b‚-Ç@“™ÂM_˜ @“Œ•/c¬@“©mašý›@’ÖóiÌÞD@‘æ"nH½@‘y~ºýø@‘Qvo´t@r±Õilþ@£¤’Ú‹@– +Ææù˜@ØYôÊ’º@ˆ³ÔHZ@7®šðX¼@Fó“žQ@Ž\ÃßÓ„@%“¼»y@ŽSB„v&@*‰þ›@êÇÁ‘ßè@êÙ©DªU@>•¤9"q@5¦:ž]@Œi]¦¡Ö@(hjv>¯@‹£ÝV´é@Œ…]$Cp@‹sVm(²@‹c‡Òm”¼@‰ÓQÚ,F@ŠÏ· +%»À@Š?xŸ€@Š —üÈ*@Šã%kø§@‰/BŽ«Ÿ@ˆçÍB»@ˆ\7‰ÒÞ‰@‰A{ý´Ó@ˆºb@‡ñ:d¶@ˆAÞðú{è@ˆôpJ3¬;@ˆEƒX–¹ô@…¶*i^8!@‡Ô@‡|à)0̇@‡µ}µ¼þÎ@†AÌ@…Ù~fá½ @‡#’Îñ¶ƒ@†°˜Øö2T@†öÊA·@†Z÷=  @†‡ÞÖÚ6@†HP¸Ñ³@‡  @‡ñ„†Ð=×@‰¼sÈw @†`Îs…™@† +p†•y¥@…†ˆ[ÙãÈ@…'´)v@„ªˆM–ŽN@„æH¥Þ9Ö@…*yu~¡è@…)Ã+w@„Ww\V@†“ÉsQ@ƒÞÖ1‚|Å@…DŽÝØb@…àj.—iÎ@„𯒣â@ƒÎ,Öó@„¯ì@.Þ@„EH·]ü@ƒTÙ#n¾@@„|CϲãÛ@ƒâ¯ÈñR‰@„j™ôë°@ƒÃöcó„ha{)@ÎùOY +p@¥ C¶#ñ@‘i‘7sÓp@ÕJç»”@’©„¤ñZ@’+a-¿ö¢@’,Û¤hj@’P¥¶‰C@’{ Ë™>@‘çûIOP¤@’®}L¨]I@’³³S@“C?ª_@”/Ö¢¸2@”h]Ëaðx@”%üTëª\@”±Óˆïo@•æº%®_@•PÖ­£%Q@–P0ù•b@•æz'g@— U{¢­@—>fLóñ@˜8 Pv¼ß@—ê­T=V@˜ä ¶ÿeÔ@™Vw±ßdó@˜4¦¥oq@™5v{›@šËÄ |s @šÂ xÙ$@šå&q¸ä@›¸Õ·Rä@œ|Æ ƒL@n÷ÍØé@?Ôµ,A@™"ËJ%¶@žñ²Z¥^@Ÿa÷ÛCÆ‹@ dfê}´ô@ ´ßvl$z@ øyg&@¡t•´-ÜN@¡ÌŠ8 n@¢3{Mvmz@¢yñY,@£sJ ̱@£ËŒ£nF@£Ò ÏÊŽ@¤âZX&Á¡@¥Ö5ùÍÎ@¦‘àÓ¢@§b•mÿF@§-ì½–óC@¨œ…¶K¡@©ñÔÍ3@©;f9»+©@ª_z‘Ó?;@ª¸jW†@«¹TÅ• ê@¬`ÌŒd:¥@®"lG $0@¯K#;\@°$ö2\fF@°çð̵S@²!.-(¾@²$þ +@³B±ü‰@³À"vâÈ @´É»¿@´¸üê}+=@¶wñ‡Dîn@¸‰j6:°@¸ÿu†ù@¹O.ƒ²à@ºú“ñ1_@ºæiã¥t&@¼8}Ç'èn@½\%f5 +M@¿ EtáUé@¿yÉÚ( @À°tá—¬3@ÁHï¹Ø9m@ ð(  º@ w’@Â]XëïM“@ÁÏ$¤j@ÂHY¨ï@ÂçBgT¶í@Ã`V®\î@ÄŽ»‚hÉu@Å›¢¦¯É@Ç +Ĺ˜Š³@ȼÙZZi@Ì&ƒ¡@ÔlÕEv~(@ð£þ» CñAvQ¨µA[h“È•”Aö~½î¢µA È‚ãïOâ@æ\f {@@Ñfçm¥^@Ê3.Ìí&†@Ç0 ++]„@ćK\©h±@ÂÕ©]S@Á¢ ‚Â&æ@Àæ8¥ žÆ@ÀyÛ‘ˆm4@¿¸Õ>†}@¾4#å&`@¼Üfý±œ@¼ ÿ¥}R@»n9WPµí@»MB:o·@º™êei6e@¹»0®v@¸‘ˆšNh@·È„†RÄ@·h‘Ám·@¶F»álq@¶sƒ$¨C@¶”O׫@µÁ|Å)s@·ƒ‹]@·ÒÂììé@·¡ü{‡‹@¸ë9ÖQQ>@º÷Ù£ž@ºOÙ°ñÂ=@»ð nÓ`M@½B°üZ5+@½Ó§´6Ôæ@¿ r2¡Ó#@¿ó•Ñ ,@Àk#Þ®V@Á²É¬Ã@Áò±MŽni@Ãú;ˆÒéñ@ÄÒq®K¥â@Å(ŸýCÕE@Å‹ƒ0"@ÅÉkmà-@Ç%Ϻ¬wÑ@ÇâÒÌ é@É lͨœÁ@ɲŠ×©ò@Ë×҉”@ÌoTGäy@ÍŸ%fk@ÎÛLJèz@ΟãŠ×÷×@ΰ]é‡8Ç@Îa×Ô»@Σœ1û6ç@ÏØL JüÅ@ÐÔ®¤_w@Ñý:j ”@Ò…Ë?Ë©’@Ô#¢RÛŠò@Õñm‚Âœ@ÚÿÒˆA q@çÞ{Å‚y:AÚBÂùÚ A ïF%ÆNA'<2S1AMA#j"úC³(A5H²fà@ë—˜ƒú•@ÜH„½Åá@Õî‘wfÓ”@Ò×yÌU» @ж’=@@Î)ËIîÏ@̪ô ¸A@Êô¦3š@É¿±iÈž@È]tï_WE@dž[Û^Ge@ÆÁ+{„$@Æ-~J6j›@ÅŸl½­¿@ÅL6¢@Ä­t>Bi @ÃðÀ¡™µ@Ã(ló[j@Â'+9×@Á£›‹Ø@Àú°±“2@Ài¬Ñè@À3hò‚l@¿zì!ÒM@¾gn=‡%¹@½ŠmÊ“@¼-Þóbz@»{.>´gE@»-ïÉ/ú•@º`¹Þ¬@¸Ä¸&`9@·û¹B½¾ä@¶s\¬cÞ@µùg„-t@µóð_RÝ'@´žbÛpI@´k}.W@³ÀSíç@³ã³1Ìó@²q´ÿíÀã@²ENá>˜@±r­ßãWp@°ñáÀ¸@°?X§ø _@®ØNát³³@­Û +@­™(;M(@­YU³c]É@¬éñ„@¬@³¼° +@¬3lÊ…÷w@«(M£âœ@ª'%÷Î@©lÔ²v@¨S³º¢Mý@¨iÒ&Ò†@§g¿öÅÓ@§ú¢Ò~@¦iÉ{Nîž@¥×½g¡6¼@¥“X‡ª7@¥HP”Ÿ@¤|̶:ØH@¤ªBçìì%@¤;ÛÉ”WH@¤AÂ/ÆÀ@¢÷Ý!€Ø#@¢¢ö»eû@¢¡A[•È@¡×õ1š“@¡I†Q†tô@ Ù…Ñ82@ †l3#ß’@ 5±Àì;è@ x~šAG¾@ AJ­·d%@žZ @žÜ‘»Mt@®Jò—@®ïÇò¥@>¦¹Ú@œÆùÅBï@›äq @›ªô(5ß@œÌ:R@šŠ¾ë«@™¬Z .@šLÝw_@š‚¼V|M@˜:æXë—Á@˜2f±"î@˜áú•7Í@˜CølÆà@˜T \Ôß@—¡knÊ$ë@˜œªsw~@œY#Ò57@œ~ú4ƒ@šrô9@˜µ¦Ì„Ò‰@•x‚EO@•3Ê â@•Mâx@[¯@”U¯%)@”d/¾Üp@“»I‚ؼÁ@“w9”T‚É@“@iZ?Ã@’NìÈu@“"íŒþôß@“¨ª%”šh@‘•ß/ +3@’I¬¯Ñ@‘تÈ_“@‘Þ ñE@‘Ïv]<@à‘úH#@‘=ָ̦ù@‘*Oó-@‘p #šd@‘F¹Ùï:@Xâ´1U@G|й@™Ë­Å[@iì“{1³@mÉœI|J@Ž†]õ¡tÙ@ÃW-~@Qqsþ´,@Žyðó¼­@–Ÿåm @^,Ÿ¼™@-¿,@OÂÁ)ñ@è•‚.J@Žä1g0å1@á>ÑÂO@‰6U¦Þ»$@Œ[AêsÞÆ@‹?rai@ŠýF`±@‹ ƒjÀ@ˆPóÄý™Ç@Š Ù.6d@Š[×UÖ  @ŠuPV )Ý@Š¹›8 ý@‰) õ«@@ˆûNF,ƒ@‡fÚ3»@‰'ó6"Ò@ˆ8); +@‰ž˜èÜêç@ˆÆ¸¾£@ˆªKòÑþ¯@‡hËŸv@‡–{ƒzü²@†3eðÒ†ü@†ËÚ„Òô@‡µØä +µ @†fàû™Z‚@…ÑÔ\Y¨W@†4Ÿ‹á/@…§MŠŽ›@†“Ún@„~çøz@…·÷»Ï@„.¶[b«@†2Ÿo”@ù@„ƒ[áxå{@…§²A)y|@„épÜm)@„»Jñ.ý@…IõÅî~@„Sr ù@ƒÁŸ%\¹ç@…"=ñœGØ@„y$,)v@ƒïÉÕÒG—@ƒ?ŠØsŽp@ƒ¢1~M$«@‚teŽM:–@„xÍâåI@ƒØÚ÷ì.Ù@ƒ=!'²P@â/Â"½æ@ƒ2 $p @éÖ«Ÿ.D@‚Öº`©s@‚wjÞ‹@‚g…&Mz@óJ‰V~e@ˆ‚£ç@‚÷^\ ’I@‚dÜw)ÕÑ@ž6þO@‚‰¾›„¦9@Q®®ª£%@‚ëßb@‚Iüf³„w@€§E„%©ƒ@‰‹‰p÷g@€µ;íý»Ÿ@€sTèÚ@NÞ£|ÝÐ@€ãásB;@€Ò›(”ê@€›Jn×Á@€©lhå/¦@ðIk + @€D™êz@F¸ãdÙ@€:@ÃÙ>@€X­ÃŸo@èZn”@€8ýš–À@€gDßš@.oÃÑ®@}û¡±@€Ò[åKƒ@€DÈE_Ž@€i Á@Ã@}Âsg^,Ý@Öˆcì„^@R¹Ðw‚1@}³Š6|+Á@~`÷³ÑŒÔ@€¤ÃË@€M ì?Eþ@{÷"ô›H@}-! ±²@} +î‘XßÒ@~‚`…Xé?@|i&ˆ•¸ê@|žòó‡Îz@}úZ'ÁC@·ƒ ©¡@}9ÎB—@~~”/\@|2ÉöXÕá@|ƒEæÕè·@€‚ÏlÊ´@}+€±xF@}5ÑÁFM@}×y’Uº@}·¡û€^e@}Ç÷‡ÕAª@zþïµCE&@}UÕ’—2å@yø¾oÆ@|ŠAeLX@}jÆò¯i@z°™d Ë:@zRÚVcÇ@{áUä* S@{¸¨W—–@{+|k“‰…@|x-^7™@yô2ð—n@{8Š-f€¡@y`7™ÎF@{Mâ9Dí0@yzŸí7»l@|êLmæ@y¾ncêûM@|¨Ó @z°G±@{ƒöÙùë@{ª‘àï@zÖˆœuPÜ@{(Oº@ &@z ”vUc@{Ë–ø¹w@yôªÝúŸ@z±ŸA*…Ò@{vNñ‰5@{œuœ·°@z÷Ðû@y+Ïé%ú@z0«&AQ`@|9Š:@x®Àšü—§@xê‰öÅc@y½³ +OŽ@{LY.Ï’@x}Aiûµä@x} s]™Q@yŒå¼fr@ywqO½q @xø–‚U:@x8ŸöÙcþ@xåä¹r<@{‰iðŠ0@za6u@z%, +fÓ“@z¥b˜°@xrcÕ¾ú°@~î†ÛÆ”@€ðL ìq@~ÂL³°@óûÀMÊ@{î*R)@z +€éBÕ@y¶%‚R@yr@OØ@xê8c-Ìo@wí‘%›çw@xÛÙSCÌy@w•ö½’B@v¦×yV¸@xÎ7+`/l@w™h[«a@wk-?ö..@xÁׂ@yÞöDh·Š@u¿jhQ3%@y9ÎÊÊQe@wx®E”þ@xÚ¥‹·Ì@xŠ§=w@w›µ¬¯¦Ð@vïßò¦±@w6£ëÿq©@xÂÚýà®@w6—orN@u‹”ÑÊä@w ØÍ@ß$@x<`ÊXîj@vµÃÇC ÿ@tEËWÂ@w­$‚S§@xßæ€D@w¡7€Ä•@vŒ²{§ËG@wNˆµB¨(@vøMÔÓ.*@x7öÕ!,Ä@wöïX†à@wˆHi‘N@u³|•¾@x]lœ @x â³ô@wp4톬@wî¾cÛy@x Ù·œ¥˜@vVÉQvþa@wµŸÙ‡zž@xŠ°ê•9u@xùÆ|•¼@w¿M(ý|@t¨Æ,i²@t¸G*×\@vN$Q1@t+AR*Qj@uÀÃÈ/<@w_ç–g@u å3&jÍ@wÀ5÷2fÿ@t‹/p ;@tØñølÿ@v²Bø\ò@uA\½O½@s_…ïßX@ud™®ñÐ@u§ÄÔŒX¾@tÀÈLÐüQ@v…+ÛÓ³Ì@u š¥ + @v9¸“›â@v]£E¼Ýq@u_±jp·@u»·+îW¼@w¥AOÍ:@v­5ÅH@vXDüÑÊl@v©hXd"¥@w¸ÄflÈã@uu4èK¿@u¯ìûS²d@vµ2ð@t“ö'tb@vcDz™ôx@t)’̓t@w)!kÒ@sðå*ÃÖ@tIŸ:ÇiÊ@tYÕÁ+:@wƒ­O£Ã@u7JJ¶÷@scâ[¢N@uk!œóÁù@sìð×Ä6#@xCà÷M@u\žz=™@wD’ÉL@tÙåûky@v¶Wì a @v> ~ë­@tùYÕêþ@vˆ 3%‹™@s© mãk@v#æPed@vºÂ:f?‰@tJ‘!—å¨@t¼ªëŒÀ@t"%ÍGÔ@uBÍšÿ@t.ÃéË>@v6ÀµÂŒ@w»{hãö@w¶üO/‰@uüŠBp^@sÛ¤1c@tÈ]uDK@vËú˜@sþµ˜uÒ@u¦›8"@s1¼w: ‡@t4ò~cת@wù +†@uG0q#¼@tÂ`I8š"@u?”o#ç–@tô¼Eø«¯@vª/Ñv˜U@t­NßwG@tæÎqñêÞ@wPÅ5BR™@w$J‚V @u¡Îœô@tÞ9.\Ì@uF(äÂÍ@uÉhx½/@uD8¥@s·\6@sþØ :?@w½ðK v“@t’E›T·7@wTX¹*Ž@sj«›v»³@v£¾×dV.@sqØ-—n­@t'0¡G|a@tÝ=âôî@tdúíxŸ@uiw™–DÍ@tº¸¤’@v€CŸÒ@u¨I.–.¯@w?Ýæèt@wq0‹Æ­•@sù¥5<Æ@s®žÞ3?è@v"j¡‚{@uƒŸ›Jûs@uÈ3˜é@wN®â¢@t2¢ +/Ôü@wìRÊ|@vb­üÃX@v0iØ”@vtöWY£±@uÀÛüìñá@wV ËÅF@v TéáþÝ@uÃÏîaƒ@ud`'q÷A@uY]$•nì@vMy_[M³@vt+ã+@wž¼@u"»ÿ¨¡@v +¨A)J@vÌ+«DÚY@uˆ•.¯z@wª*짜>@sÉL6ÊÁ@v2ó¾aá@vGGxâ™@tX‚þÔy=@w߸Ë;{Ä@uiãF%P2@yi'®ò#@xEå5bV2@v:¯‚7CÃ@xLäמv@wÒžïÊÄÂ@và;F_H—@w@?€–g@vƒbÿ"À@v)°Î…± @v‰®¨´@u:F6ý]@x`ï»OD‘@v÷®&@uæJHš¿¹@ttbÃЃ!@w’~$EöÂ@wý_ïBÎ)@wó‡„ÿ@v…™å@uªgÄÒ^ò@u¤1Œ@w¤€¾A v@wÁN;¼‹@u·aF8eŸ@udlS¿@@xŠY&md@wžÔöªB@w*ê÷ÃG@wC«V¤@wRÊØÝv@vA<4wç·@u™u9’€ü@wÖ|ÖxÝp@w¯*ˆ«Èp@v¯N-j @wè@xÞ8„›â@yÕ*þõ6&@zYÖZLJ@x!ÝLºy¹@wú1H#Íð@y ‹Áã\[@xýXK©ß@yÚÇÚŒº*@zû[èL@yäÍÎÆ@xtÇ5°-@|¹M0Åph@{.ûFìž‘@{vô=iU›@|a +šú‚@z/+??Ò@z¡ÏÕT„^@|ü/Fwä@{`Õg÷Ô‡@{oKâA‡}@xŒx‚à«@{v*Јñ@|kׯ—@{u7ï’ÇŸ@{çų\N@|9;CÒ¿@|ñ:Zt³u@xh*ð€è@|×»…ƒc@|"4‚äÿ@}§  çèª@{Fä/ö@}ÇĘ/7÷@{öZ0<³@}}=×Éš@~OicÙ¦@|&k#91N@~?pçwN¢@~\~þC%@ Õ‰ËQ@ ì·¥N @€fñ¢îá@~–äÿ(‹@}òD+@}”<”º¿Ð@}ôzÿ¯éÒ@€Ã CP@2GYx@@~‹SÅèì®@~–YïÌ£È@}¼Þ.ΙÅ@~X8ö$ @€Ö-F—Ž@~M¹Œ5ö@€Ë˜ÏÙ©Y@® +µ+v@;ŽZ-ò@G|þÐ÷,@€®p8$å@¡ +úóœ@vÚÃl-@€Òd6œ“@,§*ú@¢h©oU@‚˜°\ßÝ2@ùÐõÑ`­@‚fÆc=@ƒ>ËDY”l@üààÚ¨@ƒ£Î|Õ¸£@„é©ncr@ƒë²ù@ƒÅ!'x¼6@ƒJ>{“ì@„8óÖ—Ð@„GWö»@ƒÃ0°V°@„ j2rS@ƒBHjoÏT@„“/ÌqRô@„AbÝüÖ@…K*¦­ûë@…;´ ‡)@†¦'˜œ@†Aç([ç@†± î»:¶@‡hû‘5:ô@†¿7ïó%A@†ãÜÔ)ß@†èÈqo¿@‡Í“XEßr@ˆ^â9X@É@‡v^X¸ùq@‰Eð¿sA@ˆU†£Áà7@‰…90$Ë@‰{¸ÁKýÞ@ŠcÆ‹dÎ@Š¢$“›Á|@‹=^%hX@‹ªC`F®@‹úaÐ@é½@Œæ÷µÒ@ŒfÙã…ÝG@‹¨ól,Íù@ÞmèN@ŽeÞÖî§z@Žžµ)8&â@´s@3-÷ÍžÐ@ªñn@ €Ò¶ñ@]ty§nV@õH3qO6@‘góuŸì@h7F( @’<~Íw£e@’ˆTH Í@’Ä[úµu«@“@¶Ü+ +†@“¨û QS@“ +§ìʈ@“õÄ”(9@“Ã&öx@”¨›â€¾@”±w¼ªÞí@–kßìÚ®P@•Ãª‡‹+ü@•^Іƒ@—Òˆ+Ï!@—š›#šOô@—Ž\[ç@˜8.Øæŵ@™³ÛÅ3Vž@™¶,YÍ2û@šR³„W5Œ@šŸ˜ª*q½@›¤„­‡ò/@œ9ünJ™‡@­ÇS»ñB@æIãµé@Ÿx‘ß‘ø@Ÿ½F£ÏÅX@ ` +lÚÐ÷@ Ú£i@ é‡) ­­@¡#z«N-@¢¥¶Þ§G@@¢­q®Â‡@£8Îp-î @¤Q=`¹@¤"Éû?C@¤}·‚@¥ƒ¹×Së@¦WŠU¦^¿@¦¡d¤~×d@§¡¦à%ß@§¹½*ËJ’@¨ Oá@©¹3_öB@ª–Ö{ï@¬®ŠÃÚ@­‰Dcüå@®âÁ<Ês@¯[½\¿@°Ë²·ìL@°¾EÆà@±M’¾ÉRÛ@²MÈZ’„‰@´Tá~%h@´ãŽbÇu@µ¡.‘gœ—@¶Bw0þ·@¶ô¤h± X@·w‚nÔE¢@¸—©mr5Ø@¹Ç¥Ý—b@»V(Ä*§@»mê‰iFH@¼îkt:è,@½˜Òç¡·@¿÷ƒ« @ÀKSLÙ@À@ " ªª Š@Ÿ|#§æ@ÈäÂ_z—@œ¿œ 5 @œëˉ)‰3@œ&ž¬’ZŸ@œ‚S +@œ¾ +AÛbr@›iøH´‘o@™Á:uúT@™#>xÍñ×@™Ýn0½@™9S¯xá¦@˜Ê)½ìEa@˜£šÎ`õ&@˜—kÎè#@˜÷ðP»Ë +@—Á`¦Ü f@–à$òMyä@–~°NNÔ@—Q73ø7R@–„ýd.@–-­‚¸m@•EWª_T@•Ik‚Çà@@”‹áyRHk@”} \ÄHe@”å«zÉ1@”ôAjE3š@“×oS7òQ@“AwÊt@“П¢@“Æä5lª$@“7ñmÌäb@”;©ƒ@“GŽÞÌ?@’“&ôfÎG@’‡å2@“=>@’Œ?±Ñâ@‘ûS²z: +@’*ìBˆ’@’ÜKþn׬@’ +Z +0T@‘ücPŽ·\@‘¸1]fw9@‘†`í®:ý@‘œæ@µ@‘—Ýüo @‘8ã¼zô@‘hYÁß6@‘4UÑy’·@‘Wà áT@‘ߜץ§@»™ŸðI@ꮥ'¤@‘ +ñë«@‘¡ÞìD„â@’q5tÒ@‘k'—RÏ@‘ÙSάGÑ@‘»¬à@’a1‚þô|@‘‚¼öX@‘=£þ\r@’9›‘Ÿ}N@‘fÏsʼ¬@ù#º½å&@’g\Ýóó@“¢î—¨î@“òòÎ-‹‚@’&ú˜j…@“9_ùÎq­@‘úqŸ=£Ö@“Ò-u%ˆ@“Nm5¦”@“¹q6}î@“¡„Å2×@•Í×Q?Ò@”‡øŒ6<@”$.öPœ@”U¥¹ç¼@–$KHSƒU@–ð©|å@•ø(Ý?Ò@–*è¸Â‘@–ÜY ÿÒü@—3Åöê£@—’ÊŠ~¬ç@—ƒòˆ@—åðL²”ì@™ÁÕ·û@™‘QÞ"d@™ö 3³@šv*%sm¤@šž0ó…\§@šÉzK“,½@›µñëê@›)\©Lb0@œ®+må @øÓY‚³@Ÿ4 Ùû•@ -¨ö¼ÂH@ ‰ >lÆ@ êê"(Ž@¡r¦ª@¡É„ &¿@¢ÕÇ_ O@£½¡9Ó[@¤±²ÿÎ#ÿ@¦-Ø£ @¦ˆzûŠÚï@¦A(d2£€@¦¯úo8l@§ÍÄaý@¨5òò_Ò@©ù%5€b@ªR–+¸ô@ª…Úœ1@«òïù-rø@¬¶7ÎãÞZ@¯%wÚ¸«Ç@¯ˆºP2Ð@¯/wiòNr@¯\¸;,H@®=ÅŸô@®yEÃr©‚@°"üh½±@°bB„¡Ü @±VOM¤¬z@±æ,Ñ¿xŽ@³Nln\@´D[ßÖ)=@¶Œ±¯ @½¸÷)ßê[@ÒF÷‹t|#@ð0@åí®AJ +R^ñAfíb~AÉò7²5@é›ØõéGº@ÈÔŸŒ@ºQÈ ¯3@µFÕ} +@²®”Ïeí@°ÆîÎ]¢I@¯2öÿË#@­cɾ¿zz@«´;¹´'@ª«¾ú܈¨@©juˆù¦@¨ŒUŽ@§„ÃrØÎ@§=4ÓÄCË@¦êÛ-½–@¦'|{ý|@¦QÝc]×$@¥ºc¾åAV@¤[nÁ±@£¶÷_r@¢Ó˜˜˜‚~@¢l4ÙðÙN@¡ªEÉÇ5@¡º +òï@ Î\@!Ö!@ OÕ$%@ >Z¤>,Æ@žúŽ½Å†@@ø¤º~wy@• «‡@0ÿr @\¯ö6@›d<ÿ…Ì@šÔ|ªÓ—@™"6ÆOL@—ÎE·=6H@—‘`žó'…@—Ì»@=8@—P?>Ѓ@–ïÌÏ((@–© bk@”8¿T7½E@•$ì}>ª)@”³@ÝŒìV@”äu‘¢@“÷–U<-L@’o¶!ZB@’12z[¿@‘ò¢wJx~@ñöǃ›@’ NŸZ +\@‘š@Š«ªàºù@ŠJ¯dÞ t@‹ÑdÁ‰@‰…‹‚u’@ˆó“­ò@ˆè-øšt@ˆØÂ÷¾Ï@‡m¡›C@‰5Ä®y@†Øí:))@‡nâa·§è@†}ØÛ0xT@†a\Õâu6@†óÄÓ@†?킇ît@†»L¼`¼x@„ÌEÊv4A@„HÓ¥Ò¿J@ƒÞ«[»Zé@„‹uöÉm@„.§WŸtx@„YàÞ´v’@ƒ·p߇ +@‚ƇàmD@ƒ´A“Í» @ƒbˆaó@‚eë—@‚„ÏbvÄ‹@R©´¶Š@ëë‚Áo¸@Ï-Ê/m@‚=ÜëªQÐ@Ifá"fþ@Š×›É`@€[äu +ò@?sý@S8ÝmJZ@€©^ͼ¼Ñ@ý©H›ºô@U•A’N@€,S ß¤@}€OyCŒú@·æ¹Ú@~,¶ß"G@6–.xÿ7@\eéÒZÁ@}A.(i ß@~eæQ¦”@~Ù:5KBa@ ×N¤Oû@~\bÝ<‰@~¢Kà”Ò@|}äý„Nì@z¤âx®¢@{XoY¬«D@y»@«ÖÑ@|Ö—² +4@zV9£rmf@{&q§¢‘@}ÌC‡ê&ó@zê4æG5@zÊ\CFü@{ˆgémH?@{,7%Û Ž@x½‹­>@vf ÊŽ2¿@wLŠsøßY@vû3T@w)¥Äà—±@u6•œ·`@tÏ£„= F@vNYÈ´†@uÂFVÏêK@v§J1'ƒ@tþ rîýò@vê£ ¸a @uÀl)À@xí5q`@v|Þ T@uGÝþ«§@tÙg}rÞ@w»ä{ו@víò̦©@vv$ý€”Ö@vPòvÙ­@uzõÀu@uÍÎx ×@uŽi"‡´î@uÖ5å@tYˆEþ±O@uM»«H8@tÊá{Ëó@tòümªDü@v–³ êCo@t8¨€ @tƒy®–ZŽ@u3bPš–Ö@vB¹÷Ê@sEˆÛC·B@t˜¨Çï~¨@sƒ‰£Ðe@s[Ù.¨@tf¶U{®@tû-[¾ÈÆ@tºŸ¼-[@s)Š¸Ü@sf•A¨@uRA‘ï @u`l¸qÐ@riª×áÒ–@sL}Ϩ@rcì«Ô€@tVŒñ¿'D@t۵ݪ:m@qÀøZL@s Û¨@q¹ù-´"@t‹È½4à@tˆ!œtÍ@sþìî@tÒDçð^@r©Qß\@sØåu€@t‚ÿu4@tYMAjÄ@s×S ëa@rg°å£¸:@rC#Ú&-ú@rÚÁU;|@tE‹ò@r¸ši[•£@q²PYi¸@sàïˆ9û@uujº +ü@r¶ P’@rÇUüòDB@tŒ;5~¶²@s2•Ù(ˆ@s1Ôš¯š@t™í,OÎ@qн±?k@tWÒ¥ +e@tj Rz@rßéÙ£©@s¿×¾Ýr²@qê^è"‚ý@r<Þª™W@pF ²ÅÉ@qÑ4™v>*@q–M2{»@qÔü³©<ˆ@s©†¼”2Ñ@ró’Œm€@r¼ ·×Ī@pìWÜC×@rZä˜7µM@tX6ŠÜ@qÿÓÀ°IR@qˆw[ñÞ@rBêTä@tcÃÇe=þ@râåìÛ8@qÜ¿óOô@r™Jß<~Õ@r£Ì*Ê•@r¥Ì£¥}@r9(.Âðü@p˜æØ4G@suN Bpu@q6î·¸5ç@r¨Ìñ¨ö@qŽB´?6a@r)‚Ö¿v@s‰¢Cªp @sÅc*³@rd° +æâ…@rFèr´“n@spÄ9aÑÈ@r“ ËxÜ„@qŠW‘ËÍK@qîÛ°I¿Ã@s\¦lò@tÜH…®@q[¼¨ø|M@tùÈšU @p—üKD)@q÷ò;k¡ÿ@rtóƒT@s ¦²šõ@qŸ^â{¤@s8”Xî@s¼Ã +Ìs@rÜÛï®;T@rI„·‘ß®@sæZ—@s"-cËÌ“@r¸é¯Ó Ï@ržìújš @sl¡\K<ö@r°æw*@tÉ Þ `@rŸô¥«5@rîÁ¸@u@ÁõkÁt@sÄþš£#@t¨9eõÒš@r<óQcÃ@t¨@£óÒ @r‚ñ2kã@t|µû íî@t0¾×²€Ð@råaHÛê @sœl°ø¨U@t0îIg@r]¸{lo&@rdüó3@sQ±{ë@s*ìË” @sÆyËŠÆ´@sä¹4"©?@uW†6>^@uf÷2—åï@r[¢.Œ@r´]œ¨ó@tXÛ›@twÑ£oô@s¨99ès@tM…ÊÔü’@s”2‹·Í½@v tk­ðí@t#jÆá@uoâõg_j@tx:Yþ&Ë@s§øQ¾s@rF²Q¾ë@tœ,‚ij°@xæØþó@sUéM\¶@um/î”Fß@tvÖb¯4@tÆ&I@w>ær‚~@vJr[LÃ;@t–ÜÓ<+@vù]µÈ6K@v©+«@uzïBx¡@v½4Ê@v¸Éaà¤@v]Ë(@ur‡íšd@vr§úqÏÖ@vC÷FF@uãÒ Ñ@u@IVüÒ‚@x–PxQX@tŒd²Ü™}@xdB?@x·\ØÏèÐ@v©w†£ @xXŒ¤Eë@x+Ô÷ð×@xÄLQåš@xfD‹A¤x@xÌy e@zn`eHJ;@zÖ q·²T@wô>ïG4L@{"ÿO°™@{¿oë¡@z|bà@zhÆä +Ë@|¬rüÏK@zíŸPÆ@{z•°½0v@y²ñvù1@}ÂW” ªX@{©š/\¬@zæ|§§WJ@}’è9ñ¾¿@}³ œúM)@~.Dt†X@}Ð2ɶ#ž@9íj~KÝ@}lh¤hô@~“~Ïÿ\@€‹W$§h@~è牖&h@ã„#ù@~3$쥜@á@Ý5Üõ@€Yè¾¼¡@€Q&¼=Wê@Þ[økÅ„@€;½®úá@€ÿºM12@MÚ1•E@æÆ1º@Ï-ðÄ@@‚]U%¶@ÏÚB.1¡@ƒXÐ%$*€@‚ øÖl m@•’i»»Õ@Þ²âžÍµ@ƒxšˆ4@ƒô])_Ö@ƒïÃíÇõ@…¨±öV @‚,³¼Z@…2Çż}l@†hH/nC@†Ík>°Çç@…îP[^¶i@†GžK…R@†t_v^@‡ho€”&@tö»†@à@r (ÜîÊk@rÊLµá¾@tš‘—]w@sÒÔHTT@rrÂ(²!@r° ´@rdbp|¶@s„)£~Z@píÄ^ŠRð@rº÷Lê¶@q²KÈê`„@pGô©¶ÄÓ@rŽâ^(Ð@péØM”Ù@qw°Æôº@q´"P"‘@rW ¤+@qª½Þ¹Ø‰@pˆYLtâ@qYxò®åÅ@qLó ÇÉÚ@pï5 æw°@rm,¥Î“Ê@q€ãº)³@pý +à3ø@qáè7q#´@rcb!\@s•Ž†@rcö¯kàr@q<¯÷ä–@q’MA¢ùB@qè|-&Ǹ@p&@h…U@nå%NËÉ@q­Ç#`Èé@pœ”Ë®Ý-@pКÂTø6@pÎÒù@rT°¨²˜@sc¶PªÖ@pcò'9¶@p÷ ”D¾¨@r‘¼–€ÀT@q,õû´§Î@qÒ4ñÜä@pŽ[ñNé@pô?‰–Gm@pù¸O{@qKG8jê@qdÙò>=@qøPE7@p‰= ›.:@q]»g÷iî@pÝž¦Ê€ª@p÷ïv(@qÍ`ãfÙô@o¹+üi­@q$¶Mþ¸ž@q Ç5KM@p÷5 §­@qdŽ¤£J@oUîy€]k@q½³Ë“HO@r1A¼À@n`kŠHÈ„@p¥»içÍ@p/€ÙÂ>É@qÑÅgâ‚‘@n5L–”qG@p–[Þ»¸u@q ç›@mÍ{W$@oλý}@pw+ŠÎ­ƒ@nqidZi»@qlÂá@@p Q,̤@ri*8$@p8þ¤“Ø@o©Ææ4@@r¡P|Oe@nu+qÜ@pkàìÒ½@o¢ÓaÃÍ@p¥”ò–äë@qMþ c”¾@pÆ•*˜V1@n‹¥@=eÙ@pkkë³ ­@p@d¿ù@o ,tú7Ú@q+HP|q@qÍl'JÝw@p 霉Þ@rt/dü@pF*;¡F@p™µ·‰@o6¹Úƒ}2@oyÚ¬ävà@psMƒ«ÈH@p?ÐôÜ'@m“±æÂ!@qšZ*ñ<@p]p´ +@pÈZå‹ey@m^a]w@m¨ìýª@pÉ7ßп@q¾5±@p’Û)ëâ¯@m^mè:x@qlÂgüL@r+û \Ÿ¬@pÜ +à†@p•5þKɉ@péCÇ:}@p„ÓÍœäÜ@kä¦Ý<['@p~+/›Þ@rwÊ#7@q [ÔÍ@@q‚a”>6é@q¥^O¯-ë@p«äÂ¥$/@rÈ8RN3L@q4JZ@p¹À±1ÿ@pSѤ¼Ä@p¸¼šˆ"@mAP`Å@p}(ŽŒò@q`°îFQÇ@p2;´ @qÿ´'·ø@rÏ‹ÙÀ @q &«¹µ@r2«²3_@q¾~´„@q¨AœòÓ@pkŒ8r˜x@pn +T&)@qàÁúJý@qEXðãÕ^@r0j­o™@nÿÉë„…@s”{U/û@qß1xÈš@p¹veƒè@q«bŸ™U@r‰¹@q-œ:hÊm@qô9cô>`@pÙ=MZo@sõbµ@r”ñao@re¤éÇ2"@p‘htäg@r—£we¿@q"Ó ©8|@s…¤[uŽ@qPí®ær8@tãôÎÒ0@qôƒ·¡›ô@qB—oî!Z@r ج@q/ ½PTè@q‚·V"rd@s2y½+p@q'*É6Û@s˜ˆ…K·@qé“5{@ré#‹)‚É@nòÓ“dy@qàÉø/´@s[(yþ·@sÂg‚kcv@töÜëÍ@sÞiLdW@rYüȠ΄@sZ'‹3@riPjÌ0¨@s5>Ãs@uWÕŸ@sšêälh‹@t³ù ê†@tDW0<4†@‰z}¿ëŠs@‘C³üo¹~@‘;•·,š@‹‚>ä¬XK@€™˜SÜQé@z''=+'?@x«™e݆@vÍ›P})å@vš|kàC@uÊó£xQˆ@x|äÖ[“@v§”mRÙ±@wj zÞ'@v6T›°§›@wZvÙ)Šc@veÚ +¯‹@voDVLŲ@x·eœò@x¯³ðJ@tÔ|ZßI½@w¨z(—¢@wÊÅS7))@z½/‘#W@y ü›¤@wÎ$¾HÚ@y¦f™ ¢@vöæDtY@w4Ÿ¨@yÚ€·P@x}gÒ–Á°@zÖÿø¤•Å@w¬-(€À@z¡ð)'zØ@y6ü¥“³@vý®¥Tš@xC`qý¢@y¾ @x,™h@w¨sÍtë@wêúœû.f@yÞhÚ*”´@{d%:¡A@yO%-ÜÂ@zFÙg}Ó@{K¦†Ù@{Mw¤—`@wïå>Â@zþi{^0@yìSðúa;@}o¦2d@}ƒ>ÔP@zëÀ(ù"u@{eŠ„@y' +¸Ô‚@~±À —Þ¹@{³¤iy\@}Ï”@}‡Òg`[Ì@{Lìg”‡~@ 2ŒnÊ@}ùctßÈÀ@ÈPd`B@~O! «ÈÐ@€ 'WO“ä@€Pcô–uÐ@ÖíøÄ—@hä“æá@€Z²°„q@‚ð·;dQ@€Ê€âì¡ï@€ƒ‘Šs#@€ç€pŽóä@é§î @€¤I=ão@€¿×£G@€>Œß¢@ãÖ÷¦—@ÛMʾîX@e‰bøà@.¤$Æl@„0*ш@ƒ<Òzmw@„' Ñ‚sŽ@‚N"KÚ­z@ƒäb¿m@„KïÎEž’@„ù"ê«@‚Ñ×h@„¹ýs#¼Â@„–CGç@„c FD†@…þ]­…@‡|Å#»Z@†°j‡23I@†ñ?¦|š‘@†/H¾×@@‡›Þ{eÂ@†‡ÇØhÝa@†ãP¸ +2@†h2…P@‡bIÖœÔI@ˆïX ;@‰2èÍÁ`@‰×Z=g÷@ˆÇ²ð¦º@ˆÿ£Y/)@‰ãTæNÀ[@‰ñ ™ƒj®@‹;Gî«@@‹ªR—»?è@Œ/¹žcý@0›­bG@Yª¾Ð@µOS•ß@ŽšÙ˜q@ŽˆnÏGwf@/!S/4¥@Ž} &"_@㓽üàF@ŽfxeTƒò@5NÌ€ô@£öØÇf·@Ýâ‘Ü@‘ž„T;ý¾@’q òÄ@‘@zŠìŠ+@’–¹äñ²F@“O¤ðDJ@’ý·.™ @“á óÝï@’¨žXOà–@“ù㇕@•mˆŠ‰;|@•vžó‡@•h;öV©G@•œøÏÜ.@–z¦°2íR@—¨\M'· @˜QRŸ"j@˜å#'4@™h„~ô°@šÐ*äÍus@›è×´I”@›jX»0(ô@œqñÏzb@›Ô8ì?|°@œæò¤Mºe@Ÿý¡Sa@Ÿzõæ­Ã“@ c +S`9@ ïõ=õpm@¡,PVk†@¡BŠ%²@¢<à 9èÕ@¢¥•5õ @£Ÿ§É‰{¯@¤ ¨óÓâ@£æø›p1©@¤ËÊ4@¥œ†|üñ@¥õ¿ÁwÚ„@§f£ø§I@¨±*„´@¨"Š¯Ìtq@©Šòyó–˜@ªP­]… U@ªâꨟm­@¬b˜¯W:@­Þ'¶ÏS@¯h¸Á½÷@¯ùÃ.Ïú@±T‹õçe¦@²¤ÎÇhUž@²ÄânL+@³ÍØS„ƒ@³ÌÉ"Mn@´ö‹ó,t“@µ…)2+ê@¶•¡ÞÂ@¶ž[‡;÷1@·íÑgG@¸-”¼Q@º-%!ïÔ@ºyÊ83@»×M¸e¸@¼@"÷1ó@»e0úŠ@»Áõ×€ºþ@»ÖoY°Åù@¼ÉPål[@¾L ÌM@¿Y‘Êëˆ@Á +ˆu(d@Ãð|îCD@Äå˜Ãxþ@Ç Ft£‡“@Ê"Žó×÷^@ÑQˆJå×@áï¾…¾Þö@õ£®:’ŸAz¥TzÞAYKGáùA0ðbú”A ›3wq;œ@ô]«c¥}@ÛŠ†·@λE×(wÿ@ÇëHCê®@ÃTl™ }@ÁYÒñ¸R@¿•?®@¼Ë ŠI[@ºßgŸÓ¢@¹ž]GZå5@¸’[Ì\þ +@¶°”ÜÆ\@¶u2nˆ/@µM~é€r­@µl/Ždá@´pkXŒ¤@³Ôƹqy@³jŠzçFR@²¤Þ(å$@±à˜:U°@±x‰Ô¹™y@°±  +òð@¯Æ4ÎË‹ñ@¯é7‰K!@¯=‚UÂj­@­õs÷m@­•Ã^˜@«Þ“ÁÈQÜ@«.Ѫ*v@©§~,ÔÐÇ@© âè(s”@¨Ú¤ }ÛÉ@¨¬±4c°ì@¨A`ÅX2@¨jWáe@¦ JkÌEV@¥sßqÞzÂ@¤vßç@@£xÚ‚V­@£Z‘TyUž@¢ 5ïsY²@£ I˜q¨v@¢'P¿j@¡b&Ê×Y5@ ýAÒ¡µ@ È¶C‚šÎ@ &sE#š@ {ØÇ; q@ i¬¾ +0@ŸFÏ í4B@!½vÕÚ$@œÈ =£üc@œ¢ýâc¹@œhh3@›ÈkÝÍœ.@š3ÒÊ°ƒÓ@˜æ¸èŸÙ+@™ºšÞP¢¿@—õ­!òë@—TjÞØ?K@—Z&šjq@–dç=X¤ @–vI‰Å@–‚?ç"—@–@cÒteÝ@•pïÿ^ª‘@”à0Wœ´@”å€', ^@“÷­ÙNl@“¿Î 5›@“‘·mÕ@“ŠÆëL@“{QŒB@’ $,2à@’Ôø“=&Ã@‘ªÄI@–S@‘v@ºóÓB@‘z NI% @ËâS +¹@uz®MãR@‘²&åÆ0@•¡#Þ}@¸·½¤@Ž0qT“Ì@µW×XH@œìŽJÙ +@í ðÈ[@´-å¨k$@Ž [›<«Ó@[¥Cá@Œý|E¨]@‹¥ƒ-Ýþ@‹À|€áÃj@ŠóÊ…—(•@Šuá–ö@ŠöGàØ<@Š¶«Ç^m`@‰Óœ2OÎ@Š#{?–7@‰„'³lÏ@‡Át'ûÔ @‰ß3>>óD@ˆÖB3Ì¢\@ˆw¶t…²x@ˆE´&·³§@‰ähðµŽ@‡',O¬»à@‡VÌB.ûÜ@ˆO÷Öû‚@ˆ5ÁŠU/Ð@†µ%”õs@‡%uîÐ@†(·]Ä Ò@…à’¼­ß@‡$ù«×?@„z …z @†=똨}Y@…t*ÂMSs@„@m ü@„|—Ã@„Ø‚9þ@„~-¡Ùï@„£è4â@…€b}_@@ƒ¬—¥ôÕ@‚óq±ãqí@‚ùèïR.Q@„ç6°v#J@‚àèåušÓ@ƒ5Ä”nØ@ƒ{¹lÍ´%@ƒÚzñ^6@‚~Gßý@ƒ1¯¨­»@‚Ùt ê¿Œ@ƒ×&¸Þ ë@ƒ¸¨$§9@‚ÜÉÿSC@‚d8R[*@‚6ª×†$¨@ƒa€¡•@‚þZˆko@‰• j@‚\8$àB@‚IyEþ3@p»Ú /@a GÞ@‚3ë·°‰@‚8xf…ØÙ@bMë@>|Ü@‚•nLÔö¥@‚¥=rqt@ƒi>8§ @‚Á[,Ù!î@ƒAˆ 0Íl@‚ä짙éj@‚À¸B|~Á@ƒëeòu«ò@‚ë3ú²ú~@ƒMÇÕa5"@»ôÊ+u@I¯CeN˜@zFì „8@‚¨ŽÝê£ì@¦.–ƒz@ƒäÌðmÏ@‚ššÄK3@ƒ¡Å6ý`@­= u .@F³Z‹ö@ƒ úª(@‚`òS´_ð@¡÷«êß@„³‰›4Ú@ƒ–þçÙ @¦MǤý@„G¢a{c@„"ü£p…7@ƒ]ÞYÇÑ@ƒ< 3Å @ƒä 8ÔA@„ÃcœÍÀ$@„]-èc@‚ì‡V¬Æ@„.³å5D@„ËÉK9I0@ƒ”RŽÜlÀ@„Ç>0@„S*«¡Û@…S¿[ä@†6C€l± @…@у„*@…GTH™rˆ \ No newline at end of file diff --git a/goodman_pipeline/images/__init__.py b/goodman_pipeline/images/__init__.py index 20bff635..286a3bbc 100644 --- a/goodman_pipeline/images/__init__.py +++ b/goodman_pipeline/images/__init__.py @@ -1,6 +1,4 @@ -"""Goodman CCD Reduction Tool +# flake8: noqa -""" - -from .goodman_ccd import MainApp, get_args +from .goodman_ccd import RedCCD, get_args from .image_processor import ImageProcessor diff --git a/goodman_pipeline/images/data_classifier.py b/goodman_pipeline/images/data_classifier.py index acf22eac..089c92fc 100644 --- a/goodman_pipeline/images/data_classifier.py +++ b/goodman_pipeline/images/data_classifier.py @@ -143,18 +143,18 @@ def _get_obs_technique(self): wavmodes = [str(w).upper() for w in self.objects_collection.wavmode.unique()] if len(wavmodes) == 1 and wavmodes[0] == 'IMAGING': - self.technique = 'Imaging' + self.technique = 'Imaging' elif 'IMAGING' in wavmodes and len(wavmodes) > 1: - self.log.error('There seems to be Imaging and Spectroscopic ' - 'data. I will assume the Imaging data are ' - 'acquisition images therefore they will be ' - 'ignored.') - self.log.info("If you really have Imaging data, please process " - "them in a separated folder.") - self.technique = 'Spectroscopy' + self.log.error('There seems to be Imaging and Spectroscopic ' + 'data. I will assume the Imaging data are ' + 'acquisition images therefore they will be ' + 'ignored.') + self.log.info("If you really have Imaging data, please process " + "them in a separated folder.") + self.technique = 'Spectroscopy' else: - self.technique = 'Spectroscopy' + self.technique = 'Spectroscopy' # inform the results, no need to return self.log.info('Detected {:s} Data from {:s} ' 'Camera'.format(self.technique, self.instrument)) diff --git a/goodman_pipeline/images/goodman_ccd.py b/goodman_pipeline/images/goodman_ccd.py index 3072ddca..dc2b9670 100755 --- a/goodman_pipeline/images/goodman_ccd.py +++ b/goodman_pipeline/images/goodman_ccd.py @@ -152,10 +152,10 @@ def get_args(arguments=None): return args -class MainApp(object): +class RedCCD(object): def __init__(self): - """This method initializes the MainApp class + """This method initializes the RedCCD class The main task of this method is to call the get_args function that returns an argparse object. @@ -174,7 +174,7 @@ def __init__(self): self._pipeline_version = __version__ def __call__(self, args=None): - """Call method for MainApp + """Call method for RedCCD From the arguments this method finds the raw_path attribute and checks its contents for the existence of files containing the '.fits' string. @@ -392,5 +392,5 @@ def _check_args(self): if __name__ == '__main__': # pragma: no cover - main_app = MainApp() - main_app() + RED_CCD = RedCCD() + RED_CCD() diff --git a/goodman_pipeline/images/night_organizer.py b/goodman_pipeline/images/night_organizer.py index 3f34af0f..8354a51b 100644 --- a/goodman_pipeline/images/night_organizer.py +++ b/goodman_pipeline/images/night_organizer.py @@ -95,7 +95,7 @@ def __call__(self): _imaging_file = self.file_collection[ self.file_collection.wavmode == 'IMAGING'] if not _imaging_file.empty: - self.log.warning("Ignoring all Imaging data. Assuming they are " + self.log.warning("Ignoring all files where `WAVMODE` is `IMAGING`. Assuming they are " "not science exposures.") self.log.info("If any of the files listed below is a science " "file, please process it/them in a separate " @@ -108,8 +108,8 @@ def __call__(self): self.file_collection.wavmode != 'IMAGING'].reset_index(drop=True) elif self.technique == 'Imaging': - self.log.warning("Ignoring all files where `wavmode` is not " - "Imaging.") + self.log.warning("Ignoring all files where `WAVMODE` is not " + "IMAGING.") self.file_collection = self.file_collection[ self.file_collection.wavmode == 'IMAGING'].reset_index(drop=True) diff --git a/goodman_pipeline/images/tests/test_goodman_ccd.py b/goodman_pipeline/images/tests/test_goodman_ccd.py index 91d62e1f..e677e5c0 100644 --- a/goodman_pipeline/images/tests/test_goodman_ccd.py +++ b/goodman_pipeline/images/tests/test_goodman_ccd.py @@ -1,18 +1,18 @@ from __future__ import absolute_import from unittest import TestCase, skip -from ..goodman_ccd import get_args, MainApp +from ..goodman_ccd import get_args, RedCCD class MainAppTest(TestCase): def setUp(self): - self.main_app = MainApp() + self.red_ccd = RedCCD() def test___call__(self): - self.assertRaises(SystemExit, self.main_app) + self.assertRaises(SystemExit, self.red_ccd) def test___call___show_version(self): arguments = ['--version'] args = get_args(arguments=arguments) - self.assertRaises(SystemExit, self.main_app, args) + self.assertRaises(SystemExit, self.red_ccd, args) diff --git a/goodman_pipeline/scripts/redccd b/goodman_pipeline/scripts/redccd index 4f1115ca..b2ce6d9f 100755 --- a/goodman_pipeline/scripts/redccd +++ b/goodman_pipeline/scripts/redccd @@ -3,7 +3,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) from goodman_pipeline.core import setup_logging -from goodman_pipeline.images import MainApp +from goodman_pipeline.images import RedCCD import sys @@ -13,5 +13,5 @@ if '-h' not in sys.argv and \ setup_logging() if __name__ == '__main__': - GOODMAN_CCD = MainApp() + GOODMAN_CCD = RedCCD() GOODMAN_CCD() diff --git a/goodman_pipeline/scripts/redspec b/goodman_pipeline/scripts/redspec index dfed4911..028cd2a5 100755 --- a/goodman_pipeline/scripts/redspec +++ b/goodman_pipeline/scripts/redspec @@ -3,7 +3,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) from goodman_pipeline.core import setup_logging -from goodman_pipeline.spectroscopy import MainApp +from goodman_pipeline.spectroscopy import RedSpec import sys @@ -14,5 +14,5 @@ if '-h' not in sys.argv and \ if __name__ == '__main__': - GOODMAN_SPECTROSCOPY = MainApp() + GOODMAN_SPECTROSCOPY = RedSpec() GOODMAN_SPECTROSCOPY() diff --git a/goodman_pipeline/spectroscopy/__init__.py b/goodman_pipeline/spectroscopy/__init__.py index 3674e444..1ce52103 100644 --- a/goodman_pipeline/spectroscopy/__init__.py +++ b/goodman_pipeline/spectroscopy/__init__.py @@ -1,6 +1,6 @@ -"""Goodman Spectroscopic Tools +# flake8: noqa -""" from __future__ import absolute_import -from .redspec import MainApp, get_args +from .redspec import RedSpec, get_args from .wavelength import WavelengthCalibration +from ._interactive_wavelength import InteractiveWavelengthCalibration diff --git a/goodman_pipeline/spectroscopy/_interactive_wavelength.py b/goodman_pipeline/spectroscopy/_interactive_wavelength.py new file mode 100644 index 00000000..2c25bc7f --- /dev/null +++ b/goodman_pipeline/spectroscopy/_interactive_wavelength.py @@ -0,0 +1,1577 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import logging +import os +import re +import sys + +import astropy.units as u +import matplotlib.pyplot as plt +import matplotlib.ticker as mtick +import numpy as np +from astropy.convolution import convolve, Gaussian1DKernel +from astropy.modeling import models, fitting +from astropy.stats import sigma_clip +from ccdproc import CCDData + +from ..wcs.wcs import WCS +from ..core import (evaluate_wavelength_solution, + get_lines_in_lamp, + get_spectral_characteristics, + record_wavelength_solution_evaluation) + +from ..core import ReferenceData + +FORMAT = '%(levelname)s:%(filename)s:%(module)s: %(message)s' +logging.basicConfig(level=logging.DEBUG, format=FORMAT) +log = logging.getLogger(__name__) + + +SHOW_PLOTS = False + + +class InteractiveWavelengthCalibration(object): + """Wavelength Calibration Class + + The WavelengthCalibration class is instantiated for each of the science + images, which are treated as a "science object". In this first release it + can find a wavelength solution for a given comparison lamp using an + interactive GUI based on Matplotlib. Although it works very good, for the + next release there is a plan for creating an independent GUI based on QT in + order to work better in different screen sizes and other topic such as + showing warnings, messages and help. + + This class takes 1D spectrum with no wavelength calibration and returns fits + files with wavelength solutions using the FITS standard for linear + solutions. Goodman spectra are slightly non-linear therefore they are + linearized and smoothed before they are returned for the user. + + """ + + def __init__(self): + """Wavelength Calibration Class Initialization + + A WavelengthCalibration class is instantiated for each science target + being processed, i.e. every science image. + + Notes: + This class violates some conventions as for length and number of + attributes is concerned. Solving this is part of a prioritary plans + for next release. + + Args: + args (object): Runtime arguments. + + """ + + # TODO - Documentation missing + self.lamp = None + self.poly_order = 2 + self.wcs = WCS() + self._wsolution = None + self._rms_error = None + self._n_rejections = None + self._n_points = None + # print(self.args.reference_dir) + self.reference_data = None + # self.science_object = science_object + self.slit_offset = None + self.interpolation_size = 200 + self.line_search_method = 'derivative' + """Instrument configuration and spectral characteristics""" + self.pixel_size = 15 * u.micrometer + self.pixel_scale = 0.15 * u.arcsec + self.goodman_focal_length = 377.3 * u.mm + self.grating_frequency = None + self.grating_angle = None + self.camera_angle = None + self.serial_binning = None + self.parallel_binning = None + + self.pixel_count = None + self.alpha = None + self.beta = None + self.center_wavelength = None + self.blue_limit = None + self.red_limit = None + """Interactive wavelength finding""" + self.reference_marks_x = [] + self.reference_marks_y = [] + self.raw_data_marks_x = [] + self.raw_data_marks_y = [] + self.click_input_enabled = True + self.reference_bb = None + self.raw_data_bb = None + self.contextual_bb = None + self.i_fig = None + self.ax1 = None + self.ax2 = None + self.ax3 = None + self.ax4 = None + self.ax4_plots = None + self.ax4_com = None + self.ax4_rlv = None + self.legends = None + self.points_ref = None + self.points_raw = None + self.line_raw = None + self.line_list = [] + self.line_pixels = [] + self.line_angstroms = [] + self.ref_filling_value = 1000 + self.raw_filling_value = 1000 + self.events = True + self.first = True + self.evaluation_comment = None + # self.binning = self.lamp.header[] + self.pixel_center = [] + """this data must come parsed""" + # self.science_pack = sci_pack + # self.sci_filename = self.science_object.file_name + # self.history_of_lamps_solutions = {} + self._reference_lamp = None + self.reference_solution = None + + def __call__(self, + ccd: CCDData, + comparison_lamp: CCDData, + save_data_to, + reference_data, + object_number=None, + output_prefix='w', + wsolution_obj=None, + linearize=False, + plot_results=False, + save_plots=False, + plots=False): + """Call method for the WavelengthSolution Class + + It takes extracted data and produces wavelength calibrated 1D FITS file. + The call method takes care of the order and logic needed to call the + different methods. A wavelength solution can be recycled for the next + science object. In that case, the wavelength solution is parsed as an + argument and then there is no need to calculate it again. The recycling + part has to be implemented in the caller function. + + Args: + ccd (object): a ccdproc.CCDData instance + comp_list (list): Comparison lamps for the science target that will + be processed here. Every element of this list is an instance of + ccdproc.CCDData. + object_number (int): In case of multiple detections in a single + image this number will be added as a suffix before `.fits` in + order to allow for multiple 1D files. Default value is None. + wsolution_obj (object): Mathematical model of the wavelength + solution if exists. If it doesn't is a None + + Returns: + wavelength_solution (object): The mathematical model of the + wavelength solution. If it fails to create it will return a + None element. + + """ + assert isinstance(ccd, CCDData) + assert isinstance(comparison_lamp, CCDData) + + self._save_data_to = save_data_to + + if os.path.isdir(reference_data): + self.reference_data = ReferenceData(reference_dir=reference_data) + else: + log.error(f"Reference Data Directory provided :{reference_data} does not exist.") + + self.i_fig = None + + log.info('Processing Science Target: ' + '{:s}'.format(ccd.header['OBJECT'])) + + self.lamp = comparison_lamp + + self.line_list = self.reference_data.get_line_list_by_lamp(ccd=self.lamp) + try: + self.calibration_lamp = self.lamp.header['GSP_FNAM'] + except KeyError: + self.calibration_lamp = '' + + self.raw_pixel_axis = range(self.lamp.shape[0]) + self.lamp_name = self.lamp.header['OBJECT'] + + log.info('Processing Comparison Lamp: ' + '{:s}'.format(self.lamp_name)) + self.lines_center = get_lines_in_lamp(ccd=self.lamp) + self.spectral = get_spectral_characteristics( + ccd=self.lamp, + pixel_size=self.pixel_size, + instrument_focal_length=self.goodman_focal_length) + object_name = ccd.header['OBJECT'] + # try: + self._interactive_wavelength_solution( + object_name=object_name) + # except TypeError as error: + # log.error(error) + + if self._wsolution is not None: + if plots or plot_results or save_plots: + + plt.close(1) + if not plots: + plt.ion() + # plt.show() + else: + plt.ioff() + + wavelength_axis = self._wsolution(range(ccd.data.size)) + + object_name = ccd.header['OBJECT'] + grating = ccd.header['GRATING'] + + fig_title = 'Wavelength Calibrated Data : ' \ + '{:s}\n{:s}'.format(object_name, grating) + + fig, ax1 = plt.subplots(1) + fig.canvas.set_window_title(ccd.header['GSP_FNAM']) + # ax1 = fig.add_subplot(111) + manager = plt.get_current_fig_manager() + if plt.get_backend() == u'GTK3Agg': + manager.window.maximize() + elif plt.get_backend() == u'Qt5Agg': + manager.window.showMaximized() + + ax1.set_title(fig_title) + ax1.set_xlabel('Wavelength (Angstrom)') + ax1.set_ylabel('Intensity (ADU)') + ax1.set_xlim((wavelength_axis[0], wavelength_axis[-1])) + # plt.close(1) + + ax1.plot(wavelength_axis, + ccd.data, + color='k', + label='Data') + + ax1.legend(loc='best') + fig.tight_layout() + if save_plots: + log.info('Saving plots') + plots_dir = os.path.join(save_data_to, 'plots') + if not os.path.isdir(plots_dir): + os.mkdir(plots_dir) + plot_name = re.sub('.fits', + '.png', + ccd.header['GSP_FNAM']) + plot_path = os.path.join(plots_dir, plot_name) + # print(plot_path) + plt.savefig(plot_path, dpi=300) + log.info('Saved plot as {:s} file ' + 'DPI=300'.format(plot_name)) + + if plot_results: + plt.show() + else: + plt.draw() + plt.pause(2) + plt.ioff() + + return self._wsolution + else: + log.error('It was not possible to get a wavelength ' + 'solution from this lamp.') + return None + + @property + def reference_lamp(self): + return self._reference_lamp + + @property + def wavelength_solution(self): + return self._wsolution + + @property + def wavelength_solution_evaluation(self): + return self._rms_error, self._n_points, self._n_rejections + + def create_reference_lamp(self): + log.info("Saving as template") + self.lamp = record_wavelength_solution_evaluation(ccd=self.lamp, + rms_error=self._rms_error, + n_points=self._n_points, + n_rejections=self._n_rejections) + + self.reference_data.create_reference_lamp( + ccd=self.lamp, + wavelength_solution=self._wsolution, + lines_pixel=self.line_pixels, + lines_angstrom=self.line_angstroms) + + def get_best_filling_value(self, data): + """Find the best y-value to locate marks + + The autmatically added points will be placed at a fixed location in the + y-axis. This value is calculated by doing a 2-sigma clipping with 5 + iterations. Then the masked out values are removed and the median is + calculated. + + Args: + data (array): Array of 1D data + + Returns: + Median value of clipped data. + + """ + clipped_data = sigma_clip(data, sigma=2, maxiters=5) + clean_data = clipped_data[~clipped_data.mask] + log.debug("Found best filling value" + " at {:f}".format(np.median(clean_data))) + return np.median(clean_data) + + def recenter_lines(self, data, lines, plots=False): + """Finds the centroid of an emission line + + For every line center (pixel value) it will scan left first until the + data stops decreasing, it assumes it is an emission line and then will + scan right until it stops decreasing too. Defined those limits it will + use the line data in between and calculate the centroid. + + Notes: + This method is used to recenter relatively narrow lines only, there + is a special method for dealing with broad lines. + + Args: + data (array): numpy.ndarray instance. or the data attribute of a + ccdproc.CCDData instance. + lines (list): A line list in pixel values. + plots (bool): If True will plot spectral line as well as the input + center and the recentered value. + + Returns: + A list containing the recentered line positions. + + """ + new_center = [] + x_size = data.shape[0] + median = np.median(data) + for line in lines: + # TODO (simon): Check if this definition is valid, so far is not + # TODO (cont..): critical + left_limit = 0 + right_limit = 1 + condition = True + left_index = int(line) + + while condition and left_index - 2 > 0: + + if (data[left_index - 1] > data[left_index]) and \ + (data[left_index - 2] > data[left_index - 1]): + + condition = False + left_limit = left_index + + elif data[left_index] < median: + condition = False + left_limit = left_index + + else: + left_limit = left_index + + left_index -= 1 + + # id right limit + condition = True + right_index = int(line) + while condition and right_index + 2 < x_size - 1: + + if (data[right_index + 1] > data[right_index]) and \ + (data[right_index + 2] > data[right_index + 1]): + + condition = False + right_limit = right_index + + elif data[right_index] < median: + condition = False + right_limit = right_index + + else: + right_limit = right_index + right_index += 1 + index_diff = [abs(line - left_index), abs(line - right_index)] + + sub_x_axis = range(line - min(index_diff), + (line + min(index_diff)) + 1) + + sub_data = data[line - min(index_diff):(line + min(index_diff)) + 1] + centroid = np.sum(sub_x_axis * sub_data) / np.sum(sub_data) + + # checks for asymmetries + differences = [abs(data[line] - data[left_limit]), + abs(data[line] - data[right_limit])] + + if max(differences) / min(differences) >= 2.: + if plots: + plt.axvspan(line - 1, line + 1, color='g', alpha=0.3) + new_center.append(line) + else: + new_center.append(centroid) + if plots: + fig = plt.figure(9) + fig.canvas.set_window_title('Lines Detected in Lamp') + plt.axhline(median, color='b') + + plt.plot(self.raw_pixel_axis, + data, + color='k', + label='Lamp Data') + + for line in lines: + + plt.axvline(line + 1, + color='k', + linestyle=':', + label='First Detected Center') + + for center in new_center: + + plt.axvline(center, + color='k', + linestyle='.-', + label='New Center') + + plt.show() + return new_center + + @staticmethod + def recenter_broad_lines(lamp_data, lines, order): + """Recenter broad lines + + Notes: + This method is used to recenter broad lines only, there is a special + method for dealing with narrower lines. + + Args: + lamp_data (array): numpy.ndarray instance. It contains the lamp + data. + lines (list): A line list in pixel values. + order (float): A rough estimate of the FWHM of the lines in pixels + in the data. It is calculated using the slit size divided by the + pixel scale multiplied by the binning. + + Returns: + A list containing the recentered line positions. + + """ + # TODO (simon): use slit size information for a square function + # TODO (simon): convolution + new_line_centers = [] + gaussian_kernel = Gaussian1DKernel(stddev=2.) + lamp_data = convolve(lamp_data, gaussian_kernel) + for line in lines: + lower_index = max(0, int(line - order)) + upper_index = min(len(lamp_data), int(line + order)) + lamp_sample = lamp_data[lower_index:upper_index] + x_axis = np.linspace(lower_index, upper_index, len(lamp_sample)) + line_max = np.max(lamp_sample) + + gaussian_model = models.Gaussian1D(amplitude=line_max, + mean=line, + stddev=order) + + fit_gaussian = fitting.LevMarLSQFitter() + fitted_gaussian = fit_gaussian(gaussian_model, x_axis, lamp_sample) + new_line_centers.append(fitted_gaussian.mean.value) + # if self.args.debug_mode: + # plt.plot(x_axis, lamp_sample) + # plt.plot(x_axis, gaussian_model(x_axis)) + # plt.plot(x_axis, fitted_gaussian(x_axis), color='k') + # plt.axvline(line) + # plt.show() + return new_line_centers + + def recenter_line_by_data(self, data_name, x_data): + """Finds a better center for a click-selected line + + This method is called by another method that handles click events. An + argument is parsed that will tell which plot was clicked and what is + the x-value in data coordinates. Then the closest pixel center will be + found and from there will extract a 20 pixel wide sample of the data + (this could be a future improvement: the width of the extraction should + depend on the FWHM of the lines). The sample of the data is used to + calculate a centroid (center of mass) which is a good approximation but + could be influenced by data shape or if the click was too far + (unquantified yet). That is why for the reference data, a database of + laboratory line center will be + used and for raw data the line centers are calculated earlier in the + process, independently of any human input. + + It wil also plot the sample, the centroid and the reference line center + at the fourth subplot, bottom right corner. + + Args: + data_name (str): 'reference' or 'raw-data' is where the click was + done + x_data (float): click x-axis value in data coordinates + + Returns: + reference_line_value (float): The value of the line center that will + be used later to do the wavelength fit + + """ + if data_name == 'reference': + print(f"{self.reference_solution=}") + + reference_line_index = np.argmin( + abs(self.line_list - x_data)) + + reference_line_value = self.line_list[reference_line_index] + + if not self.reference_solution: + return reference_line_value + + pseudo_center = np.argmin(abs(self.reference_solution[0] - x_data)) + + sub_x = self.reference_solution[0][ + pseudo_center - 10: pseudo_center + 10] + + sub_y = self.reference_solution[1][ + pseudo_center - 10: pseudo_center + 10] + + center_of_mass = np.sum(sub_x * sub_y) / np.sum(sub_y) + # print 'centroid ', center_of_mass + # plt.figure(3) + # if self.ax4_plots is not None or self.ax4_com is not None or + # self.ax4_rlv is not None: + try: + self.ax4.cla() + self.ax4.relim() + except NameError as err: + log.error(err) + + self.ax4.set_title('Reference Data Clicked Line') + self.ax4.set_xlabel('Wavelength (Angstrom)') + self.ax4.set_ylabel('Intensity (Counts)') + + self.ax4_plots = self.ax4.plot(sub_x, + sub_y, + color='k', + label='Data') + + self.ax4_rlv = self.ax4.axvline(reference_line_value, + linestyle='-', + color='r', + label='Reference Line Value') + + self.ax4_com = self.ax4.axvline(center_of_mass, + linestyle='--', + color='b', + label='Centroid') + + self.ax4.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e')) + self.ax4.legend(loc=3, framealpha=0.5) + self.i_fig.canvas.draw() + # return center_of_mass + return reference_line_value + elif data_name == 'raw-data': + pseudo_center = np.argmin(abs(self.raw_pixel_axis - x_data)) + raw_line_index = np.argmin(abs(self.lines_center - x_data)) + raw_line_value = self.lines_center[raw_line_index] + # print(raw_line_value) + sub_x = self.raw_pixel_axis[pseudo_center - 10: pseudo_center + 10] + sub_y = self.lamp.data[pseudo_center - 10: pseudo_center + 10] + center_of_mass = np.sum(sub_x * sub_y) / np.sum(sub_y) + # print 'centroid ', center_of_mass + # plt.figure(3) + # if self.ax4_plots is not None or self.ax4_com is not None or + # self.ax4_rlv is not None: + try: + self.ax4.cla() + self.ax4.relim() + except NameError as err: + log.error(err) + self.ax4.set_title('Raw Data Clicked Line') + self.ax4.set_xlabel('Pixel Axis') + self.ax4.set_ylabel('Intensity (Counts)') + + self.ax4_plots = self.ax4.plot(sub_x, + sub_y, + color='k', + label='Data') + + self.ax4_rlv = self.ax4.axvline(raw_line_value, + linestyle='-', + color='r', + label='Line Center') + self.ax4_com = self.ax4.axvline(center_of_mass, + linestyle='--', + color='b', + label='Centroid') + + self.ax4.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e')) + self.ax4.legend(loc=3, framealpha=0.5) + self.i_fig.canvas.draw() + # return center_of_mass + return raw_line_value + else: + log.error('Unrecognized data name') + + def _interactive_wavelength_solution(self, object_name=''): + """Find the wavelength solution interactively + + This method uses the graphical capabilities of matplotlib in particular + the user interface (UI) capabilities such as click and key-pressed + events. We could say that it implements a matplotlib Graphical User + Interface. There are some limitation though, for instance, the access to + a better formatted help text. + + It will display four plots of the same height but their width have a + 4:1 ratio being the leftmost the wider. + + Top Left Plot: Displays the spectrum of the lamp we want to calibrate, + or as it is called in the plot, the Raw Data, therefore the axis are + intensity (ADU) versus dispersion (pixels). There will be red vertical + dashed lines that are lines detected by pipeline. If you see the a line + without a red dashed line it means it was not detected. + + Bottom Left Plot: Displays the reference lamp data, therefore the axis + are intensity (ADU) versus wavelength (Angstrom). In this case there are + vertical dotted lines plotted with a relatively low alpha value. They + represent reference laboratory lines obtained from the NIST database at + https://physics.nist.gov/PhysRefData/ASD/lines_form.html. + + Top Right Plot: This is a quick reference help text. The formatting of + the text is quite difficult here due to matplotlib's own limitations. + This text is also displayed on the terminal. + + Bottom Right Plot: This is a dynamic plot since it shows different kind + of information. First it shows short messages, such as warnings, errors, + or general information. Once you select a line it will show a zoomed + version of the line plus the reference value chosen as well as were the + click was done. And finally, it shows the dispersion of the wavelength + solution. This could be one of the most useful plots in the entire + screen. + + Usage: A quick reference of the instructions is shown in the Top Right + Plot, but in general you have to select the matching lines in both data + plots this will create a table of pixels versus angstrom which at its + roots a wavelength solution. Then this will have to be translated to a + mathematical model. Having this mathematical model the wavelength + solution can be applied, evaluated etc. Among the features implemented + you can: + - Delete a line or a matching pair. + - Find more lines automatically once you have a decent wavelength + solution. + - Remove all the automatically added lines at once, this is useful in + some cases when adding more lines actually makes the solution worst. + - Find the RMS Error of the solution + - Remove ALL the points at a given point to start over. + - Fit a wavelength solution + + + + Notes: + This method uses the Qt5Agg backend, in theory it could also work + with GTK3Agg but it is being forced to use Qt5Agg. + + """ + log.debug("Starting interactive wavelength calibration") + plt.switch_backend('Qt5Agg') + + # disable full screen to allow the use of f for fitting the solution + + plt.rcParams['keymap.fullscreen'] = [u'ctrl+f'] + + try: + self._reference_lamp = self.reference_data.get_reference_lamp( + header=self.lamp.header) + except NotImplementedError: + self._reference_lamp = self.reference_data.get_reference_lamps_by_name( + lamp_name=self.lamp.header['OBJECT']) + log.warning('Could not find a perfect match for reference ' + 'data') + # reference_file = None + # log.critical('Could not find a comparison lamp in the + # reference.') + + # reference_file = self.reference_data.get_reference_lamps_by_name( + # self.lamp_name) + + if self._reference_lamp is not None: + log.info('Using reference file: {:s}'.format(self._reference_lamp.header['GSP_FNAM'])) + reference_plots_enabled = True + + self.reference_solution = self.wcs.read_gsp_wcs(ccd=self._reference_lamp) + print(self.reference_solution) + else: + reference_plots_enabled = False + log.error('Please Check the OBJECT Keyword of your reference ' + 'data') + + # update filling value + self.raw_filling_value = self.get_best_filling_value( + data=self.lamp.data) + + if self._reference_lamp is not None: + self.ref_filling_value = self.get_best_filling_value(data=self._reference_lamp.data) + + # ------- Plots ------- + self.i_fig, ((self.ax1, self.ax2), (self.ax3, self.ax4)) = \ + plt.subplots(2, + 2, + gridspec_kw={'width_ratios': [4, 1]}) + + self.i_fig.canvas.setWindowTitle('Science Target: {:s}'.format( + object_name)) + + manager = plt.get_current_fig_manager() + if plt.get_backend() == u'GTK3Agg': + manager.window.maximize() + elif plt.get_backend() == u'Qt5Agg': + manager.window.showMaximized() + + self.ax1.set_title('Raw Data - {:s}\n{:s} - {:s}'.format( + self.lamp_name, + self.lamp.header['GRATING'], + self.lamp.header['SLIT'])) + + self.ax1.set_xlabel('Pixels') + self.ax1.set_ylabel('Intensity (counts)') + self.ax1.plot([], linestyle='--', color='r', label='Detected Lines') + for idline in self.lines_center: + self.ax1.axvline(idline, linestyle='--', color='r') + + self.ax1.plot(self.raw_pixel_axis, + self.lamp.data, + color='k', + label='Raw Data') + + self.ax1.set_xlim((0, len(self.lamp.data))) + self.ax1.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e')) + self.ax1.legend(loc=2) + + # Update y limits to have an extra 5% to top and bottom + ax1_ylim = self.ax1.get_ylim() + ax1_y_range = ax1_ylim[1] - ax1_ylim[0] + + self.ax1.set_ylim((ax1_ylim[0] - 0.05 * ax1_y_range, + ax1_ylim[1] + 0.05 * ax1_y_range)) + + self.ax3.set_title('Reference Data - {:s}'.format(self.lamp_name)) + self.ax3.set_xlabel('Wavelength (Angstrom)') + self.ax3.set_ylabel('Intensity (counts)') + self.ax3.set_xlim((self.spectral['blue'].value, self.spectral['red'].value)) + self.ax3.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e')) + + self.ax3.plot([], + linestyle=':', + color='r', + label='Reference Line Values') + + for rline in self.line_list: + self.ax3.axvline(rline, linestyle=':', color='r') + + if reference_plots_enabled: + + self.ax3.plot(self.reference_solution[0], + self.reference_solution[1], + color='k', + label='Reference Lamp Data') + + self.ax3.legend(loc=2) + + # Update y limits to have an extra 5% to top and bottom + ax3_ylim = self.ax3.get_ylim() + ax3_y_range = ax3_ylim[1] - ax3_ylim[0] + + self.ax3.set_ylim((ax3_ylim[0] - 0.05 * ax3_y_range, + ax3_ylim[1] + 0.05 * ax3_y_range)) + # print(ax3_ylim) + + self.display_help_text() + + # zoomed plot + self.display_onscreen_message('Use middle button click to select a ' + 'line') + + plt.subplots_adjust(left=0.05, + right=0.99, + top=0.96, + bottom=0.04, + hspace=0.17, + wspace=0.11) + + self.raw_data_bb = self.ax1.get_position() + self.reference_bb = self.ax3.get_position() + self.contextual_bb = self.ax4.get_position() + + # if self.click_input_enabled: + self.i_fig.canvas.mpl_connect('button_press_event', self._on_click) + self.i_fig.canvas.mpl_connect('key_press_event', self._key_pressed) + # print self._wsolution + plt.show() + return True + + def _on_click(self, event): + """Handles Click events for Interactive Mode + + Calls the method register_mark + + Args: + event (object): Click event + """ + if event.button == 3: + self._register_mark(event) + # TODO (simon): Make sure the text below is useless + # else: + # print(event.button) + # elif event.button == 3: + # if len(self.reference_marks_x) == len(self.raw_data_marks_x): + # self.click_input_enabled = False + # log.info('Leaving interactive mode') + # else: + # if len(self.reference_marks_x) < len(self.raw_data_marks_x): + # log.info('There is {:d} click missing in the ' + # 'Reference plot'.format( + # len(self.raw_data_marks_x) - + # len(self.reference_marks_x))) + # else: + # log.info('There is {:d} click missing in the ' + # 'New Data plot'.format( + # len(self.reference_marks_x) - + # len(self.raw_data_marks_x))) + + def _key_pressed(self, event): + """Key event handler + + There are several key events that need to be taken care of. + See a brief description of each one of them below: + + F1 or ?: Prints a help message + F2 or f: Fit wavelength solution model. + F3 or a: Find new lines. + F4: Evaluate solution + F6 or l: Linearize data although this is done automatically after the + wavelength function is fit + d: deletes closest point + ctrl+d: deletes all recorded marks + ctrl+z: Reverts the action of F3 or a. + Middle Button Click or m: records data location. + Enter: Close figure and apply solution if exists. + Shift+Enter: Close the program with sys.exit(0) + + Notes: + This method must be simplified + + Args: + event (object): Key pressed event + + """ + self.events = True + if event.key == 'f1' or event.key == '?': + log.info('Print help regarding interactive mode') + print("F1 or ?: Prints Help.") + print("F2 or f: Fit wavelength solution model.") + print("F3 or a: Find new lines.") + print("F4: Evaluate solution") + print("F6 or l: Linearize data (for testing not definitive)") + print("d: deletes closest point") + # print("l : resample spectrum to a linear dispersion axis") + print("ctrl+d: deletes all recorded marks") + print("ctrl+t: Save as template") + print("ctrl+z: Go back to previous solution " + "(deletes automatic added points") + print('Right Button Click or p: records data location.') + print("Enter: Close figure and apply solution if exists.") + elif event.key == 'f2' or event.key == 'f': + log.debug('Calling function to fit wavelength Solution') + self.fit_pixel_to_wavelength() + self._plot_raw_over_reference() + elif event.key == 'f3' or event.key == 'a': + if self._wsolution is not None: + self._find_more_lines() + self._update_marks_plot('reference') + self._update_marks_plot('raw_data') + else: + log.debug('Wavelength solution is None') + elif event.key == 'f4': + if self._wsolution is not None and len(self.raw_data_marks_x) > 0: + self.evaluate_solution(plots=True) + elif event.key == 'f5' or event.key == 'd': + # TODO (simon): simplify this code. + + figure_x, figure_y = \ + self.i_fig.transFigure.inverted().transform((event.x, event.y)) + + if self.raw_data_bb.contains(figure_x, figure_y): + log.debug('Deleting raw point') + # print abs(self.raw_data_marks_x - event.xdata) a[:] = + + closer_index = int(np.argmin( + [abs(list_val - event.xdata) for list_val in + self.raw_data_marks_x])) + + # print 'Index ', closer_index + if len(self.raw_data_marks_x) == len(self.reference_marks_x): + self.raw_data_marks_x.pop(closer_index) + self.raw_data_marks_y.pop(closer_index) + self.reference_marks_x.pop(closer_index) + self.reference_marks_y.pop(closer_index) + self._update_marks_plot('reference') + self._update_marks_plot('raw_data') + else: + if closer_index == len(self.raw_data_marks_x) - 1: + self.raw_data_marks_x.pop(closer_index) + self.raw_data_marks_y.pop(closer_index) + self._update_marks_plot('raw_data') + else: + self.raw_data_marks_x.pop(closer_index) + self.raw_data_marks_y.pop(closer_index) + self.reference_marks_x.pop(closer_index) + self.reference_marks_y.pop(closer_index) + self._update_marks_plot('reference') + self._update_marks_plot('raw_data') + + elif self.reference_bb.contains(figure_x, figure_y): + log.debug('Deleting reference point') + # print 'reference ', self.reference_marks_x, self.re + # print self.reference_marks_x + # print abs(self.reference_marks_x - event.xdata) + + closer_index = int(np.argmin( + [abs(list_val - event.xdata) for list_val in + self.reference_marks_x])) + + if len(self.raw_data_marks_x) == len(self.reference_marks_x): + self.raw_data_marks_x.pop(closer_index) + self.raw_data_marks_y.pop(closer_index) + self.reference_marks_x.pop(closer_index) + self.reference_marks_y.pop(closer_index) + self._update_marks_plot('reference') + self._update_marks_plot('raw_data') + else: + if closer_index == len(self.reference_marks_x) - 1: + self.reference_marks_x.pop(closer_index) + self.reference_marks_y.pop(closer_index) + self._update_marks_plot('reference') + else: + self.raw_data_marks_x.pop(closer_index) + self.raw_data_marks_y.pop(closer_index) + self.reference_marks_x.pop(closer_index) + self.reference_marks_y.pop(closer_index) + self._update_marks_plot('reference') + self._update_marks_plot('raw_data') + + elif self.contextual_bb.contains(figure_x, figure_y): + log.warning("Can't delete points from here because points " + "represent each detected line in the raw data.") + # closer_index_ref = int(np.argmin( + # [abs(list_val - event.ydata) for list_val in + # self.reference_marks_x])) + # + # closer_index_raw = int(np.argmin( + # [abs(list_val - event.xdata) for list_val in + # self.raw_data_marks_x])) + # + # log.debug('Raw Index {:d}, Ref Index {:d}'.format( + # closer_index_raw, + # closer_index_ref)) + # self.raw_data_marks_x.pop(closer_index_raw) + # self.raw_data_marks_y.pop(closer_index_raw) + # self.reference_marks_x.pop(closer_index_raw) + # self.reference_marks_y.pop(closer_index_raw) + # self.update_marks_plot('reference') + # self.update_marks_plot('raw_data') + # self.update_marks_plot('eval_plots') + # elif event.key == 'i': + # figure_x, figure_y = self.i_fig.transFigure.inverted().transform( + # (event.x, event.y)) + # if self.contextual_bb.contains(figure_x, figure_y): + # log.debug("Trying to identify point.") + # + # closer_x_index = int(np.argmin( + # [abs(list_val - event.xdata) for list_val in + # self.raw_data_marks_x])) + # print(self.raw_data_marks_x[closer_x_index]) + # print(self.reference_marks_x[closer_x_index]) + + elif event.key == 'f6' or event.key == 'l': + log.info('Linearize and smoothing spectrum') + if self._wsolution is not None: + self.linearize_spectrum(self.lamp.data, plots=True) + + elif event.key == 'ctrl+z': + log.info('Deleting automatic added points. If exist.') + + if self.raw_data_marks_x is not [] and \ + self.reference_marks_x is not []: + + to_remove = [] + for i in range(len(self.raw_data_marks_x)): + # print self.raw_data_marks[i], self.filling_value + if self.raw_data_marks_y[i] == self.raw_filling_value: + to_remove.append(i) + # print to_remove + to_remove = np.array(sorted(to_remove, reverse=True)) + if len(to_remove) > 0: + for index in to_remove: + self.raw_data_marks_x.pop(index) + self.raw_data_marks_y.pop(index) + self.reference_marks_x.pop(index) + self.reference_marks_y.pop(index) + self._update_marks_plot('reference') + self._update_marks_plot('raw_data') + # else: + # print self.raw_click_plot, self.ref_click_plot, 'mmm' + + elif event.key == 'ctrl+d': + try: + log.info('Deleting all recording Clicks') + self.display_onscreen_message( + message='All points deleted') + self.reference_marks_x = [] + self.reference_marks_y = [] + self.raw_data_marks_x = [] + self.raw_data_marks_y = [] + self._update_marks_plot('delete') + self._plot_raw_over_reference(remove=True) + log.info('All points deleted!') + except: + log.error('No points deleted') + elif event.key == 'ctrl+t': + self.create_reference_lamp() + + elif event.key == 'p': + self._register_mark(event) + + elif event.key == 'enter': + if self._wsolution is not None: + log.info('Closing figure') + plt.close('all') + else: + message = 'There is still no wavelength solution!' + log.info(message) + self.display_onscreen_message(message) + + elif event.key == 'm': + self._register_mark(event) + + elif event.key in ['ctrl+q', 'ctrl+c']: + log.info('Pressed Ctrl+q. Closing the program') + sys.exit(0) + + else: + log.debug("No action for key pressed: {:s}".format(event.key)) + pass + + def _register_mark(self, event): + """Marks a line + + Detects where the click was done or m-key was pressed and calls the + corresponding method. It handles the middle button click and m-key being + pressed. There are two regions of interest as for where a click was + done. The raw and reference data respectively. For any of such regions + it will call the method that recenter the line and once the desired + value is returned it will be appended to the list that contains all the + correspondent line positions, raw (pixels) and reference (angstrom) + + Args: + event (object): Click or m-key pressed event + """ + if event.xdata is not None and event.ydata is not None: + figure_x, figure_y = \ + self.i_fig.transFigure.inverted().transform((event.x, event.y)) + + if self.reference_bb.contains(figure_x, figure_y): + # self.reference_marks.append([event.xdata, event.ydata]) + self.reference_marks_x.append( + self.recenter_line_by_data('reference', event.xdata)) + + self.reference_marks_y.append(event.ydata) + self._update_marks_plot('reference') + elif self.raw_data_bb.contains(figure_x, figure_y): + # self.raw_data_marks.append([event.xdata, event.ydata]) + self.raw_data_marks_x.append( + self.recenter_line_by_data('raw-data', event.xdata)) + + self.raw_data_marks_y.append(event.ydata) + self._update_marks_plot('raw_data') + else: + log.debug('{:f} {:f} Are not contained'.format(figure_x, + figure_y)) + else: + log.error('Clicked Region is out of boundaries') + + def _find_more_lines(self): + """Method to add more lines given that a wavelength solution already + exists + + This method is part of the interactive wavelength solution mechanism. + If a wavelength solution exist it uses the line centers in pixels to + estimate their respective wavelength and then search for the closest + value in the list of reference lines for the elements in the comparison + lamp. Then it filters the worst of them by doing sigma clipping. + Finally it adds them to the class' attributes that contains the list of + reference points. + + Better results are obtained if the solution is already decent. Visual + inspection also improves final result. + """ + new_physical = [] + new_wavelength = [] + square_differences = [] + if self._wsolution is not None: + wlines = self._wsolution(self.lines_center) + for i in range(len(wlines)): + # [abs(list_val - wlines[i]) for list_val in \ + # self.reference_data.get_line_list_by_name(self.lamp_name)] + + closer_index = np.argmin( + abs(self.line_list - wlines[i])) + + rline = self.line_list[closer_index] + + rw_difference = wlines[i] - rline + # print('Difference w - r ', rw_difference, rline) + square_differences.append(rw_difference ** 2) + new_physical.append(self.lines_center[i]) + new_wavelength.append(rline) + clipped_differences = sigma_clip(square_differences, + sigma=2, + maxiters=3) + + if len(new_wavelength) == len(new_physical) == \ + len(clipped_differences): + + for i in range(len(new_wavelength)): + if clipped_differences[i] is not \ + np.ma.masked and new_wavelength[i] not in \ + self.reference_marks_x: + + self.reference_marks_x.append(new_wavelength[i]) + self.reference_marks_y.append(self.ref_filling_value) + self.raw_data_marks_x.append(new_physical[i]) + self.raw_data_marks_y.append(self.raw_filling_value) + return True + + def _update_marks_plot(self, action=None): + """Update the points that represent marks on lamp plots + + When you mark a line a red dot marks the position of the line at the + exact y location of the click, for the x location it will use the value + obtained by means of the recentering method. There are three possible + actions: Update the reference plot's click, the raw data marks or + delete them all. + + Args: + action (str): A string that could be 'reference', 'raw_data' or + 'delete' depending on the action desired + """ + # print(type(action), type(pixel_axis), type(differences)) + if action == 'reference': + if self.points_ref is not None: + try: + self.points_ref.remove() + self.ax3.relim() + log.debug('Removing reference marks') + except: + log.debug('Reference points is None') + pass + log.debug("Plot new marks") + self.points_ref, = self.ax3.plot(self.reference_marks_x, + self.reference_marks_y, + linestyle='None', + marker='o', + color='r') + self.i_fig.canvas.draw() + elif action == 'raw_data': + # print self.points_raw + # print dir(self.points_raw) + if self.points_raw is not None: + try: + self.points_raw.remove() + self.ax1.relim() + except ValueError as err: + log.error(err) + self.points_raw, = self.ax1.plot(self.raw_data_marks_x, + self.raw_data_marks_y, + linestyle='None', + marker='o', + color='r') + self.i_fig.canvas.draw() + elif action == 'delete': + if self.points_raw is not None and self.points_ref is not None: + self.points_raw.remove() + self.ax1.relim() + self.points_ref.remove() + self.ax3.relim() + self.i_fig.canvas.draw() + else: + log.error('Unknown Action {:s}'.format(action)) + + def _plot_raw_over_reference(self, remove=False): + """Overplot raw data over reference lamp using current wavelength + solution model + + Once the wavelength solution is obtained this method is called to apply + the already mentioned solution to the raw data and then overplot it on + the reference lamp plot. This is very useful to have a visual idea of + how far or close the solution is. + + Args: + remove (bool): True or False depending whether you want to remove + the overplotted lamp or not + """ + if self._wsolution is not None: + if self.line_raw is not None: + try: + self.line_raw.remove() + self.ax3.relim() + except: + pass + if not remove: + # TODO(simon): catch TypeError Exception and correct what is + # TODO (cont): causing it + self.line_raw, = self.ax3.plot( + self._wsolution(self.raw_pixel_axis), + self.lamp.data, + linestyle='-', + color='r', + alpha=0.4, + label='New Wavelength Solution') + + self.ax3.legend(loc=2) + self.i_fig.canvas.draw() + + def evaluate_solution(self, plots=False): + """Calculate the Root Mean Square Error of the solution + + Once the wavelength solution is obtained it has to be evaluated. The + line centers found for the raw comparison lamp will be converted to, + according to the new solution, angstrom. Then for each line the closest + reference line value is obtained. The difference is stored. Then these + differences are cleaned by means of a sigma clipping method that will + rule out any outlier or any line that is not well-matched. Then, using + the sigma clipped differences the Root Mean Square error is calculated. + + It also creates a plot in the bottom right subplot of the interactive + window, showing a scatter plot plus some information regarding the + quality of the fit. + + Args: + plots (bool): Whether to create the plot or not + + Returns: + results (list): Contains three elements: _rms_error (float), + npoints (int), _n_rejections (int) + + """ + if self._wsolution is not None: + differences = np.array([]) + wavelength_line_centers = self._wsolution(self.lines_center) + + for wline in wavelength_line_centers: + closer_index = np.argmin( + abs(self.line_list - wline)) + + rline = self.line_list[closer_index] + + rw_difference = wline - rline + differences = np.append(differences, rw_difference) + + clipping_sigma = 2. + + clipped_differences = differences + once_clipped_differences = differences + + old_rms_error = None + if self._rms_error is not None: + old_rms_error = float(self._rms_error) + + self._rms_error, self._n_points, self._n_rejections = evaluate_wavelength_solution( + clipped_differences=clipped_differences) + + log.info('RMS Error : {:.3f}'.format(self._rms_error)) + + if plots: + if self.ax4_plots is not None or \ + self.ax4_com is not None or \ + self.ax4_rlv is not None: + + try: + self.ax4.cla() + self.ax4.relim() + except NameError as err: + log.error(err) + + self.ax4.set_title('RMS Error {:.3f} \n' + '{:d} points ({:d} ' + 'rejected)'.format(self._rms_error, + self._n_points, + self._n_rejections)) + + self.ax4.set_ylim(once_clipped_differences.min(), + once_clipped_differences.max()) + + self.ax4.set_xlim(np.min(self.lines_center), + np.max(self.lines_center)) + + self.ax4_rlv = self.ax4.scatter(self.lines_center, + differences, + marker='x', + color='k', + label='Rejected Points') + + self.ax4_com = self.ax4.axhspan(clipped_differences.min(), + clipped_differences.max(), + color='k', + alpha=0.4, + edgecolor=None, + label='{:.1f} Sigma'.format( + clipping_sigma)) + + self.ax4_plots = self.ax4.scatter(self.lines_center, + clipped_differences, + label='Differences') + + if self._rms_error is not None and old_rms_error is not None: + rms_error_difference = self._rms_error - old_rms_error + + if rms_error_difference > 0.001: + increment_color = 'red' + elif rms_error_difference < -0.001: + increment_color = 'green' + else: + increment_color = 'white' + + message = r'$\Delta$ RMSE {:+.3f}'.format( + rms_error_difference) + + self.ax4.text(0.05, 0.95, + message, + verticalalignment='top', + horizontalalignment='left', + transform=self.ax4.transAxes, + color=increment_color, + fontsize=15) + + self.ax4.set_xlabel('Pixel Axis (Pixels)') + self.ax4.set_ylabel('Difference (Angstroms)') + + self.ax4.legend(loc=3, framealpha=0.5) + self.i_fig.canvas.draw() + + results = [self._rms_error, self._n_points, self._n_rejections] + return results + else: + log.error('Solution is still non-existent!') + + def fit_pixel_to_wavelength(self): + """Does the fit to find the wavelength solution + + Once you have four data points on each side (raw and reference or pixel + and angstrom) it calculates the fit using a Chebyshev model of third + degree. This was chosen because it worked better compared to the rest. + There is a slight deviation from linearity in all Goodman data, + therefore a linear model could not be used, also is said that a Spline + of third degree is "too flexible" which I also experienced and since the + deviation from linearity is not extreme it seemed that it was not + necessary to implement. + + This method checks that the data that will be used as input to calculate + the fit have the same dimensions and warns the user in case is not. + + Returns: + None (None): An empty return is created to finish the execution of + the method when a fit will not be possible + + """ + if len(self.reference_marks_x) and len(self.raw_data_marks_x) > 0: + + if len(self.reference_marks_x) < 4 or \ + len(self.raw_data_marks_x) < 4: + + message = 'Not enough marks! Minimum 4 each side.' + self.display_onscreen_message(message) + return + + if len(self.reference_marks_x) != len(self.raw_data_marks_x): + if len(self.reference_marks_x) < len(self.raw_data_marks_x): + n = len(self.raw_data_marks_x) - len(self.reference_marks_x) + if n == 1: + message_text = '{:d} Reference Click is ' \ + 'missing!.'.format(n) + else: + message_text = '{:d} Reference Clicks are ' \ + 'missing!.'.format(n) + else: + n = len(self.reference_marks_x) - len(self.raw_data_marks_x) + if n == 1: + message_text = '{:d} Raw Click is missing!.'.format(n) + else: + message_text = '{:d} Raw Clicks are missing!.'.format(n) + self.display_onscreen_message(message_text) + else: + self.line_pixels = [] + self.line_angstroms = [] + for i in range(len(self.reference_marks_x)): + self.line_pixels.append(self.raw_data_marks_x[i]) + self.line_angstroms.append(self.reference_marks_x[i]) + + self._wsolution = self.wcs.fit(physical=self.line_pixels, + wavelength=self.line_angstroms, + model_name='chebyshev', + degree=self.poly_order) + + self.evaluate_solution(plots=True) + + else: + log.error('Clicks record is empty') + self.display_onscreen_message(message='Clicks record is empty') + if self._wsolution is not None: + self._wsolution = None + + def add_gsp_wcs(self): + pass + + def display_onscreen_message(self, message='', color='red'): + """Uses the fourth subplot to display a message + + Displays a warning message on the bottom right subplot of the + interactive window. It is capable to break down the message in more + than one line if necessary. + + Args: + message (str): The message to be displayed + color (str): Color name for the font's color + + """ + full_message = [message] + if len(message) > 30: + full_message = [] + split_message = message.split(' ') + line_length = 0 + # new_line = '' + e = 0 + for i in range(len(split_message)): + # print(i, len(split_message)) + line_length += len(split_message[i]) + 1 + if line_length >= 30: + new_line = ' '.join(split_message[e:i]) + # print(new_line, len(new_line)) + full_message.append(new_line) + # new_line = '' + line_length = 0 + e = i + if i == len(split_message) - 1: + new_line = ' '.join(split_message[e:]) + # print(new_line, len(new_line)) + full_message.append(new_line) + + self.ax4.cla() + self.ax4.relim() + self.ax4.set_xticks([]) + self.ax4.set_yticks([]) + self.ax4.set_title('Message') + for i in range(len(full_message)): + self.ax4.text(0.05, 0.95 - i * 0.05, + full_message[i], + verticalalignment='top', + horizontalalignment='left', + transform=self.ax4.transAxes, + color=color, + fontsize=15) + self.i_fig.canvas.draw() + + return + + def display_help_text(self): + """Shows static text on the top right subplot + + This will print static help text on the top right subplot of the + interactive window. + + Notes: + This is really hard to format and having a proper GUI should help + to have probably richer formatted text on the screen. + + """ + self.ax2.set_title('Help') + self.ax2.set_xticks([]) + self.ax2.set_yticks([]) + + self.ax2.text(1, 11, 'F1 or ?:', + fontsize=13) + + self.ax2.text(1.46, 11, 'Prints Help.', + fontsize=13) + + self.ax2.text(1, 10.5, 'F2 or f:', + fontsize=13) + + self.ax2.text(1.46, 10.5, 'Fit Wavelength Solution to points', + fontsize=13) + + self.ax2.text(1.46, 10, 'collected', + fontsize=13) + + self.ax2.text(1, 9.5, 'F3 or a:', + fontsize=13) + + self.ax2.text(1.46, 9.5, 'Find new lines, use when the solution', + fontsize=13) + + self.ax2.text(1.46, 9, 'is already decent.', + fontsize=13) + + self.ax2.text(1, 8.5, 'F4:', + fontsize=13) + + self.ax2.text(1.46, 8.5, 'Evaluate Solution', + fontsize=13) + + self.ax2.text(1, 8, 'F6 or l:', + fontsize=13) + + self.ax2.text(1.46, 8, 'Linearize Data', + fontsize=13) + + self.ax2.text(1, 7.5, 'd :', + fontsize=13) + + self.ax2.text(1.46, 7.5, 'Delete Closest Point', + fontsize=13) + + self.ax2.text(1, 7, 'Ctrl+d:', + fontsize=13) + + self.ax2.text(1.46, 7, 'Delete all recorded marks.', + fontsize=13) + + self.ax2.text(1, 6.5, 'Ctrl+t:', + fontsize=13) + + self.ax2.text(1.46, 6.5, 'Save as reference lamp template.', + fontsize=13) + + self.ax2.text(1, 6, 'Ctrl+z:', + fontsize=13) + + self.ax2.text(1.46, 6, 'Remove all automatic added points.', + fontsize=13) + + self.ax2.text(1.46, 5.5, 'Undo what F3 does.', + fontsize=13) + + self.ax2.text(1, 5, 'Middle Button or p:', + fontsize=13) + + self.ax2.text(1.46, 4.5, 'Finds and records line position', + fontsize=13) + + self.ax2.text(1, 4, 'Enter :', + fontsize=13) + + self.ax2.text(1.46, 4, 'Close Figure and apply wavelength', + fontsize=13) + + self.ax2.text(1.46, 3.5, 'solution', + fontsize=13) + + self.ax2.set_ylim((0, 12)) + self.ax2.set_xlim((0.95, 3.5)) diff --git a/goodman_pipeline/spectroscopy/redspec.py b/goodman_pipeline/spectroscopy/redspec.py index 3afeaf6e..f2b12af9 100755 --- a/goodman_pipeline/spectroscopy/redspec.py +++ b/goodman_pipeline/spectroscopy/redspec.py @@ -17,7 +17,6 @@ from .wavelength import WavelengthCalibration from ..core import (classify_spectroscopic_data, search_comp_group, - add_wcs_keys, identify_targets, trace_targets, extraction, @@ -172,6 +171,16 @@ def get_args(arguments=None): "discriminate usable targets. Default 3 times " "background level") + parser.add_argument('--interactive-wavelength', + action='store_true', + dest='interactive_wavelength', + help="Interactive Wavelength Calibration.") + + parser.add_argument('--linearize', + action='store_true', + dest='linearize', + help="Save as a linear wavelength solution, requires resampling.") + parser.add_argument('--save-plots', action='store_true', dest='save_plots', @@ -237,16 +246,16 @@ def get_args(arguments=None): return args -class MainApp(object): +class RedSpec(object): """Defines and initialize all important variables for processing the data - The MainApp class controls the way the night is organized for further + The RedSpec class controls the way the night is organized for further processing. It also sets the appropriate parameters that will allow for a smooth working in all the other modules. """ def __init__(self): - """Init method for MainApp class + """Init method for RedSpec class This method initializes the arguments for the class, if they are not provided it will get them. @@ -261,7 +270,7 @@ def __init__(self): self._pipeline_version = __version__ def __call__(self, args=None): - """Call method for the MainApp class + """Call method for the RedSpec class This method call the higher level functions in order to do the spectroscopic data reduction. @@ -295,7 +304,7 @@ def __call__(self, args=None): else: self.log.debug("Received non-empty data container.") - self.log.debug("Calling _run method for MainApp") + self.log.debug("Calling _run method for RedSpec") self._run(data_container=data_container, extraction_type=self.args.extraction_type, target_fit_model=self.args.target_fit_model, @@ -328,7 +337,9 @@ def _run(self, obj_groupby = object_group.groupby(['object']).size( ).reset_index().rename(columns={0: 'count'}) - self.log.info("Processing Science Target: " + self.log.debug("Here is where the process starts") + + self.log.info("Starting process for Science Target: " "{:s} with {:d} files." "".format(obj_groupby.iloc[0]['object'], obj_groupby.iloc[0]['count'])) @@ -342,7 +353,7 @@ def _run(self, comp_group = self.reference.check_comp_group(comp_group) if comp_group is None: - self.log.debug('Group does not have comparison lamps') + self.log.warning('Group does not have comparison lamps') if data_container.comp_groups is not None: self.log.debug('There are comparison lamp group ' 'candidates') @@ -380,16 +391,15 @@ def _run(self, file_path = os.path.join(full_path, spec_file) ccd = CCDData.read(file_path, unit=u.adu) ccd.header.set('GSP_PNAM', value=spec_file) - ccd = add_wcs_keys(ccd=ccd) # ccd.header['GSP_FNAM'] = spec_file if comp_group is not None and comp_ccd_list == []: for comp_file in comp_group.file.tolist(): + self.log.debug(f"Preparing comparison lamp file {comp_file} for processing.") comp_path = os.path.join(full_path, comp_file) comp_ccd = CCDData.read(comp_path, unit=u.adu) - comp_ccd = add_wcs_keys(ccd=comp_ccd) comp_ccd.header.set('GSP_PNAM', value=comp_file) comp_ccd_list.append(comp_ccd) @@ -399,8 +409,7 @@ def _run(self, 'Comp Group is None or comp list already exist') # identify - self.log.debug("Calling procedure for target " - "identification.") + self.log.info(f"Starting target identification for file {spec_file}") target_list = identify_targets( ccd=ccd, fit_model=target_fit_model, @@ -424,12 +433,20 @@ def _run(self, continue # if len(trace_list) > 0: + # Experimental interactive extraction + # if False: + # ie = InteractiveExtraction() + # # ie = InteractiveExtractionGUI() + # ie(ccd=ccd, lamps=comp_ccd_list, traces=trace_list) + extracted_target_and_lamps = [] for single_trace, single_profile, trace_info in trace_list: if single_profile.__class__.name == 'Gaussian1D': single_profile_center = single_profile.mean.value elif single_profile.__class__.name == 'Moffat1D': single_profile_center = single_profile.x_0.value + self.log.info(f"Processing traced profile fitted wit {single_profile.__class__.name} " + f"centered at {single_profile_center}") if len(trace_list) > 1: target_number = trace_list.index( @@ -454,6 +471,7 @@ def _run(self, # print(spec_file) # lamp extraction + self.log.debug(f"Starting lamps extraction for science file {spec_file}") all_lamps = [] if comp_ccd_list: for comp_lamp in comp_ccd_list: @@ -525,6 +543,7 @@ def _run(self, self.log.error('No target was identified in file' ' {:s}'.format(spec_file)) continue + self.log.debug(f"Starting Wavelength Calibration Process for File {spec_file}.") object_number = None for sci_target, comp_list in extracted_target_and_lamps: try: @@ -536,18 +555,22 @@ def _run(self, reference_data=self.args.reference_dir, object_number=object_number, output_prefix=self.args.output_prefix, + interactive_wavelength=self.args.interactive_wavelength, + linearize=self.args.linearize, plot_results=self.args.plot_results, save_plots=self.args.save_plots, plots=self.args.debug_with_plots) except NoMatchFound as no_match_error: self.log.error(no_match_error) - except NotImplemented as error: + except NotImplementedError as error: self.log.error(error) + self.log.debug(f"End of process for file {spec_file}.") + if __name__ == '__main__': # pragma: no cover - MAIN_APP = MainApp() + RED_SPEC = RedSpec() try: - MAIN_APP() + RED_SPEC() except KeyboardInterrupt: sys.exit(0) diff --git a/goodman_pipeline/spectroscopy/tests/__init__.py b/goodman_pipeline/spectroscopy/tests/__init__.py index 3be6bd29..c3961685 100644 --- a/goodman_pipeline/spectroscopy/tests/__init__.py +++ b/goodman_pipeline/spectroscopy/tests/__init__.py @@ -1 +1 @@ -from __future__ import absolute_import \ No newline at end of file +from __future__ import absolute_import diff --git a/goodman_pipeline/spectroscopy/tests/test_redspec.py b/goodman_pipeline/spectroscopy/tests/test_redspec.py index 534c846c..b9adeec6 100644 --- a/goodman_pipeline/spectroscopy/tests/test_redspec.py +++ b/goodman_pipeline/spectroscopy/tests/test_redspec.py @@ -1,11 +1,9 @@ from __future__ import absolute_import -import argparse import os -import shutil from unittest import TestCase -from ...spectroscopy.redspec import (get_args, MainApp) +from ...spectroscopy.redspec import (get_args, RedSpec) class TestArguments(TestCase): @@ -57,17 +55,6 @@ def test_absolute_path_exists(self): self.assertTrue(os.path.exists(args.source)) self.assertTrue(os.path.isdir(args.source)) - # If yes, carry on - - # If not, convert it to become absolute - - # Check if the source folder exists - - # If it exists, carry on - - # If source folder does not exists, print message and leave program - # error_message = 'Input folder "{} does not exists."' - def test_get_args_output_path_does_not_exist(self): arguments = ['--data-path', 'does_not_exist'] self.assertRaises(SystemExit, get_args, arguments) @@ -118,20 +105,20 @@ def test_get_args(): return args -class TestMainApp(TestCase): +class TestRedSpec(TestCase): def setUp(self): - self.main_app = MainApp() + self.red_spec = RedSpec() def test_instantiation_without_args(self): - self.assertIsInstance(self.main_app, MainApp) - self.assertIsNone(self.main_app.args) - self.assertIsNone(self.main_app.wavelength_solution_obj) - self.assertIsNone(self.main_app.wavelength_calibration) - self.assertIsNone(self.main_app.reference) + self.assertIsInstance(self.red_spec, RedSpec) + self.assertIsNone(self.red_spec.args) + self.assertIsNone(self.red_spec.wavelength_solution_obj) + self.assertIsNone(self.red_spec.wavelength_calibration) + self.assertIsNone(self.red_spec.reference) def test___call___no_args(self): - self.assertRaises(SystemExit, self.main_app) + self.assertRaises(SystemExit, self.red_spec) def test___call___with_valid_arguments(self): arguments = ['--data-path', './', @@ -140,7 +127,7 @@ def test___call___with_valid_arguments(self): '--output-prefix', 'w', '--extraction', 'fractional'] args = get_args(arguments=arguments) - self.assertRaises(SystemExit, self.main_app, args) + self.assertRaises(SystemExit, self.red_spec, args) if __name__ == '__main__': diff --git a/goodman_pipeline/spectroscopy/tests/test_wavelength.py b/goodman_pipeline/spectroscopy/tests/test_wavelength.py index e4b9ad3d..0e4eb6a9 100644 --- a/goodman_pipeline/spectroscopy/tests/test_wavelength.py +++ b/goodman_pipeline/spectroscopy/tests/test_wavelength.py @@ -1,20 +1,18 @@ from __future__ import absolute_import -import json import numpy as np import os import re -from astropy.convolution import convolve, Gaussian1DKernel, Box1DKernel from astropy.io import fits -from astropy.modeling import models, Model +from astropy.modeling import models from ccdproc import CCDData -from unittest import TestCase, skip +from unittest import TestCase from ..wavelength import (WavelengthCalibration) from ..redspec import get_args -from ...core import add_wcs_keys, write_fits -from ...core import ReferenceData, NoMatchFound +from ...core import add_linear_wcs_keys, write_fits +from ...core import NoMatchFound class WavelengthCalibrationTests(TestCase): @@ -35,7 +33,7 @@ def setUp(self): self.ccd = CCDData(data=np.random.random_sample(200), meta=fits.Header(), unit='adu') - self.ccd = add_wcs_keys(ccd=self.ccd) + self.ccd = add_linear_wcs_keys(ccd=self.ccd) self.ccd.header.set('SLIT', value='1.0_LONG_SLIT', comment="slit [arcsec]") @@ -158,7 +156,7 @@ def test___call___method_one_comparison_lamps(self): reference_data='goodman_pipeline/data/ref_comp', json_output=True) - self.assertEqual(json_output['error'], 'Unable to obtain wavelength solution') + self.assertEqual(json_output['error'], 'Unable to obtain wavelength solution.') self.assertEqual(json_output['warning'], '') self.assertEqual(json_output['wavelength_solution'], []) @@ -214,6 +212,3 @@ def test___call___method_two_solution(self): for _solution in json_output['wavelength_solution']: self.file_list.append(_solution['file_name']) self.file_list.append(_solution['reference_lamp']) - - - diff --git a/goodman_pipeline/spectroscopy/wavelength.py b/goodman_pipeline/spectroscopy/wavelength.py index 96cb1eb7..9e95479b 100644 --- a/goodman_pipeline/spectroscopy/wavelength.py +++ b/goodman_pipeline/spectroscopy/wavelength.py @@ -30,10 +30,13 @@ evaluate_wavelength_solution, get_lines_in_lamp, linearize_spectrum, + record_wavelength_solution_evaluation, write_fits) from ..core import (ReferenceData, NoMatchFound) +from ._interactive_wavelength import InteractiveWavelengthCalibration + log = logging.getLogger(__name__) @@ -73,6 +76,7 @@ def __init__(self): self.poly_order = 3 self.wcs = WCS() self.wsolution = None + self.linearize = False self.wcal_lamp_file = None self.sci_target_file = None self.n_points = None @@ -81,7 +85,7 @@ def __init__(self): self.cross_corr_tolerance = 5 self.reference_data_dir = None self.reference_data = None - self.calibration_lamp = '' + self.comparison_lamp_file_name = '' self.wcal_lamp_file = '' # Instrument configuration and spectral characteristics @@ -89,17 +93,19 @@ def __init__(self): self.parallel_binning = None def __call__(self, - ccd, - comp_list, - save_data_to, - reference_data, - object_number=None, - corr_tolerance=15, - output_prefix='w', - plot_results=False, - save_plots=False, - plots=False, - json_output=False): + ccd: CCDData, + comp_list: list, + save_data_to: str | os.PathLike, + reference_data: str, + object_number: int | None = None, + corr_tolerance: int = 15, + output_prefix: str = 'w', + interactive_wavelength: bool = False, + linearize: bool = False, + plot_results: bool = False, + save_plots: bool = False, + plots: bool = False, + json_output: bool = False): """Call method for the WavelengthSolution Class It takes extracted data and produces wavelength calibrated 1D FITS file. @@ -140,6 +146,8 @@ def __call__(self, assert isinstance(ccd, CCDData) assert isinstance(comp_list, list) + self.linearize = linearize + json_payload = {'wavelength_solution': [], 'warning': '', 'error': ''} @@ -155,77 +163,96 @@ def __call__(self, self.i_fig = None - log.info('Processing Science Target: ' - '{:s}'.format(ccd.header['OBJECT'])) + log.info(f"Starting Wavelength calibration of Science Target: {ccd.header['OBJECT']} " + f"File: {self.sci_target_file}.") if len(comp_list) == 0: log.warning("No comparison lamps were provided for file {}" "".format(self.sci_target_file)) log.error("Ending processing of {}".format(self.sci_target_file)) if json_output: - json_payload['error'] ='Unable to process without reference lamps' + json_payload['error'] = 'Unable to process without reference lamps' return json_payload else: return else: + log.debug(f"Science file {self.sci_target_file} has {len(comp_list)} comparison lamps.") wavelength_solutions = [] reference_lamp_names = [] for self.lamp in comp_list: - self.calibration_lamp = self.lamp.header['GSP_FNAM'] - log.info('Using reference lamp {}'.format(self.calibration_lamp)) + self.comparison_lamp_file_name = self.lamp.header['GSP_FNAM'] self.raw_pixel_axis = range(self.lamp.shape[0]) self.lamp_name = self.lamp.header['OBJECT'] - log.info('Processing Comparison Lamp: ' - '{:s}'.format(self.lamp_name)) + log.info(f"Using Comparison lamp {self.lamp_name} {self.comparison_lamp_file_name}") self.lines_center = get_lines_in_lamp( ccd=self.lamp, plots=plots) try: - self._automatic_wavelength_solution( - save_data_to=save_data_to, - corr_tolerance=self.cross_corr_tolerance) + if interactive_wavelength: + interactive_wavelength = InteractiveWavelengthCalibration() + interactive_wavelength(ccd=ccd, + comparison_lamp=self.lamp, + save_data_to=save_data_to, + reference_data=reference_data) + self.wsolution = interactive_wavelength.wavelength_solution + self.rms_error, self.n_points, self.n_rejections = interactive_wavelength.wavelength_solution_evaluation + + else: + self._automatic_wavelength_solution( + save_data_to=save_data_to, + corr_tolerance=self.cross_corr_tolerance) except NoMatchFound as message: - raise NoMatchFound(message) + log.error(message) + continue if self.wsolution is not None: - ccd.header.set('GSP_WRMS', value=self.rms_error) - ccd.header.set('GSP_WPOI', value=self.n_points) - ccd.header.set('GSP_WREJ', value=self.n_rejections) - - linear_x_axis, self.lamp.data = linearize_spectrum( - self.lamp.data, - wavelength_solution=self.wsolution) - - self.lamp = self.wcs.write_gsp_wcs(ccd=self.lamp, - model=self.wsolution) - - self.lamp = add_linear_wavelength_solution( - ccd=self.lamp, - x_axis=linear_x_axis, - reference_lamp=self.calibration_lamp) - - self.wcal_lamp_file = self._save_wavelength_calibrated( - ccd=self.lamp, - original_filename=self.calibration_lamp, - save_data_to=save_data_to, - output_prefix=output_prefix, - index=object_number, - lamp=True) + ccd = record_wavelength_solution_evaluation(ccd=ccd, + rms_error=self.rms_error, + n_points=self.n_points, + n_rejections=self.n_rejections) + self.lamp = record_wavelength_solution_evaluation(ccd=self.lamp, + rms_error=self.rms_error, + n_points=self.n_points, + n_rejections=self.n_rejections) + + if self.linearize: + + self.lamp.header.set('GSP_LINE', value='TRUE') + ccd.header.set('GSP_LINE', value='TRUE') + linear_x_axis, self.lamp.data = linearize_spectrum( + self.lamp.data, + wavelength_solution=self.wsolution) + + self.lamp = self.wcs.write_gsp_wcs(ccd=self.lamp, + model=self.wsolution) + + self.lamp = add_linear_wavelength_solution( + ccd=self.lamp, + x_axis=linear_x_axis, + reference_lamp=self.comparison_lamp_file_name) + + self.wcal_lamp_file = self._save_wavelength_calibrated( + ccd=self.lamp, + original_filename=self.comparison_lamp_file_name, + save_data_to=save_data_to, + output_prefix=output_prefix, + index=object_number, + lamp=True) + else: + pass wavelength_solutions.append(self.wsolution) reference_lamp_names.append(self.wcal_lamp_file) else: - log.error('It was not possible to get a wavelength ' - 'solution from lamp ' - '{:s} {:s}.'.format( - self.lamp.header['GSP_FNAM'], - self.lamp.header['OBJECT'])) + log.error(f"It was not possible to get a wavelength solution from lamp {self.lamp_name} " + f"{self.comparison_lamp_file_name}.") continue if len(wavelength_solutions) > 1: + log.warning(f"Multiple ({len(wavelength_solutions)}) wavelength solutions found.") warning_message = str("The current version of the pipeline " "does not combine multiple solution " "instead it saves a single version of " @@ -281,9 +308,9 @@ def __call__(self, return json_payload else: - log.error("No wavelength solution.") + log.error("Unable to obtain wavelength solution.") if json_output: - json_payload['error'] = "Unable to obtain wavelength solution" + json_payload['error'] = "Unable to obtain wavelength solution." return json_payload def _automatic_wavelength_solution(self, @@ -332,10 +359,11 @@ def _automatic_wavelength_solution(self, reference_lamp_ccd = self.reference_data.get_reference_lamp( header=self.lamp.header) - log.debug('Found reference lamp: ' - '{:s}'.format(reference_lamp_ccd.header['GSP_FNAM'])) - except NoMatchFound as error: - raise NoMatchFound(error) + if reference_lamp_ccd is not None: + log.debug(f"Found reference lamp {reference_lamp_ccd.header['GSP_FNAM']} " + f"for {self.sci_target_file}'s lamp {self.lamp.header['OBJECT']}: ") + else: + raise NoMatchFound(f"Unable to find a suitable reference lamp for {self.lamp.header['OBJECT']} {self.lamp.header['GSP_FNAM']}.") except NotImplementedError as error: raise NotImplementedError(error) @@ -377,7 +405,8 @@ def _automatic_wavelength_solution(self, reference=reference_lamp_ccd.data, compared=self.lamp.data, slit_size=slit_size, - serial_binning=self.serial_binning) + serial_binning=self.serial_binning, + selection_bias='center') log.debug(f"Found global cross-correlation value of: {global_cross_corr}") if plots: @@ -491,7 +520,7 @@ def _automatic_wavelength_solution(self, if self.wsolution is None: log.error('Failed to find wavelength solution using reference ' - 'file: {:s}'.format(self.calibration_lamp)) + 'file: {:s}'.format(self.comparison_lamp_file_name)) return None # finding differences in order to improve the wavelength solution @@ -662,14 +691,18 @@ def _save_science_data(self, """ ccd = ccd.copy() - linear_x_axis, ccd.data = linearize_spectrum( - data=ccd.data, - wavelength_solution=wavelength_solution) - - ccd = add_linear_wavelength_solution( - ccd=ccd, - x_axis=linear_x_axis, - reference_lamp=self.calibration_lamp) + resample = False + if resample: + linear_x_axis, ccd.data = linearize_spectrum( + data=ccd.data, + wavelength_solution=wavelength_solution) + + ccd = add_linear_wavelength_solution( + ccd=ccd, + x_axis=linear_x_axis, + reference_lamp=self.wcal_lamp_file) + else: + wavelength_axis = wavelength_solution(range(ccd.data.shape[0])) save_file_name = self._save_wavelength_calibrated( ccd=ccd, @@ -726,8 +759,7 @@ def _save_science_data(self, plot_path = os.path.join(plots_dir, plot_name) # print(plot_path) plt.savefig(plot_path, dpi=300) - log.info('Saved plot as {:s} file ' - 'DPI=300'.format(plot_name)) + log.info('Saved plot as {:s} file DPI=300'.format(plot_name)) if plots or plot_results: # pragma: no cover manager = plt.get_current_fig_manager() @@ -758,8 +790,7 @@ def _save_wavelength_calibrated(self, f_end = '_ws_{:d}.fits'.format(index) file_full_path = os.path.join(save_data_to, - output_prefix + - original_filename.replace('.fits', f_end)) + output_prefix + original_filename.replace('.fits', f_end)) if lamp: log.info('Wavelength-calibrated {:s} file saved to: ' diff --git a/goodman_pipeline/wcs/__init__.py b/goodman_pipeline/wcs/__init__.py index ce7a0599..1b72ac48 100644 --- a/goodman_pipeline/wcs/__init__.py +++ b/goodman_pipeline/wcs/__init__.py @@ -1 +1 @@ -from .wcs import WCS \ No newline at end of file +from .wcs import WCS diff --git a/goodman_pipeline/wcs/tests/test_functional.py b/goodman_pipeline/wcs/tests/test_functional.py index 706b0212..29923bc8 100644 --- a/goodman_pipeline/wcs/tests/test_functional.py +++ b/goodman_pipeline/wcs/tests/test_functional.py @@ -1,17 +1,18 @@ from __future__ import absolute_import -from unittest import TestCase, skip -from ..wcs import WCS +from unittest import TestCase + import numpy as np import os import re import sys -from astropy.io import fits -from astropy.modeling import (models, fitting, Model) -import matplotlib.pyplot as plt +from astropy.io import fits +from astropy.modeling import (models, Model) from ccdproc import CCDData +from ..wcs import WCS + class TestWCSBase(TestCase): @@ -54,7 +55,7 @@ def test_fit_chebyshev(self): self.assertEqual(model.degree, ccd.header['GSP_ORDR']) for i in range(model.degree + 1): self.assertAlmostEqual(model.__getattribute__('c{:d}'.format(i)).value, - ccd.header['GSP_C{:03d}'.format(i)]) + ccd.header['GSP_C{:03d}'.format(i)]) def test_fit_linear(self): test_file = os.path.join(self.data_path, @@ -127,7 +128,7 @@ def test_read__non_linear_chebyshev(self): ccd = CCDData.read(test_file, unit='adu') - result = self.wcs.read(ccd=ccd) + self.wcs.read(ccd=ccd) self.assertIsInstance(self.wcs.model, Model) self.assertEqual(self.wcs.model.__class__.__name__, 'Chebyshev1D') @@ -138,7 +139,7 @@ def test_read__non_linear_legendre(self): ccd = CCDData.read(test_file, unit='adu') - result = self.wcs.read(ccd=ccd) + self.wcs.read(ccd=ccd) self.assertIsInstance(self.wcs.model, Model) self.assertEqual(self.wcs.model.__class__.__name__, 'Legendre1D') @@ -193,8 +194,8 @@ def test_write_gsp_wcs(self): self.assertIsInstance(model, Model) blank_ccd = CCDData(data=np.ones(ccd.data.shape), - meta=fits.Header(), - unit='adu') + meta=fits.Header(), + unit='adu') blank_ccd.header.set('GSP_WREJ', value=None, comment='empty') new_ccd = self.wcs.write_gsp_wcs(ccd=blank_ccd, model=model) @@ -204,7 +205,7 @@ def test_write_gsp_wcs(self): self.assertEqual(new_ccd.header['GSP_NPIX'], ccd.header['GSP_NPIX']) for i in range(model.degree + 1): self.assertAlmostEqual(new_ccd.header['GSP_C{:03d}'.format(i)], - ccd.header['GSP_C{:03d}'.format(i)]) + ccd.header['GSP_C{:03d}'.format(i)]) def test_read_gsp_wcs(self): test_file = os.path.join(self.data_path, diff --git a/goodman_pipeline/wcs/tests/test_unittest.py b/goodman_pipeline/wcs/tests/test_unittest.py index 746363bd..d6130da6 100644 --- a/goodman_pipeline/wcs/tests/test_unittest.py +++ b/goodman_pipeline/wcs/tests/test_unittest.py @@ -1,19 +1,13 @@ from __future__ import absolute_import -from unittest import TestCase, skip +from unittest import TestCase from ..wcs import WCS -import numpy as np -import os -import re -import sys -from astropy.io import fits -from astropy.modeling import (models, fitting, Model) -from ccdproc import CCDData +from astropy.modeling import Model class TestWCS(TestCase): - + def setUp(self): self.wcs = WCS() @@ -66,7 +60,7 @@ def test_pm_fitter_undefined_model_and_fitter(self): # self.wcs._fitter(physical=pixel, wavelength=angstrom) def test_pm_fitter_not_enough_points(self): - pixel = [1,2] + pixel = [1, 2] angstrom = [8000, 8001] self.wcs.model_name = 'chebyshev' @@ -76,7 +70,6 @@ def test_pm_fitter_not_enough_points(self): result = self.wcs._fitter(physical=pixel, wavelength=angstrom) self.assertIsNone(result) - def test_pm_set_math_model__none(self): self.wcs.wcs_dict['dtype'] = -1 self.assertRaises(NotImplementedError, self.wcs._set_math_model) @@ -113,7 +106,7 @@ def test_pm_set_math_model__wrong_ftype(self): self.assertRaisesRegex(SyntaxError, 'ftype {:d} is not defined in the ' 'standard'.format(self.wcs.wcs_dict['ftype'])) - + def test_pm_none(self): self.assertRaises(NotImplementedError, self.wcs._none) @@ -155,4 +148,3 @@ def test_pm_non_linear_lspline(self): def test_pm_non_linear_cspline(self): self.assertRaises(NotImplementedError, self.wcs._non_linear_cspline) - diff --git a/goodman_pipeline/wcs/wcs.py b/goodman_pipeline/wcs/wcs.py index 21c0cb27..a6431c47 100644 --- a/goodman_pipeline/wcs/wcs.py +++ b/goodman_pipeline/wcs/wcs.py @@ -252,7 +252,7 @@ def _fitter(self, physical, wavelength): # wavelength solution reader private methods. def _read_non_linear(self, dimension): - """Non linear solutions reader + """Non-linear solutions reader Notes: Not all kind of non-linear solutions are implemented. Apparently is diff --git a/requirements.txt b/requirements.txt index 43dc3f23..40d1f507 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ astropy astroplan astroscrappy ccdproc +coloredlogs coveralls cython recommonmark @@ -9,6 +10,7 @@ matplotlib mock numpy pandas +setuptools scipy sphinx sphinxcontrib.napoleon diff --git a/setup.py b/setup.py index caa5c8ed..61afabe1 100644 --- a/setup.py +++ b/setup.py @@ -155,6 +155,4 @@ def create_version_py(packagename, version, source_dir='.'): scripts=['goodman_pipeline/scripts/redccd', 'goodman_pipeline/scripts/redspec', ], - ) - - +)