├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── README_zh.md ├── bld.bat ├── build.sh ├── cinrad ├── __init__.py ├── _typing.py ├── _utils.pyx ├── calc.py ├── common.py ├── correct │ ├── __init__.py │ ├── _unwrap_2d.pyx │ ├── dealias.py │ └── unwrap_2d_ljmu.c ├── data │ ├── chinaCity.json │ ├── colormap │ │ ├── CC.cmap │ │ ├── CC2.cmap │ │ ├── CC_s.cmap │ │ ├── ET.cmap │ │ ├── HCL.cmap │ │ ├── KDP.cmap │ │ ├── KDP_s.cmap │ │ ├── OHP.cmap │ │ ├── REF.cmap │ │ ├── REF_s.cmap │ │ ├── VEL.cmap │ │ ├── VEL_reverse.cmap │ │ ├── VEL_s.cmap │ │ ├── VIL.cmap │ │ ├── ZDR.cmap │ │ └── ZDR_s.cmap │ ├── hca_params.npy │ └── radar_station.json ├── deprecation.py ├── error.py ├── grid.py ├── hca.py ├── io │ ├── __init__.py │ ├── _dtype.py │ ├── _radar_struct │ │ ├── CC.py │ │ ├── CC2.py │ │ ├── CD.py │ │ ├── PA.py │ │ ├── __init__.py │ │ ├── parse_spec.py │ │ └── standard_data.py │ ├── base.py │ ├── export.py │ ├── level2.py │ └── level3.py ├── projection.py ├── utils.py └── visualize │ ├── __init__.py │ ├── gpf.py │ ├── layout.py │ ├── ppi.py │ ├── rhi.py │ └── utils.py ├── docs ├── Makefile ├── api │ ├── api.rst │ ├── cinrad.io.rst │ └── cinrad.visualize.rst ├── conf.py ├── environment.yml ├── index.rst └── make.bat ├── environment.yaml ├── example ├── read_data.ipynb ├── to_pyart.ipynb └── visualization.ipynb ├── meta.yaml ├── pictures ├── XXX_XXX_RHI_299_100_REF.png ├── Z9574_20190321025715_0.5_230_ZDR_29.47N121.44E_29.4N122.04E.png ├── Z9735_20180304004209_VCS_25.5N111E_26.5N112E.png ├── Z9735_20180304120845_0.6_230_REF.png ├── Z9735_20180304125031_0.6_230_REF.png └── ZGZ02_20200826123326_0.9_40_REF.png ├── requirements.txt ├── setup.py ├── test ├── test_calc.py └── test_io.py └── ui ├── gen_ui.bat ├── main_ui.pyw └── ui_struct.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-vendored -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | defaults: 14 | run: 15 | shell: bash -l {0} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: ["ubuntu-latest", "windows-latest"] 20 | python-version: ["3.9", "3.10", "3.11", "3.12"] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: conda-incubator/setup-miniconda@v3 25 | with: 26 | auto-update-conda: true 27 | python-version: ${{ matrix.python-version }} 28 | environment-file: environment.yaml 29 | activate-environment: build 30 | - name: Build 31 | run: | 32 | python -m pip install --upgrade setuptools 33 | python -m pip install --upgrade wheel 34 | python -m pip install --upgrade numpy>=2.0 35 | python setup.py bdist_wheel 36 | - name: Upload artifact 37 | uses: actions/upload-artifact@v4 38 | if: ${{ !github.head_ref }} 39 | with: 40 | name: ${{ matrix.os }}_${{ matrix.python-version }} 41 | path: dist/ 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - uses: conda-incubator/setup-miniconda@v2 18 | with: 19 | auto-update-conda: true 20 | python-version: "3.11" 21 | environment-file: environment.yaml 22 | activate-environment: build 23 | - name: Install 24 | run: | 25 | python -m pip install --upgrade setuptools 26 | python -m pip install --upgrade numpy>=2.0 27 | python -m pip install . 28 | - name: Run tests 29 | run: | 30 | python -m pip install pytest 31 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /cinrad.egg-info 3 | /build 4 | *.pyc 5 | /.vs 6 | .idea/* 7 | *.ui 8 | /.vscode 9 | /cinrad/_utils.c 10 | /cinrad/correct/_unwrap_2d.c 11 | /.mypy_cache/* 12 | *.spec 13 | docs/build 14 | .coverage 15 | htmlcov -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF 17 | formats: 18 | - pdf 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | conda: 22 | environment: docs/environment.yml 23 | 24 | python: 25 | version: 3.7 26 | install: 27 | - method: setuptools 28 | path: . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyCINRAD 2 | 3 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 4 | [![Downloads](https://pepy.tech/badge/cinrad)](https://pepy.tech/project/cinrad) 5 | [![DOI](https://zenodo.org/badge/139155365.svg)](https://zenodo.org/badge/latestdoi/139155365) 6 | 7 | Decode CINRAD (China New Generation Weather Radar) data and visualize. 8 | 9 | To check out live examples and docs, visit [pycinrad.cn](https://pycinrad.cn/). 10 | 11 | [中文说明](https://github.com/CyanideCN/PyCINRAD/blob/master/README_zh.md) 12 | 13 | **`example` folder contains detailed examples!** 14 | 15 | ## Installation 16 | 17 | PyCINRAD supports Python version 3.9 and higher. 18 | 19 | ``` 20 | pip install cinrad 21 | ``` 22 | 23 | You can also download from github page and build from source 24 | 25 | ``` 26 | python setup.py install 27 | ``` 28 | 29 | ## Modules 30 | 31 | ### cinrad.io 32 | 33 | Decode CINRAD radar data. 34 | 35 | ```python 36 | from cinrad.io import CinradReader, StandardData 37 | f = CinradReader(your_radar_file) #Old version data 38 | f = StandardData(your_radar_file) #New standard data (or phased array data) 39 | f.get_data(tilt, drange, dtype) #Get data 40 | f.get_raw(tilt, drange, dtype) 41 | ``` 42 | 43 | The `get_raw` method returns radar records without other geographic information. 44 | 45 | The `get_data` method returns `xarray.Dataset` with radar records, geographic coordinates, and all extra attributes. So, all benefits of `xarray` can be enjoyed. Check xarray [documentation](https://docs.xarray.dev/en/latest/generated/xarray.Dataset.html) for more detailed explanation. 46 | 47 | For example, it's very convenient to save data as netcdf format. 48 | ```python 49 | >>> data.to_netcdf('1.nc') 50 | ``` 51 | 52 | `xarray` also makes interpolation very convenient. 53 | ```python 54 | >>> data.interp(azimuth=np.deg2rad(300), distance=180) 55 | ``` 56 | 57 | For single-tilt data (i.e. files that contain only one elevation angle), `cinrad.io.StandardData.merge` can merge these files to a file contains full volumetric scan. 58 | 59 | #### Export data to `Py-ART` defined class 60 | 61 | Convert data structure defined in this module into `pyart.core.Radar` is very simple. `cinrad.io.export` has a function `standard_data_to_pyart`, which can take `cinrad.io.StandardData` as input and return `pyart.core.Radar` as output. 62 | 63 | `example` folder contains a simple demo about this. 64 | 65 | #### Decode PUP data and SWAN data 66 | 67 | `cinrad.io.StandardPUP` provides functions to decode Standard PUP(rose) data. The extracted data can be further used to create PPI. 68 | 69 | `cinrad.io.SWAN` provides similar interface to decode SWAN data. 70 | 71 | ```python 72 | from cinrad.io import StandardPUP 73 | f = StandardPUP(your_radar_file) 74 | data = f.get_data() 75 | ``` 76 | 77 | #### Decode phased array radar data 78 | 79 | `cinrad.io.PhasedArrayData` provides similar interface to decode level 2 data from phased array radar with old format. 80 | 81 | ```python 82 | from cinrad.io import PhasedArrayData 83 | f = PhasedArrayData(your_radar_file) 84 | data = f.get_data(0, 40, 'REF') 85 | ``` 86 | 87 | ### cinrad.utils 88 | 89 | This submodule provides some useful algorithms in radar meteorology. All functions only accept `numpy.ndarray` as input data. This submodule extends the usage of this program, as these functions can accept customized data rather than only the data decoded by `cinrad.io`. 90 | 91 | ### cinrad.calc 92 | 93 | For direct computation of decoded data, `cinrad.calc` provides functions that simplify the process of calculation. For functions contained in this submodule, only a list of reflectivity data is required as the argument. 94 | 95 | Code to generate the required list: 96 | 97 | ```python 98 | r_list = [f.get_data(i, 230, 'REF') for i in f.angleindex_r] 99 | # or 100 | r_list = list(f.iter_tilt(230, 'REF')) 101 | ``` 102 | 103 | #### VCS 104 | 105 | `cinrad.calc.VCS` provides calculation of vertical cross-section for **all variables**. 106 | 107 | ```python 108 | import cinrad 109 | from cinrad.visualize import Section 110 | f = cinrad.io.CinradReader(your_radar_file) 111 | rl = [f.get_data(i, 230, 'REF') for i in f.angleindex_r] 112 | vcs = cinrad.calc.VCS(rl) 113 | sec = vcs.get_section(start_cart=(111, 25.5), end_cart=(112, 26.7)) # pass geographic coordinates (longitude, latitude) 114 | sec = vcs.get_section(start_polar=(115, 350), end_polar=(130, 30)) # pass polar coordinates (distance, azimuth) 115 | fig = Section(sec) 116 | fig('D:\\') 117 | ``` 118 | 119 | #### Radar mosaic 120 | 121 | `cinrad.calc.GridMapper` can merge different radar scans into a cartesian grid, also supports CR. 122 | 123 | #### Hydrometeor classification 124 | 125 | `cinrad.calc.hydro_class` uses algorithm suggested by Dolan to classify hydrometeors into 10 categories. (Requires REF, ZDR, RHO, and KDP) 126 | 127 | ### cinrad.correct 128 | 129 | This submodule provides algorithms to correct raw radar fields. 130 | 131 | #### cinrad.correct.dealias 132 | 133 | This function can unwrap the folded velocity using algorithm originated from `pyart`. (needs C compiler) 134 | 135 | ```python 136 | import cinrad 137 | #(some codes omitted) 138 | v = f.get_data(1, 230, 'VEL') 139 | v_corrected = cinrad.correct.dealias(v) 140 | ``` 141 | 142 | ### cinrad.visualize 143 | 144 | Visualize the data stored in acceptable format (`cinrad.datastruct`). It also means that you can using customized data to perform visualization, as long as the data is stored as `xarray.Dataset` and constructed by the same protocol (variables naming conventions, data coordinates and dimensions, etc.) For further information about this method, please see the examples contained in `example` folder. 145 | 146 | ```python 147 | from cinrad.visualize import PPI 148 | fig = PPI(R) #Plot PPI 149 | fig('D:\\') #Pass the path to save the fig 150 | from cinrad.visualize import Section 151 | fig = Section(Slice_) #Plot VCS 152 | fig('D:\\') 153 | ``` 154 | 155 | The path passed into the class can either be the folder path or the file path. Also, if no path is passed, the figure will be saved at the folder named `PyCINRAD` in the home folder (e.g. `C:\Users\tom`). 156 | 157 | #### Customize plot settings 158 | 159 | The summary of args that can be passed into `PPI` are listed as follows. 160 | 161 | |arg|function| 162 | |:-:|:-:| 163 | |`cmap`|colormaps used for plotting| 164 | |`norm`|norm used for plotting| 165 | |`nlabel`|number of labels on the colorbar| 166 | |`label`|labels on the colorbar| 167 | |`highlight`|highlight area of input name| 168 | |`dpi`|dpi of figure| 169 | |`extent`|area to plot e.g. `extent=[90, 91, 29, 30]`| 170 | |`section`|cross-section data to ppi plot| 171 | |`style`|background color:`black`/`white`/`transparent`| 172 | |`add_city_names`|annotate name of city on the plot| 173 | 174 | Beside args, class `PPI` has some other auxiliary plotting functions. 175 | 176 | ##### PPI.plot_range_rings(self, _range, color='white', linewidth=0.5, **kwargs) 177 | 178 | Plot range rings on the PPI plot. 179 | 180 | ##### PPI.plot_cross_section(self, data, ymax=None) 181 | 182 | Plot VCS section under the PPI plot. 183 | 184 | This function is very similar to `vcs` argument of class `PPI`, but the range of y-axis can be adjusted only by this function. 185 | 186 | ##### PPI.storm_track_info(self, filepath) 187 | 188 | Plot PUP STI product on the current PPI map, including past positions, current position, and forecast positions. 189 | 190 | ## Gallery 191 | 192 | #### PPI reflectivity 193 | 194 | ![PPI reflectivity](https://raw.githubusercontent.com/CyanideCN/PyCINRAD/master/pictures/Z9735_20180304125031_0.6_230_REF.png) 195 | 196 | #### Phased array radar reflectivity 197 | 198 | ![Phased array radar reflectivity](https://raw.githubusercontent.com/CyanideCN/PyCINRAD/master/pictures/ZGZ02_20200826123326_0.9_40_REF.png) 199 | 200 | #### PPI reflectivity combined with cross-section 201 | 202 | ![PPI reflectivity combined with cross-section](https://raw.githubusercontent.com/CyanideCN/PyCINRAD/master/pictures/Z9735_20180304120845_0.6_230_REF.png) 203 | 204 | #### Cross-section 205 | 206 | ![Cross-section](https://raw.githubusercontent.com/CyanideCN/PyCINRAD/master/pictures/Z9735_20180304004209_VCS_25.5N111E_26.5N112E.png) 207 | 208 | #### Cross-section other than reflectivity 209 | 210 | ![ZDR cross-section](https://raw.githubusercontent.com/CyanideCN/PyCINRAD/master/pictures/Z9574_20190321025715_0.5_230_ZDR_29.47N121.44E_29.4N122.04E.png) 211 | 212 | #### RHI reflectivity 213 | 214 | ![RHI reflectivity](https://raw.githubusercontent.com/CyanideCN/PyCINRAD/master/pictures/XXX_XXX_RHI_299_100_REF.png) 215 | 216 | ## Citation 217 | 218 | If you use PyCINRAD in your paper, please cite PyCINRAD using the DOI below. 219 | 220 | [![DOI](https://zenodo.org/badge/139155365.svg)](https://zenodo.org/badge/latestdoi/139155365) 221 | 222 | ## Papers that use plots generated by `PyCINRAD` 223 | 224 | 1. Recognition and Analysis of Biological Echo Using WSR-88D Dual-polarization Weather Radar in Nanhui of Shanghai doi: 10.16765/j.cnki.1673-7148.2019.03.015 225 | 226 | ## Notes 227 | 228 | The hydrometeor classfication algorithm comes from Dolan, B., S. A. Rutledge, S. Lim, V. Chandrasekar, and M. Thurai, 2013: A Robust C-Band Hydrometeor Identification Algorithm and Application to a Long-Term Polarimetric Radar Dataset. J. Appl. Meteor. Climatol., 52, 2162–2186, https://doi.org/10.1175/JAMC-D-12-0275.1. 229 | 230 | If you are interested in this program, you can join the developers of this program. Any contribution is appreciated! 231 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # PyCINRAD 2 | 3 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 4 | [![Downloads](https://pepy.tech/badge/cinrad)](https://pepy.tech/project/cinrad) 5 | [![DOI](https://zenodo.org/badge/139155365.svg)](https://zenodo.org/badge/latestdoi/139155365) 6 | 7 | Decode CINRAD (China New Generation Weather Radar) data and visualize. 8 | 9 | 查看示例,请访问官网 [pycinrad.cn](https://pycinrad.cn/). 10 | 11 | 读取CINRAD雷达数据,进行相关计算并可视化的模块。 12 | 13 | **使用交流群:480305660** 14 | 15 | **`example`文件夹里有详细的使用示例!** 16 | 17 | ## 安装 18 | 19 | ### 安装方法 20 | 21 | 支持Python 3.9 及以上 22 | 23 | ``` 24 | pip install cinrad 25 | ``` 26 | 27 | 或在此页面下载并执行 28 | ``` 29 | git clone https://github.com/CyanideCN/PyCINRAD.git 30 | cd PyCINRAD 31 | python setup.py install 32 | ``` 33 | 34 | ## 模块介绍 35 | 36 | ### cinrad.io 37 | 38 | 读取CINRAD雷达数据。 39 | 40 | 例子: 41 | 42 | ```python 43 | from cinrad.io import CinradReader, StandardData 44 | f = CinradReader(your_radar_file) #老版本数据 45 | f = StandardData(your_radar_file) #新版本标准(相控阵)基数据 46 | f.get_data(tilt, drange, dtype) #获取数据 47 | f.get_raw(tilt, drange, dtype) 48 | ``` 49 | 对于单层RHI数据,传入`get_data`的`tilt`参数将会被设置成0。 50 | `get_raw`方法只会以ndarray的形式返回雷达的数据,不会返回其他的地理信息,因此速度会更快,内存占用更少,在大批量分析数据的时候比较推荐使用此方法。`get_data`返回的数据类型为`xarray.Dataset`,因此可以享受`xarray`模块的便利。[xarray.Dataset的文档](https://docs.xarray.dev/en/latest/generated/xarray.Dataset.html) 51 | 52 | 例如,可以很轻松的把数据保存成netcdf格式。 53 | ```python 54 | >>> data.to_netcdf('1.nc') 55 | ``` 56 | `xarray`的插值也很简单,例如获取方位角300度,距离180km的数据。 57 | ```python 58 | >>> data.interp(azimuth=np.deg2rad(300), distance=180) 59 | ``` 60 | 61 | `cinrad.io.StandardData.merge`可以合并单仰角的数据,返回一个完整的体扫文件。 62 | 63 | 64 | #### 转换为`pyart.core.Radar`类型 65 | 66 | `cinrad.io.export.standard_data_to_pyart`可以将`cinrad.io.StandardData`转换为`pyart.core.Radar`。 67 | 68 | `example`文件夹里有简单示例。 69 | 70 | #### 读取PUP数据和SWAN数据 71 | 72 | `cinrad.io.PUP`提供读取PUP数据的功能,目前只支持径向类型数据以及组合反射率。新格式的PUP产品(ROSE产品)可以用`cinrad.io.level3.StandardPUP`来读取,目前支持大部分产品。 73 | `cinrad.io.SWAN`提供相似的接口来解码SWAN数据。 74 | 75 | ```python 76 | from cinrad.io import StandardPUP 77 | f = StandardPUP(your_radar_file) 78 | data = f.get_data() 79 | ``` 80 | 81 | #### 读取旧格式相控阵雷达数据 82 | 83 | `cinrad.io.PhasedArrayData`提供读取旧格式的相控阵雷达基数据的功能,用法和其他接口非常类似。 84 | 85 | ```python 86 | from cinrad.io import PhasedArrayData 87 | f = PhasedArrayData(your_radar_file) 88 | data = f.get_data(0, 40, 'REF') 89 | ``` 90 | 91 | ### cinrad.utils 92 | 93 | 提供雷达衍生产品的计算(接受`numpy.ndarray`)。将这些功能独立出来的目的是使得计算程序更加通用。 94 | 95 | 函数名: 96 | `composite_reflectivity`, `echo_tops`, `vert_integrated_liquid` 97 | 98 | 计算ET和VIL时,考虑到速度问题,模块提供由cython转换而来的python扩展,可以大大提升速度。如果要使用此扩展,请安装cython以及C编译器,并重新安装此模块。(由pip直接安装的版本都是带有cython扩展的。) 99 | 100 | 注:对于当反射率很强时,得到的VIL值可能会很大,这是因为该计算函数没有对强回波进行滤除,算法本身并无问题,如有滤除需要可以先使用`np.clip`将回波最大值设置为55dBZ再进行计算。 101 | 102 | ### cinrad.calc 103 | 104 | 提供雷达衍生产品的计算 105 | 使用`cinrad.io`读取的数据可直接带入该模块下的函数来计算。 106 | 107 | 传入一个包含每个仰角数据的list即可计算。 108 | 109 | 注:当模块使用编译的C扩展的时候提供VIL密度的计算。 110 | 111 | 列表生成示例: 112 | ```python 113 | r_list = [f.get_data(i, drange, 'REF') for i in f.angleindex_r] 114 | # 或者 115 | r_list = list(f.iter_tilt(230, 'REF')) 116 | ``` 117 | #### VCS 118 | 119 | `cinrad.calc.VCS`用于计算任意两点剖面,目前支持所有要素。 120 | 121 | 示例代码: 122 | ```python 123 | import cinrad 124 | from cinrad.visualize import Section 125 | f = cinrad.io.CinradReader(your_radar_file) 126 | rl = list(f.iter_tilt(230, 'REF')) 127 | vcs = cinrad.calc.VCS(rl) 128 | sec = vcs.get_section(start_cart=(111, 25.5), end_cart=(112, 26.7)) # 传入经纬度坐标 129 | sec = vcs.get_section(start_polar=(115, 350), end_polar=(130, 30)) # 传入极坐标 130 | fig = Section(sec) 131 | fig('D:\\') 132 | ``` 133 | 134 | #### 雷达拼图 135 | 136 | `cinrad.calc.GridMapper`可以将不同雷达的扫描数据合并成雷达格点拼图,支持基本反射率和组合反射率。 137 | 138 | #### 水凝物分类 139 | 140 | `cinrad.calc.hydro_class`从反射率,差分反射率,协相关系数和差分传播相移率计算出10种水凝物类型。 141 | 142 | ### cinrad.correct 143 | 144 | 提供雷达原数据的校正。 145 | 146 | #### cinrad.correct.dealias 147 | 148 | 利用`pyart`的算法进行速度退模糊。(需要C编译器) 149 | 150 | ```python 151 | import cinrad 152 | #(文件处理部分省略) 153 | v = f.get_data(1, 230, 'VEL') 154 | v_corrected = cinrad.correct.dealias(v) 155 | ``` 156 | 157 | ### cinrad.visualize 158 | 159 | 雷达数据可视化,包括`PPI`和`Section`。如果传入的是自定义的数据,需要符合本模块构建`xarray.Dataset`的方式,比如坐标和维度的名字,变量的命名,等等。 160 | 161 | 示例: 162 | 163 | ```python 164 | from cinrad.visualize import PPI 165 | fig = PPI(R) #绘制基本反射率图片 166 | fig('D:\\') #传入文件夹路径保存图片 167 | from cinrad.visualize import Section 168 | fig = Section(sec) #绘制剖面 169 | fig('D:\\') 170 | ``` 171 | 172 | 如果读取了其他雷达的数据,转换成上文提到的合适的`xarray.Dataset`类型也可使用此模块画图。 173 | 传入的文件路径可以是文件夹路径也可以是文件路径(仅接受以`.png`结尾的文件路径),如果没有传入路径,程序将会把图片保存在用户目录 174 | (Windows 下称为「个人文件夹」,如 `C:\Users\tom`)下的`PyCINRAD`文件夹。 175 | 176 | #### 自定义绘图 177 | 178 | `PPI`支持传入其他参数,总结如下。 179 | 180 | |参数|功能| 181 | |:-:|:-:| 182 | |`cmap`|色阶| 183 | |`norm`|色阶范围| 184 | |`nlabel`|色阶条标注个数| 185 | |`label`|色阶条标注| 186 | |`highlight`|地区边界高亮| 187 | |`dpi`|分辨率| 188 | |`extent`|绘图的经纬度范围 e.g. `extent=[90, 91, 29, 30]`| 189 | |`section`|在`ppi`图中绘制的剖面的数据,为`xarray.Dataset`类型| 190 | |`style`|背景:黑色`black`或白色`white`或透明`transparent`| 191 | |`add_city_names`|标注城市名| 192 | 193 | 同时`PPI`类中定义有其他绘图函数: 194 | ##### PPI.plot_range_rings(self, _range, color='white', linewidth=0.5, **kwargs) 195 | 196 | 在PPI图上绘制圆圈。 197 | 198 | ##### PPI.plot_cross_section(self, data, ymax=None) 199 | 200 | 在PPI图下方加入VCS剖面图,和`vcs`参数相似,用此函数还可以自定义y轴的范围。需要注意的是,在`%matplotlib inline`环境下,不能使用此方法插入剖面。请在实例化`PPI`时就使用`section`参数来插入剖面。 201 | 202 | ```python 203 | fig = cinrad.visualize.PPI(data, section=section_data) 204 | ``` 205 | 206 | ##### PPI.storm_track_info(self, filepath) 207 | 208 | 在PPI图上叠加PUP的STI产品。 209 | 210 | ## 相关链接 211 | 212 | [利用PyCINRAD处理、显示天气雷达基数据](http://climate2weather.cc/2019/05/12/radar/) 213 | 214 | ## 引用 215 | 216 | 如果你在你的论文中使用了本模块,请使用下方的DOI添加引用。 217 | 218 | [![DOI](https://zenodo.org/badge/139155365.svg)](https://zenodo.org/badge/latestdoi/139155365) 219 | 220 | ### 使用本模块绘制图片的论文 221 | 222 | 1. 上海南汇WSR-88D双偏振天气雷达的生物回波识别与分析 doi: 10.16765/j.cnki.1673-7148.2019.03.015 223 | 224 | ## 其他 225 | 226 | 回波顶高及垂直积分液态含水量(密度)算法来源:肖艳姣, 马中元, 李中华. 改进的雷达回波顶高、垂直积分液态水含量及其密度算法[J]. 暴雨灾害, 2009, 28(3):20-24. 227 | 228 | 水凝物分类算法来源:Dolan, B., S. A. Rutledge, S. Lim, V. Chandrasekar, and M. Thurai, 2013: A Robust C-Band Hydrometeor Identification Algorithm and Application to a Long-Term Polarimetric Radar Dataset. J. Appl. Meteor. Climatol., 52, 2162–2186, https://doi.org/10.1175/JAMC-D-12-0275.1. 229 | 230 | 如果你对这个模块感兴趣,欢迎加入这个模块的开发者行列! 231 | -------------------------------------------------------------------------------- /bld.bat: -------------------------------------------------------------------------------- 1 | "%PYTHON%" setup.py install --single-version-externally-managed --record=record.txt 2 | if errorlevel 1 exit 1 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | $PYTHON $SRC_DIR/setup.py install --single-version-externally-managed --record=record.txt 4 | -------------------------------------------------------------------------------- /cinrad/__init__.py: -------------------------------------------------------------------------------- 1 | from . import io 2 | from . import grid 3 | from . import utils 4 | from . import calc 5 | from . import visualize 6 | from . import correct 7 | 8 | from .io import read_level2 9 | 10 | __version__ = "1.9.2" 11 | -------------------------------------------------------------------------------- /cinrad/_typing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | from typing import List, Union 4 | from numpy import ndarray 5 | from xarray import Dataset 6 | 7 | Array_T = Union[list, ndarray] 8 | Volume_T = List[Dataset] 9 | Boardcast_T = Union[int, float, ndarray] 10 | Number_T = Union[int, float] 11 | -------------------------------------------------------------------------------- /cinrad/_utils.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | cimport numpy as np 3 | import numpy as np 4 | cimport cython 5 | from libc.math cimport sin 6 | 7 | cdef double deg2rad, vil_const 8 | cdef int rm = 8500 9 | 10 | deg2rad = 3.141592653589793 / 180 11 | vil_const = 3.44e-6 12 | 13 | cdef np.ndarray height(np.ndarray distance, double elevation, double radarheight): 14 | # unit is meter 15 | return distance * sin(elevation * deg2rad) + distance ** 2 / (2 * rm) + radarheight 16 | 17 | @cython.cdivision(True) 18 | cdef double height_single(double distance, double elevation): 19 | return distance * sin(elevation * deg2rad) + distance ** 2 / (2 * rm) 20 | 21 | @cython.boundscheck(False) 22 | @cython.wraparound(False) 23 | @cython.cpow(True) 24 | def vert_integrated_liquid(double[:, :, ::1] ref, double[:, ::1] distance, double[::1] elev, 25 | double beam_width=0.99, double threshold=18., bint density=False): 26 | cdef double v_beam_width, m1, mb, mt, factor, ht, dist, r_tmp, h_higher, h_lower 27 | cdef long long[::1] position 28 | cdef Py_ssize_t xshape, yshape, zshape 29 | cdef int pos_s, pos_e, l, i, j, k 30 | cdef double[:] vert_r, vert_z 31 | v_beam_width = beam_width * deg2rad 32 | zshape, xshape, yshape = ref.shape[0], ref.shape[1], ref.shape[2] 33 | cdef double[:, ::1] VIL = np.zeros((xshape, yshape)) 34 | for i in range(xshape): 35 | for j in range(yshape): 36 | vert_r = ref[:, i, j] 37 | vert_z = vert_r.copy() 38 | for k in range(zshape): 39 | vert_z[k] = 10 ** (vert_z[k] / 10) 40 | dist = distance[i][j] * 1000 41 | hi = dist * sin(v_beam_width / 2) 42 | pos_s = -1 43 | pos_e = -1 44 | for k in range(zshape): 45 | if vert_r[k] > threshold: 46 | pos_e = k 47 | if pos_s == -1: 48 | pos_s = k 49 | if pos_s == -1: 50 | continue 51 | m1 = 0. 52 | for l in range(pos_e): 53 | ht = dist * (sin(elev[l + 1] * deg2rad) - sin(elev[l] * deg2rad)) 54 | factor = ((vert_z[l] + vert_z[l + 1]) / 2) ** (4 / 7) 55 | m1 += vil_const * factor * ht 56 | if density == False: 57 | mb = vil_const * vert_z[pos_s] ** (4 / 7) * hi 58 | mt = vil_const * vert_z[pos_e] ** (4 / 7) * hi 59 | VIL[i][j] = m1 + mb + mt 60 | elif density == True: 61 | if pos_s == pos_e: 62 | # If there's only one gate satisfying threshold, assigning VILD as zero 63 | VIL[i][j] = 0 64 | else: 65 | h_lower = height_single(dist / 1000, elev[pos_s]) 66 | h_higher = height_single(dist / 1000, elev[pos_e]) 67 | VIL[i][j] = m1 / (h_higher - h_lower) 68 | return np.asarray(VIL) 69 | 70 | @cython.boundscheck(False) 71 | @cython.cdivision(True) 72 | @cython.wraparound(False) 73 | def echo_top(double[:, :, ::1] ref, double[:, ::1] distance, double[::1] elev, 74 | double radarheight, double threshold=18.): 75 | cdef np.ndarray r, h 76 | cdef int xshape, yshape, zshape, pos, i, j, k 77 | cdef list h_ 78 | cdef double z1, z2, h1, h2, w1, w2, e 79 | zshape, xshape, yshape = ref.shape[0], ref.shape[1], ref.shape[2] 80 | cdef double[:, ::1] et = np.zeros((xshape, yshape)) 81 | cdef double[:] vert_h, vert_r 82 | h_ = list() 83 | for e in elev: 84 | h = height(np.asarray(distance), e, radarheight) 85 | h_.append(h) 86 | cdef double[:, :, :] hght = np.concatenate(h_).reshape(ref.shape[0], ref.shape[1], ref.shape[2]) 87 | #r = np.asarray(ref) 88 | #r[np.isnan(ref)] = 0 89 | for i in range(xshape): 90 | for j in range(yshape): 91 | vert_h = hght[:, i, j] 92 | vert_r = ref[:, i, j] 93 | pos = -1 94 | for k in range(zshape): 95 | if vert_r[k] >= threshold: 96 | pos = k 97 | if pos == -1: # Vertical points don't satisfy threshold 98 | et[i][j] = 0 99 | continue 100 | elif vert_r[zshape - 1] >= threshold: # Point in highest scan exceeds threshold 101 | et[i][j] = vert_h[zshape - 1] 102 | continue 103 | else: 104 | # position = np.nonzero(vert_r >= threshold)[0] 105 | if pos == 0: 106 | et[i][j] = vert_h[0] 107 | continue 108 | else: 109 | z1 = vert_r[pos] 110 | z2 = vert_r[pos + 1] 111 | h1 = vert_h[pos] 112 | h2 = vert_h[pos + 1] 113 | w1 = (z1 - threshold) / (z1 - z2) 114 | w2 = 1 - w1 115 | et[i][j] = w1 * h2 + w2 * h1 116 | return np.asarray(et) -------------------------------------------------------------------------------- /cinrad/calc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import datetime 5 | import time 6 | from typing import * 7 | from functools import wraps 8 | 9 | import numpy as np 10 | from xarray import DataArray, Dataset 11 | 12 | try: 13 | from pykdtree.kdtree import KDTree 14 | except ImportError: 15 | from scipy.spatial import KDTree 16 | 17 | from cinrad.utils import * 18 | from cinrad.grid import grid_2d, resample 19 | from cinrad.projection import height, get_coordinate 20 | from cinrad.error import RadarCalculationError 21 | from cinrad._typing import Volume_T 22 | from cinrad.common import get_dtype 23 | from cinrad.hca import hydro_class as _hca 24 | 25 | __all__ = [ 26 | "quick_cr", 27 | "quick_et", 28 | "quick_vil", 29 | "VCS", 30 | "quick_vild", 31 | "GridMapper", 32 | "hydro_class", 33 | ] 34 | 35 | 36 | def require(var_names: List[str]) -> Callable: 37 | def wrap(func: Callable) -> Callable: 38 | @wraps(func) 39 | def deco(*args, **kwargs) -> Any: 40 | if len(args) == 1: 41 | varset = args[0] 42 | else: 43 | varset = list(args) 44 | if isinstance(varset, Dataset): 45 | var_list = list(varset.keys()) 46 | elif isinstance(varset, list): 47 | var_list = list() 48 | for var in varset: 49 | var_list += list(var.keys()) 50 | var_list = list(set(var_list)) 51 | for v in var_names: 52 | if v not in var_list: 53 | raise ValueError( 54 | "Function {} requires variable {}".format(func.__name__, v) 55 | ) 56 | return func(*args, **kwargs) 57 | 58 | return deco 59 | 60 | return wrap 61 | 62 | 63 | def _extract(r_list: Volume_T, dtype: str) -> tuple: 64 | if len(set(i.range for i in r_list)) > 1: 65 | raise ValueError("Input radials must have same data range") 66 | adim_shape = set(i.dims["azimuth"] for i in r_list) 67 | if max(adim_shape) > 400: 68 | # CC radar 69 | adim_interp_to = 512 70 | else: 71 | adim_interp_to = 360 72 | r_data = list() 73 | elev = list() 74 | for i in r_list: 75 | x, d, a = resample( 76 | i[dtype].values, 77 | i["distance"].values, 78 | i["azimuth"].values, 79 | i.tangential_reso, 80 | adim_interp_to, 81 | ) 82 | r_data.append(x) 83 | elev.append(i.elevation) 84 | data = np.concatenate(r_data).reshape( 85 | len(r_list), r_data[0].shape[0], r_data[0].shape[1] 86 | ) 87 | return data, d, a, np.array(elev) 88 | 89 | 90 | @require(["REF"]) 91 | def quick_cr(r_list: Volume_T, resolution: tuple = (1000, 1000)) -> Dataset: 92 | r"""Calculate composite reflectivity 93 | 94 | Args: 95 | r_list (list(xarray.Dataset)): Reflectivity data. 96 | 97 | Returns: 98 | xarray.Dataset: composite reflectivity 99 | """ 100 | r_data = list() 101 | # Get grid from the first tilt 102 | r, x, y = grid_2d( 103 | r_list[0]["REF"].values, 104 | r_list[0]["longitude"].values, 105 | r_list[0]["latitude"].values, 106 | resolution=resolution, 107 | ) 108 | r_data.append(r) 109 | for i in r_list[1:]: 110 | r, x, y = grid_2d( 111 | i["REF"].values, 112 | i["longitude"].values, 113 | i["latitude"].values, 114 | x_out=x, 115 | y_out=y, 116 | resolution=resolution, 117 | ) 118 | r_data.append(r) 119 | cr = np.nanmax(r_data, axis=0) 120 | ret = Dataset({"CR": DataArray(cr, coords=[y, x], dims=["latitude", "longitude"])}) 121 | ret.attrs = i.attrs 122 | ret.attrs["elevation"] = 0 123 | return ret 124 | 125 | 126 | @require(["REF"]) 127 | def quick_et(r_list: Volume_T) -> Dataset: 128 | r"""Calculate echo tops 129 | 130 | Args: 131 | r_list (list(xarray.Dataset)): Reflectivity data. 132 | 133 | Returns: 134 | xarray.Dataset: echo tops 135 | """ 136 | r_data, d, a, elev = _extract(r_list, "REF") 137 | r_data[np.isnan(r_data)] = 0 138 | i = r_list[0] 139 | et = echo_top( 140 | r_data.astype(np.double), d.astype(np.double), elev.astype(np.double), 0.0 141 | ) 142 | azimuth = a[:, 0] 143 | distance = d[0] 144 | ret = Dataset( 145 | { 146 | "ET": DataArray( 147 | np.ma.masked_less(et, 2), 148 | coords=[azimuth, distance], 149 | dims=["azimuth", "distance"], 150 | ) 151 | } 152 | ) 153 | ret.attrs = i.attrs 154 | ret.attrs["elevation"] = 0 155 | lon, lat = get_coordinate(distance, azimuth, 0, i.site_longitude, i.site_latitude) 156 | ret["longitude"] = (["azimuth", "distance"], lon) 157 | ret["latitude"] = (["azimuth", "distance"], lat) 158 | return ret 159 | 160 | 161 | @require(["REF"]) 162 | def quick_vil(r_list: Volume_T) -> Dataset: 163 | r"""Calculate vertically integrated liquid. 164 | 165 | This algorithm process data in polar coordinates, which avoids the loss of 166 | data. By default, this function calls low-level function `vert_integrated_liquid` 167 | in C-extension. If the C-extension is not available, the python version will 168 | be used instead but with much slower speed. 169 | 170 | Args: 171 | r_list (list(xarray.Dataset)): Reflectivity data. 172 | 173 | Returns: 174 | xarray.Dataset: vertically integrated liquid 175 | """ 176 | r_data, d, a, elev = _extract(r_list, "REF") 177 | r_data[np.isnan(r_data)] = 0 178 | i = r_list[0] 179 | vil = vert_integrated_liquid( 180 | r_data.astype(np.double), d.astype(np.double), elev.astype(np.double) 181 | ) 182 | azimuth = a[:, 0] 183 | distance = d[0] 184 | ret = Dataset( 185 | { 186 | "VIL": DataArray( 187 | np.ma.masked_less(vil, 0.1), 188 | coords=[azimuth, distance], 189 | dims=["azimuth", "distance"], 190 | ) 191 | } 192 | ) 193 | ret.attrs = i.attrs 194 | ret.attrs["elevation"] = 0 195 | lon, lat = get_coordinate(distance, azimuth, 0, i.site_longitude, i.site_latitude) 196 | ret["longitude"] = (["azimuth", "distance"], lon) 197 | ret["latitude"] = (["azimuth", "distance"], lat) 198 | return ret 199 | 200 | 201 | def quick_vild(r_list: Volume_T) -> Dataset: 202 | r"""Calculate vertically integrated liquid density. 203 | 204 | By default, this function calls low-level function `vert_integrated_liquid` 205 | in C-extension. If the C-extension is not available, the python version will 206 | be used instead but with much slower speed. 207 | 208 | Args: 209 | r_list (list(xarray.Dataset)): Reflectivity data. 210 | 211 | Returns: 212 | xarray.Dataset: Vertically integrated liquid 213 | """ 214 | r_data, d, a, elev = _extract(r_list, "REF") 215 | i = r_list[0] 216 | vild = vert_integrated_liquid( 217 | r_data.astype(np.double), 218 | d.astype(np.double), 219 | elev.astype(np.double), 220 | density=True, 221 | ) 222 | azimuth = a[:, 0] 223 | distance = d[0] 224 | ret = Dataset( 225 | { 226 | "VILD": DataArray( 227 | np.ma.masked_less(vild, 0.1), 228 | coords=[azimuth, distance], 229 | dims=["azimuth", "distance"], 230 | ) 231 | } 232 | ) 233 | ret.attrs = i.attrs 234 | ret.attrs["elevation"] = 0 235 | lon, lat = get_coordinate(distance, azimuth, 0, i.site_longitude, i.site_latitude) 236 | ret["longitude"] = (["azimuth", "distance"], lon) 237 | ret["latitude"] = (["azimuth", "distance"], lat) 238 | return ret 239 | 240 | 241 | def polar_to_xy(field: Dataset, resolution: tuple = (1000, 1000)) -> Dataset: 242 | r""" 243 | Interpolate single volume data in polar coordinates into geographic coordinates 244 | 245 | 将单仰角数据从极坐标插值转换为经纬度坐标 246 | 247 | Args: 248 | field (xarray.Dataset): Original radial. 249 | 250 | Returns: 251 | xarray.Dataset: Interpolated data in grid 252 | """ 253 | dtype = get_dtype(field) 254 | r, x, y = grid_2d( 255 | field[dtype].values, 256 | field["longitude"].values, 257 | field["latitude"].values, 258 | resolution=resolution, 259 | ) 260 | ret = Dataset({dtype: DataArray(r, coords=[y, x], dims=["latitude", "longitude"])}) 261 | ret.attrs = field.attrs 262 | return ret 263 | 264 | 265 | class VCS(object): 266 | r""" 267 | Class performing vertical cross-section calculation 268 | 269 | Args: 270 | r_list (list(xarray.Dataset)): The whole volume scan. 271 | """ 272 | 273 | def __init__(self, r_list: Volume_T): 274 | el = [i.elevation for i in r_list] 275 | if len(el) != len(set(el)): 276 | self.rl = list() 277 | el_list = list() 278 | for data in r_list: 279 | if data.elevation not in el_list: 280 | self.rl.append(data) 281 | el_list.append(data.elevation) 282 | else: 283 | self.rl = r_list 284 | self.dtype = get_dtype(r_list[0]) 285 | self.x, self.y, self.h, self.r = self._geocoor() 286 | self.attrs = r_list[0].attrs 287 | 288 | def _geocoor(self) -> Tuple[list]: 289 | r_data = list() 290 | x_data = list() 291 | y_data = list() 292 | h_data = list() 293 | for i in self.rl: 294 | _lon = i["longitude"].values 295 | _lat = i["latitude"].values 296 | r, x, y = grid_2d(i[self.dtype].values, _lon, _lat) 297 | r, x, y = grid_2d(i[self.dtype].values, _lon, _lat) 298 | r_data.append(r) 299 | x_data.append(x) 300 | y_data.append(y) 301 | hgh_grid, x, y = grid_2d(i["height"].values, _lon, _lat) 302 | h_data.append(hgh_grid) 303 | return x_data, y_data, h_data, r_data 304 | 305 | def _get_section( 306 | self, stp: Tuple[float, float], enp: Tuple[float, float], spacing: int 307 | ) -> Dataset: 308 | r_sec = list() 309 | h_sec = list() 310 | for x, y, h, r in zip(self.x, self.y, self.h, self.r): 311 | d_x = DataArray(r, [("lat", y), ("lon", x)]) 312 | d_h = DataArray(h, [("lat", y), ("lon", x)]) 313 | x_new = DataArray(np.linspace(stp[0], enp[0], spacing), dims="z") 314 | y_new = DataArray(np.linspace(stp[1], enp[1], spacing), dims="z") 315 | r_section = d_x.interp(lon=x_new, lat=y_new) 316 | h_section = d_h.interp(lon=x_new, lat=y_new) 317 | r_sec.append(r_section) 318 | h_sec.append(h_section) 319 | r = np.asarray(r_sec) 320 | h = np.asarray(h_sec) 321 | x = np.linspace(0, 1, spacing) * np.ones(r.shape[0])[:, np.newaxis] 322 | ret = Dataset( 323 | { 324 | self.dtype: DataArray(r, dims=["distance", "tilt"]), 325 | "y_cor": DataArray(h, dims=["distance", "tilt"]), 326 | "x_cor": DataArray(x, dims=["distance", "tilt"]), 327 | } 328 | ) 329 | r_attr = self.attrs.copy() 330 | del r_attr["elevation"], r_attr["tangential_reso"], r_attr["range"] 331 | r_attr["start_lon"] = stp[0] 332 | r_attr["start_lat"] = stp[1] 333 | r_attr["end_lon"] = enp[0] 334 | r_attr["end_lat"] = enp[1] 335 | ret.attrs = r_attr 336 | return ret 337 | 338 | def get_section( 339 | self, 340 | start_polar: Optional[Tuple[float, float]] = None, 341 | end_polar: Optional[Tuple[float, float]] = None, 342 | start_cart: Optional[Tuple[float, float]] = None, 343 | end_cart: Optional[Tuple[float, float]] = None, 344 | spacing: int = 500, 345 | ) -> Dataset: 346 | r""" 347 | Get cross-section data from input points 348 | 349 | Args: 350 | start_polar (tuple): polar coordinates of start point i.e.(distance, azimuth) 351 | 352 | end_polar (tuple): polar coordinates of end point i.e.(distance, azimuth) 353 | 354 | start_cart (tuple): geographic coordinates of start point i.e.(longitude, latitude) 355 | 356 | end_cart (tuple): geographic coordinates of end point i.e.(longitude, latitude) 357 | 358 | Returns: 359 | xarray.Dataset: Cross-section data 360 | """ 361 | if start_polar and end_polar: 362 | stlat = self.rl[0].site_latitude 363 | stlon = self.rl[0].site_longitude 364 | stp = np.round( 365 | get_coordinate( 366 | start_polar[0], np.deg2rad(start_polar[1]), 0, stlon, stlat 367 | ), 368 | 2, 369 | ) 370 | enp = np.round( 371 | get_coordinate(end_polar[0], np.deg2rad(end_polar[1]), 0, stlon, stlat), 372 | 2, 373 | ) 374 | elif start_cart and end_cart: 375 | stp = start_cart 376 | enp = end_cart 377 | else: 378 | raise RadarCalculationError("Invalid input") 379 | return self._get_section(stp, enp, spacing) 380 | 381 | 382 | class GridMapper(object): 383 | r""" 384 | This class can merge scans from different radars to a single cartesian grid. 385 | support BR or CR or any list(xarray.Dataset). 386 | merge_xy method inspiration comes from OLDLee_GIFT@bilibili. 387 | 388 | Args: 389 | fields (list(xarray.Dataset)): Lists of scans to be merged. 390 | 391 | max_dist (int, float): The maximum distance in kdtree searching. 392 | 393 | Example: 394 | >>> gm = GridMapper([r1, r2, r3]) 395 | >>> grid = gm(0.1) 396 | """ 397 | 398 | def __init__(self, fields: Volume_T, max_dist: Number_T = 0.1): 399 | # Process data type 400 | self.dtype = get_dtype(fields[0]) 401 | # Process time 402 | t_arr = np.array( 403 | [ 404 | time.mktime( 405 | datetime.datetime.strptime( 406 | i.scan_time, "%Y-%m-%d %H:%M:%S" 407 | ).timetuple() 408 | ) 409 | for i in fields 410 | ] 411 | ) 412 | if (t_arr.max() - t_arr.min()) / 60 > 10: 413 | raise RadarCalculationError( 414 | "Time difference of input data should not exceed 10 minutes" 415 | ) 416 | mean_time = t_arr.mean() 417 | mean_dtime = datetime.datetime(*time.localtime(int(mean_time))[:6]) 418 | time_increment = 10 419 | time_rest = mean_dtime.minute % time_increment 420 | if time_rest > time_increment / 2: 421 | mean_dtime += datetime.timedelta(minutes=(time_increment - time_rest)) 422 | else: 423 | mean_dtime -= datetime.timedelta(minutes=time_rest) 424 | self.scan_time = mean_dtime 425 | self.lon_ravel = np.hstack([i["longitude"].values.ravel() for i in fields]) 426 | self.lat_ravel = np.hstack([i["latitude"].values.ravel() for i in fields]) 427 | self.is_polar = "distance" in fields[0].coords 428 | if self.is_polar: 429 | self.data_ravel = np.ma.hstack( 430 | [i[self.dtype].values.ravel() for i in fields] 431 | ) 432 | self.dist_ravel = np.hstack( 433 | [ 434 | np.broadcast_to(i["distance"], i["longitude"].shape).ravel() 435 | for i in fields 436 | ] 437 | ) 438 | self.tree = KDTree(np.dstack((self.lon_ravel, self.lat_ravel))[0]) 439 | self.md = max_dist 440 | self.attr = fields[0].attrs.copy() 441 | self.fields = fields 442 | 443 | def _process_grid(self, x_step: Number_T, y_step: Number_T) -> Tuple[np.ndarray]: 444 | x_lower = np.round(self.lon_ravel.min(), 2) 445 | x_upper = np.round(self.lon_ravel.max(), 2) 446 | y_lower = np.round(self.lat_ravel.min(), 2) 447 | y_upper = np.round(self.lat_ravel.max(), 2) 448 | x_grid = np.arange(x_lower, x_upper + x_step, x_step) 449 | y_grid = np.arange(y_lower, y_upper + y_step, y_step) 450 | return np.meshgrid(x_grid, y_grid) 451 | 452 | def _map_points(self, x: np.ndarray, y: np.ndarray) -> np.ma.MaskedArray: 453 | _MAX_RETURN = 5 454 | _FILL_VALUE = -1e5 455 | xdim, ydim = x.shape 456 | _, idx = self.tree.query( 457 | np.dstack((x.ravel(), y.ravel()))[0], 458 | distance_upper_bound=self.md, 459 | k=_MAX_RETURN, 460 | ) 461 | idx_all = idx.ravel() 462 | data_indexing = np.append(self.data_ravel, _FILL_VALUE) 463 | dist_indexing = np.append(self.dist_ravel, 0) 464 | target_rad = np.ma.masked_equal(data_indexing[idx_all], _FILL_VALUE) 465 | weight = dist_indexing[idx_all] 466 | inp = target_rad.reshape(xdim, ydim, _MAX_RETURN) 467 | wgt = weight.reshape(xdim, ydim, _MAX_RETURN) 468 | return np.ma.average(inp, weights=1 / wgt, axis=2) 469 | 470 | def _merge_xy(self, x: np.ndarray, y: np.ndarray) -> Dataset: 471 | # interpolate datas to full grid 472 | r_data = list() 473 | for field in self.fields: 474 | field_grid = field.interp(longitude=x[0], latitude=y[:, 0], method="linear") 475 | r_data.append(field_grid[self.dtype].values) 476 | # select max value in each grid 477 | r_data_max = np.nanmax(r_data, axis=0) 478 | ret = Dataset( 479 | { 480 | self.dtype: DataArray( 481 | r_data_max, coords=[y[:, 0], x[0]], dims=["latitude", "longitude"] 482 | ) 483 | } 484 | ) 485 | # interpolate Nan values 486 | lat_interp = ret[self.dtype].interpolate_na( 487 | "latitude", method="linear", limit=len(self.fields) 488 | ) 489 | lon_interp = lat_interp.interpolate_na( 490 | "longitude", method="nearest", limit=len(self.fields) 491 | ) 492 | grid = lon_interp.interp(longitude=x[0], latitude=y[:, 0], method="linear") 493 | return grid 494 | 495 | def __call__(self, step: Number_T) -> Dataset: 496 | r""" 497 | Args: 498 | step (int, float): Output grid spacing. 499 | 500 | Returns: 501 | xarray.Dataset: Merged grid data. 502 | """ 503 | x, y = self._process_grid(step, step) 504 | if self.is_polar: 505 | grid = self._map_points(x, y) 506 | 507 | else: 508 | grid = self._merge_xy(x, y) 509 | grid = np.ma.masked_outside(grid, 0.1, 100) 510 | ret = Dataset( 511 | { 512 | self.dtype: DataArray( 513 | grid, coords=[y[:, 0], x[0]], dims=["latitude", "longitude"] 514 | ) 515 | } 516 | ) 517 | r_attr = self.attr 518 | # Keep this attribute temporarily waiting for future fix 519 | r_attr["tangential_reso"] = np.nan 520 | r_attr["elevation"] = 0 521 | r_attr["site_name"] = "RADMAP" 522 | r_attr["site_code"] = "RADMAP" 523 | r_attr["scan_time"] = self.scan_time.strftime("%Y-%m-%d %H:%M:%S") 524 | for k in ["site_longitude", "site_latitude", "nyquist_vel"]: 525 | if k in r_attr: 526 | del r_attr[k] 527 | ret.attrs = r_attr 528 | return ret 529 | 530 | 531 | @require(["REF", "ZDR", "RHO", "KDP"]) 532 | def hydro_class( 533 | z: Dataset, zdr: Dataset, rho: Dataset, kdp: Dataset, band: str = "S" 534 | ) -> Dataset: 535 | r"""Hydrometeor classification 536 | 537 | Args: 538 | z (xarray.Dataset): Reflectivity data. 539 | 540 | zdr (xarray.Dataset): Differential reflectivity data. 541 | 542 | rho (xarray.Dataset): Cross-correlation coefficient data. 543 | 544 | kdp (xarray.Dataset): Specific differential phase data. 545 | 546 | band (str): Band of the radar, default to S. 547 | 548 | Returns: 549 | xarray.Dataset: Classification result. 550 | """ 551 | z_data = z["REF"].values 552 | zdr_data = zdr["ZDR"].values 553 | rho_data = rho["RHO"].values 554 | kdp_data = kdp["KDP"].values 555 | result = _hca( 556 | z_data.ravel(), zdr_data.ravel(), rho_data.ravel(), kdp_data.ravel(), band=band 557 | ) 558 | result = result.reshape(z_data.shape).astype(float) 559 | result[np.isnan(z_data)] = np.nan 560 | hcl = z.copy() 561 | hcl["cHCL"] = (["azimuth", "distance"], result) 562 | del hcl["REF"] 563 | return hcl 564 | -------------------------------------------------------------------------------- /cinrad/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | from xarray import Dataset 5 | 6 | 7 | def get_dtype(data: Dataset) -> str: 8 | all_data = list(data.keys()) 9 | geo_var_name = ["longitude", "latitude", "height", "x_cor", "y_cor", "RF"] 10 | for i in geo_var_name: 11 | if i in all_data: 12 | all_data.remove(i) 13 | return all_data[0] 14 | 15 | 16 | def is_radial(data: Dataset) -> bool: 17 | coords = set(data.coords.keys()) 18 | return coords == set(("distance", "azimuth")) 19 | -------------------------------------------------------------------------------- /cinrad/correct/__init__.py: -------------------------------------------------------------------------------- 1 | from .dealias import dealias 2 | -------------------------------------------------------------------------------- /cinrad/correct/_unwrap_2d.pyx: -------------------------------------------------------------------------------- 1 | cdef extern from "unwrap_2d_ljmu.c": 2 | void unwrap2D(double* wrapped_image, 3 | double* unwrapped_image, 4 | unsigned char* input_mask, 5 | int image_width, int image_height, 6 | int wrap_around_x, int wrap_around_y) 7 | 8 | def unwrap_2d(double[:, ::1] image, 9 | unsigned char[:, ::1] mask, 10 | double[:, ::1] unwrapped_image, 11 | wrap_around): 12 | """ 2D phase unwrapping. """ 13 | unwrap2D(&image[0, 0], 14 | &unwrapped_image[0, 0], 15 | &mask[0, 0], 16 | image.shape[1], image.shape[0], 17 | wrap_around[1], wrap_around[0], 18 | ) 19 | -------------------------------------------------------------------------------- /cinrad/correct/dealias.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import numpy as np 5 | from xarray import Dataset 6 | 7 | try: 8 | from cinrad.correct._unwrap_2d import unwrap_2d 9 | except ImportError: 10 | from cinrad.error import RadarCalculationError, ExceptionOnCall 11 | 12 | unwrap_2d = ExceptionOnCall( 13 | RadarCalculationError, 14 | "Cython is not installed, velocity dealias function cannot be used. If you " 15 | "installed Cython after installing cinrad, please re-install cinrad.", 16 | ) 17 | 18 | 19 | def dealias_unwrap_2d(vdata: np.ndarray, nyquist_vel: float) -> np.ndarray: 20 | """Dealias using 2D phase unwrapping (sweep-by-sweep).""" 21 | scaled_sweep = vdata * np.pi / nyquist_vel 22 | sweep_mask = np.isnan(vdata) 23 | scaled_sweep[sweep_mask] = 0 24 | wrapped = np.require(scaled_sweep, np.float64, ["C"]) 25 | mask = np.require(sweep_mask, np.uint8, ["C"]) 26 | unwrapped = np.empty_like(wrapped, dtype=np.float64, order="C") 27 | unwrap_2d(wrapped, mask, unwrapped, [True, False]) 28 | return unwrapped * nyquist_vel / np.pi 29 | 30 | 31 | def dealias(v_data: Dataset) -> Dataset: 32 | v_field = v_data["VEL"].data 33 | nyq = v_data.attrs.get("nyquist_vel") 34 | out_data = dealias_unwrap_2d(v_field, nyq) 35 | out_masked = np.ma.array(out_data, mask=np.isnan(v_field)) 36 | v_ret = v_data.copy() 37 | v_ret["VEL"] = (tuple(v_data.dims.keys()), out_masked) 38 | return v_ret 39 | -------------------------------------------------------------------------------- /cinrad/data/colormap/CC.cmap: -------------------------------------------------------------------------------- 1 | *NAME:CC 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *UNDER:0/60/252 5 | *OVER:213/0/173 6 | 0 BEGIN 0/60/252 7 | 0.1 0/60/252 0/242/238 8 | 0.3 0/242/238 0/186/187 9 | 0.5 0/186/187 0/129/121 10 | 0.6 0/129/121 0/138/58 11 | 0.7 0/138/58 0/183/41 12 | 0.8 0/183/41 0/218/5 13 | 0.85 0/218/5 0/254/1 14 | 0.9 0/254/1 253/254/56 15 | 0.92 253/254/56 255/242/0 16 | 0.94 255/242/0 255/198/0 17 | 0.95 255/198/0 254/166/0 18 | 0.96 254/166/0 255/113/1 19 | 0.97 255/113/1 253/28/3 20 | 0.98 253/28/3 197/0/1 21 | 0.99 197/0/1 213/0/173 22 | 1 213/0/173 END -------------------------------------------------------------------------------- /cinrad/data/colormap/CC2.cmap: -------------------------------------------------------------------------------- 1 | *NAME:CC 2 | *AUTHOR:GR 3 | *TYPE:LINEAR 4 | *UNDER:22/20/140 5 | *OVER:250/172/209 6 | 0 BEGIN 22/20/140 7 | 0.45 22/20/140 22/20/140 8 | 0.65 9/2/217 137/135/214 9 | 0.8 92/255/89 139/207/2 10 | 0.9 255/251/0 255/137/0 11 | 0.96 255/43/0 161/0/0 12 | 0.99 151/5/86 151/5/86 13 | 1 250/172/209 END -------------------------------------------------------------------------------- /cinrad/data/colormap/CC_s.cmap: -------------------------------------------------------------------------------- 1 | *NAME:CC 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *LEVEL:0.1 5 | *UNDER:0/60/252 6 | *OVER:213/0/173 7 | 0 BEGIN 0/60/252 8 | 0.1 0/242/238 0/242/238 9 | 0.3 0/186/187 0/186/187 10 | 0.5 0/129/121 0/129/121 11 | 0.6 0/138/58 0/138/58 12 | 0.7 0/183/41 0/183/41 13 | 0.8 0/218/5 0/218/5 14 | 0.85 0/254/1 0/254/1 15 | 0.9 253/254/56 253/254/56 16 | 0.92 255/242/0 255/242/0 17 | 0.94 255/198/0 255/198/0 18 | 0.95 254/166/0 254/166/0 19 | 0.96 255/113/1 255/113/1 20 | 0.97 253/28/3 253/28/3 21 | 0.98 197/0/1 197/0/1 22 | 0.99 213/0/173 213/0/173 23 | 1 213/0/173 END -------------------------------------------------------------------------------- /cinrad/data/colormap/ET.cmap: -------------------------------------------------------------------------------- 1 | *NAME:ET 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *UNDER:0/0/0 5 | *OVER:230/0/254 6 | 0 BEGIN 0/0/0 7 | 2 0/0/0 118/118/118 8 | 3 118/118/118 0/224/254 9 | 5 0/224/254 0/176/254 10 | 6 0/176/254 0/144/204 11 | 8 0/144/204 50/0/150 12 | 9 50/0/150 0/250/144 13 | 11 0/250/144 0/186/0 14 | 12 0/186/0 0/238/0 15 | 14 0/238/0 254/190/0 16 | 15 254/190/0 254/254/0 17 | 17 254/254/0 174/0/0 18 | 18 174/0/0 254/0/0 19 | 20 254/0/0 254/254/254 20 | 21 254/254/254 230/0/254 21 | 22 230/0/254 END -------------------------------------------------------------------------------- /cinrad/data/colormap/HCL.cmap: -------------------------------------------------------------------------------- 1 | *NAME:HCL 2 | *AUTHOR:NMC 3 | *TYPE:LISTED 4 | *UNIT:None 5 | *UNDER:0/251/144 6 | *OVER:231/0/255 7 | 0 0/251/144 8 | 1 0/187/0 9 | 2 255/0/0 10 | 3 208/208/96 11 | 4 156/156/156 12 | 5 118/118/118 13 | 6 0/255/255 14 | 7 0/144/255 15 | 8 255/176/176 16 | 9 210/132/132 17 | 10 231/0/255 -------------------------------------------------------------------------------- /cinrad/data/colormap/KDP.cmap: -------------------------------------------------------------------------------- 1 | *NAME:KDP 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *UNDER:0/0/0 5 | *OVER:230/0/254 6 | -0.8 BEGIN 0/255/255 7 | -0.4 0/255/255 0/244/239 8 | -0.2 0/244/239 4/170/170 9 | -0.1 4/170/170 182/182/182 10 | 0.1 182/182/182 182/182/182 11 | 0.15 182/182/182 0/195/33 12 | 0.22 0/195/33 0/235/11 13 | 0.33 0/235/11 34/254/32 14 | 0.5 34/254/32 255/255/22 15 | 0.75 255/255/22 254/232/0 16 | 1.1 254/232/0 252/192/0 17 | 1.7 252/192/0 255/154/0 18 | 2.4 255/154/0 254/94/0 19 | 3.1 254/94/0 248/12/1 20 | 7 248/12/1 189/0/58 21 | 20 189/0/58 255/0/255 22 | 21 255/0/255 END -------------------------------------------------------------------------------- /cinrad/data/colormap/KDP_s.cmap: -------------------------------------------------------------------------------- 1 | *NAME:KDP 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *LEVEL:0.1 5 | *UNDER:0/0/0 6 | *OVER:230/0/254 7 | -0.8 BEGIN 0/255/255 8 | -0.4 0/244/239 0/244/239 9 | -0.2 4/170/170 4/170/170 10 | -0.1 182/182/182 182/182/182 11 | 0.1 182/182/182 182/182/182 12 | 0.15 0/195/33 0/195/33 13 | 0.22 0/235/11 0/235/11 14 | 0.33 34/254/32 34/254/32 15 | 0.5 255/255/22 255/255/22 16 | 0.75 254/232/0 254/232/0 17 | 1.1 252/192/0 252/192/0 18 | 1.7 255/154/0 255/154/0 19 | 2.4 254/94/0 254/94/0 20 | 3.1 248/12/1 248/12/1 21 | 7 189/0/58 189/0/58 22 | 20 255/0/255 255/0/255 23 | 21 255/0/255 END -------------------------------------------------------------------------------- /cinrad/data/colormap/OHP.cmap: -------------------------------------------------------------------------------- 1 | *NAME:OHP 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *UNIT:mm 5 | *UNDER:170/170/170 6 | *OVER:254/254/254 7 | 0 BEGIN 170/170/170 8 | 2 170/170/170 118/118/118 9 | 6 118/118/118 0/254/254 10 | 12 0/254/254 0/174/174 11 | 19 0/174/174 0/254/0 12 | 25 0/254/0 0/142/0 13 | 31 0/142/0 254/0/254 14 | 37.5 254/0/254 174/50/124 15 | 43.5 174/50/124 0/0/254 16 | 50 0/0/254 50/0/150 17 | 63 50/0/150 254/254/0 18 | 75.5 254/254/0 254/170/0 19 | 100.5 254/170/0 254/0/0 20 | 152 254/0/0 174/0/0 21 | 203 174/0/0 254/254/254 22 | 204 254/254/254 END -------------------------------------------------------------------------------- /cinrad/data/colormap/REF.cmap: -------------------------------------------------------------------------------- 1 | *NAME:REF 2 | *AUTHOR:NMC 3 | *TYPE:LISTED 4 | *UNIT:dBz 5 | *UNDER:0/0/246 6 | *OVER:173/144/240 7 | 0 0/0/246 8 | 5 1/160/246 9 | 10 0/236/236 10 | 15 1/255/0 11 | 20 0/200/0 12 | 25 1/144/0 13 | 30 255/255/0 14 | 35 231/192/0 15 | 40 255/144/0 16 | 45 255/0/0 17 | 50 214/0/0 18 | 55 192/0/0 19 | 60 255/0/240 20 | 65 120/0/132 21 | 70 173/144/240 -------------------------------------------------------------------------------- /cinrad/data/colormap/REF_s.cmap: -------------------------------------------------------------------------------- 1 | *NAME:REF 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *LEVEL:0.1 5 | *UNIT:dBz 6 | *UNDER:0/0/0 7 | *OVER:173/144/240 8 | 0 BEGIN 0/0/246 9 | 5 1/160/246 1/160/246 10 | 10 0/236/236 0/236/236 11 | 15 1/255/0 1/255/0 12 | 20 0/200/0 0/200/0 13 | 25 1/144/0 1/144/0 14 | 30 255/255/0 255/255/0 15 | 35 231/192/0 231/192/0 16 | 40 255/144/0 255/144/0 17 | 45 255/0/0 255/0/0 18 | 50 214/0/0 214/0/0 19 | 55 192/0/0 192/0/0 20 | 60 255/0/240 255/0/240 21 | 65 120/0/132 120/0/132 22 | 70 173/144/240 END -------------------------------------------------------------------------------- /cinrad/data/colormap/VEL.cmap: -------------------------------------------------------------------------------- 1 | *NAME:VEL 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *UNIT:m/s 5 | *UNDER:102/204/255 6 | *OVER:255/255/0 7 | -35 BEGIN 102/204/255 8 | -27 102/204/255 0/204/255 9 | -20 0/204/255 0/153/153 10 | -15 0/153/153 0/255/0 11 | -10 0/255/0 0/196/0 12 | -5 0/196/0 0/102/0 13 | -1 0/102/0 255/255/255 14 | 1 255/255/255 255/0/0 15 | 5 255/0/0 255/51/51 16 | 10 255/51/51 255/153/153 17 | 15 255/153/153 255/102/0 18 | 20 255/102/0 255/204/0 19 | 27 255/204/0 255/255/0 20 | 28 255/255/0 END -------------------------------------------------------------------------------- /cinrad/data/colormap/VEL_reverse.cmap: -------------------------------------------------------------------------------- 1 | *NAME:REF 2 | *AUTHOR:NMC 3 | *TYPE:LISTED 4 | *UNIT:dBz 5 | *LEVEL:0.1 6 | *UNDER:255/255/0 7 | *OVER:102/204/255 8 | 0 102/0/102 9 | 1 255/255/0 10 | 2 255/204/0 11 | 3 255/102/0 12 | 4 255/153/153 13 | 5 255/51/51 14 | 6 255/0/0 15 | 7 255/255/255 16 | 8 255/255/255 17 | 9 0/102/0 18 | 10 0/196/0 19 | 11 0/255/0 20 | 12 0/153/153 21 | 13 0/204/255 22 | 14 102/204/255 -------------------------------------------------------------------------------- /cinrad/data/colormap/VEL_s.cmap: -------------------------------------------------------------------------------- 1 | *NAME:VEL 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *LEVEL:0.1 5 | *UNIT:m/s 6 | *UNDER:102/204/255 7 | *OVER:255/255/0 8 | -35 BEGIN 102/204/255 9 | -27 0/204/255 0/204/255 10 | -20 0/153/153 0/153/153 11 | -15 0/255/0 0/255/0 12 | -10 0/196/0 0/196/0 13 | -5 0/102/0 0/102/0 14 | -1 255/255/255 255/255/255 15 | 1 255/0/0 255/0/0 16 | 5 255/51/51 255/51/51 17 | 10 255/153/153 255/153/153 18 | 15 255/102/0 255/102/0 19 | 20 255/204/0 255/204/0 20 | 27 255/255/0 255/255/0 21 | 28 255/255/0 END -------------------------------------------------------------------------------- /cinrad/data/colormap/VIL.cmap: -------------------------------------------------------------------------------- 1 | *NAME:VIL 2 | *AUTHOR:NMC 3 | *TYPE:LISTED 4 | *UNDER:153/153/153 5 | *OVER:204/0/255 6 | 0 153/153/153 7 | 1 102/102/102 8 | 2 204/153/153 9 | 3 204/102/102 10 | 4 204/102/102 11 | 5 0/204/102 12 | 6 0/151/0 13 | 7 252/252/101 14 | 8 204/204/51 15 | 9 254/51/51 16 | 10 204/0/0 17 | 11 153/0/0 18 | 12 0/0/255 19 | 13 255/255/255 20 | 14 204/0/255 -------------------------------------------------------------------------------- /cinrad/data/colormap/ZDR.cmap: -------------------------------------------------------------------------------- 1 | *NAME:ZDR 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *UNDER:67/69/66 5 | *OVER:255/0/253 6 | -4 BEGIN 67/69/66 7 | -3 67/69/66 107/109/106 8 | -2 107/109/106 148/150/147 9 | -1 148/150/147 203/203/208 10 | 0 203/203/208 220/243/222 11 | 0.2 220/243/222 1/194/31 12 | 0.5 1/194/31 0/235/11 13 | 0.8 0/235/11 34/253/34 14 | 1 34/253/34 255/254/25 15 | 1.5 255/254/25 255/230/1 16 | 2 255/230/1 255/188/2 17 | 2.5 255/188/2 255/154/0 18 | 3 255/154/0 254/93/0 19 | 3.5 254/93/0 245/14/0 20 | 4 245/14/0 189/0/58 21 | 5 189/0/58 255/0/253 22 | 6 253/0/255 END -------------------------------------------------------------------------------- /cinrad/data/colormap/ZDR_s.cmap: -------------------------------------------------------------------------------- 1 | *NAME:ZDR 2 | *AUTHOR:NMC 3 | *TYPE:LINEAR 4 | *LEVEL:0.1 5 | *UNDER:67/69/66 6 | *OVER:255/0/253 7 | -4 BEGIN 67/69/66 8 | -3 107/109/106 107/109/106 9 | -2 148/150/147 148/150/147 10 | -1 203/203/208 203/203/208 11 | 0 220/243/222 220/243/222 12 | 0.2 1/194/31 1/194/31 13 | 0.5 0/235/11 0/235/11 14 | 0.8 34/253/34 34/253/34 15 | 1 255/254/25 255/254/25 16 | 1.5 255/230/1 255/230/1 17 | 2 255/188/2 255/188/2 18 | 2.5 255/154/0 255/154/0 19 | 3 254/93/0 254/93/0 20 | 3.5 245/14/0 245/14/0 21 | 4 189/0/58 189/0/58 22 | 5 255/0/253 255/0/253 23 | 6 253/0/255 END -------------------------------------------------------------------------------- /cinrad/data/hca_params.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/cinrad/data/hca_params.npy -------------------------------------------------------------------------------- /cinrad/deprecation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import warnings 5 | 6 | 7 | class Deprecated(object): 8 | def __init__(self, obj, warn_msg): 9 | self.obj = obj 10 | self.msg = warn_msg 11 | 12 | def __getattr__(self, attr): 13 | warnings.warn(self.msg) 14 | return getattr(self.obj, attr) 15 | 16 | def __call__(self, *args, **kwargs): 17 | warnings.warn(self.msg) 18 | return self.obj(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /cinrad/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | 5 | class RadarDecodeError(Exception): 6 | r"""Unable to decode radar files correctly""" 7 | pass 8 | 9 | 10 | class RadarPlotError(Exception): 11 | r"""Unable to generate visualization of radar data""" 12 | pass 13 | 14 | 15 | class RadarCalculationError(Exception): 16 | r"""Unable to calculate derivatives of radar data""" 17 | pass 18 | 19 | 20 | class ExceptionOnCall(object): 21 | r"""Raise exception when calling""" 22 | 23 | def __init__(self, exec_: Exception, msg: str): 24 | self.exec = exec_ 25 | self.msg = msg 26 | 27 | def __call__(self, *args, **kwargs): 28 | raise self.exec(self.msg) 29 | -------------------------------------------------------------------------------- /cinrad/grid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | from typing import Tuple, Optional 5 | 6 | import numpy as np 7 | 8 | try: 9 | from pykdtree.kdtree import KDTree 10 | except ImportError: 11 | from scipy.spatial import KDTree 12 | 13 | from cinrad._typing import Number_T 14 | 15 | 16 | class KDResampler(object): 17 | def __init__( 18 | self, data: np.ndarray, x: np.ndarray, y: np.ndarray, roi: Number_T = 0.02 19 | ): 20 | x_ravel = x.ravel() 21 | y_ravel = y.ravel() 22 | self.tree = KDTree(np.dstack((x_ravel, y_ravel))[0]) 23 | self.data = data 24 | self.roi = roi 25 | 26 | def map_data(self, x_out: np.ndarray, y_out: np.ndarray) -> np.ndarray: 27 | out_coords = np.dstack((x_out.ravel(), y_out.ravel()))[0] 28 | _, indices = self.tree.query(out_coords, distance_upper_bound=self.roi) 29 | invalid_mask = indices == self.tree.n 30 | indices[invalid_mask] = 0 31 | data = self.data.ravel()[indices] 32 | data[invalid_mask] = np.nan 33 | return data.reshape(x_out.shape) 34 | 35 | 36 | def resample( 37 | data: np.ndarray, 38 | distance: np.ndarray, 39 | azimuth: np.ndarray, 40 | d_reso: Number_T, 41 | a_reso: int, 42 | ) -> tuple: 43 | r""" 44 | Resample radar radial data which have different number of radials 45 | in one scan into that of 360 radials 46 | 47 | Args: 48 | data (numpy.ndarray): Radar radial data. 49 | 50 | distance (numpy.ndarray): Original distance. 51 | 52 | azimuth (numpy.ndarray): Original azimuth. 53 | 54 | Returns: 55 | numpy.ndarray: Resampled radial data. 56 | 57 | numpy.ndarray: Resampled distance. 58 | 59 | numpy.ndarray: Resampled azimuth. 60 | """ 61 | # Target grid 62 | Rrange = np.arange(d_reso, distance.max() + d_reso, d_reso) 63 | Trange = np.linspace(0, np.pi * 2, a_reso + 1) 64 | dist, theta = np.meshgrid(Rrange, Trange) 65 | # Original grid 66 | d, t = np.meshgrid(distance, azimuth) 67 | kds = KDResampler(data, d, t, 1) 68 | r = kds.map_data(dist, theta) 69 | return r, dist, theta 70 | 71 | 72 | def grid_2d( 73 | data: np.ndarray, 74 | x: np.ndarray, 75 | y: np.ndarray, 76 | x_out: Optional[np.ndarray] = None, 77 | y_out: Optional[np.ndarray] = None, 78 | resolution: tuple = (1000, 1000), 79 | ) -> tuple: 80 | r""" 81 | Interpolate data in polar coordinates into geographic coordinates 82 | 83 | Args: 84 | data (numpy.ndarray): Original radial data. 85 | 86 | x (numpy.ndarray): Original longitude data arranged in radials. 87 | 88 | y (numpy.ndarray): Original latitude data arranged in radials. 89 | 90 | resolution (tuple): The size of output. 91 | 92 | Returns: 93 | numpy.ndarray: Interpolated data in grid. 94 | 95 | numpy.ndarray: Interpolated longitude in grid. 96 | 97 | numpy.ndarray: Interpolated latitude in grid. 98 | """ 99 | r_x, r_y = resolution 100 | if isinstance(x_out, type(None)): 101 | x_out = np.linspace(x.min(), x.max(), r_x) 102 | if isinstance(y_out, type(None)): 103 | y_out = np.linspace(y.min(), y.max(), r_y) 104 | t_x, t_y = np.meshgrid(x_out, y_out) 105 | kds = KDResampler(data, x, y) 106 | # TODO: Rewrite the logic for conversion between np.ma.masked and np.nan 107 | result = kds.map_data(t_x, t_y) 108 | return result, x_out, y_out 109 | -------------------------------------------------------------------------------- /cinrad/hca.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | # Rewrite from https://github.com/YvZheng/pycwr/blob/master/pycwr/retrieve/HID.py 4 | 5 | import os 6 | 7 | import numpy as np 8 | 9 | from cinrad.utils import MODULE_DIR 10 | from cinrad._typing import Boardcast_T 11 | 12 | PARAMS = np.load(os.path.join(MODULE_DIR, "data", "hca_params.npy")) 13 | 14 | BAND_MAPPING = {"S": 0, "C": 1, "X": 2} 15 | DEFAULT_WEIGHTS = [0.8, 1.0, 0.8] 16 | # ZDR KDP RHO 17 | 18 | 19 | def beta_func( 20 | x: Boardcast_T, m: Boardcast_T, a: Boardcast_T, b: Boardcast_T 21 | ) -> Boardcast_T: 22 | return 1 / (1 + ((x - m) / a) ** (2 * b)) 23 | 24 | 25 | def hydro_class( 26 | z: Boardcast_T, 27 | zdr: Boardcast_T, 28 | kdp: Boardcast_T, 29 | rho: Boardcast_T, 30 | band: str = "S", 31 | ): 32 | r""" 33 | Types: Species #: 34 | ------------------------------- 35 | Drizzle 1 36 | 37 | Rain 2 38 | 39 | Ice Crystals 3 40 | 41 | Dry Aggregates Snow 4 42 | 43 | Wet Snow 5 44 | 45 | Vertical Ice 6 46 | 47 | Low-Density Graupel 7 48 | 49 | High-Density Graupel 8 50 | 51 | Hail 9 52 | 53 | Big Drops 10 54 | """ 55 | params = PARAMS[BAND_MAPPING[band]] 56 | # Process variables 57 | z = np.repeat(z[:, None], 10, axis=1) 58 | zdr = np.repeat(zdr[:, None], 10, axis=1) 59 | kdp = np.repeat(kdp[:, None], 10, axis=1) 60 | rho = np.repeat(rho[:, None], 10, axis=1) 61 | p_z = beta_func(z, params[0][:, 0], params[0][:, 1], params[0][:, 2]) 62 | p_zdr = beta_func(zdr, params[1][:, 0], params[1][:, 1], params[1][:, 2]) 63 | p_kdp = beta_func(kdp, params[2][:, 0], params[2][:, 1], params[2][:, 2]) 64 | p_rho = beta_func(rho, params[3][:, 0], params[3][:, 1], params[3][:, 2]) 65 | hclass = np.average([p_zdr, p_kdp, p_rho], weights=DEFAULT_WEIGHTS, axis=0) 66 | hclass *= p_z 67 | return np.argmax(hclass, axis=1) + 1 68 | -------------------------------------------------------------------------------- /cinrad/io/__init__.py: -------------------------------------------------------------------------------- 1 | """Classes for decoding level 2 and level 3 radar data. 2 | """ 3 | 4 | from cinrad.io.base import RadarBase, prepare_file 5 | from cinrad.io.level2 import * 6 | from cinrad.io.level3 import * 7 | 8 | 9 | def read_level2(filename: str) -> RadarBase: 10 | """Read CINRAD level 2 data.""" 11 | with prepare_file(filename) as file: 12 | magic = file.read(4) 13 | if magic == b"RSTM": 14 | return StandardData(filename) 15 | else: 16 | return CinradReader(filename) 17 | 18 | 19 | def read_auto(filename: str) -> RadarBase: 20 | """Read radar data, auto detected type of file . 21 | Args: 22 | filename: file name of radar data. 23 | 24 | Radar types: 25 | 26 | 1. StandardData, standard format based data. 27 | 2. StandardPUP, standard PUP format data. 28 | 3. MocMosaic, mosaic format data v3.0. 29 | 4. SWAN, SWAN format data. 30 | 5. CinradReader, cinrad format based data. 31 | 6. PhasedArrayData, standard format phased array radar data(XAD-2023). 32 | TODO:PUP & CinradReader(fix later) 33 | """ 34 | with prepare_file(filename) as file: 35 | flag = file.read(125) 36 | if flag[0:4] == b"RSTM": 37 | if flag[8:12] == b"\x01\x00\x00\x00": 38 | return StandardData(filename) 39 | elif flag[8:12] == b"\x02\x00\x00\x00": 40 | return StandardPUP(filename) 41 | elif flag[8:12] == b"\x10\x00\x00\x00": 42 | return StandardData(filename) 43 | else: 44 | raise Exception("Unknown standard radar type") 45 | elif flag[0:2] == b"\x01\x00": 46 | return PhasedArrayData(filename) 47 | elif flag[0:3] == b"MOC": 48 | return MocMosaic(filename) 49 | if flag[50:54] == b"SWAN": 50 | return SWAN(filename) 51 | sc_flag = flag[100:106] 52 | cc_flag = flag[116:122] 53 | if flag[14:16] == b"\x01\x00" or sc_flag == b"CINRAD" or cc_flag == b"CINRAD": 54 | return CinradReader(filename) 55 | raise Exception("Unknown radar type") 56 | -------------------------------------------------------------------------------- /cinrad/io/_dtype.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import numpy as np 5 | 6 | # fmt: off 7 | __all__ = [ 8 | 'CC_param', 'CC_header', 'CC_data', 'SDD_header', 'SDD_site', 'SDD_task', 9 | 'SDD_cut', 'SDD_rad_header', 'SDD_mom_header', 'SAB_dtype', 'CAB_dtype', 10 | 'SWAN_dtype', 'CD_dtype', 'CD_DATA', 'SDD_pheader', 'L3_radial', 'L3_rblock', 11 | 'S_SPECIAL_dtype', 'CC2_header', 'CC2_obs', 'CC2_data', 'CC2_other', 'PA_radial', 12 | 'L3_raster','L3_hail','L3_meso','L3_feature','L3_tvs','L3_sti_header','L3_sti_motion', 13 | 'L3_sti_position','L3_sti_attribute','L3_sti_component','L3_sti_adaptation', 14 | 'L3_vwp_header','L3_vwp','L3_swp','L3_uam','mocm_dtype','mocm_si_dtype','mocm_si_block', 15 | 'L3_wer_header','PA_SDD_site', 'PA_SDD_task', 'PA_SDD_beam', 'PA_SDD_cut', 'PA_SDD_rad_header', 16 | ] 17 | # fmt: on 18 | from cinrad.io._radar_struct.CC import ( 19 | scan_param_dtype as CC_param, 20 | header_dtype as CC_header, 21 | data_dtype as CC_data, 22 | ) 23 | from cinrad.io._radar_struct.standard_data import ( 24 | generic_header_dtype as SDD_header, 25 | site_config_dtype as SDD_site, 26 | task_config_dtype as SDD_task, 27 | cut_config_dtype as SDD_cut, 28 | radial_header_dtype as SDD_rad_header, 29 | moment_header_dtype as SDD_mom_header, 30 | product_header_dtype as SDD_pheader, 31 | pa_site_config_dtype as PA_SDD_site, 32 | pa_task_config_dtype as PA_SDD_task, 33 | pa_beam_dtype as PA_SDD_beam, 34 | pa_cut_config_dtype as PA_SDD_cut, 35 | pa_radial_header_dtype as PA_SDD_rad_header, 36 | l3_radial_header_dtype as L3_radial, 37 | l3_radial_block_dtype as L3_rblock, 38 | l3_raster_header_dtype as L3_raster, 39 | l3_hail_table_dtype as L3_hail, 40 | l3_meso_table_dtype as L3_meso, 41 | l3_feature_table_dtype as L3_feature, 42 | l3_tvs_table_dtype as L3_tvs, 43 | l3_sti_header_dtype as L3_sti_header, 44 | l3_sti_motion_dtype as L3_sti_motion, 45 | l3_sti_position_dtype as L3_sti_position, 46 | l3_sti_attribute_dtype as L3_sti_attribute, 47 | l3_sti_component_dtype as L3_sti_component, 48 | l3_sti_adaptation_dtype as L3_sti_adaptation, 49 | l3_vwp_header_dtype as L3_vwp_header, 50 | l3_vwp_table_dtype as L3_vwp, 51 | l3_swp as L3_swp, 52 | l3_uam_dtype as L3_uam, 53 | l3_wer_header_dtype as L3_wer_header, 54 | ) 55 | from cinrad.io._radar_struct.CD import ( 56 | radarsite_dtype as CD_site, 57 | performance_dtype as CD_perf, 58 | observation_param_dtype as CD_obs, 59 | ) 60 | from cinrad.io._radar_struct.CC2 import ( 61 | header_dtype as CC2_header, 62 | observation_param_dtype as CC2_obs, 63 | other_info_dtype as CC2_other, 64 | data_block_dtype as CC2_data, 65 | ) 66 | from cinrad.io._radar_struct.PA import radial_dtype as PA_radial 67 | 68 | _S_HEADER = [("spare", "u2", 7), ("a", "u2"), ("res", "u2", 6)] 69 | 70 | _S_INFO = [ 71 | ("time", "u4"), 72 | ("day", "u2"), 73 | ("unambiguous_distance", "u2"), 74 | ("azimuth", "u2"), 75 | ("radial_num", "u2"), 76 | ("radial_state", "u2"), 77 | ("elevation", "u2"), 78 | ("el_num", "u2"), 79 | ("first_gate_r", "u2"), 80 | ("first_gate_v", "u2"), 81 | ("gate_length_r", "u2"), 82 | ("gate_length_v", "u2"), 83 | ("gate_num_r", "u2"), 84 | ("gate_num_v", "u2"), 85 | ("sector_num", "u2"), 86 | ("system_coff", "u4"), 87 | ("r_pointer", "u2"), 88 | ("v_pointer", "u2"), 89 | ("w_pointer", "u2"), 90 | ("v_reso", "u2"), 91 | ("vcp_mode", "u2"), 92 | ("res2", "u2", 4), 93 | ("r_pointer_2", "u2"), 94 | ("v_pointer_2", "u2"), 95 | ("w_pointer_2", "u2"), 96 | ("nyquist_vel", "u2"), 97 | ] 98 | 99 | _S_RES = [("res3", "u2", 19)] 100 | _S_RES_2 = [("res3", "u2", 21)] 101 | 102 | _SAB_DATA = [("r", "u1", 460), ("v", "u1", 920), ("w", "u1", 920), ("res4", "u2", 2)] 103 | 104 | _CAB_DATA = [("r", "u1", 800), ("v", "u1", 1600), ("w", "u1", 1600), ("res4", "u2", 2)] 105 | 106 | _SPECIAL_DATA = [("r", "u1", 1000), ("v", "u1", 1000), ("w", "u1", 1000)] 107 | 108 | SAB_dtype = np.dtype(_S_HEADER + _S_INFO + _S_RES + _SAB_DATA) 109 | CAB_dtype = np.dtype(_S_HEADER + _S_INFO + _S_RES + _CAB_DATA) 110 | S_SPECIAL_dtype = np.dtype(_S_HEADER + _S_INFO + _S_RES_2 + _SPECIAL_DATA) 111 | 112 | SWAN_HEADER = [ 113 | ("data_type", "12c"), 114 | ("data_name", "38c"), 115 | ("name", "8c"), 116 | ("version", "8c"), 117 | ("year", "u2"), 118 | ("month", "u2"), 119 | ("day", "u2"), 120 | ("hour", "u2"), 121 | ("minute", "u2"), 122 | ("interval", "u2"), 123 | ("x_grid_num", "u2"), 124 | ("y_grid_num", "u2"), 125 | ("z_grid_num", "u2"), 126 | ("radar_count", "i4"), 127 | ("start_lon", "f4"), 128 | ("start_lat", "f4"), 129 | ("center_lon", "f4"), 130 | ("center_lat", "f4"), 131 | ("x_reso", "f4"), 132 | ("y_reso", "f4"), 133 | ("height", "40f4"), 134 | ("station_names", "20S16"), 135 | ("station_lons", "20f4"), 136 | ("station_lats", "20f4"), 137 | ("station_alts", "20f4"), 138 | ("mosaic_flags", "20B"), 139 | ("m_data_type", "i2"), 140 | ("dimension", "i2"), 141 | ("res", "168c"), 142 | ] 143 | 144 | SWAN_dtype = np.dtype(SWAN_HEADER) 145 | 146 | CD_dtype = np.dtype( 147 | [("site_info", CD_site), ("performance", CD_perf), ("obs", CD_obs), ("res", "163c")] 148 | ) 149 | 150 | _CD_record = np.dtype([("m_dbz", "B"), ("m_vel", "B"), ("m_undbz", "B"), ("m_sw", "B")]) 151 | 152 | CD_DATA = np.dtype( 153 | [ 154 | ("s_az", "u2"), 155 | ("s_el", "u2"), 156 | ("e_az", "u2"), 157 | ("e_el", "u2"), 158 | ("rec", _CD_record, 998), 159 | ] 160 | ) 161 | 162 | MOCM_HEADER = [ 163 | ("label", "4c"), 164 | ("version", "4c"), 165 | ("file_bytes", "i4"), 166 | ("mosaic_id", "u2"), 167 | ("coordinate", "u2"), 168 | ("varname", "8c"), 169 | ("description", "64c"), 170 | ("block_pos", "i4"), 171 | ("block_len", "i4"), 172 | ("time_zone", "i4"), 173 | ("year", "u2"), 174 | ("month", "u2"), 175 | ("day", "u2"), 176 | ("hour", "u2"), 177 | ("minute", "u2"), 178 | ("second", "u2"), 179 | ("obs_seconds", "i4"), 180 | ("obs_dates", "u2"), 181 | ("gen_dates", "u2"), 182 | ("gen_seconds", "i4"), 183 | ("edge_s", "i4"), 184 | ("edge_w", "i4"), 185 | ("edge_n", "i4"), 186 | ("edge_e", "i4"), 187 | ("cx", "i4"), 188 | ("cy", "i4"), 189 | ("nx", "i4"), 190 | ("ny", "i4"), 191 | ("dx", "i4"), 192 | ("dy", "i4"), 193 | ("height", "u2"), 194 | ("compress", "u2"), 195 | ("num_of_radars", "i4"), 196 | ("un_zip_bytes", "i4"), 197 | ("scale", "u2"), 198 | ("unused", "u2"), 199 | ("rgn_id", "8c"), 200 | ("units", "8c"), 201 | ("res", "60c"), 202 | ] 203 | 204 | mocm_dtype = np.dtype(MOCM_HEADER) 205 | 206 | MOCM_SI_HEADER = [ 207 | ("label", "4c"), 208 | ("version", "4c"), 209 | ("file_bytes", "i4"), 210 | ("site_code", "8c"), 211 | ("site_name", "20c"), 212 | ("province_name", "20c"), 213 | ("radar_type", "12c"), 214 | ("Latitude", "i4"), 215 | ("Longitude", "i4"), 216 | ("antenna_height", "i4"), 217 | ("time_zone", "i4"), 218 | ("year", "u2"), 219 | ("month", "u2"), 220 | ("day", "u2"), 221 | ("hour", "u2"), 222 | ("minute", "u2"), 223 | ("second", "u2"), 224 | ("obs_seconds", "i4"), 225 | ("obs_dates", "u2"), 226 | ("gen_dates", "u2"), 227 | ("gen_seconds", "i4"), 228 | ("block_num", "u2"), 229 | ("compress", "u2"), 230 | ("pid", "64u2"), 231 | ("block_pos", "64i4"), 232 | ("block_len", "64i4"), 233 | ("vcp", "u2"), 234 | ] 235 | 236 | mocm_si_dtype = np.dtype(MOCM_SI_HEADER) 237 | 238 | MOCM_SI_BLOCK = [ 239 | ("pid", "u2"), 240 | ("coordinate", "u2"), 241 | ("varname", "8c"), 242 | ("description", "52c"), 243 | ("edge_s", "i4"), 244 | ("edge_w", "i4"), 245 | ("edge_n", "i4"), 246 | ("edge_e", "i4"), 247 | ("cy", "i4"), 248 | ("cx", "i4"), 249 | ("ny", "i4"), 250 | ("nx", "i4"), 251 | ("dy", "i4"), 252 | ("dx", "i4"), 253 | ("ry", "u2"), 254 | ("rx", "u2"), 255 | ("range", "u2"), 256 | ("height", "u2"), 257 | ("block_size", "i4"), 258 | ("gc_counts", "i4"), 259 | ("scale", "u2"), 260 | ("res", "10c"), 261 | ] 262 | 263 | mocm_si_block = np.dtype(MOCM_SI_BLOCK) 264 | -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/CC.py: -------------------------------------------------------------------------------- 1 | # Generated by parse_spec.py 2 | # Do not modify 3 | import numpy as np 4 | 5 | header = [ 6 | ("cFileType", "S16"), 7 | ("cCountry", "S30"), 8 | ("cProvince", "S20"), 9 | ("cStation", "S40"), 10 | ("cStationNumber", "S10"), 11 | ("cRadarType", "S20"), 12 | ("cLongitude", "S16"), 13 | ("cLatitude", "S16"), 14 | ("lLongitudeValue", "i4"), 15 | ("lLatitudeValue", "i4"), 16 | ("lHeight", "i4"), 17 | ("sMaxAngle", "i2"), 18 | ("sOptAngle", "i2"), 19 | ("ucSYear1", "B"), 20 | ("ucSYear2", "B"), 21 | ("ucSMonth", "B"), 22 | ("ucSDay", "B"), 23 | ("ucSHour", "B"), 24 | ("ucSMinute", "B"), 25 | ("ucSSecond", "B"), 26 | ("ucTimeFrom", "B"), 27 | ("ucEYear1", "B"), 28 | ("ucEYear2", "B"), 29 | ("ucEMonth", "B"), 30 | ("ucEDay", "B"), 31 | ("ucEHour", "B"), 32 | ("ucEMinute", "B"), 33 | ("ucESecond", "B"), 34 | ("ucScanMode", "B"), 35 | ("ulSmilliSecond", "u4"), 36 | ("usRHIA", "u2"), 37 | ("sRHIL", "i2"), 38 | ("sRHIH", "i2"), 39 | ("usEchoType", "u2"), 40 | ("usProdCode", "u2"), 41 | ("ucCalibration", "B"), 42 | ("remain1", "3B"), 43 | ("remain2", "660B"), 44 | ("test", "2B"), 45 | ("lAntennaG", "i4"), 46 | ("lPower", "i4"), 47 | ("lWavelength", "i4"), 48 | ("usBeamH", "u2"), 49 | ("usBeamL", "u2"), 50 | ("usPolarization", "u2"), 51 | ("usLogA", "u2"), 52 | ("usLineA", "u2"), 53 | ("usAGCP", "u2"), 54 | ("usFreqMode", "u2"), 55 | ("usFreqRepeat", "u2"), 56 | ("usPPPPulse", "u2"), 57 | ("usFFTPoint", "u2"), 58 | ("usProcessType", "u2"), 59 | ("ucClutterT", "B"), 60 | ("cSidelobe", "c"), 61 | ("ucVelocityT", "B"), 62 | ("ucFilderP", "B"), 63 | ("ucNoiseT", "B"), 64 | ("ucSQIT", "B"), 65 | ("ucIntensityC", "B"), 66 | ("ucIntensityR", "B"), 67 | ("ucCalNoise", "B"), 68 | ("ucCalPower", "B"), 69 | ("ucCalPulseWidth", "B"), 70 | ("ucCalWorkFreq", "B"), 71 | ("ucCalLog", "B"), 72 | ("remain3", "92B"), 73 | ("test2", "B"), 74 | ("liDataOffset", "u4"), 75 | ] 76 | 77 | header_dtype = np.dtype(header) 78 | 79 | scan_param = [ 80 | ("usMaxV", "u2"), 81 | ("usMaxL", "u2"), 82 | ("usBindWidth", "u2"), 83 | ("usBinNumber", "u2"), 84 | ("usRecordNumber", "u2"), 85 | ("usArotate", "u2"), 86 | ("usPrf1", "u2"), 87 | ("usPrf2", "u2"), 88 | ("usSpulseW", "u2"), 89 | ("usAngle", "i2"), 90 | ("cSweepStatus", "B"), 91 | ("cAmbiguousp", "B"), 92 | ] 93 | 94 | scan_param_dtype = np.dtype(scan_param) 95 | 96 | data = [("Z", "500i2"), ("V", "500i2"), ("W", "500i2")] 97 | 98 | data_dtype = np.dtype(data) 99 | -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/CC2.py: -------------------------------------------------------------------------------- 1 | # Generated by parse_spec.py 2 | # Do not modify 3 | import numpy as np 4 | 5 | header = [ 6 | ("sFileID", "S4"), 7 | ("fVersionNo", "f4"), 8 | ("lFileHeaderLength", "i4"), 9 | ("sCountry", "S30"), 10 | ("sProvince", "S20"), 11 | ("sStation", "S40"), 12 | ("sStationNumber", "S10"), 13 | ("sRadarType", "S20"), 14 | ("sLongitude", "S16"), 15 | ("sLatitude", "S16"), 16 | ("lLongitudeValue", "i4"), 17 | ("lLatitudeValue", "i4"), 18 | ("lHeight", "i4"), 19 | ("shMaxAngle", "i2"), 20 | ("shOptiAngle", "i2"), 21 | ("lAntennaG", "i4"), 22 | ("usBeamH", "u2"), 23 | ("usBeamL", "u2"), 24 | ("ucPolarization", "B"), 25 | ("usSidelobe", "u2"), 26 | ("lPower", "i4"), 27 | ("lWavelength", "i4"), 28 | ("usLogA", "u2"), 29 | ("usLineA", "u2"), 30 | ("usAGCP", "u2"), 31 | ("usLogMinPower", "u2"), 32 | ("usLineMinPower", "u2"), 33 | ("ucClutterT", "B"), 34 | ("ucVelocityP", "B"), 35 | ("ucFilterP", "B"), 36 | ("ucNoiseT", "B"), 37 | ("ucSQIT", "B"), 38 | ("ucIntensityC", "B"), 39 | ("ucIntensityR", "B"), 40 | ] 41 | 42 | header_dtype = np.dtype(header) 43 | 44 | bin_param = [("sCode", "i2"), ("Begin", "i2"), ("End", "i2"), ("BinLength", "i2")] 45 | 46 | bin_param_dtype = np.dtype(bin_param) 47 | 48 | layer_param = [ 49 | ("ucDataType", "B"), 50 | ("ucAmbiguousP", "B"), 51 | ("usSpeed", "u2"), 52 | ("usPRF1", "u2"), 53 | ("usPRF2", "u2"), 54 | ("usPulseWidth", "u2"), 55 | ("usMaxV", "u2"), 56 | ("usMaxL", "u2"), 57 | ("usZBinWidth", "u2"), 58 | ("usVBinWidth", "u2"), 59 | ("usWBinWidth", "u2"), 60 | ("usZBinNumber", "u2"), 61 | ("usVBinNumber", "u2"), 62 | ("usWBinNumber", "u2"), 63 | ("usRecordNumber", "u2"), 64 | ("sSwpAngle", "i2"), 65 | ("cDataForm", "c"), 66 | ("ulDataBegin", "u4"), 67 | ] 68 | 69 | layer_param_dtype = np.dtype(layer_param) 70 | 71 | observation_param = [ 72 | ("ucType", "B"), 73 | ("usSYear", "u2"), 74 | ("ucSMonth", "B"), 75 | ("ucSDay", "B"), 76 | ("ucSHour", "B"), 77 | ("ucSMinute", "B"), 78 | ("ucSSecond", "B"), 79 | ("ucTimeP", "B"), 80 | ("ulSMillisecond", "u4"), 81 | ("ucCalibration", "B"), 82 | ("ucIntensityI", "B"), 83 | ("ucVelocityP", "B"), 84 | ("usZStartBin", "u2"), 85 | ("usVStartBin", "u2"), 86 | ("usWStartBin", "u2"), 87 | ("LayerInfo", layer_param_dtype, 32), 88 | ("usRHIA", "u2"), 89 | ("sRHIL", "i2"), 90 | ("sRHIH", "i2"), 91 | ("usEYear", "u2"), 92 | ("ucEMonth", "B"), 93 | ("ucEDay", "B"), 94 | ("ucEHour", "B"), 95 | ("ucEMinute", "B"), 96 | ("ucESecond", "B"), 97 | ("ucETenth", "B"), 98 | ("usZBinByte", "u2"), 99 | ("BinRange1", bin_param_dtype, 5), 100 | ("usVBinByte", "u2"), 101 | ("BinRange2", bin_param_dtype, 5), 102 | ("usWBinByte", "u2"), 103 | ("BinRange3", bin_param_dtype, 5), 104 | ] 105 | 106 | observation_param_dtype = np.dtype(observation_param) 107 | 108 | other_info = [("sStationID", "S2"), ("sScanName", "S10"), ("sSpare", "S550")] 109 | 110 | other_info_dtype = np.dtype(other_info) 111 | 112 | data_block = [ 113 | ("sElevation", "i2"), 114 | ("usAzimuth", "u2"), 115 | ("ucHour", "B"), 116 | ("ucMinute", "B"), 117 | ("ucSecond", "B"), 118 | ("ulMillisecond", "u4"), 119 | ("ucCorZ", "4096B"), 120 | ("ucUnZ", "4096B"), 121 | ("cV", "4096c"), 122 | ("ucW", "4096B"), 123 | ("siZDR", "4096i2"), 124 | ("siPHDP", "4096i2"), 125 | ("siROHV", "4096i2"), 126 | ("uKDP", "4096i2"), 127 | ] 128 | 129 | data_block_dtype = np.dtype(data_block) 130 | -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/CD.py: -------------------------------------------------------------------------------- 1 | # Generated by parse_spec.py 2 | # Do not modify 3 | import numpy as np 4 | 5 | radarsite = [ 6 | ("country", "S30"), 7 | ("province", "S20"), 8 | ("station", "S40"), 9 | ("stationnumber", "S10"), 10 | ("radartype", "S20"), 11 | ("Longitude", "S16"), 12 | ("Latitude", "S16"), 13 | ("Longitudevalue", "i4"), 14 | ("Latitudevalue", "i4"), 15 | ("height", "i4"), 16 | ("Maxangle", "i2"), 17 | ("Opangle", "i2"), 18 | ("MangFreq", "i2"), 19 | ] 20 | 21 | radarsite_dtype = np.dtype(radarsite) 22 | 23 | performance = [ 24 | ("AntennaG", "i4"), 25 | ("BeamH", "u2"), 26 | ("BeamL", "u2"), 27 | ("polarizations", "B"), 28 | ("sidelobe", "c"), 29 | ("Power", "i4"), 30 | ("wavelength", "i4"), 31 | ("logA", "u2"), 32 | ("LineA", "u2"), 33 | ("AGCP", "u2"), 34 | ("clutterT", "B"), 35 | ("VelocityP", "B"), 36 | ("filderP", "B"), 37 | ("noiseT", "B"), 38 | ("SQIT", "B"), 39 | ("intensityC", "B"), 40 | ("intensityR", "B"), 41 | ] 42 | 43 | performance_dtype = np.dtype(performance) 44 | 45 | layer_param = [ 46 | ("ambiguousp", "B"), 47 | ("Arotate", "u2"), 48 | ("Prf1", "u2"), 49 | ("Prf2", "u2"), 50 | ("spulseW", "u2"), 51 | ("MaxV", "u2"), 52 | ("MaxL", "u2"), 53 | ("binWidth", "u2"), 54 | ("binnumber", "u2"), 55 | ("recordnumber", "u2"), 56 | ("Swangles", "i2"), 57 | ] 58 | 59 | layer_param_dtype = np.dtype(layer_param) 60 | 61 | observation_param = [ 62 | ("stype", "B"), 63 | ("syear", "u2"), 64 | ("smonth", "B"), 65 | ("sday", "B"), 66 | ("shour", "B"), 67 | ("sminute", "B"), 68 | ("ssecond", "B"), 69 | ("Timep", "B"), 70 | ("smillisecond", "u4"), 71 | ("calibration", "B"), 72 | ("intensityI", "B"), 73 | ("VelocityP_1", "B"), 74 | ("layerparam", layer_param_dtype, 30), 75 | ("RHIA", "u2"), 76 | ("RHIL", "i2"), 77 | ("RHIH", "i2"), 78 | ("Eyear", "u2"), 79 | ("Emonth", "B"), 80 | ("Eday", "B"), 81 | ("Ehour", "B"), 82 | ("Eminute", "B"), 83 | ("Esecond", "B"), 84 | ("Etenth", "B"), 85 | ] 86 | 87 | observation_param_dtype = np.dtype(observation_param) 88 | -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/PA.py: -------------------------------------------------------------------------------- 1 | # Generated by parse_spec.py 2 | # Do not modify 3 | import numpy as np 4 | 5 | header = [ 6 | ("filetype", "u2"), 7 | ("longitude", "i2"), 8 | ("latitude", "i2"), 9 | ("v_beam_width", "B"), 10 | ("h_beam_width", "B"), 11 | ("height", "u2"), 12 | ("reserved", "S22"), 13 | ] 14 | 15 | header_dtype = np.dtype(header) 16 | 17 | data_block = [ 18 | ("radial_time", "u4"), 19 | ("radial_date", "u4"), 20 | ("unambiguous_dist", "u2"), 21 | ("azimuth", "u2"), 22 | ("az_num", "u2"), 23 | ("elevation", "u2"), 24 | ("el_num", "u2"), 25 | ("radial_index", "u2"), 26 | ("first_gate_dist", "u2"), 27 | ("gate_length", "u2"), 28 | ("gate_num", "u2"), 29 | ("reserved", "S26"), 30 | ("tref_pt", "u2"), 31 | ("ref_pt", "u2"), 32 | ("vel_pt", "u2"), 33 | ("sw_pt", "u2"), 34 | ("zdr_pt", "u2"), 35 | ("phi_pt", "u2"), 36 | ("kdp_pt", "u2"), 37 | ("rho_pt", "u2"), 38 | ("tref", "2000B"), 39 | ("ref", "2000B"), 40 | ("vel", "2000B"), 41 | ("sw", "2000B"), 42 | ("zdr", "2000B"), 43 | ("phi", "2000B"), 44 | ("kdp", "2000B"), 45 | ("rho", "2000B"), 46 | ("res2", "S4"), 47 | ] 48 | 49 | data_block_dtype = np.dtype(data_block) 50 | 51 | radial = [("header", header_dtype), ("data", data_block_dtype)] 52 | 53 | radial_dtype = np.dtype(radial) 54 | -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/cinrad/io/_radar_struct/__init__.py -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/parse_spec.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | 3 | 4 | def replace_dtype(s: str) -> str: 5 | s = s.replace("ulong", "u4") 6 | s = s.replace("ushort", "u2") 7 | s = s.replace(" long", " i4") 8 | s = s.replace("short", "i2") 9 | return s.replace("\n", " ") 10 | 11 | 12 | if __name__ == "__main__": 13 | spec_list = glob("*.spec") 14 | 15 | for fn in spec_list: 16 | f = open(fn, "r") 17 | content = f.read() 18 | f.close() 19 | sections = content.split("#")[1:] 20 | out = open(fn.replace("spec", "py"), "w") 21 | out.write("# Generated by parse_spec.py\n") 22 | out.write("# Do not modify\n") 23 | out.write("import numpy as np\n\n") 24 | for name, rawtype in zip(sections[0::2], sections[1::2]): 25 | content_r = replace_dtype(rawtype) 26 | content_list = content_r.split(" ")[1:] 27 | content_str = ['"{}"'.format(i) for i in content_list] 28 | dtype_list = [ 29 | ", ".join((i, j)) for i, j in zip(content_str[0::2], content_str[1::2]) 30 | ] 31 | ident_step = len(str("{} = [".format(name))) * " " 32 | out.write( 33 | "{} = [".format(name) + "({}),\n".format(dtype_list[0]) 34 | ) # write first line 35 | for i in dtype_list[1:-1]: 36 | out.write(ident_step + "({}),\n".format(i)) 37 | out.write(ident_step + "({})]\n\n".format(dtype_list[-1])) 38 | out.write("{}_dtype = np.dtype({})\n\n".format(name, name)) 39 | out.close() 40 | -------------------------------------------------------------------------------- /cinrad/io/_radar_struct/standard_data.py: -------------------------------------------------------------------------------- 1 | # Generated by parse_spec.py 2 | # Do not modify 3 | import numpy as np 4 | 5 | generic_header = [ 6 | ("magic_number", "i4"), 7 | ("major_version", "i2"), 8 | ("minor_version", "i2"), 9 | ("generic_type", "i4"), 10 | ("product_type", "i4"), 11 | ("res1", "16c"), 12 | ] 13 | 14 | generic_header_dtype = np.dtype(generic_header) 15 | 16 | site_config = [ 17 | ("site_code", "S8"), 18 | ("site_name", "S32"), 19 | ("Latitude", "f4"), 20 | ("Longitude", "f4"), 21 | ("antenna_height", "i4"), 22 | ("ground_height", "i4"), 23 | ("frequency", "f4"), 24 | ("beam_width_hori", "f4"), 25 | ("beam_width_vert", "f4"), 26 | ("RDA_version", "i4"), 27 | ("radar_type", "i2"), 28 | ("antenna_gain", "i2"), 29 | ("trans_loss", "i2"), 30 | ("recv_loss", "i2"), 31 | ("other_loss", "i2"), 32 | ("res2", "46c"), 33 | ] 34 | 35 | site_config_dtype = np.dtype(site_config) 36 | 37 | task_config = [ 38 | ("task_name", "S32"), 39 | ("task_dsc", "S128"), 40 | ("polar_type", "i4"), 41 | ("scan_type", "i4"), 42 | ("pulse_width", "i4"), 43 | ("scan_start_time", "i4"), 44 | ("cut_number", "i4"), 45 | ("hori_noise", "f4"), 46 | ("vert_noise", "f4"), 47 | ("hori_cali", "f4"), 48 | ("vert_cali", "f4"), 49 | ("hori_tmp", "f4"), 50 | ("vert_tmp", "f4"), 51 | ("ZDR_cali", "f4"), 52 | ("PHIDP_cali", "f4"), 53 | ("LDR_cali", "f4"), 54 | ("res3", "40c"), 55 | ] 56 | 57 | task_config_dtype = np.dtype(task_config) 58 | 59 | cut_config = [ 60 | ("process_mode", "i4"), 61 | ("wave_form", "i4"), 62 | ("PRF1", "f4"), 63 | ("PRF2", "f4"), 64 | ("dealias_mode", "i4"), 65 | ("azimuth", "f4"), 66 | ("elev", "f4"), 67 | ("start_angle", "f4"), 68 | ("end_angle", "f4"), 69 | ("angular_reso", "f4"), 70 | ("scan_spd", "f4"), 71 | ("log_reso", "i4"), 72 | ("dop_reso", "i4"), 73 | ("max_range1", "i4"), 74 | ("max_range2", "i4"), 75 | ("start_range", "i4"), 76 | ("sample1", "i4"), 77 | ("sample2", "i4"), 78 | ("phase_mode", "i4"), 79 | ("atmos_loss", "f4"), 80 | ("nyquist_spd", "f4"), 81 | ("moments_mask", "i8"), 82 | ("moments_size_mask", "i8"), 83 | ("misc_filter_mask", "i4"), 84 | ("SQI_thres", "f4"), 85 | ("SIG_thres", "f4"), 86 | ("CSR_thres", "f4"), 87 | ("LOG_thres", "f4"), 88 | ("CPA_thres", "f4"), 89 | ("PMI_thres", "f4"), 90 | ("DPLOG_thres", "f4"), 91 | ("res_thres", "4V"), 92 | ("dBT_mask", "i4"), 93 | ("dBZ_mask", "i4"), 94 | ("vel_mask", "i4"), 95 | ("sw_mask", "i4"), 96 | ("DP_mask", "i4"), 97 | ("res_mask", "12V"), 98 | ("scan_sync", "i4"), 99 | ("direction", "i4"), 100 | ("ground_clutter_classifier_type", "i2"), 101 | ("ground_clutter_filter_type", "i2"), 102 | ("ground_clutter_filter_notch_width", "i2"), 103 | ("ground_clutter_filter_window", "i2"), 104 | ("res4", "72V"), 105 | ] 106 | 107 | cut_config_dtype = np.dtype(cut_config) 108 | 109 | radial_header = [ 110 | ("radial_state", "i4"), 111 | ("spot_blank", "i4"), 112 | ("seq_number", "i4"), 113 | ("radial_number", "i4"), 114 | ("elevation_number", "i4"), 115 | ("azimuth", "f4"), 116 | ("elevation", "f4"), 117 | ("seconds", "i4"), 118 | ("microseconds", "i4"), 119 | ("data_length", "i4"), 120 | ("moment_number", "i4"), 121 | ("res5", "i2"), 122 | ("hori_est_noise", "i2"), 123 | ("vert_est_noise", "i2"), 124 | ("zip_type", "c"), 125 | ("res6", "13c"), 126 | ] 127 | 128 | radial_header_dtype = np.dtype(radial_header) 129 | 130 | moment_header = [ 131 | ("data_type", "i4"), 132 | ("scale", "i4"), 133 | ("offset", "i4"), 134 | ("bin_length", "i2"), 135 | ("flags", "i2"), 136 | ("block_length", "i4"), 137 | ("res", "12c"), 138 | ] 139 | 140 | moment_header_dtype = np.dtype(moment_header) 141 | 142 | product_header = [ 143 | ("product_type", "i4"), 144 | ("product_name", "S32"), 145 | ("product_gentime", "i4"), 146 | ("scan_start_time", "i4"), 147 | ("data_start_time", "i4"), 148 | ("data_end_time", "i4"), 149 | ("proj_type", "i4"), 150 | ("dtype_1", "i4"), 151 | ("dtype_2", "i4"), 152 | ("res", "64c"), 153 | ] 154 | 155 | product_header_dtype = np.dtype(product_header) 156 | 157 | pa_site_config = [ 158 | ("site_code", "S8"), 159 | ("site_name", "S32"), 160 | ("Latitude", "f4"), 161 | ("Longitude", "f4"), 162 | ("antenna_height", "i4"), 163 | ("ground_height", "i4"), 164 | ("frequency", "f4"), 165 | ("antenna_type", "i4"), 166 | ("tr_number", "i4"), 167 | ("RDA_version", "i4"), 168 | ("radar_type", "i2"), 169 | ("res2", "54c"), 170 | ] 171 | 172 | pa_site_config_dtype = np.dtype(pa_site_config) 173 | 174 | pa_task_config = [ 175 | ("task_name", "S32"), 176 | ("task_dsc", "S128"), 177 | ("polar_type", "i4"), 178 | ("scan_type", "i4"), 179 | ("san_beam_number", "i4"), 180 | ("cut_number", "i4"), 181 | ("ray_order", "i4"), 182 | ("scan_start_time", "i8"), 183 | ("res3", "68c"), 184 | ] 185 | 186 | pa_task_config_dtype = np.dtype(pa_task_config) 187 | 188 | pa_beam = [ 189 | ('beam_index', 'i4'), 190 | ('beam_type', 'i4'), 191 | ('sub_pulse_number', 'i4'), 192 | ('tx_beam_direction', 'f4'), 193 | ('tx_beam_width_h', 'f4'), 194 | ('tx_beam_width_v', 'f4'), 195 | ('tx_beam_gain', 'f4'), 196 | ('reserved_00', '100c'), 197 | ('sub_pulse_strategy', 'i4'), 198 | ('sub_pulse_modulation', 'i4'), 199 | ('sub_pulse_frequency', 'f4'), 200 | ('sub_pulse_band_width', 'f4'), 201 | ('sub_pulse_width', 'i4'), 202 | ('reserved_01', '492c'), 203 | ] 204 | 205 | pa_beam_dtype = np.dtype(pa_beam) 206 | 207 | pa_cut_config = [ 208 | ('cut_index', 'i2'), 209 | ('tx_beam_index', 'i2'), 210 | ('elev', 'f4'), 211 | ('tx_beam_gain', 'f4'), 212 | ('rx_beam_width_h', 'f4'), 213 | ('rx_beam_width_v', 'f4'), 214 | ('rx_beam_gain', 'f4'), 215 | ('process_mode', 'i4'), 216 | ('wave_form', 'i4'), 217 | ('n1_prf_1', 'f4'), 218 | ('n1_prf_2', 'f4'), 219 | ('n2_prf_1', 'f4'), 220 | ('n2_prf_2', 'f4'), 221 | ('unfold_mode', 'i4'), 222 | ('azimuth', 'f4'), 223 | ('start_angle', 'f4'), 224 | ('end_angle', 'f4'), 225 | ('angle_resolution', 'f4'), 226 | ('scan_speed', 'f4'), 227 | ('log_reso', 'f4'), 228 | ('dop_reso', 'f4'), 229 | ('max_range1', 'i4'), 230 | ('max_range2', 'i4'), 231 | ('start_range', 'i4'), 232 | ('sample_1', 'i4'), 233 | ('sample_2', 'i4'), 234 | ('phase_mode', 'i4'), 235 | ('atmospheric_loss', 'f4'), 236 | ('nyquist_spd', 'f4'), 237 | ('moments_mask', 'i8'), 238 | ('moments_size_mask', 'i8'), 239 | ('misc_filter_mask', 'i4'), 240 | ('sqi_threshold', 'f4'), 241 | ('sig_threshold', 'f4'), 242 | ('csr_threshold', 'f4'), 243 | ('log_threshold', 'f4'), 244 | ('cpa_threshold', 'f4'), 245 | ('pmi_threshold', 'f4'), 246 | ('dplog_threshold', 'f4'), 247 | ('thresholds_reserved', '4c'), 248 | ('dbt_mask', 'i4'), 249 | ('dbz_mask', 'i4'), 250 | ('velocity', 'i4'), 251 | ('spectrum_width_mask', 'i4'), 252 | ('zdr_mask', 'i4'), 253 | ('mask_reserved', '12c'), 254 | ('scan_sync', '4c'), 255 | ('direction', 'i4'), 256 | ('ground_clutter_classifier_type', 'i2'), 257 | ('ground_clutter_filter_type', 'i2'), 258 | ('ground_clutter_filter_notch_width', 'i2'), 259 | ('ground_clutter_filter_window', 'i2'), 260 | ('reserved', '44c') 261 | ] 262 | 263 | pa_cut_config_dtype = np.dtype(pa_cut_config) 264 | 265 | pa_radial_header = [ 266 | ("radial_state", "i4"), 267 | ("spot_blank", "i4"), 268 | ("seq_number", "i4"), 269 | ("radial_number", "i4"), 270 | ("elevation_number", "i4"), 271 | ("azimuth", "f4"), 272 | ("elevation", "f4"), 273 | ("seconds", "i8"), 274 | ("microseconds", "i4"), 275 | ("data_length", "i4"), 276 | ("moment_number", "i4"), 277 | ("scan_beam_index", "i2"), 278 | ("hori_est_noise", "i2"), 279 | ("vert_est_noise", "i2"), 280 | ("zip_type", "i4"), # acctualy it's ref flag,ziptype is reserved 281 | ("res6", "70c"), 282 | ] 283 | 284 | pa_radial_header_dtype = np.dtype(pa_radial_header) 285 | 286 | l3_radial_header = [ 287 | ("dtype", "i4"), 288 | ("scale", "i4"), 289 | ("offset", "i4"), 290 | ("bin_length", "i2"), 291 | ("flags", "i2"), 292 | ("reso", "i4"), 293 | ("start_range", "i4"), 294 | ("max_range", "i4"), 295 | ("nradial", "i4"), 296 | ("max_val", "i4"), 297 | ("range_of_max", "i4"), 298 | ("az_of_max", "f4"), 299 | ("min_val", "i4"), 300 | ("range_of_min", "i4"), 301 | ("az_of_min", "f4"), 302 | ("res", "8c"), 303 | ] 304 | 305 | l3_radial_header_dtype = np.dtype(l3_radial_header) 306 | 307 | l3_radial_block = [ 308 | ("start_az", "f4"), 309 | ("angular_reso", "f4"), 310 | ("nbins", "i4"), 311 | ("res", "20c"), 312 | ] 313 | 314 | l3_radial_block_dtype = np.dtype(l3_radial_block) 315 | 316 | l3_raster_header = [ 317 | ("dtype", "i4"), 318 | ("scale", "i4"), 319 | ("offset", "i4"), 320 | ("bin_length", "i2"), 321 | ("flags", "i2"), 322 | ("row_reso", "i4"), 323 | ("col_reso", "i4"), 324 | ("row_side_length", "i4"), 325 | ("col_side_length", "i4"), 326 | ("max_val", "i4"), 327 | ("range_of_max", "i4"), 328 | ("az_of_max", "f4"), 329 | ("min_val", "i4"), 330 | ("range_of_min", "i4"), 331 | ("az_of_min", "f4"), 332 | ("res", "8c"), 333 | ] 334 | 335 | l3_raster_header_dtype = np.dtype(l3_raster_header) 336 | 337 | l3_hail_table = [ 338 | ("hail_id", "i4"), 339 | ("hail_azimuth", "f4"), 340 | ("hail_range", "i4"), 341 | ("hail_possibility", "i4"), 342 | ("hail_severe_possibility", "i4"), 343 | ("hail_size", "f4"), 344 | ("rcm", "i4"), 345 | ] 346 | 347 | l3_hail_table_dtype = np.dtype(l3_hail_table) 348 | 349 | l3_meso_table = [ 350 | ("feature_id", "i4"), 351 | ("storm_id", "i4"), 352 | ("meso_azimuth", "f4"), 353 | ("meso_range", "i4"), 354 | ("meso_elevation", "f4"), 355 | ("meso_avgshr", "f4"), 356 | ("meso_height", "i4"), 357 | ("meso_azdia", "i4"), 358 | ("meso_radius", "i4"), 359 | ("meso_avgrv", "f4"), 360 | ("meso_mxrv", "f4"), 361 | ("meso_top", "i4"), 362 | ("meso_base", "i4"), 363 | ("meso_baseazim", "f4"), 364 | ("meso_baserange", "i4"), 365 | ("meso_baseelevation", "f4"), 366 | ("meso_mxtanshr", "f4"), 367 | ] 368 | 369 | l3_meso_table_dtype = np.dtype(l3_meso_table) 370 | 371 | l3_feature_table = [ 372 | ("feature_id", "i4"), 373 | ("storm_id", "i4"), 374 | ("feature_type", "i4"), 375 | ("feature_azimuth", "f4"), 376 | ("feature_range", "i4"), 377 | ("feature_elevation", "f4"), 378 | ("feature_avgshr", "f4"), 379 | ("feature_height", "i4"), 380 | ("feature_azdia", "i4"), 381 | ("feature_radius", "i4"), 382 | ("feature_avgrv", "f4"), 383 | ("feature_mxrv", "f4"), 384 | ("feature_top", "i4"), 385 | ("feature_base", "i4"), 386 | ("feature_baseazim", "f4"), 387 | ("feature_baserange", "i4"), 388 | ("feature_baseelevation", "f4"), 389 | ("feature_mxtanshr", "f4"), 390 | ] 391 | 392 | l3_feature_table_dtype = np.dtype(l3_feature_table) 393 | 394 | l3_tvs_table = [ 395 | ("tvs_id", "i4"), 396 | ("tvs_stormtype", "i4"), 397 | ("tvs_azimuth", "f4"), 398 | ("tvs_range", "i4"), 399 | ("tvs_elevation", "f4"), 400 | ("tvs_lldv", "f4"), 401 | ("tvs_avgdv", "f4"), 402 | ("tvs_mxdv", "f4"), 403 | ("tvs_mxdvhgt", "i4"), 404 | ("tvs_depth", "i4"), 405 | ("tvs_base", "i4"), 406 | ("tvs_top", "i4"), 407 | ("tvs_mxshr", "f4"), 408 | ("tvs_mxshrhgt", "i4"), 409 | ] 410 | 411 | l3_tvs_table_dtype = np.dtype(l3_tvs_table) 412 | 413 | l3_sti_header = [ 414 | ("num_of_storms", "i4"), 415 | ("num_of_continuous_storms", "i4"), 416 | ("num_of_components", "i4"), 417 | ("avg_speed", "f4"), 418 | ("avg_direction", "f4"), 419 | ] 420 | 421 | l3_sti_header_dtype = np.dtype(l3_sti_header) 422 | 423 | l3_sti_motion = [ 424 | ("azimuth", "f4"), 425 | ("range", "i4"), 426 | ("speed", "f4"), 427 | ("direction", "f4"), 428 | ("forecast_error", "i4"), 429 | ("mean_forecast_error", "i4"), 430 | ] 431 | 432 | l3_sti_motion_dtype = np.dtype(l3_sti_motion) 433 | 434 | l3_sti_position = [ 435 | ("azimuth", "f4"), 436 | ("range", "i4"), 437 | ("volume_time", "i4"), 438 | ] 439 | 440 | l3_sti_position_dtype = np.dtype(l3_sti_position) 441 | 442 | l3_sti_attribute = [ 443 | ("id", "i4"), 444 | ("type", "i4"), 445 | ("num_of_volumes", "i4"), 446 | ("azimuth", "f4"), 447 | ("range", "i4"), 448 | ("height", "i4"), 449 | ("max_ref", "f4"), 450 | ("max_ref_height", "i4"), 451 | ("vil", "f4"), 452 | ("num_of_components", "i4"), 453 | ("index_of_first", "i4"), 454 | ("top_height", "i4"), 455 | ("index_of_top", "i4"), 456 | ("bottom_height", "i4"), 457 | ("index_of_bottom", "i4"), 458 | ] 459 | 460 | l3_sti_attribute_dtype = np.dtype(l3_sti_attribute) 461 | 462 | l3_sti_component = [ 463 | ("height", "i4"), 464 | ("max_ref", "f4"), 465 | ("index_of_next", "i4"), 466 | ] 467 | 468 | l3_sti_component_dtype = np.dtype(l3_sti_component) 469 | 470 | l3_sti_adaptation = [ 471 | ("def_dir", "i4"), 472 | ("def_spd", "f4"), 473 | ("max_vtime", "i4"), 474 | ("num_of_past_volume", "i4"), 475 | ("corr_speed", "f4"), 476 | ("min_speed", "f4"), 477 | ("allow_error", "i4"), 478 | ("frc_intvl", "i4"), 479 | ("num_frc_intvl", "i4"), 480 | ("err_intvl", "i4"), 481 | ] 482 | 483 | l3_sti_adaptation_dtype = np.dtype(l3_sti_adaptation) 484 | 485 | l3_vwp_header = [ 486 | ("nyquist_velocity", "f4"), 487 | ("number_of_vols", "i4"), 488 | ("wind_speed_max", "f4"), 489 | ("wind_direction_max", "f4"), 490 | ("height_max", "f4"), 491 | ("res", "12c"), 492 | ] 493 | 494 | l3_vwp_header_dtype = np.dtype(l3_vwp_header) 495 | 496 | l3_vwp_table = [ 497 | ("start_time", "i4"), 498 | ("height", "i4"), 499 | ("fitvalid", "i4"), 500 | ("wind_direction", "f4"), 501 | ("wind_speed", "f4"), 502 | ("rms_std", "f4"), 503 | ("res", "8c"), 504 | ] 505 | 506 | l3_vwp_table_dtype = np.dtype(l3_vwp_table) 507 | 508 | l3_swp = [ 509 | ("range", "i4"), 510 | ("azimuth", "f4"), 511 | ("swp", "i4"), 512 | ] 513 | 514 | l3_swp_dtype = np.dtype(l3_swp) 515 | 516 | l3_uam = [ 517 | ("range", "i4"), 518 | ("azimuth", "f4"), 519 | ("a", "i4"), 520 | ("b", "i4"), 521 | ("deg", "i4"), 522 | ("max1", "f4"), 523 | ("max2", "f4"), 524 | ("max3", "f4"), 525 | ("max4", "f4"), 526 | ("max5", "f4"), 527 | ("area", "f4"), 528 | ] 529 | 530 | l3_uam_dtype = np.dtype(l3_uam) 531 | 532 | l3_wer_header = [ 533 | ("elevation", "f4"), 534 | ("scan_time", "i4"), 535 | ("center_height", "i4"), 536 | ("res", "20c"), 537 | ] 538 | 539 | l3_wer_header_dtype = np.dtype(l3_wer_header) 540 | -------------------------------------------------------------------------------- /cinrad/io/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import abc 5 | import os 6 | import json 7 | from typing import Optional, Any 8 | import bz2 9 | import gzip 10 | 11 | import numpy as np 12 | 13 | from cinrad.utils import MODULE_DIR 14 | from cinrad._typing import Number_T 15 | 16 | with open(os.path.join(MODULE_DIR, "data", "radar_station.json"), "r") as buf: 17 | radarinfo = json.load(buf) 18 | 19 | 20 | def _get_radar_info(code: Optional[str]) -> tuple: 21 | r"""Get radar station info from the station database according to the station code.""" 22 | try: 23 | return radarinfo[code] 24 | except KeyError: 25 | return ("None", 0, 0, "", 0) 26 | 27 | 28 | def prepare_file(file: Any) -> Any: 29 | if hasattr(file, "read"): 30 | return file 31 | f = open(file, "rb") 32 | magic = f.read(3) 33 | f.close() 34 | if magic.startswith(b"\x1f\x8b"): 35 | return gzip.GzipFile(file, "rb") 36 | if magic.startswith(b"BZh"): 37 | return bz2.BZ2File(file, "rb") 38 | return open(file, "rb") 39 | 40 | 41 | class RadarBase(abc.ABC): 42 | r""" 43 | Base class for readers in `cinrad.io`. 44 | Only used when subclassed 45 | """ 46 | 47 | # Same methods for all radar classes 48 | def _update_radar_info(self): 49 | r"""Update radar station info automatically.""" 50 | info = _get_radar_info(self.code) 51 | self.stationlon = info[1] 52 | self.stationlat = info[2] 53 | self.name = info[0] 54 | self.radarheight = info[4] 55 | 56 | def set_code(self, code: str): 57 | self.code = code 58 | self._update_radar_info() 59 | 60 | def get_nscans(self) -> int: 61 | return len(self.el) 62 | 63 | def available_product(self, tilt: int) -> list: 64 | r"""Get all available products in given tilt""" 65 | return list(self.data[tilt].keys()) 66 | 67 | @staticmethod 68 | def get_range(drange: Number_T, reso: Number_T) -> np.ndarray: 69 | rng = np.arange(reso, drange + reso, reso) 70 | valid_entry = int(drange // reso) 71 | return rng[:valid_entry] 72 | 73 | @staticmethod 74 | def get_range_safe(start: Number_T, stop: Number_T, step: Number_T) -> np.ndarray: 75 | r""" 76 | Level2中切片时使用了//, 所以只能使用上面的get_range, 否则dist长度不一致; 77 | Level3中没有切片功能, 所以使用这个函数来确保不出现浮点数溢出的长度BUG. 78 | """ 79 | int_start = int(start / step) + 1 80 | int_stop = int(stop / step) + 1 81 | return step * np.arange(int_start, int_stop) 82 | -------------------------------------------------------------------------------- /cinrad/io/export.py: -------------------------------------------------------------------------------- 1 | # coding = utf-8 2 | # Author: Puyuan Du 3 | 4 | from typing import Callable 5 | from functools import wraps 6 | 7 | import numpy as np 8 | 9 | from cinrad.io.level2 import StandardData 10 | 11 | try: 12 | import pyart 13 | 14 | PYART_INSTALLED = True 15 | except ImportError: 16 | PYART_INSTALLED = False 17 | 18 | 19 | def check_pyart_installed(func: Callable) -> Callable: 20 | @wraps(func) 21 | def deco(*args, **kwargs): 22 | if not PYART_INSTALLED: 23 | raise ImportError("pyart is not installed") 24 | return func(*args, **kwargs) 25 | 26 | return deco 27 | 28 | 29 | mapping = { 30 | "REF": "reflectivity", 31 | "VEL": "velocity", 32 | "SW": "spectrum_width", 33 | "PHI": "differential_phase", 34 | "ZDR": "differential_reflectivity", 35 | "RHO": "cross_correlation_ratio", 36 | "KDP": "specific_differential_phase", 37 | } 38 | 39 | 40 | @check_pyart_installed 41 | def standard_data_to_pyart(f: StandardData, radius: int = 460) -> pyart.core.Radar: 42 | filemetadata = pyart.config.FileMetadata("cinrad standard") 43 | time = filemetadata("time") 44 | time["calendar"] = "standard" 45 | time["units"] = "seconds since 1970-01-01 00:00" 46 | time["standard_name"] = "time" 47 | time["long_name"] = "time in seconds since volume start" 48 | time["data"] = np.array(f._time_radial) 49 | 50 | _range = filemetadata("range") 51 | reso = f.scan_config[0].dop_reso 52 | _range["data"] = f.get_range(radius * 1000, reso) 53 | _range["meters_to_center_of_first_gate"] = float(reso) 54 | _range["meters_between_gates"] = float(reso) 55 | 56 | metadata = filemetadata("metadata") 57 | metadata["original_container"] = "CINRAD" 58 | vcp_pattern = f.scan_type 59 | metadata["instrument_name"] = f.code 60 | 61 | scan_type = "ppi" 62 | 63 | latitude = filemetadata("latitude") 64 | longitude = filemetadata("longitude") 65 | altitude = filemetadata("altitude") 66 | 67 | latitude["data"] = np.array([f.stationlat], dtype="float64") 68 | longitude["data"] = np.array([f.stationlon], dtype="float64") 69 | altitude["data"] = np.array([f.radarheight], dtype="float64") 70 | 71 | sweep_number = filemetadata("sweep_number") 72 | sweep_mode = filemetadata("sweep_mode") 73 | sweep_start_ray_index = filemetadata("sweep_start_ray_index") 74 | sweep_end_ray_index = filemetadata("sweep_end_ray_index") 75 | 76 | nsweeps = len(f.el) 77 | sweep_number["data"] = np.arange(nsweeps, dtype="int32") 78 | sweep_mode["data"] = np.array(nsweeps * ["azimuth_surveillance"], dtype="S") 79 | 80 | sweep_end_ray_index["data"] = np.array(f._sweep_end_ray_index) 81 | sweep_start_ray_index["data"] = np.array(f._sweep_start_ray_index) 82 | 83 | azimuth = filemetadata("azimuth") 84 | elevation = filemetadata("elevation") 85 | fixed_angle = filemetadata("fixed_angle") 86 | azimuth["data"] = np.hstack([f.aux[i]["azimuth"] for i in f.aux.keys()]) 87 | elevation["data"] = np.hstack([f.aux[i]["elevation"] for i in f.aux.keys()]) 88 | fixed_angle["data"] = np.array(f.el) 89 | 90 | fields = {} 91 | nscans = f.get_nscans() 92 | 93 | all_var = list() 94 | for lvl in f.data: 95 | all_var.extend(f.data[lvl].keys()) 96 | all_var_set = set(all_var) 97 | 98 | for mom in mapping.keys(): 99 | if mom in all_var_set: 100 | name = mapping[mom] 101 | dic = filemetadata(name) 102 | dic["_FillValue"] = pyart.config.get_fillvalue() 103 | raw_arr = [f.get_raw(nel, radius, mom) for nel in range(nscans)] 104 | sel_arr = [i if not isinstance(i, tuple) else i[0] for i in raw_arr] 105 | moment_data = np.ma.vstack(sel_arr) 106 | dic["data"] = moment_data 107 | fields[name] = dic 108 | 109 | nyquist_velocity = filemetadata("nyquist_velocity") 110 | nyquist_velocity["data"] = np.array( 111 | [i.nyquist_spd for i in f.scan_config], "float32" 112 | ) 113 | unambiguous_range = filemetadata("unambiguous_range") 114 | unambiguous_range["data"] = np.array( 115 | [i.max_range1 for i in f.scan_config], "float32" 116 | ) 117 | 118 | instrument_parameters = { 119 | "unambiguous_range": unambiguous_range, 120 | "nyquist_velocity": nyquist_velocity, 121 | } 122 | 123 | radar = pyart.core.Radar( 124 | time, 125 | _range, 126 | fields, 127 | metadata, 128 | scan_type, 129 | latitude, 130 | longitude, 131 | altitude, 132 | sweep_number, 133 | sweep_mode, 134 | fixed_angle, 135 | sweep_start_ray_index, 136 | sweep_end_ray_index, 137 | azimuth, 138 | elevation, 139 | instrument_parameters=instrument_parameters, 140 | ) 141 | return radar 142 | -------------------------------------------------------------------------------- /cinrad/projection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from cinrad._typing import Boardcast_T, Number_T 8 | 9 | RM = 8500 10 | 11 | 12 | def height( 13 | distance: Boardcast_T, elevation: Union[int, float], radarheight: Number_T 14 | ) -> np.ndarray: 15 | r""" 16 | Calculate height of radar beam considering atmospheric refraction. 17 | 18 | Parameters 19 | ---------- 20 | distance: int or float or numpy.ndarray 21 | distance in kilometer 22 | elevation: int or float 23 | elevation angle in degree 24 | radarheight: int or float 25 | height of radar in meter 26 | 27 | Returns 28 | ------- 29 | height 30 | """ 31 | 32 | return ( 33 | distance * np.sin(np.deg2rad(elevation)) 34 | + distance**2 / (2 * RM) 35 | + radarheight / 1000 36 | ) 37 | 38 | 39 | def get_coordinate( 40 | distance: Boardcast_T, 41 | azimuth: Boardcast_T, 42 | elevation: Number_T, 43 | centerlon: Number_T, 44 | centerlat: Number_T, 45 | h_offset: bool = True, 46 | ) -> tuple: 47 | r""" 48 | Convert polar coordinates to geographic coordinates with the given radar station position. 49 | 50 | Parameters 51 | ---------- 52 | distance: int or float or numpy.ndarray 53 | distance in kilometer in terms of polar coordinate 54 | azimuth: int or float or numpy.ndarray 55 | azimuth in radian in terms of polar coordinate 56 | elevation: int or float 57 | elevation angle in degree 58 | centerlon: int or float 59 | longitude of center point 60 | centerlat: int or float 61 | latitude of center point 62 | 63 | Returns 64 | ------- 65 | actuallon: float or numpy.ndarray 66 | longitude value 67 | actuallat: float or numpy.ndarray 68 | latitude value 69 | """ 70 | elev = elevation if h_offset else 0 71 | if isinstance(azimuth, np.ndarray): 72 | deltav = np.cos(azimuth[:, np.newaxis]) * distance * np.cos(np.deg2rad(elev)) 73 | deltah = np.sin(azimuth[:, np.newaxis]) * distance * np.cos(np.deg2rad(elev)) 74 | else: 75 | deltav = np.cos(azimuth) * distance * np.cos(np.deg2rad(elev)) 76 | deltah = np.sin(azimuth) * distance * np.cos(np.deg2rad(elev)) 77 | deltalat = deltav / 111 78 | actuallat = deltalat + centerlat 79 | deltalon = deltah / (111 * np.cos(np.deg2rad(actuallat))) 80 | actuallon = deltalon + centerlon 81 | return actuallon, actuallat 82 | -------------------------------------------------------------------------------- /cinrad/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import os 5 | import sys 6 | from typing import Union, Any 7 | 8 | import numpy as np 9 | 10 | from cinrad.projection import height 11 | from cinrad._typing import Array_T, Number_T 12 | 13 | MODULE_DIR = os.path.dirname(__file__) 14 | 15 | VIL_CONST = 3.44e-6 16 | 17 | 18 | def r2z(r: np.ndarray) -> np.ndarray: 19 | return 10 ** (r / 10) 20 | 21 | 22 | def vert_integrated_liquid_py( 23 | ref: np.ndarray, 24 | distance: np.ndarray, 25 | elev: Array_T, 26 | beam_width: float = 0.99, 27 | threshold: Union[float, int] = 18.0, 28 | density: bool = False, 29 | ) -> np.ndarray: 30 | r""" 31 | Calculate vertically integrated liquid (VIL) in one full scan 32 | 33 | Parameters 34 | ---------- 35 | ref: numpy.ndarray dim=3 (elevation angle, distance, azimuth) 36 | reflectivity data 37 | distance: numpy.ndarray dim=2 (distance, azimuth) 38 | distance from radar site 39 | elev: numpy.ndarray or list dim=1 40 | elevation angles in degree 41 | threshold: float 42 | minimum reflectivity value to take into calculation 43 | 44 | Returns 45 | ------- 46 | data: numpy.ndarray 47 | vertically integrated liquid data 48 | """ 49 | if density: 50 | raise NotImplementedError("VIL density calculation is not implemented") 51 | v_beam_width = np.deg2rad(beam_width) 52 | elev = np.deg2rad(elev) 53 | xshape, yshape = ref[0].shape 54 | distance *= 1000 55 | hi_arr = distance * np.sin(v_beam_width / 2) 56 | vil = _vil_iter(xshape, yshape, ref, distance, elev, hi_arr, threshold) 57 | return vil 58 | 59 | 60 | def _vil_iter( 61 | xshape: int, 62 | yshape: int, 63 | ref: np.ndarray, 64 | distance: np.ndarray, 65 | elev: Array_T, 66 | hi_arr: np.ndarray, 67 | threshold: Number_T, 68 | ) -> np.ndarray: 69 | # r = np.clip(ref, None, 55) #reduce the influence of hails 70 | r = ref 71 | z = r2z(r) 72 | VIL = np.zeros((xshape, yshape)) 73 | for i in range(xshape): 74 | for j in range(yshape): 75 | vert_r = r[:, i, j] 76 | vert_z = z[:, i, j] 77 | dist = distance[i][j] 78 | position = np.where(vert_r > threshold)[0] 79 | if position.shape[0] == 0: 80 | continue 81 | pos_s = position[0] 82 | pos_e = position[-1] 83 | m1 = 0 84 | hi = hi_arr[i][j] 85 | for l in range(pos_e): 86 | ht = dist * (np.sin(elev[l + 1]) - np.sin(elev[l])) 87 | factor = ((vert_z[l] + vert_z[l + 1]) / 2) ** (4 / 7) 88 | m1 += VIL_CONST * factor * ht 89 | mb = VIL_CONST * vert_z[pos_s] ** (4 / 7) * hi 90 | mt = VIL_CONST * vert_z[pos_e] ** (4 / 7) * hi 91 | VIL[i][j] = m1 + mb + mt 92 | return VIL 93 | 94 | 95 | def echo_top_py( 96 | ref: np.ndarray, 97 | distance: np.ndarray, 98 | elev: Array_T, 99 | radarheight: Number_T, 100 | threshold: Number_T = 18.0, 101 | ) -> np.ndarray: 102 | r""" 103 | Calculate height of echo tops (ET) in one full scan 104 | 105 | Parameters 106 | ---------- 107 | ref: numpy.ndarray dim=3 (elevation angle, distance, azimuth) 108 | reflectivity data 109 | distance: numpy.ndarray dim=2 (distance, azimuth) 110 | distance from radar site 111 | elev: numpy.ndarray or list dim=1 112 | elevation angles in degree 113 | radarheight: int or float 114 | height of radar 115 | drange: float or int 116 | range of data to be calculated 117 | threshold: float 118 | minimum value of reflectivity to be taken into calculation 119 | 120 | Returns 121 | ------- 122 | data: numpy.ndarray 123 | echo tops data 124 | """ 125 | xshape, yshape = ref[0].shape 126 | et = np.zeros((xshape, yshape)) 127 | h_ = list() 128 | for i in elev: 129 | h = height(distance, i, radarheight) 130 | h_.append(h) 131 | hght = np.concatenate(h_).reshape(ref.shape) 132 | for i in range(xshape): 133 | for j in range(yshape): 134 | vert_h = hght[:, i, j] 135 | vert_r = ref[:, i, j] 136 | if vert_r.max() < threshold: # Vertical points don't satisfy threshold 137 | et[i][j] = 0 138 | continue 139 | elif vert_r[-1] >= threshold: # Point in highest scan exceeds threshold 140 | et[i][j] = vert_h[-1] 141 | continue 142 | else: 143 | position = np.where(vert_r >= threshold)[0] 144 | if position[-1] == 0: 145 | et[i][j] = vert_h[0] 146 | continue 147 | else: 148 | pos = position[-1] 149 | z1 = vert_r[pos] 150 | z2 = vert_r[pos + 1] 151 | h1 = vert_h[pos] 152 | h2 = vert_h[pos + 1] 153 | w1 = (z1 - threshold) / (z1 - z2) 154 | w2 = 1 - w1 155 | et[i][j] = w1 * h2 + w2 * h1 156 | return et 157 | 158 | 159 | try: 160 | from cinrad._utils import * 161 | except ImportError: 162 | # When the C-extension doesn't exist, define the functions in Python. 163 | echo_top = echo_top_py 164 | vert_integrated_liquid = vert_integrated_liquid_py 165 | -------------------------------------------------------------------------------- /cinrad/visualize/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import matplotlib 5 | 6 | if "Arial" in matplotlib.rc_params()["font.sans-serif"]: 7 | matplotlib.rc("font", family="Arial") 8 | from cinrad.visualize.ppi import * 9 | from cinrad.visualize.rhi import * 10 | -------------------------------------------------------------------------------- /cinrad/visualize/gpf.py: -------------------------------------------------------------------------------- 1 | # https://github.com/crazyapril/mpkit/blob/master/gpf.py 2 | 3 | import os 4 | import sys 5 | import ast 6 | 7 | import matplotlib.colors as mclr 8 | import numpy as np 9 | 10 | from cinrad.utils import MODULE_DIR 11 | 12 | _cmapdir_ = os.path.join(MODULE_DIR, "data", "colormap") 13 | 14 | LAST_COLOR = 0 15 | CONTROL_COLOR = 1 16 | TRANSIT_COLOR = 2 17 | 18 | 19 | def cmap(inp): 20 | """return cmap dict including plotplus control information ( levels and unit )""" 21 | c = Colormap(inp) 22 | return c.process() 23 | 24 | 25 | def _cmap(inp): 26 | """return cmap dict without plotplus control information""" 27 | c = Colormap(inp) 28 | d = c.process() 29 | d.pop("levels", None) 30 | d.pop("unit", None) 31 | return d 32 | 33 | 34 | def pure_cmap(inp): 35 | """return colormap instance""" 36 | return cmap(inp)["cmap"] 37 | 38 | 39 | class ColormapDefinitionError(Exception): 40 | def __init__(self, description): 41 | self.dsc = description 42 | 43 | def __str__(self): 44 | return repr(self.dsc) 45 | 46 | 47 | class Colormap: 48 | def __init__(self, inp): 49 | self.control = dict(type="auto", level="auto") 50 | if isinstance(inp, str): 51 | if inp.endswith(".gpf"): 52 | self.filepath = os.path.join(_cmapdir_, inp) 53 | self.process = self.gpf 54 | else: 55 | self.control.update(name=inp) 56 | self.process = self.cmap_linear 57 | elif isinstance(inp, dict): 58 | if "name" not in inp: 59 | raise ColormapDefinitionError('[Error 0] Need keyword "name" in dict.') 60 | self.control.update(inp) 61 | t = self.control["type"].lower() 62 | if t == "auto": 63 | self.process = self.cmap_linear 64 | elif t == "linear": 65 | self.process = self.cmap_linear 66 | elif t == "listed": 67 | self.process = self.cmap_listed 68 | else: 69 | raise ColormapDefinitionError("[Error 1] Unknown colormap type.") 70 | 71 | def cmap_listed(self): 72 | last_tval = -1e7 73 | tval_list = list() 74 | color_list = list() 75 | filepath = os.path.join(_cmapdir_, self.control["name"] + ".cmap") 76 | if not os.path.exists(filepath): 77 | filepath = os.path.join( 78 | os.path.dirname(os.path.abspath(sys.argv[0])), 79 | self.control["name"] + ".cmap", 80 | ) 81 | with open(filepath, "r") as f: 82 | transit_count = 0 83 | for line in f: 84 | if line[0] == "*": 85 | lsplit = line[1:].split(":") 86 | tname, tval = lsplit[0].lower(), lsplit[1][:-1] 87 | if tname == "type": 88 | if tval.lower() == "linear": 89 | return self.cmap_linear() 90 | elif tval.lower() != "listed": 91 | raise ColormapDefinitionError( 92 | "[Error 1] Unknown colormap type." 93 | ) 94 | if tname not in self.control or self.control[tname] == "auto": 95 | self.control[tname] = tval 96 | else: 97 | tval, color = self.single_line_listed(line) 98 | if tval < last_tval: 99 | raise ColormapDefinitionError( 100 | "[Error 5] tval should be arranged in order. Line:" + line 101 | ) 102 | tval_list.append(tval) 103 | if color == TRANSIT_COLOR: 104 | color_list.append(0) 105 | transit_count += 1 106 | elif color != CONTROL_COLOR: 107 | if transit_count > 0: 108 | bcolor = color_list[-transit_count - 1] 109 | for i in range(-transit_count, 0): 110 | ratio = (transit_count + i + 1) / (transit_count + 1) 111 | color_list[i] = tuple( 112 | (j - i) * ratio + i for i, j in zip(bcolor, color) 113 | ) 114 | transit_count = 0 115 | color_list.append(color) 116 | last_tval = tval 117 | cmap = mclr.ListedColormap(color_list) 118 | unit = self.control.get("unit", None) 119 | over = self.control.get("over", None) 120 | extend = "neither" 121 | if over: 122 | cmap.set_over(self.get_color(over, "OVER")) 123 | extend = "max" 124 | under = self.control.get("under", None) 125 | if under: 126 | cmap.set_under(self.get_color(under, "UNDER")) 127 | extend = "min" 128 | if over and under: 129 | extend = "both" 130 | norm = mclr.BoundaryNorm(tval_list, cmap.N) 131 | return dict(cmap=cmap, levels=tval_list, norm=norm, unit=unit, extend=extend) 132 | 133 | def cmap_linear(self): 134 | last_color = None 135 | last_tval = -1e7 136 | datacache = list() 137 | filepath = os.path.join(_cmapdir_, self.control["name"] + ".cmap") 138 | if not os.path.exists(filepath): 139 | filepath = os.path.join( 140 | os.path.dirname(os.path.abspath(sys.argv[0])), 141 | self.control["name"] + ".cmap", 142 | ) 143 | with open(filepath, "r") as f: 144 | for line in f: 145 | if line[0] == "*": 146 | lsplit = line[1:].split(":") 147 | tname, tval = lsplit[0].lower(), lsplit[1][:-1] 148 | if tname == "type": 149 | if tval.lower() == "listed": 150 | return self.cmap_listed() 151 | elif tval.lower() != "linear": 152 | raise ColormapDefinitionError( 153 | "[Error 1] Unknown colormap type." 154 | ) 155 | if tname not in self.control or self.control[tname] == "auto": 156 | self.control[tname] = tval 157 | else: 158 | tval, color1, color2 = self.single_line_linear(line) 159 | if tval < last_tval: 160 | raise ColormapDefinitionError( 161 | "[Error 5] tval should be arranged in order. Line:" + line 162 | ) 163 | if color1 == LAST_COLOR: 164 | color1 = last_color 165 | if color2 == LAST_COLOR: 166 | color2 = color1 167 | datacache.append((tval, color1, color2)) 168 | last_tval = tval 169 | last_color = color2 170 | tmin = datacache[0][0] 171 | tmax = datacache[-1][0] 172 | span = tmax - tmin 173 | colormap = {"red": [], "green": [], "blue": []} 174 | for tval, color1, color2 in datacache: 175 | trel = (tval - tmin) / span 176 | colormap["red"].append((trel, color1[0], color2[0])) 177 | colormap["green"].append((trel, color1[1], color2[1])) 178 | colormap["blue"].append((trel, color1[2], color2[2])) 179 | cmap = mclr.LinearSegmentedColormap("cmap", colormap) 180 | lvctrl = self.control["level"].lower() 181 | levels = self.get_levels(lvctrl, datacache) 182 | unit = self.control.get("unit", None) 183 | over = self.control.get("over", None) 184 | extend = "neither" 185 | if over: 186 | cmap.set_over(self.get_color(over, "OVER")) 187 | extend = "max" 188 | under = self.control.get("under", None) 189 | if under: 190 | cmap.set_under(self.get_color(under, "UNDER")) 191 | extend = "min" 192 | if over and under: 193 | extend = "both" 194 | return dict(cmap=cmap, levels=levels, unit=unit, extend=extend) 195 | 196 | def single_line_linear(self, line): 197 | if line[-1] == "\n": 198 | line = line[:-1] 199 | lsplit = line.split(" ") 200 | if len(lsplit) != 3: 201 | raise ColormapDefinitionError( 202 | "[Error 2] Only 2 spaces are allowed in a line. Line:" + line 203 | ) 204 | try: 205 | tval = float(lsplit[0]) 206 | except (SyntaxError, ValueError, NameError): 207 | raise ColormapDefinitionError("[Error 3] Invalid value. Line:" + line) 208 | if lsplit[1] == "BEGIN": 209 | return tval, (0.0, 0.0, 0.0), self.get_color(lsplit[2], line) 210 | elif lsplit[2] == "END": 211 | return tval, self.get_color(lsplit[1], line), (0.0, 0.0, 0.0) 212 | else: 213 | return ( 214 | tval, 215 | self.get_color(lsplit[1], line), 216 | self.get_color(lsplit[2], line), 217 | ) 218 | 219 | def single_line_listed(self, line): 220 | if line[-1] == "\n": 221 | line = line[:-1] 222 | lsplit = line.split(" ") 223 | if len(lsplit) != 2: 224 | raise ColormapDefinitionError( 225 | "[Error 2] Only 1 space are allowed in a line. Line:" + line 226 | ) 227 | try: 228 | tval = float(lsplit[0]) 229 | except (SyntaxError, ValueError, NameError): 230 | raise ColormapDefinitionError("[Error 3] Invalid value. Line:" + line) 231 | if lsplit[1] == "END": 232 | return tval, CONTROL_COLOR 233 | elif lsplit[1] == "~": 234 | return tval, TRANSIT_COLOR 235 | else: 236 | return tval, self.get_color(lsplit[1], line) 237 | 238 | def get_levels(self, lvctrl, datacache): 239 | tmin = datacache[0][0] 240 | tmax = datacache[-1][0] 241 | if lvctrl == "file": 242 | levels = list() 243 | for r in datacache: 244 | levels.append(r[0]) 245 | elif lvctrl.startswith("s"): 246 | lvctrl = int(lvctrl[1:]) 247 | levels = list() 248 | for i in range(len(datacache) - 1): 249 | levels.extend(np.linspace(datacache[i][0], datacache[i + 1][0], lvctrl)) 250 | elif lvctrl.startswith("["): 251 | levels = ast.literal_eval(lvctrl) 252 | else: 253 | if lvctrl == "auto": 254 | lvctrl = 1 255 | elif lvctrl.startswith("c"): 256 | lvctrl = lvctrl[1:] 257 | levels = np.arange(tmin, tmax + float(lvctrl), float(lvctrl)) 258 | return levels 259 | 260 | def get_color(self, l, line): 261 | if l == "~": 262 | return LAST_COLOR 263 | b = l.split("/") 264 | if len(b) != 3: 265 | raise ColormapDefinitionError( 266 | "[Error 4] Invalid color format. Line:" + line 267 | ) 268 | for i in range(len(b)): 269 | v = b[i] 270 | try: 271 | v = float(v) 272 | except (SyntaxError, ValueError, NameError): 273 | raise ColormapDefinitionError("[Error 3] Invalid value. Line:" + line) 274 | v /= 255.0 275 | if v < 0 or v > 1: 276 | raise ColormapDefinitionError( 277 | "[Error 6] Value out of range (0~255). Line:" + line 278 | ) 279 | b[i] = v 280 | return tuple(b) 281 | 282 | def gpf(self): 283 | cmap = {"red": [], "green": [], "blue": []} 284 | with open(self.filepath, "r") as f: 285 | lastred = (0.0, 0.0, 0.0) 286 | lastgreen = lastred 287 | lastblue = lastred 288 | line = f.readline() 289 | while line: 290 | if line[0] != "#": 291 | data = [ast.literal_eval(numbyte) for numbyte in line.split()] 292 | red = (data[0], lastred[2], data[1]) 293 | green = (data[0], lastgreen[2], data[2]) 294 | blue = (data[0], lastblue[2], data[3]) 295 | cmap["red"].append(red) 296 | cmap["green"].append(green) 297 | cmap["blue"].append(blue) 298 | lastred = red 299 | lastgreen = green 300 | lastblue = blue 301 | line = f.readline() 302 | return dict(cmap=mclr.LinearSegmentedColormap("gpf", cmap)) 303 | 304 | 305 | if __name__ == "__main__": 306 | from matplotlib.colorbar import ColorbarBase 307 | import matplotlib.pyplot as plt 308 | 309 | while True: 310 | cmapname = input("Colormap name>") 311 | if not cmapname: 312 | break 313 | plt.figure(1, figsize=(11, 2)) 314 | ax = plt.gca() 315 | cmapdict = _cmap(cmapname) 316 | cmapdict.update(orientation="horizontal") 317 | ColorbarBase(ax, **cmapdict) 318 | plt.show() 319 | # plt.clf() 320 | -------------------------------------------------------------------------------- /cinrad/visualize/layout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | FIG_SIZE = (10, 8) 5 | FIG_SIZE_TRANSPARENT = (10, 10) 6 | CBAR_POS = [0.83, 0.06, 0.04, 0.38] 7 | TEXT_AXES_POS = [0.83, 0.06, 0.01, 0.35] 8 | GEOAXES_POS = [0, 0, 0.8, 1] 9 | TEXT_SPACING = 0.06 10 | INIT_TEXT_POS = 2.63 11 | -------------------------------------------------------------------------------- /cinrad/visualize/ppi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import os 5 | import warnings 6 | import json 7 | from typing import Union, Optional, Any, List 8 | from datetime import datetime 9 | 10 | import matplotlib.pyplot as plt 11 | from matplotlib.colorbar import ColorbarBase 12 | import numpy as np 13 | import cartopy.crs as ccrs 14 | from cartopy.mpl.geoaxes import GeoAxes 15 | from xarray import Dataset 16 | 17 | from cinrad.visualize.utils import * 18 | from cinrad.utils import MODULE_DIR 19 | from cinrad.projection import get_coordinate 20 | from cinrad.io.level3 import StormTrackInfo 21 | from cinrad._typing import Number_T 22 | from cinrad.common import get_dtype, is_radial 23 | from cinrad.visualize.layout import * 24 | from cartopy.io.shapereader import Reader 25 | 26 | 27 | __all__ = ["PPI"] 28 | 29 | 30 | def update_dict(d1: dict, d2: dict): 31 | r""" 32 | Update the content of the first dict with entries in the second, 33 | and return the copy. 34 | """ 35 | d = d1.copy() 36 | for k, v in d2.items(): 37 | d[k] = v 38 | return d 39 | 40 | 41 | class PPI(object): 42 | r""" 43 | Create a figure plotting plan position indicator 44 | 45 | By default, norm, cmap, and colorbar labels will be determined by the 46 | data type. 47 | 48 | Args: 49 | data (xarray.Dataset): The data to be plotted. 50 | 51 | fig (matplotlib.figure.Figure): The figure to plot on. Optional. 52 | 53 | norm (matplotlib.colors.Normalize): Customized normalize object. Optional. 54 | 55 | cmap (matplotlib.colors.Colormap): Customized colormap. Optional. 56 | 57 | nlabel (int): Number of labels on the colorbar, will only be used when label is 58 | also passed. Optional. 59 | 60 | label (list[str]): Colorbar labels. Optional. 61 | 62 | dpi (int): DPI of the figure. Default 350. 63 | 64 | highlight (str, list[str]): Areas to be highlighted. Optional. 65 | 66 | coastline (bool): Plot coastline on the figure if set to True. Default False. 67 | 68 | extent (list[float]): The extent of figure. Optional. 69 | 70 | add_city_names (bool): Label city names on the figure if set to True. Default False. 71 | 72 | plot_labels (bool): Text scan information on the side of the plot. Default True. 73 | 74 | text_param (dict): Optional parameters passed to matplotlib text function. 75 | """ 76 | 77 | # The CRS of data is believed to be PlateCarree. 78 | # i.e., the coordinates are longitude and latitude. 79 | data_crs = ccrs.PlateCarree() 80 | 81 | def __init__( 82 | self, 83 | data: Dataset, 84 | fig: Optional[Any] = None, 85 | norm: Optional[Any] = None, 86 | cmap: Optional[Any] = None, 87 | nlabel: int = 10, 88 | label: Optional[List[str]] = None, 89 | dpi: Number_T = 350, 90 | highlight: Optional[Union[str, List[str]]] = None, 91 | coastline: bool = False, 92 | extent: Optional[List[Number_T]] = None, 93 | section: Optional[Dataset] = None, 94 | style: str = "black", 95 | add_city_names: bool = False, 96 | plot_labels: bool = True, 97 | text_param: Optional[dict] = None, 98 | add_shps: bool = True, 99 | **kwargs 100 | ): 101 | self.data = data 102 | self.dtype = get_dtype(data) 103 | self.settings = { 104 | "cmap": cmap, 105 | "norm": norm, 106 | "nlabel": nlabel, 107 | "label": label, 108 | "highlight": highlight, 109 | "coastline": coastline, 110 | "path_customize": False, 111 | "extent": extent, 112 | "slice": section, 113 | "style": style, 114 | "add_city_names": add_city_names, 115 | "plot_labels": plot_labels, 116 | "is_inline": is_inline(), 117 | "add_shps": add_shps, 118 | } 119 | if fig is None: 120 | if style == "transparent": 121 | self.fig = plt.figure(figsize=FIG_SIZE_TRANSPARENT, dpi=dpi) 122 | else: 123 | self.fig = plt.figure(figsize=FIG_SIZE, dpi=dpi) 124 | self.fig.patch.set_facecolor(style) 125 | plt.axis("off") 126 | else: 127 | self.fig = fig 128 | # avoid in-place modification 129 | self.text_pos = TEXT_AXES_POS.copy() 130 | self.cbar_pos = CBAR_POS.copy() 131 | self.font_kw = default_font_kw.copy() 132 | if style == "black": 133 | self.font_kw["color"] = "white" 134 | else: 135 | self.font_kw["color"] = "black" 136 | if text_param: 137 | # Override use input setting 138 | self.font_kw = update_dict(self.font_kw, text_param) 139 | self._plot_ctx = dict() 140 | self.rf_flag = "RF" in data 141 | self._fig_init = False 142 | self._plot(**kwargs) 143 | if is_inline(): 144 | # In inline mode, figure will not be dynamically changed 145 | # call this action at initialization 146 | self._text_before_save() 147 | 148 | def __call__(self, fpath): 149 | ext_name = fpath.split(".") 150 | if len(ext_name) > 1: 151 | all_fmt = self.fig.canvas.get_supported_filetypes() 152 | if ext_name[-1] in all_fmt: 153 | self.settings["path_customize"] = True 154 | else: 155 | if not fpath.endswith(os.path.sep): 156 | fpath += os.path.sep 157 | return self._save(fpath) 158 | 159 | def _norm(self): 160 | if self.settings["norm"]: 161 | n = self.settings["norm"] 162 | if self.settings["label"]: 163 | clabel = self.settings["label"] 164 | else: 165 | nlabel = self.settings["nlabel"] 166 | clabel = np.linspace(n.vmin, n.vmax, nlabel).astype(str) 167 | return n, n, clabel 168 | else: 169 | n = norm_plot[self.dtype] 170 | n2 = norm_cbar[self.dtype] 171 | return n, n2, cbar_text[self.dtype] 172 | 173 | def _cmap(self): 174 | if self.settings["cmap"]: 175 | c = self.settings["cmap"] 176 | return c, c 177 | else: 178 | c = cmap_plot[self.dtype] 179 | c2 = cmap_cbar[self.dtype] 180 | return c, c2 181 | 182 | def _plot(self, **kwargs): 183 | lon = self.data["longitude"].values 184 | lat = self.data["latitude"].values 185 | var = self.data[self.dtype].values 186 | extent = self.settings["extent"] 187 | if not extent: 188 | extent = [lon.min(), lon.max(), lat.min(), lat.max()] 189 | self.settings["extent"] = extent 190 | # When plot single radar, azimuthal equidistant projection is used. 191 | # The data which has code like 'Z9XXX' is considered as single radar. 192 | code = self.data.site_code 193 | if is_radial(self.data) or (code.startswith("Z") and code[1:].isnumeric()): 194 | proj = ccrs.AzimuthalEquidistant( 195 | central_longitude=self.data.site_longitude, 196 | central_latitude=self.data.site_latitude, 197 | ) 198 | else: 199 | proj = ccrs.PlateCarree() 200 | self.geoax: GeoAxes = create_geoaxes( 201 | self.fig, proj, extent=extent, style=self.settings["style"] 202 | ) 203 | if self.settings["style"] == "black": 204 | self.geoax.patch.set_facecolor("black") 205 | self._plot_ctx["var"] = var 206 | pnorm, cnorm, clabel = self._norm() 207 | pcmap, ccmap = self._cmap() 208 | self.geoax.pcolormesh( 209 | lon, 210 | lat, 211 | var, 212 | norm=pnorm, 213 | cmap=pcmap, 214 | transform=self.data_crs, 215 | shading="auto", 216 | **kwargs 217 | ) 218 | if self.rf_flag: 219 | rf = self.data["RF"].values 220 | self.geoax.pcolormesh( 221 | lon, 222 | lat, 223 | rf, 224 | norm=norm_plot["RF"], 225 | cmap=cmap_plot["RF"], 226 | transform=self.data_crs, 227 | shading="auto", 228 | **kwargs 229 | ) 230 | if not self.settings["extent"]: 231 | self._autoscale() 232 | if self.settings["add_shps"]: 233 | add_shp( 234 | self.geoax, 235 | proj, 236 | coastline=self.settings["coastline"], 237 | style=self.settings["style"], 238 | extent=self.geoax.get_extent(self.data_crs), 239 | ) 240 | if self.settings["highlight"]: 241 | draw_highlight_area(self.settings["highlight"]) 242 | if self.settings["add_city_names"]: 243 | self._add_city_names() 244 | 245 | if self.settings["slice"]: 246 | self.plot_cross_section(self.settings["slice"]) 247 | self._fig_init = True 248 | 249 | def _text_product_info( 250 | self, 251 | ax: Any, 252 | drange: Number_T, 253 | reso: float, 254 | scantime: str, 255 | name: str, 256 | task: str, 257 | elev: float, 258 | ): 259 | ax.text( 260 | 0, 261 | INIT_TEXT_POS - TEXT_SPACING, 262 | "Range: {:.0f}km".format(drange), 263 | **self.font_kw 264 | ) 265 | if reso < 0.1: 266 | # Change the unit from km to m for better formatting 267 | ax.text( 268 | 0, 269 | INIT_TEXT_POS - TEXT_SPACING * 2, 270 | "Resolution: {:.0f}m".format(reso * 1000), 271 | **self.font_kw 272 | ) 273 | else: 274 | ax.text( 275 | 0, 276 | INIT_TEXT_POS - TEXT_SPACING * 2, 277 | "Resolution: {:.2f}km".format(reso), 278 | **self.font_kw 279 | ) 280 | ax.text( 281 | 0, 282 | INIT_TEXT_POS - TEXT_SPACING * 3, 283 | "Date: {}".format( 284 | datetime.strptime(scantime, "%Y-%m-%d %H:%M:%S").strftime("%Y.%m.%d") 285 | ), 286 | **self.font_kw 287 | ) 288 | ax.text( 289 | 0, 290 | INIT_TEXT_POS - TEXT_SPACING * 4, 291 | "Time: {}".format( 292 | datetime.strptime(scantime, "%Y-%m-%d %H:%M:%S").strftime("%H:%M") 293 | ), 294 | **self.font_kw 295 | ) 296 | if name is None: 297 | name = "Unknown" 298 | ax.text(0, INIT_TEXT_POS - TEXT_SPACING * 5, "RDA: " + name, **self.font_kw) 299 | ax.text( 300 | 0, INIT_TEXT_POS - TEXT_SPACING * 6, "Task: {}".format(task), **self.font_kw 301 | ) 302 | ax.text( 303 | 0, 304 | INIT_TEXT_POS - TEXT_SPACING * 7, 305 | "Elev: {:.2f}deg".format(elev), 306 | **self.font_kw 307 | ) 308 | 309 | def _text(self): 310 | def _draw(ax: Any, y_index: int, text: str): 311 | """ 312 | Draw text on the axes. 313 | """ 314 | y = INIT_TEXT_POS - TEXT_SPACING * y_index 315 | ax.text(0, y, text, **self.font_kw) 316 | 317 | # axes used for text which has the same x-position as 318 | # the colorbar axes (for matplotlib 3 compatibility) 319 | var = self._plot_ctx["var"] 320 | ax2 = self.fig.add_axes(self.text_pos) 321 | for sp in ax2.spines.values(): 322 | sp.set_visible(False) 323 | ax2.yaxis.set_visible(False) 324 | ax2.xaxis.set_visible(False) 325 | # Make VCP21 the default scanning strategy 326 | task = self.data.attrs.get("task", "VCP21") 327 | if self.data.tangential_reso >= 0.1: 328 | reso = "{:.2f}km".format(self.data.tangential_reso) 329 | else: 330 | reso = "{:.0f}m".format(self.data.tangential_reso * 1000) 331 | s_time = datetime.strptime(self.data.scan_time, "%Y-%m-%d %H:%M:%S") 332 | texts = [ 333 | prodname[self.dtype], 334 | "Range: {:.0f}km".format(self.data.range), 335 | "Resolution: {}".format(reso), 336 | "Date: {}".format(s_time.strftime("%Y.%m.%d")), 337 | "Time: {}".format(s_time.strftime("%H:%M")), 338 | "RDA: " + (self.data.site_name or "Unknown"), 339 | "Task: {}".format(task), 340 | "Elev: {:.2f}deg".format(self.data.elevation), 341 | "Max: {:.1f}{}".format(np.nanmax(var), unit[self.dtype]), 342 | ] 343 | if self.dtype.startswith("VEL"): 344 | min_vel = "Min: {:.1f}{}".format(np.nanmin(var), unit[self.dtype]) 345 | texts.append(min_vel) 346 | for i, text in enumerate(texts): 347 | _draw(ax2, i, text) 348 | 349 | def _text_before_save(self): 350 | # Finalize texting here 351 | if self.settings["style"] == "transparent": 352 | return 353 | pnorm, cnorm, clabel = self._norm() 354 | pcmap, ccmap = self._cmap() 355 | if self.settings["plot_labels"]: 356 | self._text() 357 | cax = self.fig.add_axes(self.cbar_pos) 358 | cbar = ColorbarBase( 359 | cax, cmap=ccmap, norm=cnorm, orientation="vertical", drawedges=False 360 | ) 361 | cbar.ax.tick_params( 362 | axis="both", 363 | which="both", 364 | length=0, 365 | labelsize=10, 366 | colors=self.font_kw["color"], 367 | ) 368 | cbar.outline.set_visible(False) 369 | if not isinstance(clabel, type(None)): 370 | cbar.set_ticks(np.linspace(cnorm.vmin, cnorm.vmax, len(clabel))) 371 | cbar.set_ticklabels(clabel, **self.font_kw) 372 | 373 | def _save(self, fpath: str): 374 | if not self.settings["is_inline"]: 375 | self._text_before_save() 376 | if not self.settings["path_customize"]: 377 | if not fpath.endswith(os.path.sep): 378 | fpath += os.path.sep 379 | if self.settings["slice"]: 380 | data = self.settings["slice"] 381 | sec = "_{:.2f}_{:.2f}_{:.2f}_{:.2f}".format( 382 | data.start_lat, data.start_lon, data.end_lat, data.end_lon 383 | ) 384 | else: 385 | sec = "" 386 | path_string = "{}{}_{}_{:.1f}_{}_{}{}.png".format( 387 | fpath, 388 | self.data.site_code, 389 | datetime.strptime(self.data.scan_time, "%Y-%m-%d %H:%M:%S").strftime( 390 | "%Y%m%d%H%M%S" 391 | ), 392 | self.data.elevation, 393 | self.data.range, 394 | self.dtype.upper(), 395 | sec, 396 | ) 397 | else: 398 | path_string = fpath 399 | save_options = dict(pad_inches=0) 400 | if self.settings["style"] == "transparent": 401 | save_options["transparent"] = True 402 | else: 403 | if self.settings["style"] == "white": 404 | save_options["facecolor"] = "white" 405 | elif self.settings["style"] == "black": 406 | save_options["facecolor"] = "black" 407 | plt.savefig(path_string, **save_options) 408 | # plt.close("all") 409 | return path_string 410 | 411 | def plot_range_rings( 412 | self, 413 | _range: Union[int, float, list], 414 | color: str = "white", 415 | linewidth: Number_T = 0.5, 416 | **kwargs 417 | ): 418 | r"""Plot range rings on PPI plot.""" 419 | slon, slat = self.data.site_longitude, self.data.site_latitude 420 | if isinstance(_range, (int, float)): 421 | _range = [_range] 422 | theta = np.linspace(0, 2 * np.pi, 800) 423 | for d in _range: 424 | x, y = get_coordinate(d, theta, 0, slon, slat, h_offset=False) 425 | # self.ax.plot(x, y, color=color, linewidth=linewidth, **kwargs) 426 | self.geoax.plot( 427 | x, 428 | y, 429 | color=color, 430 | linewidth=linewidth, 431 | transform=self.data_crs, 432 | **kwargs 433 | ) 434 | 435 | def plot_ring_rays( 436 | self, 437 | angle: Union[int, float, list], 438 | range: int, 439 | color: str = "white", 440 | linewidth: Number_T = 0.5, 441 | **kwargs 442 | ): 443 | r"""Plot ring rays on PPI plot.""" 444 | slon, slat = self.data.site_longitude, self.data.site_latitude 445 | if isinstance(angle, (int, float)): 446 | angle = [angle] 447 | for a in angle: 448 | theta = np.deg2rad(a) 449 | x, y = get_coordinate(range, theta, 0, slon, slat, h_offset=False) 450 | self.geoax.plot( 451 | [slon, x], 452 | [slat, y], 453 | color=color, 454 | linewidth=linewidth, 455 | transform=self.data_crs, 456 | **kwargs 457 | ) 458 | 459 | def add_custom_shp( 460 | self, 461 | shp_path: str, 462 | encoding: str = "gbk", 463 | color: str = "white", 464 | linewidth: Number_T = 0.5, 465 | **kwargs 466 | ): 467 | """ 468 | Add custom shapefile to the plot. 469 | """ 470 | reader = Reader(shp_path, encoding=encoding) 471 | self.geoax.add_geometries( 472 | geoms=list(reader.geometries()), 473 | crs=ccrs.PlateCarree(), 474 | edgecolor=color, 475 | facecolor="None", 476 | zorder=3, 477 | linewidth=linewidth, 478 | **kwargs 479 | ) 480 | 481 | def plot_cross_section( 482 | self, 483 | data: Dataset, 484 | ymax: Optional[int] = None, 485 | linecolor: Optional[str] = None, 486 | interpolate: bool = True, 487 | ): 488 | # May add check to ensure the data is slice data 489 | r"""Plot cross section data below the PPI plot.""" 490 | if self.settings["is_inline"] and self._fig_init: 491 | raise RuntimeError( 492 | "Adding cross section dynamically is not supported in" 493 | "inline backend, add section keyword when initializing PPI instead." 494 | ) 495 | if not linecolor: 496 | if self.settings["style"] == "black": 497 | linecolor = "white" 498 | elif self.settings["style"] == "white": 499 | linecolor = "black" 500 | self.settings["slice"] = data 501 | # The axes to plot c-section is below the main axes 502 | # the height of it is a quarter of the height of main axes 503 | # so the positions of the figure, the main axes, the colorbar axes 504 | # should be adjusted accordingly. 505 | # TODO: remove hardcode and calculate positions automatically 506 | self.fig.set_size_inches(10, 10) 507 | self.geoax.set_position([0, 0.2, 0.8, 0.8]) 508 | ax2 = self.fig.add_axes([0, 0.01, 0.8, 0.17]) 509 | # transform coordinates 510 | self.text_pos[1] = self.text_pos[1] * 0.8 + 0.2 511 | self.text_pos[3] = self.text_pos[3] * 0.8 512 | self.cbar_pos[1] = self.cbar_pos[1] * 0.8 + 0.2 513 | self.cbar_pos[3] = self.cbar_pos[3] * 0.8 514 | ax2.yaxis.set_ticks_position("right") 515 | ax2.set_xticks([]) 516 | dtype = get_dtype(data) 517 | sl = data[dtype].values 518 | if dtype == "REF": 519 | # visualization improvement for reflectivity 520 | sl[np.isnan(sl)] = -0.1 521 | xcor = data["x_cor"] 522 | ycor = data["y_cor"] 523 | cmap = sec_plot[dtype] 524 | norm = norm_plot[dtype] 525 | if interpolate: 526 | ax2.contourf(xcor, ycor, sl, 256, cmap=cmap, norm=norm) 527 | else: 528 | ax2.pcolormesh(xcor, ycor, sl, cmap=cmap, norm=norm, shading="auto") 529 | if ymax: 530 | ax2.set_ylim(0, ymax) 531 | else: 532 | ax2.set_ylim(0, 15) 533 | ax2.set_title( 534 | "Start: {}N {}E".format(data.start_lat, data.start_lon) 535 | + " End: {}N {}E".format(data.end_lat, data.end_lon) 536 | ) 537 | self.geoax.plot( 538 | [data.start_lon, data.end_lon], 539 | [data.start_lat, data.end_lat], 540 | marker="x", 541 | color=linecolor, 542 | transform=self.data_crs, 543 | zorder=5, 544 | ) 545 | 546 | def storm_track_info(self, filepath: str): 547 | r""" 548 | Add storm tracks from Nexrad Level III (PUP) STI product file 549 | """ 550 | sti = StormTrackInfo(filepath) 551 | if len(sti.info.keys()) == 0: 552 | warnings.warn("No storm track to plot", RuntimeWarning) 553 | return 554 | else: 555 | stlist = sti.storm_list 556 | # extent = self.geoax.get_extent() 557 | for st in stlist: 558 | past = sti.track(st, "past") 559 | fcs = sti.track(st, "forecast") 560 | current = sti.current(st) 561 | if past: 562 | self.geoax.plot( 563 | *past, 564 | marker=".", 565 | color="white", 566 | zorder=4, 567 | markersize=5, 568 | transform=self.data_crs 569 | ) 570 | if fcs: 571 | self.geoax.plot( 572 | *fcs, 573 | marker="+", 574 | color="white", 575 | zorder=4, 576 | markersize=5, 577 | transform=self.data_crs 578 | ) 579 | self.geoax.scatter( 580 | *current, 581 | marker="o", 582 | s=15, 583 | zorder=5, 584 | color="lightgrey", 585 | transform=self.data_crs 586 | ) 587 | # if (current[0] > extent[0]) and (current[0] < extent[1]) and (current[1] > extent[2]) and (current[1] < extent[3]): 588 | # self.geoax.text(current[0] - 0.03, current[1] - 0.03, st, color='white', zorder=4) 589 | 590 | def gridlines(self, draw_labels: bool = True, linewidth: Number_T = 0, **kwargs): 591 | r"""Draw grid lines on cartopy axes""" 592 | from cartopy import __version__ 593 | 594 | if not isinstance(self.geoax.projection, ccrs.PlateCarree): 595 | # Some workaround about the issue that cartopy version lower than 0.18 cannot 596 | # draw ticks on AzimuthalEquidistant plot 597 | if __version__ < "0.18": 598 | warnings.warn( 599 | "Cartopy older than 0.18 cannot draw ticks on AzimuthalEquidistant plot.", 600 | RuntimeWarning, 601 | ) 602 | return 603 | liner = self.geoax.gridlines( 604 | draw_labels=draw_labels, 605 | linewidth=linewidth, 606 | transform=self.data_crs, 607 | rotate_labels=False, 608 | **kwargs 609 | ) 610 | liner.top_labels = False 611 | liner.right_labels = False 612 | if __version__ >= "0.20": 613 | liner.ypadding = -5 614 | liner.xpadding = -5 615 | 616 | def _add_city_names(self): 617 | with open( 618 | os.path.join(MODULE_DIR, "data", "chinaCity.json"), encoding="utf-8" 619 | ) as j: 620 | js = json.load(j) 621 | name = np.concatenate([[j["name"] for j in i["children"]] for i in js]) 622 | lon = np.concatenate([[j["log"] for j in i["children"]] for i in js]).astype( 623 | float 624 | ) 625 | lat = np.concatenate([[j["lat"] for j in i["children"]] for i in js]).astype( 626 | float 627 | ) 628 | extent = self.settings["extent"] 629 | fraction = (extent[1] - extent[0]) * 0.04 630 | target_city = ( 631 | (lon > (extent[0] + fraction)) 632 | & (lon < (extent[1] - fraction)) 633 | & (lat > (extent[2] + fraction)) 634 | & (lat < (extent[3] - fraction)) 635 | ) 636 | for nm, stlon, stlat in zip( 637 | name[target_city], lon[target_city], lat[target_city] 638 | ): 639 | self.geoax.text( 640 | stlon, 641 | stlat, 642 | nm, 643 | **default_font_kw, 644 | color="darkgrey", 645 | transform=self.data_crs, 646 | horizontalalignment="center", 647 | verticalalignment="center" 648 | ) 649 | 650 | def _autoscale(self): 651 | llon, ulon, llat, ulat = self.geoax.get_extent() 652 | lon_delta = ulon - llon 653 | lat_delta = ulat - llat 654 | if lon_delta == lat_delta: 655 | return 656 | if lon_delta > lat_delta: 657 | # The long axis is x-axis 658 | lat_center = (ulat + llat) / 2 659 | lat_extend = lon_delta / 2 660 | llat = lat_center - lat_extend 661 | ulat = lat_center + lat_extend 662 | elif lon_delta < lat_delta: 663 | # The long axis is y-axis 664 | lon_center = (ulon + llon) / 2 665 | lon_extend = lat_delta / 2 666 | llon = lon_center - lon_extend 667 | ulon = lon_center + lon_extend 668 | self.geoax.set_extent([llon, ulon, llat, ulat], self.geoax.projection) 669 | -------------------------------------------------------------------------------- /cinrad/visualize/rhi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import os 5 | from datetime import datetime 6 | 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | from matplotlib.cm import ScalarMappable 10 | from xarray import Dataset 11 | 12 | from cinrad.common import get_dtype 13 | from cinrad.visualize.utils import sec_plot, norm_plot, prodname, default_font_kw 14 | 15 | __all__ = ["Section"] 16 | 17 | 18 | class Section(object): 19 | def __init__( 20 | self, 21 | data: Dataset, 22 | hlim: int = 15, 23 | interpolate: bool = True, 24 | figsize: tuple = (10, 5), 25 | ): 26 | # TODO: Use context manager to control style 27 | self.data = data 28 | self.dtype = get_dtype(data) 29 | self.settings = { 30 | "hlim": hlim, 31 | "interp": interpolate, 32 | "figsize": figsize, 33 | } 34 | self.rhi_flag = "azimuth" in data.attrs 35 | self._plot() 36 | 37 | def _plot(self): 38 | 39 | rhi = self.data[self.dtype] 40 | xcor = self.data["x_cor"] 41 | ycor = self.data["y_cor"] 42 | rmax = np.nanmax(rhi.values) 43 | plt.figure(figsize=self.settings["figsize"], dpi=300) 44 | ax = plt.gca() 45 | ax.set_facecolor("black") 46 | plt.grid( 47 | True, linewidth=0.50, linestyle="-.", color="white" 48 | ) ## 修改于2019-01-22 By WU Fulang 49 | cmap = sec_plot[self.dtype] 50 | norm = norm_plot[self.dtype] 51 | if self.settings["interp"]: 52 | plt.contourf( 53 | xcor, 54 | ycor, 55 | rhi, 56 | 128, 57 | cmap=cmap, 58 | norm=norm, 59 | ) 60 | else: 61 | plt.pcolormesh(xcor, ycor, rhi, cmap=cmap, norm=norm, shading="auto") 62 | plt.ylim(0, self.settings["hlim"]) 63 | if self.rhi_flag: 64 | title = "Range-Height Indicator\n" 65 | else: 66 | title = "Vertical cross-section ({})\n".format(prodname[self.dtype]) 67 | title += "Station: {} ".format(self.data.site_name) 68 | if self.rhi_flag: 69 | # RHI scan type 70 | title += "Range: {:.0f}km Azimuth: {:.0f}° ".format( 71 | self.data.range, self.data.azimuth 72 | ) 73 | else: 74 | title += "Start: {}N {}E ".format(self.data.start_lat, self.data.start_lon) 75 | title += "End: {}N {}E ".format(self.data.end_lat, self.data.end_lon) 76 | title += "Time: " + datetime.strptime( 77 | self.data.scan_time, "%Y-%m-%d %H:%M:%S" 78 | ).strftime("%Y.%m.%d %H:%M ") 79 | title += "Max: {:.1f}".format(rmax) 80 | plt.title(title, **default_font_kw) 81 | lat_pos = np.linspace(self.data.start_lat, self.data.end_lat, 6) 82 | lon_pos = np.linspace(self.data.start_lon, self.data.end_lon, 6) 83 | tick_formatter = lambda x, y: "{:.2f}N\n{:.2f}E".format(x, y) 84 | ticks = list(map(tick_formatter, lat_pos, lon_pos)) 85 | cor_max = xcor.values.max() 86 | plt.xticks(np.array([0, 0.2, 0.4, 0.6, 0.8, 1]) * cor_max, ticks) 87 | plt.ylabel("Height (km)", **default_font_kw) ## 修改于2019-01-22 By WU Fulang 88 | sm = ScalarMappable(norm=norm, cmap=cmap) 89 | plt.colorbar(sm, ax=plt.gca()) 90 | 91 | def __call__(self, fpath: str): 92 | if os.path.isdir(fpath): 93 | if self.rhi_flag: 94 | path_string = "{}{}_{}_RHI_{:.0f}_{:.0f}_{}.png".format( 95 | fpath, 96 | self.data.site_code, 97 | datetime.strptime( 98 | self.data.scan_time, "%Y-%m-%d %H:%M:%S" 99 | ).strftime("%Y%m%d%H%M%S"), 100 | self.data.azimuth, 101 | self.data.range, 102 | self.dtype, 103 | ) 104 | else: 105 | path_string = "{}_{}_VCS_{}N{}E_{}N{}E.png".format( 106 | self.data.site_code, 107 | datetime.strptime( 108 | self.data.scan_time, "%Y-%m-%d %H:%M:%S" 109 | ).strftime("%Y%m%d%H%M%S"), 110 | self.data.start_lat, 111 | self.data.start_lon, 112 | self.data.end_lat, 113 | self.data.end_lon, 114 | ) 115 | save_path = os.path.join(fpath, path_string) 116 | else: 117 | save_path = fpath 118 | plt.savefig(save_path, bbox_inches="tight") 119 | return save_path 120 | -------------------------------------------------------------------------------- /cinrad/visualize/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Puyuan Du 3 | 4 | import os 5 | from datetime import datetime 6 | from typing import Union, Optional, Any, List 7 | from functools import lru_cache 8 | 9 | import numpy as np 10 | import matplotlib as mpl 11 | import matplotlib.pyplot as plt 12 | from matplotlib.colorbar import ColorbarBase 13 | from matplotlib.lines import Line2D 14 | import matplotlib.colors as cmx 15 | from matplotlib.font_manager import FontProperties 16 | import matplotlib.cm as mcm 17 | from cartopy.io import shapereader 18 | import cartopy.crs as ccrs 19 | from cartopy.mpl.geoaxes import GeoAxes 20 | from cartopy.feature import Feature 21 | import shapefile 22 | import shapely.geometry as sgeom 23 | from vanadis.colormap import Colormap 24 | from cinrad_data import get_font_path, get_shp_list, get_shp_file 25 | 26 | from cinrad.visualize.gpf import _cmap 27 | from cinrad.utils import MODULE_DIR 28 | from cinrad._typing import Array_T, Number_T 29 | from cinrad.error import RadarPlotError 30 | from cinrad.visualize.layout import GEOAXES_POS 31 | 32 | __all__ = [ 33 | "add_shp", 34 | "draw_highlight_area", 35 | "create_geoaxes", 36 | "norm_plot", 37 | "norm_cbar", 38 | "cmap_plot", 39 | "cmap_cbar", 40 | "sec_plot", 41 | "prodname", 42 | "unit", 43 | "cbar_text", 44 | "is_inline", 45 | "default_font_kw", 46 | ] 47 | 48 | CMAP_DIR = os.path.join(MODULE_DIR, "data", "colormap") 49 | 50 | 51 | def _get_uniform_cmap(cmap: Any) -> Any: 52 | new_cm = Colormap(cmap=cmap.reversed()).set_uniform() 53 | return new_cm.as_mpl_cmap() 54 | 55 | 56 | r_cmap = _cmap("REF")["cmap"] 57 | r_cmap_smooth = _cmap("REF_s")["cmap"] 58 | v_cmap = _cmap("VEL")["cmap"] 59 | v_cbar = _cmap("VEL_reverse")["cmap"] 60 | v_cmap_smooth = _cmap("VEL_s")["cmap"] 61 | zdr_cmap = _cmap("ZDR")["cmap"] 62 | zdr_cbar = _get_uniform_cmap(zdr_cmap) 63 | zdr_cmap_smooth = _cmap("ZDR_s")["cmap"] 64 | kdp_cmap = _cmap("KDP")["cmap"] 65 | kdp_cbar = _get_uniform_cmap(kdp_cmap) 66 | kdp_cmap_smooth = _cmap("KDP_s")["cmap"] 67 | cc_cmap = _cmap("CC")["cmap"] 68 | cc_cbar = _get_uniform_cmap(cc_cmap) 69 | cc_cmap_smooth = _cmap("CC_s")["cmap"] 70 | et_cmap = _cmap("ET")["cmap"] 71 | et_cbar = _get_uniform_cmap(et_cmap) 72 | vil_cmap = _cmap("VIL")["cmap"] 73 | vil_cbar = _get_uniform_cmap(vil_cmap) 74 | rf_cmap = cmx.ListedColormap("#660066", "#FFFFFF") 75 | ohp_cmap = _cmap("OHP")["cmap"] 76 | ohp_cbar = _get_uniform_cmap(ohp_cmap) 77 | hcl_cmap = _cmap("HCL")["cmap"] 78 | hcl_cbar = _get_uniform_cmap(hcl_cmap.reversed()) 79 | 80 | norm1 = cmx.Normalize(0, 75) # reflectivity / vertially integrated liquid 81 | norm2 = cmx.Normalize(-35, 28) # velocity 82 | norm3 = cmx.Normalize(-1, 0) # RF 83 | norm4 = cmx.Normalize(0, 1) # colorbar 84 | norm5 = cmx.Normalize(0, 21) # echo top 85 | norm6 = cmx.Normalize(-4, 5) # differential reflectivity 86 | norm7 = cmx.Normalize(260, 360) # differential phase 87 | norm8 = cmx.Normalize(0, 0.99) # correlation coefficient 88 | norm9 = cmx.Normalize(-0.8, 21) # specific differential phase 89 | norm10 = cmx.Normalize(0.1, 6) # vertically integrated liquid density 90 | norm11 = cmx.Normalize(0, 204) # One-hour precipitation 91 | norm12 = cmx.Normalize(1, 11) 92 | norm13 = cmx.Normalize(0, 10) # standard rose HCl 93 | 94 | # fmt: off 95 | norm_plot = {"REF":norm1, "VEL":norm2, "CR":norm1, "ET":norm5, "VIL":norm1, "RF":norm3, 96 | "ZDR":norm6, "PHI":norm7, "RHO":norm8, "TREF":norm1, "KDP":norm9, "VILD":norm10, 97 | "OHP":norm11, "cHCL":norm12, "HCL":norm13} # Normalize object used to plot 98 | norm_cbar = {"REF":norm1, "VEL":norm4, "CR":norm1, "ET":norm4, "VIL":norm4, 99 | "ZDR":norm4, "PHI":norm4, "RHO":norm4, "TREF":norm1, "KDP":norm4, 100 | "VILD":norm4, "OHP":norm4, "cHCL":norm4, "HCL":norm4} # Normalize object used for colorbar 101 | cmap_plot = {"REF":r_cmap, "VEL":v_cmap, "CR":r_cmap, "ET":et_cmap, "VIL":vil_cmap, "RF":rf_cmap, 102 | "ZDR":zdr_cmap, "PHI":kdp_cmap, "RHO":cc_cmap, "TREF":r_cmap, "KDP":kdp_cmap, 103 | "VILD":vil_cmap, "OHP":ohp_cmap, "cHCL":mcm.tab10, "HCL":hcl_cmap} 104 | cmap_cbar = {"REF":r_cmap, "VEL":v_cbar, "CR":r_cmap, "ET":et_cbar, "VIL":vil_cbar, 105 | "ZDR":zdr_cbar, "PHI":kdp_cbar, "RHO":cc_cbar, "TREF":r_cmap, "KDP":kdp_cbar, 106 | "VILD":vil_cbar, "OHP":ohp_cbar, "cHCL":mcm.tab10, "HCL":hcl_cbar} 107 | sec_plot = {"REF":r_cmap_smooth, "VEL":v_cmap_smooth, "ZDR":zdr_cmap_smooth, "PHI":kdp_cmap_smooth, "RHO":cc_cmap_smooth, 108 | "KDP":kdp_cmap_smooth, "cHCL":mcm.tab10, "HCL":hcl_cbar} 109 | prodname = {"REF":"Base Reflectivity", "VEL":"Base Velocity", "CR":"Composite Ref.", 110 | "ET":"Echo Tops", "VIL":"V Integrated Liquid", "ZDR":"Differential Ref.", 111 | "PHI":"Differential Phase", "RHO":"Correlation Coe.", "TREF":"Total Reflectivity", 112 | "KDP":"Spec. Diff. Phase", "VILD":"VIL Density", "OHP":"One-Hour Precip.", 113 | "cHCL":"Hydrometeor Class","HCL":"Hydrometeor Class", "VELSZ":"Velocity SZ Recovery"} 114 | unit = {"REF":"dBZ", "VEL":"m/s", "CR":"dBZ", "ET":"km", "VIL":"kg/m**2", "ZDR":"dB", "PHI":"deg", 115 | "RHO":"", "TREF":"dBZ", "KDP":"deg/km", "VILD":"g/m**3", "OHP":"mm", "cHCL":"", "HCL":""} 116 | cbar_text = {"REF":None, "VEL":["RF", "", "27", "20", "15", "10", "5", "1", "0", 117 | "-1", "-5", "-10", "-15", "-20", "-27", "-35"], 118 | "CR":None, "ET":["", "21", "20", "18", "17", "15", "14", "12", 119 | "11", "9", "8", "6", "5", "3", "2", "0"], 120 | "VIL":["", "70", "65", "60", "55", "50", "45", "40", "35", "30", 121 | "25", "20", "15", "10", "5", "0"], 122 | "ZDR":["", "5", "4", "3.5", "3", "2.5", "2", "1.5", "1", "0.8", "0.5", 123 | "0.2", "0", "-1", "-2", "-3", "-4"], 124 | "PHI":np.linspace(360, 260, 17).astype(str), 125 | "RHO":["", "0.99", "0.98", "0.97", "0.96", "0.95", "0.94", "0.92", "0.9", 126 | "0.85", "0.8", "0.7", "0.6", "0.5", "0.3", "0.1", "0"], 127 | "TREF":None, "KDP":["", "20", "7", "3.1", "2.4", "1.7", "1.1", "0.75", "0.5", 128 | "0.33", "0.22", "0.15", "0.1", "-0.1", "-0.2", "-0.4", "-0.8"], 129 | "VILD":["", "6", "5", "4", "3.5", "3", "2.5", "2.1", "1.8", "1.5", "1.2", 130 | "0.9", "0.7", "0.5", "0.3", "0.1"], 131 | "OHP":["", "203.2", "152.4", "101.6", "76.2", "63.5", "50.8", "44.45", "38.1", "31.75", 132 | "25.4", "19.05", "12.7", "6.35", "2.54", "0"], 133 | "cHCL":["Drizzle", "Rain", "Ice Crystals", "Dry Snow", "Wet Snow", "Vertical Ice", 134 | "Low-Dens Graupel", "High-Dens Graupel", "Hail", "Big Drops", ""], 135 | "HCL":["Rain", "Heavy Rain", "Hail", "Big Drops", "Clear-Air Echo", "Ground Clutter", 136 | "Dry snow", "Wet snow", "Ice Crystals", "Graupel", "Unknown", ""]} 137 | # fmt: on 138 | 139 | # Add entry for VELSZ 140 | for dic in zip([norm_plot, norm_cbar, cmap_plot, cmap_cbar, sec_plot, unit, cbar_text]): 141 | _d = dic[0] 142 | _d["VELSZ"] = _d["VEL"] 143 | 144 | font = FontProperties(fname=get_font_path()) 145 | default_font_kw = {"fontproperties": font, "fontsize": 12} 146 | 147 | 148 | def set_font(font_path: str): 149 | glb = globals() 150 | font = FontProperties(fname=font_path) 151 | glb["plot_kw"].update({"fontproperties": font}) 152 | 153 | 154 | class ShpReader(shapereader.BasicReader): 155 | r"""Customized reader to deal with encoding issue""" 156 | 157 | def __init__(self, filename: str, encoding: str = "gbk"): 158 | # Validate the filename/shapefile 159 | self._reader = reader = shapefile.Reader(filename, encoding=encoding) 160 | if reader.shp is None or reader.shx is None or reader.dbf is None: 161 | raise ValueError("Incomplete shapefile definition " "in '%s'." % filename) 162 | try: 163 | shapeType = reader.shapeType 164 | self._geometry_factory = shapereader.GEOMETRY_FACTORIES.get(shapeType) 165 | if self._geometry_factory is None: 166 | raise ValueError("Unsupported shape type: %s" % shapeType) 167 | except AttributeError: 168 | pass 169 | self._fields = self._reader.fields 170 | 171 | 172 | from cartopy import __version__ 173 | 174 | if __version__ >= "0.23.0": 175 | ShpReader = shapereader.BasicReader 176 | 177 | 178 | @lru_cache(maxsize=2) 179 | def get_shp() -> list: 180 | flist = get_shp_list() 181 | shps = [list(ShpReader(i, encoding="gbk").geometries()) for i in flist] 182 | return shps 183 | 184 | 185 | class _ShapelyFeature(Feature): 186 | r"""Copied from cartopy.feature.ShapelyFeature""" 187 | 188 | def __init__(self, geometries, crs, **kwargs): 189 | super().__init__(crs, **kwargs) 190 | if isinstance(geometries, sgeom.base.BaseGeometry): 191 | geometries = [geometries] 192 | self._geoms = tuple(geometries) 193 | 194 | def geometries(self): 195 | return iter(self._geoms) 196 | 197 | 198 | def add_shp( 199 | ax: Any, 200 | proj: ccrs.Projection, 201 | coastline: bool = False, 202 | style: str = "black", 203 | extent: Optional[Array_T] = None, 204 | ): 205 | if style == "transparent": 206 | return 207 | shp_crs = ccrs.PlateCarree() 208 | shps = get_shp() 209 | if style == "black": 210 | line_colors = ["grey", "lightgrey", "white"] 211 | elif style == "white": 212 | line_colors = ["lightgrey", "grey", "black"] 213 | ax.add_feature( 214 | _ShapelyFeature( 215 | shps[0], 216 | shp_crs, 217 | edgecolor=line_colors[0], 218 | facecolor="None", 219 | zorder=3, 220 | linewidth=0.5, 221 | ) 222 | ) 223 | ax.add_feature( 224 | _ShapelyFeature( 225 | shps[1], 226 | shp_crs, 227 | edgecolor=line_colors[1], 228 | facecolor="None", 229 | zorder=3, 230 | linewidth=0.7, 231 | ) 232 | ) 233 | ax.add_feature( 234 | _ShapelyFeature( 235 | shps[2], 236 | shp_crs, 237 | edgecolor=line_colors[2], 238 | facecolor="None", 239 | zorder=3, 240 | linewidth=1, 241 | ) 242 | ) 243 | if coastline: 244 | ax.coastlines(resolution="10m", color=line_colors[2], zorder=3, linewidth=1) 245 | 246 | 247 | def highlight_area( 248 | area: Union[Array_T, str], linecolor: str = "red", **kwargs 249 | ) -> List[Line2D]: 250 | r"""Return list of Line2D object for given area name""" 251 | fpath = get_shp_file("City") 252 | shp = shapefile.Reader(fpath, encoding="gbk") 253 | rec = shp.shapeRecords() 254 | lines = list() 255 | if isinstance(area, str): 256 | area = [area] 257 | for i in area: 258 | if not isinstance(i, str): 259 | raise RadarPlotError("Area name should be str") 260 | name = np.array([i.record[2] for i in rec]) 261 | target = np.array(rec)[(name == i).nonzero()[0]] 262 | for j in target: 263 | pts = j.shape.points 264 | x = [i[0] for i in pts] 265 | y = [i[1] for i in pts] 266 | lines.append(Line2D(x, y, color=linecolor)) 267 | return lines 268 | 269 | 270 | def draw_highlight_area(area: Union[Array_T, str]): 271 | lines = highlight_area(area) 272 | ax_ = plt.gca() 273 | for l in lines: 274 | pat = ax_.add_artist(l) 275 | pat.set_zorder(4) 276 | 277 | 278 | def create_geoaxes( 279 | fig: Any, proj: ccrs.Projection, extent: List[Number_T], style: str = "black" 280 | ) -> GeoAxes: 281 | from cartopy import __version__ 282 | 283 | if style == "transparent": 284 | ax = fig.add_axes([0, 0, 1, 1], projection=proj) 285 | ax.set_aspect("equal") 286 | else: 287 | ax = fig.add_axes(GEOAXES_POS, projection=proj) 288 | if __version__ < "0.18": 289 | ax.background_patch.set_visible(False) 290 | ax.outline_patch.set_visible(False) 291 | else: 292 | ax.patch.set_visible(False) 293 | ax.spines["geo"].set_visible(False) 294 | x_min, x_max, y_min, y_max = extent[0], extent[1], extent[2], extent[3] 295 | ax.set_extent([x_min, x_max, y_min, y_max], crs=ccrs.PlateCarree()) 296 | return ax 297 | 298 | 299 | def is_inline() -> bool: 300 | return "inline" in mpl.get_backend() 301 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api/api.rst: -------------------------------------------------------------------------------- 1 | .. _api-index: 2 | 3 | ############### 4 | API Reference 5 | ############### 6 | 7 | Subpackages 8 | ----------- 9 | 10 | .. toctree:: 11 | 12 | cinrad.io 13 | cinrad.visualize 14 | 15 | 16 | Submodules 17 | ---------- 18 | 19 | cinrad.calc module 20 | ------------------- 21 | .. automodule:: cinrad.calc 22 | :members: 23 | 24 | cinrad.common module 25 | ------------------- 26 | .. automodule:: cinrad.common 27 | :members: 28 | 29 | cinrad.grid module 30 | ------------------- 31 | .. automodule:: cinrad.grid 32 | :members: 33 | 34 | cinrad.utils module 35 | ------------------- 36 | .. automodule:: cinrad.utils 37 | :members: 38 | 39 | * :ref:`modindex` 40 | * :ref:`genindex` -------------------------------------------------------------------------------- /docs/api/cinrad.io.rst: -------------------------------------------------------------------------------- 1 | cinrad.io package 2 | ======================== 3 | 4 | cinrad.io.base module 5 | --------------------------- 6 | .. automodule:: cinrad.io.base 7 | :members: 8 | :show-inheritance: 9 | 10 | cinrad.io.level2 module 11 | --------------------------- 12 | .. automodule:: cinrad.io.level2 13 | :members: 14 | :show-inheritance: 15 | 16 | cinrad.io.level3 module 17 | --------------------------- 18 | .. automodule:: cinrad.io.level3 19 | :members: 20 | :show-inheritance: 21 | 22 | cinrad.io.export module 23 | --------------------------- 24 | .. automodule:: cinrad.io.export 25 | :members: 26 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api/cinrad.visualize.rst: -------------------------------------------------------------------------------- 1 | cinrad.visualize package 2 | ======================== 3 | 4 | cinrad.visualize.ppi module 5 | --------------------------- 6 | .. automodule:: cinrad.visualize.ppi 7 | :members: 8 | :show-inheritance: 9 | 10 | cinrad.visualize.rhi module 11 | --------------------------- 12 | .. automodule:: cinrad.visualize.rhi 13 | :members: 14 | :show-inheritance: -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.append(os.path.abspath("../../")) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "PyCINRAD" 21 | copyright = "2021, Puyuan Du" 22 | author = "Puyuan Du" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "1.9.2" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.todo", 35 | "sphinx.ext.viewcode", 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.autosummary", 38 | ] 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "sphinx_rtd_theme" 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ["_static"] 59 | 60 | master_doc = "index" 61 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: readthedocs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.7 6 | - pip 7 | - numpy 8 | - matplotlib 9 | - cartopy 10 | - setuptools 11 | - sphinx 12 | - sphinx_rtd_theme 13 | - cython 14 | - xarray 15 | - metpy 16 | - pyshp 17 | - arm_pyart -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. PyCINRAD documentation master file, created by 2 | sphinx-quickstart on Tue Jun 23 17:21:06 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | PyCINRAD的文档 7 | ==================================== 8 | 9 | PyCINRAD是一个气象雷达开源库,支持中国所有主流雷达格式的读取,并提供一些实用的算法以及可视化。 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | :caption: Contents: 14 | 15 | API Reference 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /environment.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - pip 6 | - numpy 7 | - matplotlib 8 | - cartopy 9 | - setuptools 10 | - cython 11 | - xarray 12 | - metpy 13 | - pyshp -------------------------------------------------------------------------------- /example/read_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import cinrad" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "**Read old-version level 2 data**" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "f = cinrad.io.CinradReader('Z_RADR_I_Z9200_20190421190600_O_DOR_SA_CAP.BIN.BZ2')" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "print(f)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "**Get data**\n", 42 | "\n", 43 | "`available_product` tells the names of product which are contained in this tilt." 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "f.available_product(0)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "tilt_number = 0\n", 62 | "data_radius = 230\n", 63 | "data_dtype = 'REF' # stands for reflectivity\n", 64 | "r = f.get_data(tilt_number, data_radius, data_dtype)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "Data is stored in `xarray.Dataset`" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "print(r)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "type(r)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "**Read standard data**" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "g = cinrad.io.StandardData('Z_RADR_I_Z9759_20190426090600_O_DOR_SAD_CAP_FMT.BIN.BZ2')" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "Some scan parameters" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "from pprint import pprint\n", 122 | "pprint(g.scan_config)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "print(g)" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "**Get data**\n", 139 | "\n", 140 | "In addition to `available_product` introduced above, `cinrad.io.StandardData` has `available_tilt` function to tell the product is available at which tilt / tilts." 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "g.available_product(0)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": null, 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "g.available_tilt('REF')" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "tilt_number = 0\n", 168 | "data_radius = 230\n", 169 | "data_dtype = 'ZDR' # stands for differential reflectivity\n", 170 | "zdr = g.get_data(tilt_number, data_radius, data_dtype)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [ 179 | "print(zdr)" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": null, 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "type(r)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [] 197 | } 198 | ], 199 | "metadata": { 200 | "kernelspec": { 201 | "display_name": "Python 3.7.2 64-bit", 202 | "language": "python", 203 | "name": "python37264bitaa30ee0bb40d4e99b92696ce7635ac6c" 204 | }, 205 | "language_info": { 206 | "codemirror_mode": { 207 | "name": "ipython", 208 | "version": 3 209 | }, 210 | "file_extension": ".py", 211 | "mimetype": "text/x-python", 212 | "name": "python", 213 | "nbconvert_exporter": "python", 214 | "pygments_lexer": "ipython3", 215 | "version": "3.7.2-final" 216 | } 217 | }, 218 | "nbformat": 4, 219 | "nbformat_minor": 2 220 | } 221 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: cinrad 3 | version: 1.5 4 | 5 | build: 6 | skip: True # [py<34] 7 | 8 | source: 9 | git_url: https://github.com/CyanideCN/PyCINRAD.git 10 | 11 | requirements: 12 | build: 13 | - python 14 | 15 | host: 16 | - python 17 | - setuptools 18 | 19 | run: 20 | - python 21 | - 'matplotlib>=2.2' 22 | - 'metpy >=0.8' 23 | - 'cartopy>=0.15' 24 | - 'pyshp!=2.0.0, !=2.0.1' 25 | 26 | 27 | about: 28 | home: https://github.com/CyanideCN/PyCINRAD.git 29 | license: GPL-3.0 30 | -------------------------------------------------------------------------------- /pictures/XXX_XXX_RHI_299_100_REF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/pictures/XXX_XXX_RHI_299_100_REF.png -------------------------------------------------------------------------------- /pictures/Z9574_20190321025715_0.5_230_ZDR_29.47N121.44E_29.4N122.04E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/pictures/Z9574_20190321025715_0.5_230_ZDR_29.47N121.44E_29.4N122.04E.png -------------------------------------------------------------------------------- /pictures/Z9735_20180304004209_VCS_25.5N111E_26.5N112E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/pictures/Z9735_20180304004209_VCS_25.5N111E_26.5N112E.png -------------------------------------------------------------------------------- /pictures/Z9735_20180304120845_0.6_230_REF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/pictures/Z9735_20180304120845_0.6_230_REF.png -------------------------------------------------------------------------------- /pictures/Z9735_20180304125031_0.6_230_REF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/pictures/Z9735_20180304125031_0.6_230_REF.png -------------------------------------------------------------------------------- /pictures/ZGZ02_20200826123326_0.9_40_REF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyanideCN/PyCINRAD/a1f8ad3783bbff9d87d7f3f9d72c0d8b170b48e1/pictures/ZGZ02_20200826123326_0.9_40_REF.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | cython>0.15 3 | metpy>=0.8 4 | cartopy>=0.15 5 | pyshp!=2.0.0, !=2.0.1 6 | matplotlib>=2.2 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from setuptools.extension import Extension 3 | from os.path import join, exists, sep 4 | import glob 5 | 6 | try: 7 | from Cython.Build import cythonize 8 | import numpy as np 9 | 10 | pyx_paths = [ 11 | join("cinrad", "_utils.pyx"), 12 | join("cinrad", "correct", "_unwrap_2d.pyx"), 13 | ] 14 | cythonize_flag = True 15 | for _pyx in pyx_paths: 16 | if not exists(_pyx): 17 | cythonize_flag = False 18 | break 19 | if cythonize_flag: 20 | ext_modules = cythonize(pyx_paths) 21 | else: 22 | ext_modules = list() 23 | for _pyx in pyx_paths: 24 | name = _pyx.rstrip(".pyx").replace(sep, ".") 25 | source = _pyx.replace(".pyx", ".c") 26 | ext_modules.append(Extension(name, [source])) 27 | include_dirs = [np.get_include()] 28 | except ImportError: 29 | ext_modules = None 30 | include_dirs = None 31 | 32 | data_pth = join("cinrad", "data") 33 | 34 | setup( 35 | name="cinrad", 36 | version="1.9.2", 37 | description="Decode CINRAD radar data and visualize", 38 | long_description="Decode CINRAD radar data and visualize", 39 | license="GPL Licence", 40 | author="Puyuan Du", 41 | author_email="dpy274555447@gmail.com", 42 | packages=find_packages(), 43 | include_package_data=True, 44 | platforms="Windows", 45 | python_requires=">=3.5", 46 | install_requires=[ 47 | "metpy>=0.8", 48 | "cartopy>=0.15", 49 | "pyshp!=2.0.0, !=2.0.1", 50 | "matplotlib>=2.2", 51 | "vanadis", 52 | "cinrad_data>=0.1" 53 | ], 54 | package_dir={"cinrad": "cinrad"}, 55 | package_data={"cinrad": [ 56 | "data/*.*", 57 | "data/*/*.*" 58 | ]}, 59 | scripts=[], 60 | ext_modules=ext_modules, 61 | include_dirs=include_dirs, 62 | ) 63 | -------------------------------------------------------------------------------- /test/test_calc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from cinrad.utils import vert_integrated_liquid, vert_integrated_liquid_py, echo_top, echo_top_py 3 | 4 | 5 | def test_vil(): 6 | a = np.arange(0, 27, 1, dtype=np.double).reshape(3, 3, 3) 7 | b = np.broadcast_to(np.arange(0, 3), (3, 3)) 8 | b = np.ascontiguousarray(b, dtype=np.double) 9 | c = np.arange(0, 3, 1, dtype=np.double) 10 | 11 | vil = vert_integrated_liquid(a, b, c) 12 | 13 | true_vil = np.array( 14 | [ 15 | [0.0, 0.00141174, 0.00322052], 16 | [0.0, 0.00209499, 0.0047792], 17 | [0.0, 0.00310893, 0.00709224], 18 | ] 19 | ) 20 | assert np.allclose(vil, true_vil) 21 | 22 | def test_vil_cy2py(): 23 | a = np.arange(0, 64, 1, dtype=np.double).reshape(4, 4, 4) 24 | b = np.broadcast_to(np.arange(0, 4), (4, 4)) 25 | b = np.ascontiguousarray(b, dtype=np.double) 26 | c = np.arange(0, 4, 1, dtype=np.double) 27 | 28 | vil = vert_integrated_liquid(a, b, c) 29 | vil2 = vert_integrated_liquid_py(a, b, c) 30 | assert np.allclose(vil, vil2) 31 | 32 | def test_et(): 33 | a = np.arange(0, 27, 1, dtype=np.double).reshape(3, 3, 3) 34 | b = np.broadcast_to(np.arange(0, 3), (3, 3)) 35 | b = np.ascontiguousarray(b, dtype=np.double) 36 | c = np.arange(0, 3, 1, dtype=np.double) 37 | 38 | et = echo_top(a, b, c, 0) 39 | 40 | true_et = np.array( 41 | [ 42 | [0.0, 0.03495832023191273, 0.070034287522649], 43 | [0.0, 0.03495832023191273, 0.070034287522649], 44 | [0.0, 0.03495832023191273, 0.070034287522649], 45 | ] 46 | ) 47 | assert np.array_equal(et, true_et) 48 | 49 | def test_et_cy2py(): 50 | a = np.arange(0, 64, 1, dtype=np.double).reshape(4, 4, 4) 51 | b = np.broadcast_to(np.arange(0, 4), (4, 4)) 52 | b = np.ascontiguousarray(b, dtype=np.double) 53 | c = np.arange(0, 4, 1, dtype=np.double) 54 | 55 | et = echo_top(a, b, c, 0) 56 | et2 = echo_top_py(a, b, c, 0) 57 | 58 | assert np.array_equal(et, et2) 59 | -------------------------------------------------------------------------------- /test/test_io.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | import pytest 4 | 5 | from cinrad.io.level2 import infer_type, CinradReader 6 | from cinrad.error import RadarDecodeError 7 | 8 | def test_infer_type_from_fname(): 9 | fill = bytes(200) 10 | fake_file = BytesIO(fill) 11 | code, _type = infer_type(fake_file, 'Z_RADR_I_Z9200_20000000000000_O_DOR_SA_CAP.bin') 12 | fake_file.close() 13 | assert code == 'Z9200' 14 | assert _type == 'SA' 15 | 16 | def test_infer_type_from_file_sc(): 17 | fill = bytes(100) + b"CINRAD/SC" + bytes(50) 18 | fake_file = BytesIO(fill) 19 | code, _type = infer_type(fake_file, "foo") 20 | fake_file.close() 21 | assert code == None 22 | assert _type == 'SC' 23 | 24 | def test_infer_type_from_file_cd(): 25 | fill = bytes(100) + b"CINRAD/CD" + bytes(50) 26 | fake_file = BytesIO(fill) 27 | code, _type = infer_type(fake_file, "foo") 28 | fake_file.close() 29 | assert code == None 30 | assert _type == 'CD' 31 | 32 | def test_infer_type_from_file_cc(): 33 | fill = bytes(116) + b"CINRAD/CC" + bytes(50) 34 | fake_file = BytesIO(fill) 35 | code, _type = infer_type(fake_file, "foo") 36 | fake_file.close() 37 | assert code == None 38 | assert _type == 'CC' 39 | 40 | def test_infer_type_from_incomplete_fname(): 41 | fill = bytes(200) 42 | fake_file = BytesIO(fill) 43 | code, _type = infer_type(fake_file, 'Z_RADR_I_Z9200.bin') 44 | fake_file.close() 45 | assert code == None 46 | assert _type == None 47 | 48 | def test_missing_radar_type(): 49 | fill = bytes(200) 50 | fake_file = BytesIO(fill) 51 | with pytest.raises(RadarDecodeError): 52 | CinradReader(fake_file) -------------------------------------------------------------------------------- /ui/gen_ui.bat: -------------------------------------------------------------------------------- 1 | pyuic5 RadarDisplay.ui -o ui_struct.py -------------------------------------------------------------------------------- /ui/main_ui.pyw: -------------------------------------------------------------------------------- 1 | import sys 2 | from functools import partial 3 | 4 | import matplotlib 5 | matplotlib.use('Qt5Agg') 6 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar 7 | from matplotlib.figure import Figure 8 | import matplotlib.pyplot as plt 9 | plt.style.use('dark_background') 10 | from PyQt5 import QtWidgets, QtCore 11 | import cinrad 12 | from ui_struct import Ui_MainWindow 13 | 14 | def read(fpath): 15 | try: 16 | f = cinrad.io.CinradReader(fpath) 17 | except Exception: 18 | f = cinrad.io.StandardData(fpath) 19 | return f 20 | 21 | class Figure_Canvas(FigureCanvas): 22 | def __init__(self, parent=None, width=9, height=9, dpi=90): 23 | fig = Figure(figsize=(width, height), dpi=dpi) 24 | FigureCanvas.__init__(self, fig) 25 | self.setParent(parent) 26 | 27 | class RadarUI(Ui_MainWindow): 28 | '''Further modification from infrastructure''' 29 | def __init__(self): 30 | self.last_fname = None 31 | self.tilt = 0 32 | self.dtype = 'REF' 33 | self.drange = 230 34 | self.redraw = False 35 | 36 | def setupUi(self, MainWindow): 37 | super(RadarUI, self).setupUi(MainWindow) 38 | self.main_window = MainWindow 39 | self.graphicscene = QtWidgets.QGraphicsScene() 40 | self.button_table = {'REF':self.radioButton, 'VEL':self.radioButton_2, 'RHO':self.radioButton_3, 41 | 'ZDR':self.radioButton_4, 'SW':self.radioButton_5, 'PHI':self.radioButton_6, 42 | 'ET':self.radioButton_7, 'VIL':self.radioButton_8, 'CR':self.radioButton_9} 43 | for k in self.button_table.keys(): 44 | self.button_table[k].clicked.connect(partial(self.on_button_activate, k)) 45 | self.actionOpen.triggered.connect(self._open) 46 | self.actionClose.triggered.connect(self._close) 47 | self.pushButton_2.clicked.connect(self.draw) 48 | self.comboBox.activated.connect(self.on_combobox_activate) 49 | self.comboBox.currentIndexChanged.connect(self.on_combobox_changed) 50 | self.pushButton.clicked.connect(self.on_textbox_update) 51 | for i in self.button_table.values(): 52 | i.setEnabled(False) 53 | 54 | def _open(self): 55 | f = QtWidgets.QFileDialog.getOpenFileName() 56 | fn = f[0] 57 | if fn == '': 58 | return 59 | if fn == self.last_fname: 60 | if hasattr(self, 'cinrad'): 61 | pass 62 | else: 63 | try: 64 | self.cinrad = read(fn) 65 | except Exception: 66 | self._message('无法读取该数据') 67 | return 68 | self.last_fname = fn 69 | # Display basic info 70 | self._flush() 71 | info = '站名:{}\n扫描时间:{}' 72 | self.basic_info_string = info.format(self.cinrad.name, self.cinrad.scantime.strftime('%Y-%m-%d %H:%M:%SZ')) 73 | self.label_3.setText(self.basic_info_string) 74 | # Extract available tilts and display in menu 75 | self.comboBox.addItems(['仰角{}-{:.2f}°'.format(i[0], i[1]) for i in enumerate(self.cinrad.el)]) 76 | 77 | def _flush(self): 78 | self.comboBox.clear() 79 | for i in self.button_table.values(): 80 | i.setEnabled(False) 81 | self.label_3.setText('') 82 | self.graphicscene.clear() 83 | 84 | def on_combobox_activate(self, index): 85 | self.tilt = index 86 | 87 | def on_combobox_changed(self): 88 | for i in self.button_table.values(): 89 | i.setEnabled(False) 90 | if hasattr(self, 'cinrad'): 91 | ap = self.cinrad.available_product(self.tilt) 92 | av = self.button_table.keys() 93 | for p in ap: 94 | if p in av: 95 | self.button_table[p].setEnabled(True) 96 | 97 | def on_button_activate(self, prod): 98 | self.dtype = prod 99 | 100 | def on_textbox_update(self): 101 | rad = self.plainTextEdit.toPlainText() 102 | self.drange = float(rad) 103 | 104 | def draw(self): 105 | if self.redraw: 106 | plt.close('all') 107 | dr = Figure_Canvas() 108 | data = self.cinrad.get_data(self.tilt, self.drange, self.dtype) 109 | fig = cinrad.visualize.PPI(data, fig=dr.figure, plot_labels=False) 110 | self.graphicscene.addWidget(dr) 111 | self.graphicsView.setScene(self.graphicscene) 112 | self.graphicsView.show() 113 | self.redraw = True 114 | 115 | def _close(self): 116 | plt.close('all') 117 | self.redraw = False 118 | del self.cinrad 119 | self._flush() 120 | 121 | def _message(self, message): 122 | msg = QtWidgets.QMessageBox.warning(self.main_window, 'Error!', message) 123 | 124 | if __name__ == "__main__": 125 | app = QtWidgets.QApplication(sys.argv) 126 | MainWindow = QtWidgets.QMainWindow() 127 | ui = RadarUI() 128 | ui.setupUi(MainWindow) 129 | MainWindow.show() 130 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /ui/ui_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'RadarDisplay.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.11.3 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_MainWindow(object): 13 | def setupUi(self, MainWindow): 14 | MainWindow.setObjectName("MainWindow") 15 | MainWindow.resize(991, 884) 16 | self.centralwidget = QtWidgets.QWidget(MainWindow) 17 | self.centralwidget.setObjectName("centralwidget") 18 | self.graphicsView = QtWidgets.QGraphicsView(self.centralwidget) 19 | self.graphicsView.setGeometry(QtCore.QRect(150, 0, 841, 831)) 20 | self.graphicsView.setObjectName("graphicsView") 21 | self.plainTextEdit = QtWidgets.QPlainTextEdit(self.centralwidget) 22 | self.plainTextEdit.setGeometry(QtCore.QRect(10, 30, 81, 31)) 23 | self.plainTextEdit.setObjectName("plainTextEdit") 24 | self.label = QtWidgets.QLabel(self.centralwidget) 25 | self.label.setGeometry(QtCore.QRect(10, 10, 72, 15)) 26 | self.label.setObjectName("label") 27 | self.pushButton = QtWidgets.QPushButton(self.centralwidget) 28 | self.pushButton.setGeometry(QtCore.QRect(100, 30, 41, 31)) 29 | self.pushButton.setObjectName("pushButton") 30 | self.radioButton = QtWidgets.QRadioButton(self.centralwidget) 31 | self.radioButton.setGeometry(QtCore.QRect(10, 70, 115, 19)) 32 | self.radioButton.setObjectName("radioButton") 33 | self.label_2 = QtWidgets.QLabel(self.centralwidget) 34 | self.label_2.setGeometry(QtCore.QRect(10, 330, 131, 31)) 35 | self.label_2.setObjectName("label_2") 36 | self.radioButton_2 = QtWidgets.QRadioButton(self.centralwidget) 37 | self.radioButton_2.setGeometry(QtCore.QRect(10, 90, 115, 19)) 38 | self.radioButton_2.setObjectName("radioButton_2") 39 | self.radioButton_3 = QtWidgets.QRadioButton(self.centralwidget) 40 | self.radioButton_3.setGeometry(QtCore.QRect(10, 110, 115, 19)) 41 | self.radioButton_3.setObjectName("radioButton_3") 42 | self.radioButton_4 = QtWidgets.QRadioButton(self.centralwidget) 43 | self.radioButton_4.setGeometry(QtCore.QRect(10, 130, 115, 19)) 44 | self.radioButton_4.setObjectName("radioButton_4") 45 | self.radioButton_5 = QtWidgets.QRadioButton(self.centralwidget) 46 | self.radioButton_5.setGeometry(QtCore.QRect(10, 150, 115, 19)) 47 | self.radioButton_5.setObjectName("radioButton_5") 48 | self.radioButton_6 = QtWidgets.QRadioButton(self.centralwidget) 49 | self.radioButton_6.setGeometry(QtCore.QRect(10, 170, 115, 19)) 50 | self.radioButton_6.setObjectName("radioButton_6") 51 | self.radioButton_7 = QtWidgets.QRadioButton(self.centralwidget) 52 | self.radioButton_7.setGeometry(QtCore.QRect(10, 190, 115, 19)) 53 | self.radioButton_7.setObjectName("radioButton_7") 54 | self.radioButton_8 = QtWidgets.QRadioButton(self.centralwidget) 55 | self.radioButton_8.setGeometry(QtCore.QRect(10, 210, 115, 19)) 56 | self.radioButton_8.setObjectName("radioButton_8") 57 | self.radioButton_9 = QtWidgets.QRadioButton(self.centralwidget) 58 | self.radioButton_9.setGeometry(QtCore.QRect(10, 230, 115, 19)) 59 | self.radioButton_9.setObjectName("radioButton_9") 60 | self.label_3 = QtWidgets.QLabel(self.centralwidget) 61 | self.label_3.setGeometry(QtCore.QRect(10, 360, 121, 211)) 62 | sizePolicy = QtWidgets.QSizePolicy( 63 | QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred 64 | ) 65 | sizePolicy.setHorizontalStretch(0) 66 | sizePolicy.setVerticalStretch(0) 67 | sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) 68 | self.label_3.setSizePolicy(sizePolicy) 69 | self.label_3.setText("") 70 | self.label_3.setAlignment( 71 | QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop 72 | ) 73 | self.label_3.setWordWrap(True) 74 | self.label_3.setObjectName("label_3") 75 | self.comboBox = QtWidgets.QComboBox(self.centralwidget) 76 | self.comboBox.setGeometry(QtCore.QRect(10, 280, 131, 22)) 77 | self.comboBox.setObjectName("comboBox") 78 | self.label_4 = QtWidgets.QLabel(self.centralwidget) 79 | self.label_4.setGeometry(QtCore.QRect(10, 260, 72, 15)) 80 | self.label_4.setObjectName("label_4") 81 | self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget) 82 | self.pushButton_2.setGeometry(QtCore.QRect(10, 580, 93, 61)) 83 | self.pushButton_2.setObjectName("pushButton_2") 84 | MainWindow.setCentralWidget(self.centralwidget) 85 | self.menubar = QtWidgets.QMenuBar(MainWindow) 86 | self.menubar.setGeometry(QtCore.QRect(0, 0, 991, 26)) 87 | self.menubar.setObjectName("menubar") 88 | self.menu = QtWidgets.QMenu(self.menubar) 89 | self.menu.setObjectName("menu") 90 | MainWindow.setMenuBar(self.menubar) 91 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 92 | self.statusbar.setObjectName("statusbar") 93 | MainWindow.setStatusBar(self.statusbar) 94 | self.actionOpen = QtWidgets.QAction(MainWindow) 95 | self.actionOpen.setObjectName("actionOpen") 96 | self.actionClose = QtWidgets.QAction(MainWindow) 97 | self.actionClose.setObjectName("actionClose") 98 | self.actionBase_Reflectivity = QtWidgets.QAction(MainWindow) 99 | self.actionBase_Reflectivity.setObjectName("actionBase_Reflectivity") 100 | self.actionBase_Velocity = QtWidgets.QAction(MainWindow) 101 | self.actionBase_Velocity.setObjectName("actionBase_Velocity") 102 | self.menu.addAction(self.actionOpen) 103 | self.menu.addAction(self.actionClose) 104 | self.menubar.addAction(self.menu.menuAction()) 105 | 106 | self.retranslateUi(MainWindow) 107 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 108 | 109 | def retranslateUi(self, MainWindow): 110 | _translate = QtCore.QCoreApplication.translate 111 | MainWindow.setWindowTitle(_translate("MainWindow", "CINRAD Radar Display")) 112 | self.label.setText(_translate("MainWindow", "绘图半径")) 113 | self.pushButton.setText(_translate("MainWindow", "更新")) 114 | self.radioButton.setText(_translate("MainWindow", "REF")) 115 | self.label_2.setText(_translate("MainWindow", "扫描信息")) 116 | self.radioButton_2.setText(_translate("MainWindow", "VEL")) 117 | self.radioButton_3.setText(_translate("MainWindow", "CC")) 118 | self.radioButton_4.setText(_translate("MainWindow", "ZDR")) 119 | self.radioButton_5.setText(_translate("MainWindow", "SW")) 120 | self.radioButton_6.setText(_translate("MainWindow", "PHIDP")) 121 | self.radioButton_7.setText(_translate("MainWindow", "ET")) 122 | self.radioButton_8.setText(_translate("MainWindow", "VIL")) 123 | self.radioButton_9.setText(_translate("MainWindow", "CR")) 124 | self.label_4.setText(_translate("MainWindow", "仰角")) 125 | self.pushButton_2.setText(_translate("MainWindow", "绘制")) 126 | self.menu.setTitle(_translate("MainWindow", "文件")) 127 | self.actionOpen.setText(_translate("MainWindow", "Open")) 128 | self.actionClose.setText(_translate("MainWindow", "Close")) 129 | self.actionBase_Reflectivity.setText( 130 | _translate("MainWindow", "Base Reflectivity") 131 | ) 132 | self.actionBase_Velocity.setText(_translate("MainWindow", "Base Velocity")) 133 | --------------------------------------------------------------------------------