From b31a209b9f508be93f7fe55366bf7384dec1ecf2 Mon Sep 17 00:00:00 2001 From: Roberto Susino <roberto.susino@inaf.it> Date: Mon, 19 Jul 2021 14:14:17 +0200 Subject: [PATCH] Version 3.0 --- CHANGELOG.md | 10 +- check_quality.pro | 27 ++++ decode_obt.pro | 6 +- fits_hdr2struct.pro | 15 +++ get_light_time.pro | 16 +++ interpol_param.pro | 3 +- load_spice_kernels.pro | 22 ++++ metis_l1_prep.pro | 263 ++++++++++++++++++++++---------------- metis_rectify.pro | 9 ++ metis_wcs.pro | 228 +++++++++++++++++++++++++++++++++ solo_get_carrot.pro | 39 ++++++ solo_get_coords.pro | 43 +++++++ solo_get_ephemerides.pro | 232 +++++++++++++++++++++++++++++++++ solo_get_pointing.pro | 53 ++++++++ solo_get_solar_angles.pro | 61 +++++++++ solo_obt2utc.pro | 16 ++- solo_utc2obt.pro | 12 ++ 17 files changed, 935 insertions(+), 120 deletions(-) create mode 100644 check_quality.pro create mode 100644 fits_hdr2struct.pro create mode 100644 get_light_time.pro create mode 100644 load_spice_kernels.pro create mode 100644 metis_rectify.pro create mode 100644 metis_wcs.pro create mode 100644 solo_get_carrot.pro create mode 100644 solo_get_coords.pro create mode 100644 solo_get_ephemerides.pro create mode 100644 solo_get_pointing.pro create mode 100644 solo_get_solar_angles.pro create mode 100644 solo_utc2obt.pro diff --git a/CHANGELOG.md b/CHANGELOG.md index b27a76d..ccc7900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # CHANGELOG All changes to the Metis L1 pipeline are documented in this file. +## 3.0 - 2021-07-19 +- Add the WCS (World Coordinate System) keywords to the FITS header and a set of routines to perform calculation of the instrument boresight parameters and S/C coordinates. +- Modify the pipeline to handle the new input I/F which includes the full set of Solar Orbiter SPICE kernels and the Metis Calibration Package. +- Add the derivation of the quality matrix which is now added in an extension to the L1 FITS file. +- Add several code optimisations. + ## 2.3.1 - 2021-04-19 -- Fixed a bug that caused an incorrect FITS file format: images with unsigned or long integer data type were saved as signed integers with the wrong BSCALE and BZERO scale keywords. +- Fix a bug that caused an incorrect FITS file format: images with unsigned or long integer data type were saved as signed integers with the wrong BSCALE and BZERO scale keywords. ## 2.3 - 2021-03-10 - Fix a bug that caused incorrect pixel counts in re-binned images. Pixel counts are now correctly multiplied by the square of the binning factor after re-binning. @@ -41,7 +47,7 @@ All changes to the Metis L1 pipeline are documented in this file. - Revise the structure of the binary table containing the house-keeping parameters to make it more user-friendly. ## 1.0 – 2020-03-06 -- Add several optimisations to the code. +- Add several code optimisations. - Add the decode_obt.pro routine. ## 0.0 – 2020-01-16 diff --git a/check_quality.pro b/check_quality.pro new file mode 100644 index 0000000..c495f78 --- /dev/null +++ b/check_quality.pro @@ -0,0 +1,27 @@ +function check_quality, data, cal_pack, filter = filter + + if filter.contains('VL', /fold) then $ + channel = cal_pack.vl_channel else $ + channel = cal_pack.uv_channel + + levels = channel.sat_level.value + + quality_matrix = float(data) * 0. + + s1 = where(data le levels[0], count) + if count gt 0 then quality_matrix[s1] = !values.f_nan + + s2 = where(data gt levels[0] and data le levels[1], count) + if count gt 0 then quality_matrix[s2] = 0 + + s3 = where(data gt levels[1] and data lt levels[2], count) + if count gt 0 then quality_matrix[s3] = 1 + + s4 = where(data ge levels[2] and data lt levels[3], count) + if count gt 0 then quality_matrix[s4] = 0 + + s5 = where(data ge levels[3], count) + if count gt 0 then quality_matrix[s5] = !values.f_nan + + return, quality_matrix +end diff --git a/decode_obt.pro b/decode_obt.pro index 3bca715..795f2e6 100644 --- a/decode_obt.pro +++ b/decode_obt.pro @@ -1,5 +1,5 @@ -function decode_obt, t_coarse, t_fine, to_double = to_double, from_double = from_double - if keyword_set(to_double) then return, double(t_coarse) + double(t_fine)/65536.0D - if keyword_set(from_double) then obt = [ulong(t_coarse), uint((t_coarse - ulong(t_coarse)) * 65536)] else obt = [t_coarse, t_fine] +function decode_obt, t_coarse, t_fine, to_decimal = to_decimal, from_decimal = from_decimal + if keyword_set(to_decimal) then return, double(t_coarse) + double(t_fine)/65536.D0 + if keyword_set(from_decimal) then obt = [ulong(t_coarse), uint((t_coarse - ulong(t_coarse)) * 65536)] else obt = [t_coarse, t_fine] return, (obt.tostring()).join(':') end diff --git a/fits_hdr2struct.pro b/fits_hdr2struct.pro new file mode 100644 index 0000000..db74202 --- /dev/null +++ b/fits_hdr2struct.pro @@ -0,0 +1,15 @@ +function fits_hdr2struct, string_hdr + + struct = !null + n = n_elements(string_hdr) + for i = 0, n - 1 do begin + tag = string_hdr[i].substring(0, 7) + tag = tag.trim() + if tag eq '' then continue + if tag.startswith('end', /fold) or tag.contains('continue', /fold) then continue + if isa(struct) then if where(tag_names(struct) eq tag) ge 0 then continue + struct = create_struct(struct, tag.replace('-', ' '), fxpar(string_hdr, tag)) + endfor + + return, struct +end diff --git a/get_light_time.pro b/get_light_time.pro new file mode 100644 index 0000000..4146dcf --- /dev/null +++ b/get_light_time.pro @@ -0,0 +1,16 @@ +function get_light_time, utc, body, target, radvel = radvel + + ; convert the requested date into ephemeris time + + cspice_str2et, utc, et + + ; coordinates of the body wrt the target in the j2000 system + + cspice_spkezr, body, et, 'J2000', 'NONE', target, state, ltime + + ; radial component of the velocity + + radvel = 1000.d0 * total(state[0 : 2] * state[3 : 5])/sqrt(total(state[0 : 2]^2)) + + return, ltime +end diff --git a/interpol_param.pro b/interpol_param.pro index dc5216c..839cf0c 100644 --- a/interpol_param.pro +++ b/interpol_param.pro @@ -10,6 +10,7 @@ function interpol_param, table, par_name, date, empty_params = empty_params s = sort(par_date) par_val = par_val[s] par_date = par_date[s] - value = interpol(par_val, par_date, date_conv(date, 'JULIAN')) + jul_date = date_conv(date, 'JULIAN') + if jul_date ge min(par_date) and jul_date le max(par_date) then value = interpol(par_val, par_date, jul_date) else value = 0.0 if finite(value) then return, value else return, 0.0 end diff --git a/load_spice_kernels.pro b/load_spice_kernels.pro new file mode 100644 index 0000000..aded929 --- /dev/null +++ b/load_spice_kernels.pro @@ -0,0 +1,22 @@ +pro load_spice_kernels, path, unload = unload, kernel_list = kernel_list, kernel_version = kernel_version + if keyword_set(unload) then cspice_kclear else begin + metakernel = path + '/mk/solo_ANC_soc-flown-mk.tm' + openr, in, metakernel, /get_lun + kernel_list = list() + while ~ eof(in) do begin + line = '' + readf, in, line + if line.contains('$KERNELS', /fold) then begin + line = line.replace('$KERNELS', path) + line = line.replace("'", '') + kernel_list.add, line.trim() + endif + if line.contains('SKD_VERSION', /fold) then begin + fields = stregex(line, ".*'([a-z_0-9]*)'", /extract, /sub) + kernel_version = fields[1] + endif + endwhile + close, /all + foreach item, kernel_list do cspice_furnsh, item + endelse +end diff --git a/metis_l1_prep.pro b/metis_l1_prep.pro index 534afba..f967ae0 100644 --- a/metis_l1_prep.pro +++ b/metis_l1_prep.pro @@ -1,5 +1,9 @@ pro metis_l1_prep + ; keyword defining if the detector reference frame must be used for the output + + ref_detector = 1 + ; start the log journal, 'output/metis_l1_prep_log.txt' @@ -19,16 +23,6 @@ pro metis_l1_prep 'VL light-curve' $ ; 9 ) - metis_obj_size = list( $ - 2048L, $ ; 0 - 1024L, $ ; 1 - 1024L, $ ; 2 - 2048L, $ ; 3 - 1024L, $ ; 4 - 2048L, $ ; 5 - 1024L $ ; 6 - ) - ; read the auxiliary input file input = json_parse('input/contents.json', /toarray, /tostruct) @@ -37,64 +31,47 @@ pro metis_l1_prep ; load the spice kernels - kernels = [ $ - input.tls_file_name, $ - input.tsc_file_name $ - ] + load_spice_kernels, input.spice_kernels, kernel_list = kernel_list, kernel_version = kernel_version - ; load the spice kernels + journal, 'SPICE kernel files correctly loaded:' + journal, ' SDK version= ' + kernel_version - cspice_furnsh, kernels + ; import the calibration package index file - journal, 'SPICE kernel files correctly loaded:' - journal, ' sclk kernel = ' + input.tsc_file_name - journal, ' leap seconds kernel = ' + input.tls_file_name + cal_pack = json_parse(input.cal_pack_path + '/index.json', /toarray, /tostruct) - ; read l0 fits structure + ; include the calibration package path into the cal_pack structure - fits_info, input.file_name, n_ext = n_ext, extname = extname, /silent + cal_pack = create_struct('path', input.cal_pack_path + path_sep(), cal_pack) ; read the primary hdu - ext_no = 0 - data = mrdfits(input.file_name, ext_no, primary_header, /silent) - - ; if the data is not an image, read the data binary-table extension + data = mrdfits(input.file_name, 0, primary_header, /silent) - if fxpar(primary_header, 'NAXIS') eq 0 then begin - ext_no += 1 - data_bin_table = mrdfits(input.file_name, ext_no, data_extension_header, /silent) - endif else data_bin_table = !null + naxis = fxpar(primary_header, 'NAXIS') + naxis1 = fxpar(primary_header, 'NAXIS1', missing = 0) + naxis2 = fxpar(primary_header, 'NAXIS2', missing = 0) ; read the metadata extension - ext_no += 1 - metadata_bin_table = mrdfits(input.file_name, ext_no, metadata_extension_header, /silent) + metadata_bin_table = mrdfits(input.file_name, 'metis metadata', metadata_extension_header, /silent) ; identify the data product and its nominal size datatype = fxpar(metadata_extension_header, 'DATATYPE') - if datatype le 6 then begin - naxis = 2 - naxis1 = metis_obj_size[datatype] - naxis2 = naxis1 - endif else begin - naxis = 0 - endelse + journal, 'L0 FITS file correctly read:' + journal, ' datatype = ' + string(datatype, format = '(I0)') - ; if the data product is an accumulation matrix, look for the accumulation vector extension and read it if it exists + ; if the data product is a light curve or a pcu-event list, read the data binary hk_table - if datatype eq 2 then begin - if fxpar(metadata_extension_header, 'M') eq 256 then begin - ext_no += 1 - accumul_vector = mrdfits(input.file_name, ext_no, vector_extension_header, /silent) - endif else accumul_vector = !null - endif + if datatype gt 6 then data_bin_table = mrdfits(input.file_name, 1, data_extension_header, /silent) else data_bin_table = !null - journal, 'L0 FITS file correctly read:' - journal, ' datatype = ' + string(datatype, format = '(I0)') - if datatype le 6 then journal, ' size = ' + string(naxis1, format = '(I0)') + '×' + string(naxis2, format = '(I0)') + ; if the data product is an accumulation matrix, look for the accumulation vector extension and read it if it exists + + if datatype eq 2 then $ + if fxpar(metadata_extension_header, 'M') eq 256 then $ + accumul_vector = mrdfits(input.file_name, 'accumulation vector', vector_extension_header, /silent) else accumul_vector = !null ; NOTE - the radialization extension is ignored @@ -113,11 +90,11 @@ pro metis_l1_prep obt_beg = fxpar(primary_header, 'OBT_BEG') obt_end = fxpar(primary_header, 'OBT_END') - obt_avg = (obt_beg + obt_end) / 2.0D + obt_avg = (obt_beg + obt_end)/2.0D - date_beg = solo_obt2utc(decode_obt(obt_beg, /from_double)) - date_end = solo_obt2utc(decode_obt(obt_end, /from_double)) - date_avg = solo_obt2utc(decode_obt(obt_avg, /from_double)) + date_beg = solo_obt2utc(obt_beg) + date_end = solo_obt2utc(obt_end) + date_avg = solo_obt2utc(obt_avg) date_beg_string = strmid(date_beg, 0, 19) date_beg_string = date_beg_string.replace('-', '') @@ -128,27 +105,36 @@ pro metis_l1_prep ; name of the fits file file_name_fields = strsplit(fxpar(primary_header, 'FILENAME'), '_', /extract) - file_name = 'solo_L1_' + file_name_fields[2] + '_' + date_beg_string + '_V' + version + '.fits' out_file_name = 'output/' + file_name - ; instrument keywords + ; filter keyword + + case datatype of + 0: filter = 'VL' + 1: filter = 'UV' + 2: filter = 'UV' + 3: filter = 'VL' + 4: filter = 'UV' + 5: filter = 'VL' + 6: filter = 'UV' + 7: filter = 'UV' + 8: filter = 'UV' + 9: filter = 'VL' + endcase + + ; choose the correct calibration-package structure based on the image filter + + if filter eq 'VL' then channel = cal_pack.vl_channel else channel = cal_pack.uv_channel + + ; instrument/telescope/detector keywords - if datatype eq 0 or datatype eq 3 or datatype eq 5 or datatype eq 9 then begin - filter = 'VL' - wavelnth = 610.0 - wavemin = 580.0 - wavemax = 640.0 - waveband = 'Visible light 580-640 nm' - endif else begin - filter = 'UV' - wavelnth = 121.6 - wavemin = 111.6 - wavemax = 131.6 - waveband = 'H I Lyman-alpha 121.6 nm' - endelse detector = filter + 'D' + waveband = cal_pack.vl_channel.name telescope = 'SOLO/Metis/' + detector + wavelnth = channel.bandpass.value[1] + wavemin = channel.bandpass.value[0] + wavemax = channel.bandpass.value[2] ; campaign keywords @@ -158,7 +144,7 @@ pro metis_l1_prep ; build the fits file extensions - ; join the metadata extension header to the primary header removing unwanted keywords + ; join the metadata extension header to the primary header removing useless keywords i = where(strmid(primary_header, 0, 8) eq 'COMP_RAT') j = where(strmid(metadata_extension_header, 0, 8) eq 'EXTNAME ') @@ -170,69 +156,88 @@ pro metis_l1_prep primary_header[i + 1: *] $ ] - ; rebin the image if binning was applied during the acquisition + ; rebin the image if binning was applied during the acquisition and check for the data quality + ; NOTE - this is done only for image data products if datatype le 6 then begin - if fxpar(primary_header, 'COMPR') then $ - bin_type = fxpar(primary_header, 'B0_BIN') else bin_type = 0 + bin_type = fxpar(primary_header, 'B0_BIN', missing = 0) - pol_id = fxpar(primary_header, 'POL_ID') - fpfilt = fxpar(primary_header, 'VLFPFILT', count = count) - if count gt 0 and pol_id eq 0 and fpfilt eq 0 and bin_type eq 0 then bin_type = 1 + ; check for wrong binning type in fixed-polarization filtered images + + pol_id = fxpar(primary_header, 'POL_ID', missing = -1) + vlfpfilt = fxpar(primary_header, 'VLFPFILT', missing = -1) + if vlfpfilt eq 0 and pol_id eq 0 and bin_type eq 0 then bin_type = 1 + + bin_fact = 2^bin_type ; binning factor = 1, 2, or 4 + + if bin_fact gt 1 then begin + naxis1 = naxis1/bin_fact + naxis2 = naxis2/bin_fact + data = rebin(data, naxis1, naxis2) + + ; evaluate the quality matrix + + quality_matrix = check_quality(data, cal_pack, filter = filter) - bin_fact = 2. ^ bin_type - if bin_fact gt 1. then begin - data = rebin(data, naxis1 / bin_fact, naxis2 / bin_fact) - data = data * bin_fact^2 + ; correct values including binning factor only for vl and uv images and accumulation matrices + + if datatype le 2 then data = long(data) * bin_fact^2 if ~ isa(comment) then comment = !null comment = [comment, 'Image was rebinned according to the commanded binning factor.'] journal, 'Data was binned during acquisition:' journal, ' binning = ' + string(bin_fact, format = '(I1)') + '×' + string(bin_fact, format = '(I1)') - journal, ' data was correctly rebinned:' - journal, ' new size = ' + string(naxis1 / bin_fact, format = '(I0)') + '×' + string(naxis1 / bin_fact, format = '(I0)') + journal, ' data was rebinned:' + journal, ' new size = ' + string(naxis1, format = '(I0)') + '×' + string(naxis2, format = '(I0)') endif else begin + quality_matrix = check_quality(data, cal_pack, filter = filter) + journal, 'Data was not binned during acquistion.' endelse - endif + + ; read the bad-pixel map + + bad_pixel_map = readfits(cal_pack.path + channel.bad_pixel_map[0].file_name, /silent) + + ; rebin the bad-pixel map to the size of the image to be corrected + + bad_pixel_map = rebin(bad_pixel_map, naxis1, naxis2) * bin_fact^2 + + ; correct the quality matrix according to the bad-pixel map + + bad_pixels = where(bad_pixel_map gt 0, count) + if count gt 0 then quality_matrix[bad_pixels] = 0 + endif else quality_matrix = !null ; correct for the effective exposure time + ; NOTE - this is done only for vl and uv images and accumulation matrices telapse = float(obt_end - obt_beg) dit = fxpar(metadata_extension_header, 'DIT') - ndit = fxpar(metadata_extension_header, 'NDIT', missing = 1L) - ndit1 = fxpar(metadata_extension_header, 'NDIT1', missing = 1L) - ndit2 = fxpar(metadata_extension_header, 'NDIT2', missing = 1L) + ndit = fxpar(metadata_extension_header, 'NDIT', missing = 1) + ndit1 = fxpar(metadata_extension_header, 'NDIT1', missing = 1) + ndit2 = fxpar(metadata_extension_header, 'NDIT2', missing = 1) - if datatype le 6 then begin + if datatype le 2 then begin nsumexp = ndit * ndit1 * ndit2 xposure = dit/1000. * nsumexp - data = data * nsumexp - - if max(data, /nan) lt 32768 then data = fix(data) - - datamin = min(data, /nan) - fxaddpar, primary_header, 'DATAMIN', datamin - - datamax = max(data, /nan) - fxaddpar, primary_header, 'DATAMAX', datamax + data = long(data) * nsumexp if ~ isa(comment) then comment = !null comment = [comment, 'Image values were corrected for the total exposure time.'] - if nsumexp gt 1 then journal, 'Image values were corrected for the total exposure time.' + journal, 'Image values were corrected for the total exposure time.' endif else begin - xposure = dit/1000. nsumexp = 1 - data = !null + xposure = dit/1000. endelse ; adjust the primary header (it is almost the same for all data product types) fxaddpar, primary_header, 'FILENAME', file_name - fxaddpar, primary_header, 'PARENT', file_basename(input.file_name), 'Name of the parent file that got processed to the current one', before = 'APID' + fxaddpar, primary_header, 'PARENT', file_basename(input.file_name), 'Name of the parent file', before = 'APID' fxaddpar, primary_header, 'DATE', date, 'Date and time of FITS file creation', before = 'OBT_BEG' fxaddpar, primary_header, 'DATE-OBS', date_beg, 'Same as DATE-BEG', before = 'OBT_BEG' fxaddpar, primary_header, 'DATE-BEG', date_beg, 'Start time of observation', before = 'OBT_BEG' @@ -244,6 +249,7 @@ pro metis_l1_prep fxaddpar, primary_header, 'LEVEL', 'L1' fxaddpar, primary_header, 'CREATOR', 'metis_l1_prep.pro' fxaddpar, primary_header, 'VERS_SW', input.sw_version + fxaddpar, primary_header, 'VERS_CAL', cal_pack.version, 'Version of the calibration package', after = 'VERS_SW' fxaddpar, primary_header, 'OBSRVTRY', 'Solar Orbiter', 'Satellite name', before = 'INSTRUME' fxaddpar, primary_header, 'TELESCOP', telescope, 'Telescope that took the measurement', before = 'INSTRUME' fxaddpar, primary_header, 'DETECTOR', detector, 'Subunit/sensor', before = 'DATAMIN' @@ -266,6 +272,9 @@ pro metis_l1_prep fxaddpar, primary_header, 'BZERO', 0, 'Physical value for the array value 0', before = 'DATAMIN' fxaddpar, primary_header, 'BTYPE', metis_datatype[datatype], 'Science data object type', before = 'DATAMIN' fxaddpar, primary_header, 'BUNIT', 'DN', 'Units of physical value, after application of BSCALE and BZERO', before = 'DATAMIN' + fxaddpar, primary_header, 'BLANK', 0 + fxaddpar, primary_header, 'DATAMIN', min(data, /nan) + fxaddpar, primary_header, 'DATAMAX', max(data, /nan) fxaddpar, primary_header, 'IDB_VERS', input.idb_version, '', before = 'HDR_VERS' fxaddpar, primary_header, 'INFO_URL', 'http://metis.oato.inaf.it', 'Link to more information on the instrument data', before = 'HISTORY' @@ -310,7 +319,6 @@ pro metis_l1_prep if datatype eq 1 or datatype eq 4 or datatype eq 6 then begin fxaddpar, primary_header, 'HVU_SCR', interpol_param(hk_table, 'NIT0E070', date_avg, empty_params = empty_params), '[raw] HVU Screen commanded voltage' - ; CMDHVSCR e CMDHVMCP fxaddpar, primary_header, 'HVU_MCP', interpol_param(hk_table, 'NIT0E071', date_avg, empty_params = empty_params), '[raw] HVU MCP commanded voltage' fxaddpar, primary_header, 'HV_SCR_V', interpol_param(hk_table, 'NIT0E0B7', date_avg, empty_params = empty_params), '[V] HVU Screen + MCP read voltage', after = 'HVU_MCP' fxaddpar, primary_header, 'HV_MCP_V', interpol_param(hk_table, 'NIT0E0B6', date_avg, empty_params = empty_params), '[V] HVU MCP read voltage', after = 'HV_SCR_V' @@ -330,7 +338,7 @@ pro metis_l1_prep if empty_params eq !null then empty_params = '' - ; replace verbose keyword values + ; replace with text keyword values key = fxpar(primary_header, 'MEASKIND', count = count) if count gt 0 then begin @@ -377,7 +385,26 @@ pro metis_l1_prep endif endforeach - ; remove unused keywords + ; convert the fits header into an idl structure + + header = fits_hdr2struct(primary_header) + + ; append wcs keywords + + if datatype le 6 then begin + wcs = metis_wcs(header, cal_pack, ref_detector = ref_detector) + foreach element, wcs do fxaddpar, primary_header, element.name, element.value, element.comment, before = 'DATATYPE' + endif + + ; append solar keywords + + ephemerides = solo_get_ephemerides(header, constants = cal_pack.constants) + foreach element, ephemerides do fxaddpar, primary_header, element.name, element.value, element.comment, before = 'DATATYPE' + + fxaddpar, primary_header, 'HISTORY', 'WCS and solar ephemerides:' + fxaddpar, primary_header, 'HISTORY', ' SKD version = ' + kernel_version + + ; remove redundant and misleading keywords sxdelpar, primary_header, 'WIDTH' sxdelpar, primary_header, 'HEIGHT' @@ -411,10 +438,16 @@ pro metis_l1_prep fxaddpar, primary_header, 'COMMENT', comment[k] endif - ; add checksum and datasum to the fits header + ; rectify the image if requested - fits_add_checksum, primary_header, data + if not ref_detector then begin + data = metis_rectify(data, filter) + quality_matrix = metis_rectify(quality_matrix, filter) + endif + + ; add checksum and datasum to the fits header and save + fits_add_checksum, primary_header, data mwrfits, data, out_file_name, primary_header, /no_comment, /create, /silent journal, 'Fits file created:' @@ -422,29 +455,37 @@ pro metis_l1_prep ; if applicable, save the data binary-table extension as it is - if isa(data_bin_table) then mwrfits, data_bin_table, 'output/' + file_name, data_extension_header, /no_comment, /silent + if isa(data_bin_table) then begin + fits_add_checksum, data_extension_header, data_bin_table + mwrfits, data_bin_table, out_file_name, data_extension_header, /no_comment, /silent + endif + + ; save the quality matrix + + if datatype le 6 and isa(quality_matrix) then begin + quality_matrix_header = !null + fxaddpar, quality_matrix_header, 'PCOUNT', 0, 'Parameter count' + fxaddpar, quality_matrix_header, 'GCOUNT', 1, 'Group count' + fxaddpar, quality_matrix_header, 'EXTNAME', 'Quality matrix', 'Extension name' + fits_add_checksum, quality_matrix_header, quality_matrix + mwrfits, quality_matrix, out_file_name, quality_matrix_header, /no_comment, /silent + endif ; build the telemetry extension hk_extension_header = !null - fxaddpar, hk_extension_header, 'XTENSION', 'BINTABLE', 'Extension type' - fxaddpar, hk_extension_header, 'BITPIX', 8, 'Number of bits per data pixel' - fxaddpar, hk_extension_header, 'NAXIS', 2, 'Number of data axes' - fxaddpar, hk_extension_header, 'NAXIS1', 0, 'Number of pixels along axis 1' - fxaddpar, hk_extension_header, 'NAXIS2', 0, 'Number of pixels along axis 2' fxaddpar, hk_extension_header, 'PCOUNT', 0, 'Parameter count' fxaddpar, hk_extension_header, 'GCOUNT', 1, 'Group count' fxaddpar, hk_extension_header, 'EXTNAME', 'House-keeping', 'Extension name' hk_bin_table = make_bin_table(hk_table) - mwrfits, hk_bin_table, out_file_name, hk_extension_header, /no_comment, /silent journal, ' HK binary-table extension correctly added' ; unload the spice kernels - cspice_unload, kernels + load_spice_kernels, kernel_list = kernel_list, /unload journal, 'SPICE kernel files unloaded.' diff --git a/metis_rectify.pro b/metis_rectify.pro new file mode 100644 index 0000000..327b060 --- /dev/null +++ b/metis_rectify.pro @@ -0,0 +1,9 @@ +function metis_rectify, image, filter + if filter.toupper() eq 'VL' then image = rotate(image, 1) + if filter.toupper() eq 'UV' then begin + image = reverse(image, 1) + image = rotate(image, 1) + endif + + return, image +end diff --git a/metis_wcs.pro b/metis_wcs.pro new file mode 100644 index 0000000..a7e1edf --- /dev/null +++ b/metis_wcs.pro @@ -0,0 +1,228 @@ +function metis_wcs, header, cal_pack, ref_detector = ref_detector + + if header.filter.contains('VL', /fold) then $ + channel = cal_pack.vl_channel else $ + channel = cal_pack.uv_channel + + boresight = channel.boresight[0] + detector_size = channel.detector_size.value + + ; compute correct boresight parameters to account for image orientation, binning, and fits convention + + ; plate scale + + cdelt1 = boresight.plate_scale.value * header.nbin1 + cdelt2 = boresight.plate_scale.value * header.nbin2 + cdelt = [cdelt1, cdelt2] + + ; old definitions if boresight parameters in pixel units are used + + ; pntpix1 = boresight.pntpix1.value + (nominal_size - 1)/2. + ; pntpix1 = (pntpix1 + 0.5)/header.nbin1 + 0.5 + ; pntpix2 = boresight.pntpix2.value + (nominal_size - 1)/2. + ; pntpix2 = (pntpix2 + 0.5)/header.nbin2 + 0.5 + + ; new definitions if boresight parameters in arcsec units are used + + detector_size = detector_size/sqrt(header.nbin) + xcen = (detector_size + 1)/2. + ycen = (detector_size + 1)/2. + + ; boresight, i.e., io center + + borpix1 = boresight.borpix1.value/cdelt1 + xcen + borpix2 = boresight.borpix2.value/cdelt2 + ycen + borpix = [borpix1, borpix2] + + ; s/c pointing + + pntpix1 = boresight.pntpix1.value/cdelt1 + xcen + pntpix2 = boresight.pntpix2.value/cdelt2 + ycen + pntpix = [pntpix1, pntpix2] + + ; determine spacecract pointing information in the hpc reference frame using the spice kernels + + pointing = solo_get_pointing(header.date_avg) + + ; NOTE - values are defined as follows: + ; pointing[0] = yaw (arcsec) + ; pointing[1] = pitch (arcsec) + ; pointing[2] = roll (deg) + + ; physical coordinates of s/c pointing (i.e., yaw and pitch) in the hpc reference frame + + pntval1 = pointing[0] + pntval2 = pointing[1] + pntval = [pntval1, pntval2] + + ; correct the roll angle value for metis misalignment + + roll = (pointing[2] + boresight.delta_roll.value) * !dtor + + ; wcs rotation matrix in the hpc reference frame + + pc = [[cos(roll), -sin(roll)], [sin(roll), cos(roll)]] + + ; if requested, transform the wcs matrix to the detector reference frame and adjust the boresight and spacecraft pointing parameters + + if keyword_set(ref_detector) then begin + ctype1 = 'HPLT-TAN' + ctype2 = 'HPLN-TAN' + if header.filter.contains('UV', /fold) then begin + borpix_prime = borpix + borpix[0] = detector_size - (borpix_prime[1] - 1.) + borpix[1] = detector_size - (borpix_prime[0] - 1.) + pntpix_prime = pntpix + pntpix[0] = detector_size - (pntpix_prime[1] - 1.) + pntpix[1] = detector_size - (pntpix_prime[0] - 1.) + pc = -reverse(pc, 1) + pc = transpose(pc) + endif + if header.filter.contains('VL', /fold) then begin + borpix_prime = borpix + borpix[0] = borpix_prime[1] + borpix[1] = detector_size - (borpix_prime[0] - 1.) + pntpix_prime = pntpix + pntpix[0] = pntpix_prime[1] + pntpix[1] = detector_size - (pntpix_prime[0] - 1.) + roll = roll + !dpi/2. + pc = [[cos(roll), -sin(roll)], [sin(roll), cos(roll)]] + endif + endif else begin + ctype1 = 'HPLN-TAN' + ctype2 = 'HPLT-TAN' + endelse + + ; get sun's center pixel + + sunval = [0., 0.] + sunpix = (invert(pc) ## (sunval - pntval))/cdelt + pntpix + + ; get coordinates of the image center pixel + + crpix = [xcen, ycen] + crval = (pc ## (crpix - pntpix)) * cdelt + pntval + + ; create the wcs list + + wcs = list() + + wcs.add, { $ + name: 'WCSNAME', $ + value: 'Helioprojective-Cartesian', $ + comment: 'Name of coordinate system'} + wcs.add, { $ + name: 'CTYPE1', $ + value: ctype1, $ + comment: ctype1 eq 'HPLT-TAN' ? 'Helioprojective latitude (Solar Y)' : 'Helioprojective longitude (Solar X)'} + wcs.add, { $ + name: 'CTYPE2', $ + value: ctype2, $ + comment: ctype2 eq 'HPLT-TAN' ? 'Helioprojective latitude (Solar Y)' : 'Helioprojective longitude (Solar X)'} + wcs.add, { $ + name: 'CUNIT1', $ + value: 'arcsec', $ + comment: 'Units along axis 1'} + wcs.add, { $ + name: 'CUNIT2', $ + value: 'arcsec', $ + comment: 'Units along axis 2'} + wcs.add, { $ + name: 'PC1_1', $ + value: pc[0, 0], $ + comment: 'WCS coordinate transformation matrix'} + wcs.add, { $ + name: 'PC1_2', $ + value: pc[1, 0], $ + comment: 'WCS coordinate transformation matrix'} + wcs.add, { $ + name: 'PC2_1', $ + value: pc[0, 1], $ + comment: 'WCS coordinate transformation matrix'} + wcs.add, { $ + name: 'PC2_2', $ + value: pc[1, 1], $ + comment: 'WCS coordinate transformation matrix'} + wcs.add, { $ + name: 'CDELT1', $ + value: cdelt[0], $ + comment: '[arcsec] Pixel scale along axis 1'} + wcs.add, { $ + name: 'CDELT2', $ + value: cdelt[1], $ + comment: '[arcsec] Pixel scale along axis 2'} + wcs.add, { $ + name: 'CROTA', $ + value: atan(pc[0, 1], pc[0, 0]) * !radeg, $ + comment: '[deg] Rotation angle'} + wcs.add, { $ + name: 'CRVAL1', $ + value: crval[0], $ + comment: '[arcsec] Value of reference pixel along axis 1'} + wcs.add, { $ + name: 'CRVAL2', $ + value: crval[1], $ + comment: '[arcsec] Value of reference pixel along axis 2'} + wcs.add, { $ + name: 'CRPIX1', $ + value: crpix[0], $ + comment: '[pixel] Reference pixel location along axis 1'} + wcs.add, { $ + name: 'CRPIX2', $ + value: crpix[1], $ + comment: '[pixel] Reference pixel location along axis 2'} + wcs.add, { $ + name: 'SUN_XCEN', $ + value: sunpix[0], $ + comment: '[pixel] Sun center location along axis 1'} + wcs.add, { $ + name: 'SUN_YCEN', $ + value: sunpix[1], $ + comment: '[pixel] Sun center location along axis 2'} + wcs.add, { $ + name: 'IO_XCEN', $ + value: borpix[0], $ + comment: '[pixel] Metis IO center location along axis 1'} + wcs.add, { $ + name: 'IO_YCEN', $ + value: borpix[1], $ + comment: '[pixel] Metis IO center location along axis 2'} + wcs.add, { $ + name: 'FS_XCEN', $ + value: crpix[0], $ + comment: '[pixel] Metis field-stop center location along axis 1'} + wcs.add, { $ + name: 'FS_YCEN', $ + value: crpix[1], $ + comment: '[pixel] Metis field-stop center location along axis 2'} + wcs.add, { $ + name: 'SC_XCEN', $ + value: pntpix[0], $ + comment: '[pixel] S/C pointing location along axis 1'} + wcs.add, { $ + name: 'SC_YCEN', $ + value: pntpix[1], $ + comment: '[pixel] S/C pointing location along axis 2'} + wcs.add, { $ + name: 'SC_YAW', $ + value: pointing[0], $ + comment: '[arcsec] S/C HPC yaw'} + wcs.add, { $ + name: 'SC_PITCH', $ + value: pointing[1], $ + comment: '[arcsec] S/C HPC pitch'} + wcs.add, { $ + name: 'SC_ROLL', $ + value: pointing[2], $ + comment: '[deg] S/C HPC roll angle'} + wcs.add, { $ + name: 'INN_FOV', $ + value: 1.6, $ + comment: '[deg] Inner Metis FOV'} + wcs.add, { $ + name: 'OUT_FOV', $ + value: 3.4, $ + comment: '[deg] Outer Metis FOV'} + + return, wcs +end diff --git a/solo_get_carrot.pro b/solo_get_carrot.pro new file mode 100644 index 0000000..8612729 --- /dev/null +++ b/solo_get_carrot.pro @@ -0,0 +1,39 @@ +function solo_get_carrot, utc + + ; initial estimate of the carrington rotation number + + jul_day = date_conv(utc, 'julian') + + carr = (jul_day - 2398167.D0)/27.2753D0 + 1 + + ; convert the requested date into ephemeris time + + cspice_str2et, utc, et + + ; get the carrington longitude of earth + + cspice_spkezr, 'EARTH', et, 'IAU_SUN', 'NONE', 'SUN', state, ltime + + ; convert rectangular coordinates into angular coordinates + + cspice_reclat, state[0 : 2], sun_dist, he_lon, he_lat + + ; calculate the fractional part of the decimal rotation number + + if he_lon lt 0. then he_lon = he_lon + 2. * !dpi + frac = 1.D0 - he_lon/(2. * !dpi) + n_carr = round(carr - frac) + carr = n_carr + frac + + ; correction for s/c position + + cspice_spkezr, 'SOLO', et, 'SUN_EARTH_CEQU', 'NONE', 'SUN', state, ltime + + ; convert rectangular coordinates into angular coordinates + + cspice_reclat, state[0 : 2], so_dist, so_lon, so_lat + + carr = carr - so_lon/(2. * !dpi) + + return, carr +end diff --git a/solo_get_coords.pro b/solo_get_coords.pro new file mode 100644 index 0000000..b8d2ec4 --- /dev/null +++ b/solo_get_coords.pro @@ -0,0 +1,43 @@ +function solo_get_coords, utc, frame, obs, velocity = velocity, spherical = spherical, degrees = degrees + + ; convert the requested date into ephemeris time + + cspice_str2et, utc, et + + ; switch to the proper solar orbiter frame + + if ('carrington').contains(frame, /fold) then frame = 'IAU_SUN' + + case frame.toupper() of + 'RTN' : frame = 'SOLO_SUN_RTN' + 'HEE' : frame = 'SUN_EARTH_ECL' + 'HCI' : frame = 'SUN_INERTIAL' + 'HAE' : frame = 'SUN_ARIES_ECL' + 'HEQ' : frame = 'SUN_EARTH_CEQU' + 'GSE' : frame = 'EARTH_SUN_ECL' + 'GEI' : frame = 'J2000' + else : frame = frame + endcase + + ; coordinates of the s/c in the selected reference frame + + cspice_spkezr, 'SOLO', et, frame, 'NONE', obs, state, ltime + + coord = state[0 : 2] + + ; convert rectangular coordinates into angular coordinates + + if keyword_set(spherical) then begin + cspice_reclat, coord, solo_dist, solo_lon, solo_lat + if frame eq 'IAU_SUN' then solo_lon = (solo_lon + 2. * !dpi) mod (2. * !dpi) + if keyword_set(degrees) then begin + solo_lat = solo_lat * 180.D0/!dpi + solo_lon = solo_lon * 180.D0/!dpi + endif + coord = [solo_dist, solo_lon, solo_lat] + endif + + velocity = state[3 : 5] + + return, coord +end diff --git a/solo_get_ephemerides.pro b/solo_get_ephemerides.pro new file mode 100644 index 0000000..ca3d91c --- /dev/null +++ b/solo_get_ephemerides.pro @@ -0,0 +1,232 @@ +function solo_get_ephemerides, header, constants = constants + + utc = header.date_avg + + rsun = constants.rsun.value + au = constants.au.value + + ; compute and add the wcs keyword + + ephemerides = list() + + ephemerides.add, { $ + name: 'LONPOLE', $ + value: 180., $ + comment: '[deg] Native longitude of the celestial pole'} + + ; spherical coordinates of the S/C in the heeq (i.e., stonyhurst) frame + + coord = solo_get_coords(utc, 'HEQ', 'SUN', /spherical, /degrees) + + ; solar photospheric radius as seen from the S/C + + solo_dist = coord[0] * 1000. + rsun_arc = atan(rsun, solo_dist) * !radeg * 3600.d0 + + ephemerides.add, { $ + name: 'RSUN_ARC', $ + value: rsun_arc, $ + comment: '[arcsec] Apparent photospheric solar radius'} + ephemerides.add, { $ + name: 'RSUN_REF', $ + value: rsun, $ + comment: '[m] Assumed physical solar radius'} + + solar_angles = solo_get_solar_angles(utc) + + ephemerides.add, { $ + name: 'SOLAR_B0', $ + value: solar_angles[0], $ + comment: '[deg] S/C tilt of solar North pole'} + ephemerides.add, { $ + name: 'SOLAR_P0 ', $ + value: solar_angles[1], $ + comment: '[deg] S/C celestial North to solar North angle'} + ephemerides.add, { $ + name: 'SOLAR_EP', $ + value: solar_angles[2], $ + comment: '[deg] S/C ecliptic North to solar North angle'} + + carrot = solo_get_carrot(utc) + + ephemerides.add, { $ + name: 'CAR_ROT', $ + value: carrot, $ + comment: 'Carrington rotation number'} + + ephemerides.add, { $ + name: 'HGLT_OBS', $ + value: coord[2], $ + comment: '[deg] S/C heliographic latitude (B0 angle)'} + ephemerides.add, { $ + name: 'HGLN_OBS', $ + value: coord[1], $ + comment: '[deg] S/C heliographic longitude'} + + ; coordinates of the S/C in the carrington frame and carrington rotation number + + coord = solo_get_coords(utc, 'CARRINGTON', 'SUN', /spherical, /degrees) + + ephemerides.add, { $ + name: 'CRLT_OBS', $ + value: coord[2], $ + comment: '[deg] S/C Carrington latitude (B0 angle)'} + ephemerides.add, { $ + name: 'CRLN_OBS', $ + value: coord[1], $ + comment: '[deg] S/C Carrington longitude (L0 angle)'} + + ephemerides.add, { $ + name: 'DSUN_OBS', $ + value: solo_dist, $ + comment: '[m] S/C distance from Sun'} + ephemerides.add, { $ + name: 'DSUN_AU', $ + value: solo_dist/au, $ + comment: '[AU] S/C distance from Sun'} + ephemerides.add, { $ + name: 'AU_REF', $ + value: au, $ + comment: '[m] Assumed physical Astronomical Unit'} + + ; coordinates of the S/C in the hee frame + + coord = solo_get_coords(utc, 'HEE', 'SUN') * 1000. + + ephemerides.add, { $ + name: 'HEEX_OBS', $ + value: coord[0], $ + comment: '[m] S/C Heliocentric Earth Ecliptic X'} + ephemerides.add, { $ + name: 'HEEY_OBS', $ + value: coord[1], $ + comment: '[m] S/C Heliocentric Earth Ecliptic Y'} + ephemerides.add, { $ + name: 'HEEZ_OBS', $ + value: coord[2], $ + comment: '[m] S/C Heliocentric Earth Ecliptic Z'} + + ; coordinates of the S/C in the hci frame + + coord = solo_get_coords(utc, 'HCI', 'SUN', velocity = veloc) + coord = coord * 1000. + veloc = veloc * 1000. + + ephemerides.add, { $ + name: 'HCIX_OBS', $ + value: coord[0], $ + comment: '[m] S/C Heliocentric Inertial X'} + ephemerides.add, { $ + name: 'HCIY_OBS', $ + value: coord[1], $ + comment: '[m] S/C Heliocentric Inertial Y'} + ephemerides.add, { $ + name: 'HCIZ_OBS', $ + value: coord[2], $ + comment: '[m] S/C Heliocentric Inertial Z'} + + ephemerides.add, { $ + name: 'HCIX_VOB', $ + value: veloc[0], $ + comment: '[m/s] S/C Heliocentric Inertial X velocity'} + ephemerides.add, { $ + name: 'HCIY_VOB', $ + value: veloc[1], $ + comment: '[m/s] S/C Heliocentric Inertial Y velocity'} + ephemerides.add, { $ + name: 'HCIZ_VOB', $ + value: veloc[2], $ + comment: '[m/s] S/C Heliocentric Inertial Z velocity'} + + ; coordinates of the S/C in the hae frame + + coord = solo_get_coords(utc, 'HAE', 'SUN') + coord = coord * 1000. + + ephemerides.add, { $ + name: 'HAEX_OBS', $ + value: coord[0], $ + comment: '[m] S/C Heliocentric Aries Ecliptic X'} + ephemerides.add, { $ + name: 'HAEY_OBS', $ + value: coord[1], $ + comment: '[m] S/C Heliocentric Aries Ecliptic Y'} + ephemerides.add, { $ + name: 'HAEZ_OBS', $ + value: coord[2], $ + comment: '[m] S/C Heliocentric Aries Ecliptic Z'} + + ; coordinates of the S/C in the heeq frame + + coord = solo_get_coords(utc, 'HEQ', 'SUN') + coord = coord * 1000. + + ephemerides.add, { $ + name: 'HEQX_OBS', $ + value: coord[0], $ + comment: '[m] S/C Heliocentric Earth Equatorial X'} + ephemerides.add, { $ + name: 'HEQY_OBS', $ + value: coord[1], $ + comment: '[m] S/C Heliocentric Earth Equatorial Y'} + ephemerides.add, { $ + name: 'HEQZ_OBS', $ + value: coord[2], $ + comment: '[m] S/C Heliocentric Earth Equatorial Z'} + + ; coordinates of the S/C in the gse frame + + coord = solo_get_coords(utc, 'GSE', 'EARTH') + coord = coord * 1000. + + ephemerides.add, { $ + name: 'GSEX_OBS', $ + value: coord[0], $ + comment: '[m] S/C Geocentric Solar Ecliptic X'} + ephemerides.add, { $ + name: 'GSEY_OBS', $ + value: coord[1], $ + comment: '[m] S/C Geocentric Solar Ecliptic Y'} + ephemerides.add, { $ + name: 'GSEZ_OBS', $ + value: coord[2], $ + comment: '[m] S/C Geocentric Solar Ecliptic Z'} + + ; light travel times and radial velocity of the S/C + + earth_time = get_light_time(utc, 'EARTH', 'SUN') + sun_time = get_light_time(utc, 'SOLO', 'SUN', radvel = radvel) + tdel = earth_time - sun_time + + ephemerides.add, { $ + name: 'OBS_VR', $ + value: radvel, $ + comment: '[m/s] Radial velocity of S/C relative to Sun'} + ephemerides.add, { $ + name: 'EAR_TDEL', $ + value: tdel, $ + comment: '[s] Time(Sun to Earth) - Time(Sun to S/C)'} + ephemerides.add, { $ + name: 'SUN_TIME', $ + value: sun_time, $ + comment: '[s] Time(Sun to S/C)'} + + ; corrections of the acquisition date + + utc = header.date_beg + + jul_utc = date_conv(utc, 'julian') + date_earth = date_conv(jul_utc + tdel / 86400.d0, 'fits') + date_sun = date_conv(jul_utc - sun_time / 86400.d0, 'fits') + + ephemerides.add, { $ + name: 'DATE_EAR', $ + value: date_earth, $ + comment: '[UTC] Start time of observation, corrected to Earth'} + ephemerides.add, { $ + name: 'DATE_SUN', $ + value: date_sun, $ + comment: '[UTC] Start time of observation, corrected to Sun'} + + return, ephemerides +end diff --git a/solo_get_pointing.pro b/solo_get_pointing.pro new file mode 100644 index 0000000..e18ef46 --- /dev/null +++ b/solo_get_pointing.pro @@ -0,0 +1,53 @@ +function solo_get_pointing, utc, degrees = degrees, radians = radians, celestial = celestial + + ; convert the requested date into ephemeris time + + cspice_str2et, utc, et + + ; convert the ephemeris time into s/c internal time + + cspice_sce2c, -144L, et, clk_in + + ; set the proper solar orbiter frame + + frame = 'SOLO_SUN_RTN' + + if keyword_set(celestial) then frame = 'J2000' + + cspice_ckgp, -144000L, clk_in, 1, frame, cmat, clk_out, found + + if found then cspice_m2eul, cmat, 1, 2, 3, roll, pitch, yaw else return, replicate(0., 3) + + ; make the conversion form rtn to hpc, if necessary + + if frame eq 'SOLO_SUN_RTN' then begin + yaw = !dpi * signum(yaw) - yaw + pitch = -pitch + roll = -roll + endif else begin + pitch = -pitch + endelse + + ; correct any cases where the roll is greater than +/- 180 degrees + + if abs(roll) gt !dpi then roll = roll - 2.d0 * !dpi * signum(roll) + + ; correct any cases where the pitch is greater than +/- 90 degrees + + if abs(pitch) gt !dpi / 2.d0 then begin + yaw = yaw - !dpi * signum(yaw) + pitch = !dpi * signum(pitch) - pitch + roll = roll - !dpi * signum(roll) + endif + + ; apply correct units to the pointing vector + + if keyword_set(radians) then rad_factor = 1. else rad_factor = 180.d0 / !dpi + if keyword_set(radians) or keyword_set(degrees) then arc_factor = 1. else arc_factor = 3600. + + yaw = yaw * rad_factor * arc_factor + pitch = pitch * rad_factor * arc_factor + roll = roll * rad_factor + + return, [yaw, pitch, roll] +end diff --git a/solo_get_solar_angles.pro b/solo_get_solar_angles.pro new file mode 100644 index 0000000..2538d26 --- /dev/null +++ b/solo_get_solar_angles.pro @@ -0,0 +1,61 @@ +function solo_get_solar_angles, utc + + ; convert the requested date into ephemeris time + + cspice_utc2et, utc, et + + cspice_pxform, 'IAU_SUN', 'J2000', et, rot + sun_north = rot[2, *] + + cspice_spkezr, 'SUN', et, 'J2000', 'NONE', 'SOLO', state, ltime + coord = state[0 : 2] + rad = coord / sqrt(total(coord^2, 1)) + + cel_north = [0., 0., 1.d0] + + sun_proj = sun_north - total(rad * sun_north) * rad + sun_proj = sun_proj / sqrt(total(sun_proj^2)) + + cel_proj = cel_north - total(rad * cel_north) * rad + cel_proj = cel_proj / sqrt(total(cel_proj^2)) + + vec_proj = crossp(cel_north, rad) + vec_proj = vec_proj / sqrt(total(vec_proj^2)) + + x_proj = total(sun_proj * cel_proj) + y_proj = total(sun_proj * vec_proj) + + p0 = atan(y_proj, x_proj) * 180.d0 / !dpi + + cspice_pxform, 'IAU_SUN', 'ECLIPJ2000', et, rot + sun_north = rot[2, *] + + cspice_spkezr, 'SUN', et, 'ECLIPJ2000', 'NONE', 'SOLO', state, ltime + coord = state[0 : 2] + rad = coord / sqrt(total(coord^2, 1)) + + eclip_north = cel_north + + sun_proj = sun_north - total(rad * sun_north) * rad + sun_proj = sun_proj / sqrt(total(sun_proj^2)) + + ecl_proj = eclip_north - total(rad * eclip_north) * rad + ecl_proj = ecl_proj / sqrt(total(ecl_proj^2)) + + vec_proj = crossp(eclip_north, rad) + vec_proj = vec_proj / sqrt(total(vec_proj^2)) + + x_proj = total(sun_proj * ecl_proj) + y_proj = total(sun_proj * vec_proj) + + ep = atan(y_proj, x_proj) * 180.d0 / !dpi + + cspice_spkezr, 'SUN', et, 'IAU_SUN', 'NONE', 'SOLO', state, ltime + coord = state[0 : 2] + + cspice_reclat, coord, dist, lon, lat + + b0 = -lat * 180.d0 / !dpi + + return, [b0, p0, ep] +end diff --git a/solo_obt2utc.pro b/solo_obt2utc.pro index 96e21ae..0be3aef 100644 --- a/solo_obt2utc.pro +++ b/solo_obt2utc.pro @@ -1,6 +1,16 @@ function solo_obt2utc, obt - solo_id = -144 - cspice_scs2e, solo_id, obt, et + + ; check if obt is in the correct form (string, with coarse and fine times) + + if isa(obt, /number) then obt_decoded = decode_obt(obt, /from_decimal) else obt_decoded = obt + + ; convert the requested obt into ephemeris time + + cspice_scs2e, -144L, obt_decoded, et + + ; convert the ephemeris time into utc + cspice_et2utc, et, 'ISOC', 3, utc + return, utc -end \ No newline at end of file +end diff --git a/solo_utc2obt.pro b/solo_utc2obt.pro new file mode 100644 index 0000000..ccde385 --- /dev/null +++ b/solo_utc2obt.pro @@ -0,0 +1,12 @@ +function solo_utc2obt, utc + + ; convert the requested utc into ephemeris time + + cspice_str2et, utc, et + + ; convert the ephemeris time into spacecraft obt + + cspice_sce2s, -144L, et, clk_str + obt = clk_str.substring(2) + return, obt +end -- GitLab