-
Notifications
You must be signed in to change notification settings - Fork 14
Add converter for extinction coefficient from CALIOP level 2 aerosol profile (APro) data #1674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
weiwilliam
wants to merge
36
commits into
develop
Choose a base branch
from
feature/caliop_converters
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 22 commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
7ef280d
rebuild calipso level 2 extinction converter
weiwilliam cab01e7
updates
weiwilliam 09a9f22
ready for further test with UFO
weiwilliam 4138e47
update calipso_l2 converter
weiwilliam 73af1a8
Merge branch 'develop' into feature/calipso_l2_ext
weiwilliam 752313c
updates of level2 extinction converter
weiwilliam 3d7f5f4
updates for qc flags handle
weiwilliam 37ec5ae
calipso converter test
weiwilliam 3e53d88
add CAD score and use larger QC value below 8.3km
weiwilliam 833a0f1
updates on caliop l1 and l2 data
weiwilliam 6eeb13c
Merge branch 'develop' into feature/caliop_converters
weiwilliam cb2dc1a
add converter for CALIOP level2 extinction coefficient in APro product
weiwilliam 783fbb1
update CALIOP converter and the testoutput file
weiwilliam 93e0a00
fix the output of frequency and dimension
weiwilliam 9ed18d6
Merge branch 'develop' into feature/caliop_converters
weiwilliam c22cc7e
update Channels
weiwilliam 4cca043
Merge remote-tracking branch 'refs/remotes/origin/feature/caliop_conv…
weiwilliam a14626d
change from 3d to 2d for ufo hofx output
weiwilliam 794a3f6
Merge branch 'develop' into feature/caliop_converters
weiwilliam c4e2180
updates on caliop converters and testoutputs and remove the old files
weiwilliam 070df6c
Merge remote-tracking branch 'refs/remotes/origin/feature/caliop_conv…
weiwilliam 224034c
add layer thickness which derived from layer midpoint altitude
weiwilliam 588b6a6
Merge branch 'develop' into feature/caliop_converters
weiwilliam 2df61d5
move modis_aod.hdf and caliop_l2_APro.hdf to LFS
weiwilliam cce5d95
fix the converter and update the testoutput
weiwilliam 7f6db34
update the date_range to cover the whole sample data
weiwilliam 4791a02
Migrate ioda-converters to jedi-ci action (#1696)
eap a6ed1ac
Merge remote-tracking branch 'origin/develop' into feature/caliop_con…
weiwilliam 32a83ee
store profiles indice into sequenceNumber
weiwilliam bfc6398
update caliop converter and its testoutput
weiwilliam dc0b494
Merge remote-tracking branch 'origin/develop' into feature/caliop_con…
weiwilliam 7051d6d
Merge remote-tracking branch 'origin/develop' into feature/caliop_con…
weiwilliam 1dff7a7
Merge branch 'develop' into feature/caliop_converters
weiwilliam 2126895
Merge branch 'develop' into feature/caliop_converters
weiwilliam 83a6637
Merge branch 'develop' into feature/caliop_converters
weiwilliam dc31a23
Merge branch 'develop' into feature/caliop_converters
weiwilliam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,296 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| # | ||
| # (C) Copyright 2025 UCAR | ||
| # | ||
| # This software is licensed under the terms of the Apache Licence Version 2.0 | ||
| # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. | ||
| # | ||
|
|
||
| import argparse | ||
| from datetime import datetime, timedelta | ||
| import os, sys | ||
|
|
||
| from pyhdf.HDF import * | ||
| from pyhdf.VS import * | ||
| from pyhdf.SD import SD, SDC | ||
| import numpy as np | ||
| import netCDF4 as nc | ||
|
|
||
| import pyiodaconv.ioda_conv_engines as iconv | ||
| from collections import defaultdict, OrderedDict | ||
| from pyiodaconv.orddicts import DefaultOrderedDict | ||
| from pyiodaconv.def_jedi_utils import compute_scan_angle | ||
| from pyiodaconv.def_jedi_utils import iso8601_string, epoch | ||
|
|
||
| # globals | ||
| CALIPSO_WMO_sat_ID = 787 | ||
|
|
||
| AttrData = { | ||
| 'converter': os.path.basename(__file__), | ||
| "platformCommonName": "CALIPSO", | ||
| "platformLongDescription": "CALIPSO L2 Lidar Data", | ||
| } | ||
|
|
||
| metaKeyList = [ | ||
| ("latitude", "float", "degrees_north"), | ||
| ("longitude", "float", "degrees_east"), | ||
| ("dateTime", "long", iso8601_string), | ||
| ("pressure", "float", "Pa"), | ||
| ("sensorCentralWavelength", "float", "micron"), | ||
| ("sensorCentralFrequency", "float", "Hz"), | ||
| ("height", "float", "m"), | ||
| ("atmosphereLayerThicknessZ", "float", "m"), | ||
| ("cloudAerosolDiscriminationHigher", "integer", ""), | ||
| ("cloudAerosolDiscriminationLower", "integer", ""), | ||
| ] | ||
|
|
||
| DimDict = { | ||
| } | ||
|
|
||
| VarDims = { | ||
| 'extinctionCoefficient': ['Location', 'Channel'], | ||
| 'pressure': ['Location'], | ||
| 'height': ['Level'], | ||
| 'atmosphereLayerThicknessZ': ['Level'], | ||
| 'sensorCentralWavelength': ['Channel'], | ||
| 'sensorCentralFrequency': ['Channel'], | ||
| 'cloudAerosolDiscriminationHigher': ['Location', 'Channel'], | ||
| 'cloudAerosolDiscriminationLower': ['Location', 'Channel'], | ||
| } | ||
|
|
||
| obsvars = ["extinctionCoefficient"] | ||
| channels = [1, 2] | ||
| wavelength = np.array([0.532, 1.064]) | ||
| speed_light = 2.99792458E8 | ||
| frequency = speed_light * 1.0E6 / wavelength | ||
|
|
||
| metaDataName = iconv.MetaDataName() | ||
| obsValName = iconv.OvalName() | ||
| obsErrName = iconv.OerrName() | ||
| qcName = iconv.OqcName() | ||
|
|
||
| varsKeyList = [('valKey', obsValName, 'float', 'longitude latitude height', "km-1"), | ||
| ('errKey', obsErrName, 'float', 'longitude latitude height', "km-1"), | ||
| ('qcKey', qcName, 'integer', 'longitude latitude height', None)] | ||
|
|
||
| float_missing_value = iconv.get_default_fill_val(np.float32) | ||
| double_missing_value = iconv.get_default_fill_val(np.float64) | ||
| int_missing_value = iconv.get_default_fill_val(np.int32) | ||
| long_missing_value = iconv.get_default_fill_val(np.int64) | ||
| string_missing_value = iconv.get_default_fill_val(np.str_) | ||
|
|
||
| missing_vals = {'string': string_missing_value, | ||
| 'integer': int_missing_value, | ||
| 'long': long_missing_value, | ||
| 'float': float_missing_value, | ||
| 'double': double_missing_value} | ||
|
|
||
|
|
||
| class calipso_l2ext(object): | ||
| def __init__(self, filenames, date_range): | ||
| self.filenames = filenames | ||
| self.wbeg = np.datetime64(datetime.strptime(date_range[0], "%Y%m%d%H%M")) | ||
| self.wend = np.datetime64(datetime.strptime(date_range[1], "%Y%m%d%H%M")) | ||
| self.varDict = defaultdict(lambda: defaultdict(dict)) | ||
| self.outdata = defaultdict(lambda: DefaultOrderedDict(OrderedDict)) | ||
| self.setDicts() | ||
| self._read() | ||
|
|
||
| def setDicts(self): | ||
| meta_keys = [m_item[0] for m_item in metaKeyList] | ||
| # Set units of the MetaData variables and all _FillValues. | ||
| self.varAttrs = DefaultOrderedDict(lambda: DefaultOrderedDict(dict)) | ||
| for key in meta_keys: | ||
| dtypestr = metaKeyList[meta_keys.index(key)][1] | ||
| if metaKeyList[meta_keys.index(key)][2]: | ||
| self.varAttrs[(key, metaDataName)]['units'] = metaKeyList[meta_keys.index(key)][2] | ||
| self.varAttrs[(key, metaDataName)]['_FillValue'] = missing_vals[dtypestr] | ||
|
|
||
| var_keys = [v_item[0] for v_item in varsKeyList] | ||
| # set up variable names for IODA | ||
| for iodavar in obsvars: | ||
| for key in var_keys: | ||
| varGroupName = varsKeyList[var_keys.index(key)][1] | ||
| dtypestr = varsKeyList[var_keys.index(key)][2] | ||
| coord = varsKeyList[var_keys.index(key)][3] | ||
| self.varDict[iodavar][key] = iodavar, varGroupName | ||
| self.varAttrs[iodavar, varGroupName]['coordinates'] = coord | ||
| self.varAttrs[iodavar, varGroupName]['_FillValue'] = missing_vals[dtypestr] | ||
| if varsKeyList[var_keys.index(key)][4]: | ||
| self.varAttrs[iodavar, varGroupName]['units'] = varsKeyList[var_keys.index(key)][4] | ||
|
|
||
| def calipso_time2dt(time): | ||
weiwilliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| d = np.round(np.mod(time, 100)).astype(np.int32) | ||
| m = np.round(np.mod((time-d), 10000)).astype(np.int32) | ||
| y = np.round(time-m-d).astype(np.int32) | ||
| dtarr = [datetime(2000 + yi//10000, mi//100, di, tzinfo=timezone.utc) | ||
| for yi, mi, di in zip(y, m, d)] | ||
| delta = [timedelta(frac) for frac in np.mod(time, 1)] | ||
| outarr = [dt + dl for dt, dl in zip(dtarr, delta)] | ||
| return outarr | ||
|
|
||
| def _read(self): | ||
| # default missing value in CALIPSO file | ||
| caliop_missing_value = -9999. | ||
| caliop_ref_time = datetime(1993, 1, 1, 0, 0, 0) | ||
| nchan = len(channels) | ||
| output_chidx = np.array(channels, dtype=np.int32) - 1 | ||
|
|
||
| # Make empty lists for the output vars | ||
| self.outdata[('latitude', metaDataName)] = np.array([], dtype=np.float32) | ||
| self.outdata[('longitude', metaDataName)] = np.array([], dtype=np.float32) | ||
| self.outdata[('dateTime', metaDataName)] = np.array([], dtype=np.int64) | ||
| self.outdata[('pressure', metaDataName)] = np.array([], dtype=np.float32) | ||
| self.outdata[('cloudAerosolDiscriminationHigher', metaDataName)] = np.array([], dtype=np.int32) | ||
| self.outdata[('cloudAerosolDiscriminationLower', metaDataName)] = np.array([], dtype=np.int32) | ||
| for iodavar in obsvars: | ||
| self.outdata[self.varDict[iodavar]['valKey']] = np.array([], dtype=np.float32) | ||
| self.outdata[self.varDict[iodavar]['errKey']] = np.array([], dtype=np.float32) | ||
| self.outdata[self.varDict[iodavar]['qcKey']] = np.array([], dtype=np.int32) | ||
|
|
||
| # Get the lidar data altitude | ||
| tmpfile = self.filenames[0] | ||
| tmphdf = HDF(tmpfile) | ||
| vs = tmphdf.vstart() | ||
| metaid = vs.find('metadata') | ||
| vd = vs.attach(metaid) | ||
| vd.setfields('Lidar_Data_Altitudes') | ||
| alt = np.array(vd.read()[0][0]) * 1000. | ||
| nlev = alt.size | ||
| vd.detach() | ||
| vs.end() | ||
|
|
||
| # Calculate the thickness of LiDAR profile | ||
| thickness = np.empty_like(alt) | ||
| thickness[1:-1] = 0.5 * (alt[:-2] - alt[2:]) | ||
| thickness[0] = alt[0] - alt[1] | ||
| thickness[-1] = alt[-2] - alt[-1] | ||
|
|
||
| # Adjust thickness near 20.2 km because it should be around 180m above and 60m below 20.2km | ||
| tmpidx = abs(alt-20200).argmin() | ||
weiwilliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| oldthick = thickness[tmpidx] | ||
| if alt[tmpidx] > 20200: | ||
| thickness[tmpidx] = thickness[tmpidx - 1] | ||
| thickness[tmpidx + 1] = thickness[tmpidx + 1] + abs(thickness[tmpidx] - oldthick) | ||
weiwilliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| else: | ||
| thickness[tmpidx] = thickness[tmpidx + 1] | ||
| thickness[tmpidx - 1] = thickness[tmpidx - 1] + abs(thickness[tmpidx] - oldthick) | ||
|
|
||
| for f in self.filenames: | ||
| sd = SD(f, SDC.READ) | ||
|
|
||
| pres = sd.select('Pressure').get().ravel() * 1e2 # hPa to Pa | ||
| lats = sd.select('Latitude').get()[:, 1] | ||
| nloc = lats.size | ||
| lats = np.repeat(lats, nlev) | ||
| lons = sd.select('Longitude').get()[:, 1] | ||
| lons = np.repeat(lons, nlev) | ||
| profidx = np.arange(lats.size) | ||
| proftime = sd.select('Profile_Time').get()[:, 1] | ||
| obs_time = (proftime + caliop_ref_time.timestamp()).astype('datetime64[s]') | ||
| obs_time = np.repeat(obs_time, nlev) | ||
| winmsk = ((obs_time >= self.wbeg) & (obs_time <= self.wend)) | ||
|
|
||
| obs = np.zeros((nloc * nlev, nchan)) | ||
| err = np.zeros_like(obs) | ||
| qcf = np.zeros_like(obs) | ||
| for i, chidx in enumerate(output_chidx): | ||
| wavelength_str = str(int(wavelength[chidx] * 1e3)) | ||
| obsvarname = f"Extinction_Coefficient_{wavelength_str}" | ||
| errvarname = f"Extinction_Coefficient_Uncertainty_{wavelength_str}" | ||
| qcfvarname = f"Extinction_QC_Flag_{wavelength_str}" | ||
| # Level 2 QC flag stores 30m level 1 QC flag below 8.3 km in the rightmost dimension | ||
| # Based on Young et al. (2018): qc flag value 0, 1, 2, 16, and 18 should be used. | ||
| tmpqcf = sd.select(qcfvarname).get() | ||
| tmpqcf = tmpqcf.reshape(-1, tmpqcf.shape[2]) | ||
| tmpqcf = np.where((tmpqcf[:, 0] == tmpqcf[:, 1]), tmpqcf[:, 0], | ||
| np.maximum(tmpqcf[:, 0], tmpqcf[:, 1])) | ||
| tmpqcf = np.where(np.isin(tmpqcf, [0, 1, 2, 16, 18]), 0, 1) | ||
|
|
||
| obs[:, i] = sd.select(obsvarname).get().ravel() | ||
| err[:, i] = sd.select(errvarname).get().ravel() | ||
| qcf[:, i] = tmpqcf | ||
|
|
||
| # Similar to QC_Flag, it stores higher and lower 30 meter layers' CAD score | ||
| cad1 = np.zeros_like(pres) | ||
| cad2 = np.zeros_like(pres) | ||
| tmpcad = sd.select("CAD_Score").get() | ||
| tmpcad = tmpcad.reshape(-1, tmpcad.shape[2]) | ||
| cad1 = tmpcad[:, 0] | ||
| cad2 = tmpcad[:, 1] | ||
|
|
||
| obs = np.where((obs == caliop_missing_value), float_missing_value, obs) | ||
| err = np.where((err == caliop_missing_value), float_missing_value, err) | ||
| pres = np.where(pres < 0, float_missing_value, pres) | ||
|
|
||
| self.outdata[('latitude', metaDataName)] = np.append(self.outdata[('latitude', metaDataName)], | ||
| np.array(lats[winmsk], dtype=np.float32)) | ||
| self.outdata[('longitude', metaDataName)] = np.append(self.outdata[('longitude', metaDataName)], | ||
| np.array(lons[winmsk], dtype=np.float32)) | ||
| self.outdata[('dateTime', metaDataName)] = np.append(self.outdata[('dateTime', metaDataName)], | ||
| np.array(obs_time[winmsk], dtype=np.int64)) | ||
| self.outdata[('pressure', metaDataName)] = np.append(self.outdata[('pressure', metaDataName)], | ||
| np.array(pres[winmsk], dtype=np.float32)) | ||
| self.outdata[('cloudAerosolDiscriminationHigher', metaDataName)] = np.append( | ||
| self.outdata[('cloudAerosolDiscriminationHigher', metaDataName)], np.array(cad1[winmsk], dtype=np.int32)) | ||
| self.outdata[('cloudAerosolDiscriminationLower', metaDataName)] = np.append( | ||
| self.outdata[('cloudAerosolDiscriminationLower', metaDataName)], np.array(cad2[winmsk], dtype=np.int32)) | ||
|
|
||
| for iodavar in obsvars: | ||
| self.outdata[self.varDict[iodavar]['valKey']] = np.append( | ||
| self.outdata[self.varDict[iodavar]['valKey']], np.array(obs[winmsk, :], dtype=np.float32)) | ||
| self.outdata[self.varDict[iodavar]['errKey']] = np.append( | ||
| self.outdata[self.varDict[iodavar]['errKey']], np.array(err[winmsk, :], dtype=np.float32)) | ||
| self.outdata[self.varDict[iodavar]['qcKey']] = np.append( | ||
| self.outdata[self.varDict[iodavar]['qcKey']], np.array(qcf[winmsk, :], dtype=np.int32)) | ||
|
|
||
| sd.end() | ||
|
|
||
| self.outdata[('sensorCentralWavelength', metaDataName)] = np.array(wavelength, dtype=np.float32)[output_chidx] | ||
| self.outdata[('sensorCentralFrequency', metaDataName)] = np.array(frequency, dtype=np.float32)[output_chidx] | ||
| self.outdata[('height', metaDataName)] = np.array(alt, dtype=np.float32) | ||
| self.outdata[('atmosphereLayerThicknessZ', metaDataName)] = np.array(thickness, dtype=np.float32) | ||
| DimDict['Location'] = len(self.outdata[('dateTime', metaDataName)]) | ||
| DimDict['Channel'] = np.array(channels) | ||
| DimDict['Level'] = np.arange(nlev) | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description=( | ||
| 'Reads the CALIOP Level 2 aerosol profile data ' | ||
| ' convert into IODA formatted output files. ' | ||
| ' Multiple files are concatenated') | ||
| ) | ||
|
|
||
| required = parser.add_argument_group(title='required arguments') | ||
| required.add_argument( | ||
| '-i', '--input', | ||
| help="path of CALIOP APro (HDF4) input file(s)", | ||
| type=str, nargs='+', required=True) | ||
| required.add_argument( | ||
| '-o', '--output', | ||
| help='path to output ioda file', | ||
| type=str, required=True) | ||
|
|
||
| optional = parser.add_argument_group(title='optional arguments') | ||
| optional.add_argument( | ||
| '--date_range', | ||
| help="extract a date range to fit the data assimilation window" | ||
| "format -r YYYYMMDDHHmm YYYYMMDDHHmm", | ||
| type=str, metavar=('begindate', 'enddate'), nargs=2, | ||
| default=('197001010000', '217001010000')) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| # Read CALIPSO extinction profile data | ||
| calipsol2 = calipso_l2ext(args.input, args.date_range) | ||
|
|
||
| # write everything out | ||
| writer = iconv.IodaWriter(args.output, metaKeyList, DimDict) | ||
| writer.BuildIoda(calipsol2.outdata, VarDims, calipsol2.varAttrs, AttrData) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
weiwilliam marked this conversation as resolved.
Show resolved
Hide resolved
|
Binary file not shown.
Git LFS file not shown
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.