rsplot 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rsplot-0.2.2 → rsplot-0.2.3}/PKG-INFO +4 -1
- {rsplot-0.2.2 → rsplot-0.2.3}/README.md +3 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/pyproject.toml +1 -1
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/cli.py +44 -10
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/config.py +1 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/__init__.py +35 -1
- rsplot-0.2.3/src/rsplot/readers/tropomi_o3pr.py +103 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/.gitignore +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/LICENSE +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/__init__.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/__main__.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/fnr.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/geo/__init__.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/geo/boundaries.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/geo/gridding.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/__init__.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/colormaps.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/fnr.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/overlay.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/raster.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/station.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/styles.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/base.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/guokong.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/tropomi_hcho.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/tropomi_no2.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/tropomi_o3.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/recent.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/results.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/tiles/__init__.py +0 -0
- {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/tiles/tianditu.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rsplot
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CLI tool for plotting Remote Sensing data, designed for Agent.
|
|
5
5
|
Project-URL: Repository, https://git.lug.ustc.edu.cn/yaoyhu/rsplot
|
|
6
6
|
Project-URL: GitHub, https://github.com/yaoyhu/rsplot
|
|
@@ -279,6 +279,9 @@ rsplot raster 合肥 20260115 -p no2 -o ./logs/hcho/raster_city_jiujiang_hcho.pn
|
|
|
279
279
|
# Plot a 30-day TROPOMI window mean.
|
|
280
280
|
rsplot raster 安徽 20260131 -p hcho --days 30 -o hcho_anhui_202601.png
|
|
281
281
|
|
|
282
|
+
# Plot a TROPOMI ozone-profile tropospheric column raster.
|
|
283
|
+
rsplot raster 安徽 20260227 -p o3pr -o o3pr_anhui_20260227.png
|
|
284
|
+
|
|
282
285
|
# Plot an explicit TROPOMI date range.
|
|
283
286
|
rsplot raster 长三角 20260101-20260131 -p no2 -o no2_yrd_202601.png
|
|
284
287
|
|
|
@@ -52,6 +52,9 @@ rsplot raster 合肥 20260115 -p no2 -o ./logs/hcho/raster_city_jiujiang_hcho.pn
|
|
|
52
52
|
# Plot a 30-day TROPOMI window mean.
|
|
53
53
|
rsplot raster 安徽 20260131 -p hcho --days 30 -o hcho_anhui_202601.png
|
|
54
54
|
|
|
55
|
+
# Plot a TROPOMI ozone-profile tropospheric column raster.
|
|
56
|
+
rsplot raster 安徽 20260227 -p o3pr -o o3pr_anhui_20260227.png
|
|
57
|
+
|
|
55
58
|
# Plot an explicit TROPOMI date range.
|
|
56
59
|
rsplot raster 长三角 20260101-20260131 -p no2 -o no2_yrd_202601.png
|
|
57
60
|
|
|
@@ -86,7 +86,7 @@ def raster(
|
|
|
86
86
|
"o3",
|
|
87
87
|
"--product",
|
|
88
88
|
"-p",
|
|
89
|
-
help="产品类型: no2 / o3 / hcho",
|
|
89
|
+
help="产品类型: no2 / o3 / o3pr / hcho",
|
|
90
90
|
),
|
|
91
91
|
days: int | None = typer.Option(
|
|
92
92
|
None,
|
|
@@ -98,8 +98,8 @@ def raster(
|
|
|
98
98
|
None,
|
|
99
99
|
"--n-min",
|
|
100
100
|
help=(
|
|
101
|
-
"窗口模式下每像元最少有效天数。默认: NO2/O3=
|
|
102
|
-
"HCHO=max(3, ceil(天数*0.25))"
|
|
101
|
+
"窗口模式下每像元最少有效天数。默认: NO2/O3/O3PR="
|
|
102
|
+
"max(3, 天数//2), HCHO=max(3, ceil(天数*0.25))"
|
|
103
103
|
),
|
|
104
104
|
),
|
|
105
105
|
level: str | None = typer.Option(
|
|
@@ -205,12 +205,25 @@ def raster(
|
|
|
205
205
|
|
|
206
206
|
# --- 3. Merge params (level defaults + CLI overrides) ---
|
|
207
207
|
params = info.params
|
|
208
|
-
use_res = res if res is not None else params.res
|
|
209
208
|
use_dpi = dpi if dpi is not None else cfg.dpi
|
|
210
209
|
prod_info = get_product_info(product)
|
|
210
|
+
product_defaults = prod_info.defaults_for_level(info.level)
|
|
211
|
+
use_res = (
|
|
212
|
+
res
|
|
213
|
+
if res is not None
|
|
214
|
+
else product_defaults.res
|
|
215
|
+
if product_defaults.res is not None
|
|
216
|
+
else params.res
|
|
217
|
+
)
|
|
211
218
|
use_cmap = cmap if cmap is not None else prod_info.cmap
|
|
212
219
|
use_qa = qa if qa is not None else cfg.qa_threshold
|
|
213
|
-
use_smooth =
|
|
220
|
+
use_smooth = (
|
|
221
|
+
smooth
|
|
222
|
+
if smooth is not None
|
|
223
|
+
else product_defaults.smooth_sigma
|
|
224
|
+
if product_defaults.smooth_sigma is not None
|
|
225
|
+
else params.smooth_sigma
|
|
226
|
+
)
|
|
214
227
|
|
|
215
228
|
if basemap is not None:
|
|
216
229
|
use_basemap: bool | None = basemap.lower() == "satellite"
|
|
@@ -590,6 +603,7 @@ def station(
|
|
|
590
603
|
_PRODUCT_STATION_PAIR: dict[str, str] = {
|
|
591
604
|
"no2": "NO2",
|
|
592
605
|
"o3": "O3",
|
|
606
|
+
"o3pr": "O3",
|
|
593
607
|
}
|
|
594
608
|
|
|
595
609
|
|
|
@@ -608,7 +622,7 @@ def overlay(
|
|
|
608
622
|
"o3",
|
|
609
623
|
"--product",
|
|
610
624
|
"-p",
|
|
611
|
-
help="卫星产品类型: no2 / o3",
|
|
625
|
+
help="卫星产品类型: no2 / o3 / o3pr / hcho",
|
|
612
626
|
),
|
|
613
627
|
# --- station variable ---
|
|
614
628
|
var: str | None = typer.Option(
|
|
@@ -714,12 +728,25 @@ def overlay(
|
|
|
714
728
|
|
|
715
729
|
# --- 5. Merge params ---
|
|
716
730
|
params = info.params
|
|
717
|
-
use_res = res if res is not None else params.res
|
|
718
731
|
use_dpi = dpi if dpi is not None else cfg.dpi
|
|
719
732
|
prod_info = get_product_info(product)
|
|
733
|
+
product_defaults = prod_info.defaults_for_level(info.level)
|
|
734
|
+
use_res = (
|
|
735
|
+
res
|
|
736
|
+
if res is not None
|
|
737
|
+
else product_defaults.res
|
|
738
|
+
if product_defaults.res is not None
|
|
739
|
+
else params.res
|
|
740
|
+
)
|
|
720
741
|
use_cmap = cmap if cmap is not None else prod_info.cmap
|
|
721
742
|
use_qa = qa if qa is not None else cfg.qa_threshold
|
|
722
|
-
use_smooth =
|
|
743
|
+
use_smooth = (
|
|
744
|
+
smooth
|
|
745
|
+
if smooth is not None
|
|
746
|
+
else product_defaults.smooth_sigma
|
|
747
|
+
if product_defaults.smooth_sigma is not None
|
|
748
|
+
else params.smooth_sigma
|
|
749
|
+
)
|
|
723
750
|
|
|
724
751
|
if basemap is not None:
|
|
725
752
|
use_basemap: bool | None = basemap.lower() == "satellite"
|
|
@@ -892,7 +919,7 @@ def recent(
|
|
|
892
919
|
"o3",
|
|
893
920
|
"--product",
|
|
894
921
|
"-p",
|
|
895
|
-
help="source=raster 时的产品: no2 / o3 / hcho",
|
|
922
|
+
help="source=raster 时的产品: no2 / o3 / o3pr / hcho",
|
|
896
923
|
),
|
|
897
924
|
var: str = typer.Option(
|
|
898
925
|
"AQI",
|
|
@@ -971,7 +998,14 @@ def recent(
|
|
|
971
998
|
|
|
972
999
|
if use_source == "raster":
|
|
973
1000
|
prod_info = get_product_info(product)
|
|
974
|
-
|
|
1001
|
+
product_defaults = prod_info.defaults_for_level(info.level)
|
|
1002
|
+
use_res = (
|
|
1003
|
+
res
|
|
1004
|
+
if res is not None
|
|
1005
|
+
else product_defaults.res
|
|
1006
|
+
if product_defaults.res is not None
|
|
1007
|
+
else info.params.res
|
|
1008
|
+
)
|
|
975
1009
|
use_qa = qa if qa is not None else cfg.qa_threshold
|
|
976
1010
|
use_data_dir = (
|
|
977
1011
|
data_dir if data_dir is not None else cfg.get_data_dir(product)
|
|
@@ -24,6 +24,7 @@ else:
|
|
|
24
24
|
DEFAULT_DATA_DIRS: dict[str, str] = {
|
|
25
25
|
"no2": "/satellite/exports2/yaoyhu/NO2",
|
|
26
26
|
"o3": "/satellite/exports2/yaoyhu/O3",
|
|
27
|
+
"o3pr": "/satellite/exports2/yaoyhu/O3PR",
|
|
27
28
|
"hcho": "/satellite/exports2/yaoyhu/HCHO",
|
|
28
29
|
"guokong": "/home/zzgsg/data/guokong_combine",
|
|
29
30
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
-
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
@@ -11,10 +11,19 @@ from rsplot.readers.base import BaseReader
|
|
|
11
11
|
from rsplot.readers.tropomi_hcho import TropomiHCHOReader
|
|
12
12
|
from rsplot.readers.tropomi_no2 import TropomiNO2Reader
|
|
13
13
|
from rsplot.readers.tropomi_o3 import TropomiO3Reader
|
|
14
|
+
from rsplot.readers.tropomi_o3pr import TropomiO3PRReader
|
|
14
15
|
|
|
15
16
|
HCHO_CMAP = "rsplot_hcho"
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
@dataclass
|
|
20
|
+
class ProductPlotDefaults:
|
|
21
|
+
"""Optional per-product plotting defaults by administrative level."""
|
|
22
|
+
|
|
23
|
+
res: float | None = None
|
|
24
|
+
smooth_sigma: float | None = None
|
|
25
|
+
|
|
26
|
+
|
|
18
27
|
@dataclass
|
|
19
28
|
class ProductInfo:
|
|
20
29
|
"""Per-product metadata: reader class, display settings, auto-range config."""
|
|
@@ -25,6 +34,13 @@ class ProductInfo:
|
|
|
25
34
|
vmin: float | None # None = auto from data percentiles
|
|
26
35
|
vmax: float | None
|
|
27
36
|
min_range: float # minimum vmax-vmin to avoid exaggerating small differences
|
|
37
|
+
level_defaults: dict[str, ProductPlotDefaults] = field(
|
|
38
|
+
default_factory=dict
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def defaults_for_level(self, level: str) -> ProductPlotDefaults:
|
|
42
|
+
"""Return product-specific defaults for an administrative level."""
|
|
43
|
+
return self.level_defaults.get(level, ProductPlotDefaults())
|
|
28
44
|
|
|
29
45
|
|
|
30
46
|
PRODUCT_REGISTRY: dict[str, ProductInfo] = {
|
|
@@ -44,6 +60,24 @@ PRODUCT_REGISTRY: dict[str, ProductInfo] = {
|
|
|
44
60
|
vmax=None,
|
|
45
61
|
min_range=30.0,
|
|
46
62
|
),
|
|
63
|
+
"o3pr": ProductInfo(
|
|
64
|
+
reader_cls=TropomiO3PRReader,
|
|
65
|
+
colorbar_label="O3 Tropospheric Column from Profile (DU)",
|
|
66
|
+
cmap="YlOrRd",
|
|
67
|
+
vmin=None,
|
|
68
|
+
vmax=None,
|
|
69
|
+
min_range=5.0,
|
|
70
|
+
level_defaults={
|
|
71
|
+
# O3__PR is a coarse 30x30 km profile product. These defaults
|
|
72
|
+
# keep map pixels close to the native sampling while still using
|
|
73
|
+
# modest smoothing for display.
|
|
74
|
+
"country": ProductPlotDefaults(res=0.25, smooth_sigma=1.0),
|
|
75
|
+
"key_region": ProductPlotDefaults(res=0.15, smooth_sigma=1.2),
|
|
76
|
+
"province": ProductPlotDefaults(res=0.1, smooth_sigma=1.5),
|
|
77
|
+
"city": ProductPlotDefaults(res=0.075, smooth_sigma=1.5),
|
|
78
|
+
"county": ProductPlotDefaults(res=0.05, smooth_sigma=2.0),
|
|
79
|
+
},
|
|
80
|
+
),
|
|
47
81
|
"hcho": ProductInfo(
|
|
48
82
|
reader_cls=TropomiHCHOReader,
|
|
49
83
|
colorbar_label="HCHO VCD (x10^15 molec/cm2)",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""TROPOMI O3 profile tropospheric-column reader.
|
|
2
|
+
|
|
3
|
+
Reads ``S5P_*_L2__O3__PR`` ozone profile products and uses the provided
|
|
4
|
+
``ozone_tropospheric_column`` variable directly. Values are converted from
|
|
5
|
+
``mol m-2`` to DU.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import glob
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from rich.progress import track
|
|
15
|
+
|
|
16
|
+
from rsplot.readers.base import (
|
|
17
|
+
BaseReader,
|
|
18
|
+
SwathData,
|
|
19
|
+
read_tropomi_product_arrays,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
VALUE_NAME = "ozone_tropospheric_column"
|
|
23
|
+
|
|
24
|
+
# mol/m2 -> DU, from the product attribute
|
|
25
|
+
# multiplication_factor_to_convert_to_DU = 2241.15
|
|
26
|
+
UNIT_CONVERSION = 2241.15
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TropomiO3PRReader(BaseReader):
|
|
30
|
+
"""Reader for TROPOMI L2 O3 profile tropospheric-column files."""
|
|
31
|
+
|
|
32
|
+
def read(
|
|
33
|
+
self,
|
|
34
|
+
data_dir: str,
|
|
35
|
+
date: str,
|
|
36
|
+
extent: tuple[float, float, float, float],
|
|
37
|
+
qa_threshold: float = 0.5,
|
|
38
|
+
) -> SwathData:
|
|
39
|
+
patterns = [
|
|
40
|
+
os.path.join(data_dir, f"S5P_*_L2__O3__PR_{date}T*.nc"),
|
|
41
|
+
os.path.join(data_dir, f"S5P_*_L2__O3__PR_{date}T*.nc.zip"),
|
|
42
|
+
]
|
|
43
|
+
files: list[str] = []
|
|
44
|
+
for p in patterns:
|
|
45
|
+
files.extend(glob.glob(p))
|
|
46
|
+
files = sorted(set(files))
|
|
47
|
+
if not files:
|
|
48
|
+
raise FileNotFoundError(
|
|
49
|
+
f"未找到匹配文件:\n "
|
|
50
|
+
+ "\n ".join(patterns)
|
|
51
|
+
+ "\n请检查 --data-dir 和 --date 参数。"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
lon_min, lon_max, lat_min, lat_max = extent
|
|
55
|
+
all_lon, all_lat, all_o3 = [], [], []
|
|
56
|
+
broken = 0
|
|
57
|
+
broken_examples: list[str] = []
|
|
58
|
+
|
|
59
|
+
for f in track(files, description="[cyan]读取 O3PR 轨道..."):
|
|
60
|
+
try:
|
|
61
|
+
lon, lat, o3, qa = read_tropomi_product_arrays(f, VALUE_NAME)
|
|
62
|
+
except (OSError, ValueError, KeyError) as e:
|
|
63
|
+
broken += 1
|
|
64
|
+
if len(broken_examples) < 3:
|
|
65
|
+
broken_examples.append(
|
|
66
|
+
f"{os.path.basename(f)}: {str(e).splitlines()[0]}"
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
mask = (
|
|
71
|
+
(qa > qa_threshold)
|
|
72
|
+
& (lon >= lon_min)
|
|
73
|
+
& (lon <= lon_max)
|
|
74
|
+
& (lat >= lat_min)
|
|
75
|
+
& (lat <= lat_max)
|
|
76
|
+
& np.isfinite(o3)
|
|
77
|
+
& (o3 < 1e30)
|
|
78
|
+
)
|
|
79
|
+
if mask.sum() > 0:
|
|
80
|
+
all_lon.append(lon[mask])
|
|
81
|
+
all_lat.append(lat[mask])
|
|
82
|
+
all_o3.append(o3[mask] * UNIT_CONVERSION)
|
|
83
|
+
|
|
84
|
+
if not all_lon:
|
|
85
|
+
broken_msg = (
|
|
86
|
+
"\n损坏/不可读示例:\n " + "\n ".join(broken_examples)
|
|
87
|
+
if broken_examples
|
|
88
|
+
else ""
|
|
89
|
+
)
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"{date} 当天 O3PR 无有效数据覆盖目标区域 {extent}。\n"
|
|
92
|
+
f"共扫描 {len(files)} 个文件,损坏 {broken} 个。"
|
|
93
|
+
f"{broken_msg}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return SwathData(
|
|
97
|
+
lon=np.concatenate(all_lon),
|
|
98
|
+
lat=np.concatenate(all_lat),
|
|
99
|
+
values=np.concatenate(all_o3),
|
|
100
|
+
n_pixels=sum(len(a) for a in all_lon),
|
|
101
|
+
n_files=len(files),
|
|
102
|
+
n_broken=broken,
|
|
103
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|