rsplot 0.3.1.dev3__py3-none-any.whl → 0.3.2.dev4__py3-none-any.whl
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/cli.py +48 -10
- rsplot/commands/common.py +64 -1
- rsplot/commands/forecast.py +24 -5
- rsplot/commands/overlay.py +172 -43
- rsplot/commands/raster.py +168 -44
- rsplot/commands/recent.py +29 -7
- rsplot/commands/reconstruct.py +27 -5
- rsplot/config.py +3 -0
- rsplot/geo/boundaries.py +105 -2
- rsplot/geo/footprints.py +202 -0
- rsplot/geo/processing.py +82 -0
- rsplot/plotting/map_frame.py +120 -4
- rsplot/plotting/overlay.py +5 -0
- rsplot/plotting/raster.py +5 -0
- rsplot/readers/__init__.py +38 -4
- rsplot/readers/base.py +16 -1
- rsplot/readers/gems.py +426 -0
- rsplot/recent.py +59 -16
- rsplot/result_builders/common.py +81 -0
- rsplot/result_builders/forecast.py +7 -0
- rsplot/result_builders/overlay.py +32 -2
- rsplot/result_builders/raster.py +32 -0
- rsplot/result_builders/reconstruct.py +7 -0
- rsplot/temporal.py +139 -16
- {rsplot-0.3.1.dev3.dist-info → rsplot-0.3.2.dev4.dist-info}/METADATA +215 -1
- {rsplot-0.3.1.dev3.dist-info → rsplot-0.3.2.dev4.dist-info}/RECORD +29 -27
- {rsplot-0.3.1.dev3.dist-info → rsplot-0.3.2.dev4.dist-info}/WHEEL +0 -0
- {rsplot-0.3.1.dev3.dist-info → rsplot-0.3.2.dev4.dist-info}/entry_points.txt +0 -0
- {rsplot-0.3.1.dev3.dist-info → rsplot-0.3.2.dev4.dist-info}/licenses/LICENSE +0 -0
rsplot/cli.py
CHANGED
|
@@ -45,14 +45,18 @@ def _callback(
|
|
|
45
45
|
@app.command()
|
|
46
46
|
def raster(
|
|
47
47
|
region: str = typer.Argument(
|
|
48
|
-
...,
|
|
48
|
+
...,
|
|
49
|
+
help="区域名称;支持逗号分隔多个省市县街道,如 '中山市/东区街道,中山市/南区街道',无需配置",
|
|
49
50
|
),
|
|
50
51
|
date_arg: str = typer.Argument(
|
|
51
52
|
...,
|
|
52
|
-
help="
|
|
53
|
+
help="GEMS: 北京时间 YYYYMMDDHHMM 精确扫描,YYYYMMDD 日均;单日 YYYYMMDD 走单日路径;带 --days 或写成 YYYYMMDD-YYYYMMDD "
|
|
53
54
|
"时切换为窗口均值(HCHO 单日覆盖率低,月均更可读)",
|
|
54
55
|
),
|
|
55
56
|
# --- optional overrides ---
|
|
57
|
+
sensor: str = typer.Option(
|
|
58
|
+
"tropomi", "--sensor", help="卫星来源: tropomi / gems"
|
|
59
|
+
),
|
|
56
60
|
product: str = typer.Option(
|
|
57
61
|
"o3",
|
|
58
62
|
"--product",
|
|
@@ -92,7 +96,11 @@ def raster(
|
|
|
92
96
|
help="输出文件路径,不指定则弹窗显示。",
|
|
93
97
|
),
|
|
94
98
|
dpi: int | None = typer.Option(300, "--dpi", help="输出 DPI"),
|
|
95
|
-
res: float | None = typer.Option(
|
|
99
|
+
res: float | None = typer.Option(
|
|
100
|
+
None,
|
|
101
|
+
"--res",
|
|
102
|
+
help="网格分辨率 (度);GEMS 绘图仅控制显示,分析网格固定 0.05°",
|
|
103
|
+
),
|
|
96
104
|
basemap: str | None = typer.Option(
|
|
97
105
|
None,
|
|
98
106
|
"--basemap",
|
|
@@ -106,7 +114,9 @@ def raster(
|
|
|
106
114
|
None, "--smooth", help="高斯平滑 sigma"
|
|
107
115
|
),
|
|
108
116
|
title: str | None = typer.Option(None, "--title", help="自定义标题"),
|
|
109
|
-
qa: float | None = typer.Option(
|
|
117
|
+
qa: float | None = typer.Option(
|
|
118
|
+
None, "--qa", help="TROPOMI QA 阈值 (0-1),默认取配置;GEMS 不适用"
|
|
119
|
+
),
|
|
110
120
|
cf: float = typer.Option(
|
|
111
121
|
0.5,
|
|
112
122
|
"--cf",
|
|
@@ -128,6 +138,7 @@ def raster(
|
|
|
128
138
|
region=region,
|
|
129
139
|
date_arg=date_arg,
|
|
130
140
|
product=product,
|
|
141
|
+
sensor=sensor,
|
|
131
142
|
days=days,
|
|
132
143
|
n_min=n_min,
|
|
133
144
|
level=level,
|
|
@@ -620,20 +631,29 @@ def trend(
|
|
|
620
631
|
@app.command()
|
|
621
632
|
def overlay(
|
|
622
633
|
region: str = typer.Argument(
|
|
623
|
-
...,
|
|
634
|
+
...,
|
|
635
|
+
help="区域名称;支持逗号分隔多个省市县街道,如 '中山市/东区街道,中山市/南区街道',无需配置",
|
|
624
636
|
),
|
|
625
637
|
datetime_str: str = typer.Argument(
|
|
626
638
|
...,
|
|
627
|
-
help="YYYYMMDDHH (精确小时) 或 YYYYMMDD (自动取最新小时);"
|
|
639
|
+
help="GEMS: 北京时间 YYYYMMDD 日均(需 --station-datetime),或 YYYYMMDDHHMM 精确扫描(自动匹配最近站点整点);TROPOMI: YYYYMMDDHH (精确小时) 或 YYYYMMDD (自动取最新小时);"
|
|
628
640
|
"日期部分用于卫星,小时部分用于站点",
|
|
629
641
|
),
|
|
630
642
|
# --- satellite product ---
|
|
643
|
+
sensor: str = typer.Option(
|
|
644
|
+
"tropomi", "--sensor", help="卫星来源: tropomi / gems"
|
|
645
|
+
),
|
|
631
646
|
product: str = typer.Option(
|
|
632
647
|
"o3",
|
|
633
648
|
"--product",
|
|
634
649
|
"-p",
|
|
635
650
|
help="卫星产品类型: no2 / o3 / o3pr / hcho",
|
|
636
651
|
),
|
|
652
|
+
station_datetime: str | None = typer.Option(
|
|
653
|
+
None,
|
|
654
|
+
"--station-datetime",
|
|
655
|
+
help="GEMS 叠加站点时间,北京时间 YYYYMMDDHH",
|
|
656
|
+
),
|
|
637
657
|
# --- station variable ---
|
|
638
658
|
var: str | None = typer.Option(
|
|
639
659
|
None,
|
|
@@ -665,7 +685,11 @@ def overlay(
|
|
|
665
685
|
help="输出文件路径,不指定则弹窗显示。",
|
|
666
686
|
),
|
|
667
687
|
dpi: int | None = typer.Option(300, "--dpi", help="输出 DPI"),
|
|
668
|
-
res: float | None = typer.Option(
|
|
688
|
+
res: float | None = typer.Option(
|
|
689
|
+
None,
|
|
690
|
+
"--res",
|
|
691
|
+
help="网格分辨率 (度);GEMS 绘图仅控制显示,分析网格固定 0.05°",
|
|
692
|
+
),
|
|
669
693
|
basemap: str | None = typer.Option(
|
|
670
694
|
None,
|
|
671
695
|
"--basemap",
|
|
@@ -682,7 +706,9 @@ def overlay(
|
|
|
682
706
|
smooth: float | None = typer.Option(
|
|
683
707
|
None, "--smooth", help="高斯平滑 sigma"
|
|
684
708
|
),
|
|
685
|
-
qa: float | None = typer.Option(
|
|
709
|
+
qa: float | None = typer.Option(
|
|
710
|
+
None, "--qa", help="TROPOMI QA 阈值 (0-1),默认取配置;GEMS 不适用"
|
|
711
|
+
),
|
|
686
712
|
cf: float = typer.Option(
|
|
687
713
|
0.5,
|
|
688
714
|
"--cf",
|
|
@@ -708,7 +734,9 @@ def overlay(
|
|
|
708
734
|
run_overlay(
|
|
709
735
|
region=region,
|
|
710
736
|
datetime_str=datetime_str,
|
|
737
|
+
station_datetime=station_datetime,
|
|
711
738
|
product=product,
|
|
739
|
+
sensor=sensor,
|
|
712
740
|
var=var,
|
|
713
741
|
level=level,
|
|
714
742
|
raster_dir=raster_dir,
|
|
@@ -746,6 +774,9 @@ def recent(
|
|
|
746
774
|
"--source",
|
|
747
775
|
help="数据源: raster / station",
|
|
748
776
|
),
|
|
777
|
+
sensor: str = typer.Option(
|
|
778
|
+
"tropomi", "--sensor", help="卫星来源: tropomi / gems"
|
|
779
|
+
),
|
|
749
780
|
product: str = typer.Option(
|
|
750
781
|
"o3",
|
|
751
782
|
"--product",
|
|
@@ -776,8 +807,14 @@ def recent(
|
|
|
776
807
|
"-o",
|
|
777
808
|
help="输出 JSON 路径",
|
|
778
809
|
),
|
|
779
|
-
res: float | None = typer.Option(
|
|
780
|
-
|
|
810
|
+
res: float | None = typer.Option(
|
|
811
|
+
None,
|
|
812
|
+
"--res",
|
|
813
|
+
help="TROPOMI 网格分辨率 (度);GEMS 统计固定使用 0.05°,忽略此选项",
|
|
814
|
+
),
|
|
815
|
+
qa: float | None = typer.Option(
|
|
816
|
+
None, "--qa", help="TROPOMI QA 阈值 (0-1),默认取配置;GEMS 不适用"
|
|
817
|
+
),
|
|
781
818
|
cf: float = typer.Option(
|
|
782
819
|
0.5,
|
|
783
820
|
"--cf",
|
|
@@ -800,6 +837,7 @@ def recent(
|
|
|
800
837
|
days=days,
|
|
801
838
|
source=source,
|
|
802
839
|
product=product,
|
|
840
|
+
sensor=sensor,
|
|
803
841
|
var=var,
|
|
804
842
|
level=level,
|
|
805
843
|
data_dir=data_dir,
|
rsplot/commands/common.py
CHANGED
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, ParamSpec, TypeVar
|
|
8
|
+
|
|
9
|
+
import typer
|
|
7
10
|
|
|
8
11
|
if TYPE_CHECKING:
|
|
9
12
|
import geopandas as gpd
|
|
10
13
|
from rich.console import Console
|
|
11
14
|
|
|
15
|
+
from rsplot.config import AppConfig
|
|
12
16
|
from rsplot.geo.boundaries import RegionInfo
|
|
17
|
+
from rsplot.readers import ProductInfo
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
FORECAST_STATION_PAIR: dict[str, str] = {
|
|
@@ -110,3 +115,61 @@ def print_failed_dates(
|
|
|
110
115
|
console.print(f" - {date}: {message}")
|
|
111
116
|
if len(items) > max_items:
|
|
112
117
|
console.print(f" ... 另有 {len(items) - max_items} 天")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_satellite(
|
|
121
|
+
product: str,
|
|
122
|
+
sensor: str,
|
|
123
|
+
qa: float | None,
|
|
124
|
+
cfg: AppConfig,
|
|
125
|
+
console: Console,
|
|
126
|
+
) -> tuple[ProductInfo, float]:
|
|
127
|
+
"""Resolve source and QA policy before opening regional/data files."""
|
|
128
|
+
from rsplot.readers import get_product_info
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
info = get_product_info(product, sensor=sensor)
|
|
132
|
+
if info.sensor == "gems" and qa is not None:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"--qa 仅用于 TROPOMI;GEMS 使用最终质量标记 = 0 "
|
|
135
|
+
"及 SZA/VZA < 70°,云量可通过 --cf 设置。"
|
|
136
|
+
)
|
|
137
|
+
threshold = (
|
|
138
|
+
0.5
|
|
139
|
+
if info.sensor == "gems"
|
|
140
|
+
else qa
|
|
141
|
+
if qa is not None
|
|
142
|
+
else cfg.qa_threshold
|
|
143
|
+
)
|
|
144
|
+
validate_unit_interval(threshold, "--qa")
|
|
145
|
+
except ValueError as error:
|
|
146
|
+
console.print(f"[red]{error}[/red]")
|
|
147
|
+
raise typer.Exit(1) from error
|
|
148
|
+
return info, threshold
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def satellite_filter_label(sensor: str, qa: float, cf: float) -> str:
|
|
152
|
+
"""Describe the actual filter rather than a synthetic GEMS QA value."""
|
|
153
|
+
quality = (
|
|
154
|
+
"最终质量标记 = 0, SZA/VZA < 70°" if sensor == "gems" else f"QA > {qa}"
|
|
155
|
+
)
|
|
156
|
+
return f"{quality}, 云量 <= {cf}"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_P = ParamSpec("_P")
|
|
160
|
+
_R = TypeVar("_R")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def satellite_errors(function: Callable[_P, _R]) -> Callable[_P, _R]:
|
|
164
|
+
"""Render expected satellite input/data errors in the CLI's style."""
|
|
165
|
+
from functools import wraps
|
|
166
|
+
|
|
167
|
+
@wraps(function)
|
|
168
|
+
def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
|
169
|
+
try:
|
|
170
|
+
return function(*args, **kwargs)
|
|
171
|
+
except (ValueError, FileNotFoundError, OSError) as error:
|
|
172
|
+
kwargs["console"].print(f"[red]{error}[/red]")
|
|
173
|
+
raise typer.Exit(1) from error
|
|
174
|
+
|
|
175
|
+
return wrapped
|
rsplot/commands/forecast.py
CHANGED
|
@@ -53,6 +53,7 @@ def run_forecast(
|
|
|
53
53
|
from rsplot.geo.boundaries import resolve_region
|
|
54
54
|
from rsplot.geo.processing import (
|
|
55
55
|
buffered_extent,
|
|
56
|
+
prepare_display_grid,
|
|
56
57
|
prepare_existing_grid,
|
|
57
58
|
)
|
|
58
59
|
from rsplot.plotting.colormaps import STATION_CONCENTRATION_CMAP
|
|
@@ -151,10 +152,22 @@ def run_forecast(
|
|
|
151
152
|
info.geometry,
|
|
152
153
|
fill_gaps=False,
|
|
153
154
|
)
|
|
154
|
-
|
|
155
|
+
display_source = prepared.unmasked_values
|
|
156
|
+
display_grid = prepare_display_grid(
|
|
157
|
+
prepared.lon,
|
|
158
|
+
prepared.lat,
|
|
159
|
+
display_source,
|
|
160
|
+
info.geometry,
|
|
161
|
+
smooth_sigma=smooth,
|
|
162
|
+
)
|
|
163
|
+
if not np.isfinite(display_grid).any():
|
|
155
164
|
console.print("[red]目标区域内没有有效预报网格。[/red]")
|
|
156
165
|
raise typer.Exit(1)
|
|
157
|
-
if
|
|
166
|
+
if (
|
|
167
|
+
smooth is not None
|
|
168
|
+
and smooth > 0
|
|
169
|
+
and np.isfinite(prepared.values).any()
|
|
170
|
+
):
|
|
158
171
|
console.print(f" → 高斯平滑 sigma={smooth}")
|
|
159
172
|
prepared = prepare_existing_grid(
|
|
160
173
|
prepared.lon,
|
|
@@ -170,7 +183,12 @@ def run_forecast(
|
|
|
170
183
|
f"({prepared.n_valid_region}/{prepared.total_region_pixels})"
|
|
171
184
|
)
|
|
172
185
|
|
|
173
|
-
auto_min, auto_max = auto_forecast_vrange(
|
|
186
|
+
auto_min, auto_max = auto_forecast_vrange(
|
|
187
|
+
prepared.values
|
|
188
|
+
if np.isfinite(prepared.values).any()
|
|
189
|
+
else display_grid,
|
|
190
|
+
var_info,
|
|
191
|
+
)
|
|
174
192
|
use_vmin = vmin if vmin is not None else auto_min
|
|
175
193
|
use_vmax = vmax if vmax is not None else auto_max
|
|
176
194
|
try:
|
|
@@ -289,7 +307,7 @@ def run_forecast(
|
|
|
289
307
|
plot_raster(
|
|
290
308
|
prepared.lon,
|
|
291
309
|
prepared.lat,
|
|
292
|
-
|
|
310
|
+
display_grid,
|
|
293
311
|
info,
|
|
294
312
|
dpi=use_dpi,
|
|
295
313
|
cmap=use_cmap,
|
|
@@ -306,7 +324,7 @@ def run_forecast(
|
|
|
306
324
|
plot_overlay(
|
|
307
325
|
prepared.lon,
|
|
308
326
|
prepared.lat,
|
|
309
|
-
|
|
327
|
+
display_grid,
|
|
310
328
|
station_data,
|
|
311
329
|
info,
|
|
312
330
|
raster_cmap=use_cmap,
|
|
@@ -330,6 +348,7 @@ def run_forecast(
|
|
|
330
348
|
LON=prepared.lon,
|
|
331
349
|
LAT=prepared.lat,
|
|
332
350
|
grid=prepared.values,
|
|
351
|
+
display_grid=display_grid,
|
|
333
352
|
vmin=use_vmin,
|
|
334
353
|
vmax=use_vmax,
|
|
335
354
|
output=output,
|
rsplot/commands/overlay.py
CHANGED
|
@@ -10,6 +10,9 @@ import typer
|
|
|
10
10
|
from rsplot.commands.common import (
|
|
11
11
|
load_fallback_city_boundaries,
|
|
12
12
|
parse_basemap,
|
|
13
|
+
resolve_satellite,
|
|
14
|
+
satellite_errors,
|
|
15
|
+
satellite_filter_label,
|
|
13
16
|
validate_color_range,
|
|
14
17
|
validate_non_negative,
|
|
15
18
|
validate_positive,
|
|
@@ -29,11 +32,14 @@ PRODUCT_STATION_PAIR = {
|
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
@satellite_errors
|
|
32
36
|
def run_overlay(
|
|
33
37
|
*,
|
|
34
38
|
region: str,
|
|
35
39
|
datetime_str: str,
|
|
36
40
|
product: str,
|
|
41
|
+
sensor: str = "tropomi",
|
|
42
|
+
station_datetime: str | None = None,
|
|
37
43
|
var: str | None,
|
|
38
44
|
level: str | None,
|
|
39
45
|
raster_dir: str | None,
|
|
@@ -53,15 +59,34 @@ def run_overlay(
|
|
|
53
59
|
) -> None:
|
|
54
60
|
"""Execute satellite/station overlay orchestration."""
|
|
55
61
|
from rsplot.config import STATION_VAR_META, get_exceedance_threshold
|
|
56
|
-
from rsplot.geo.boundaries import
|
|
57
|
-
from rsplot.geo.processing import
|
|
62
|
+
from rsplot.geo.boundaries import resolve_regions
|
|
63
|
+
from rsplot.geo.processing import (
|
|
64
|
+
buffered_extent,
|
|
65
|
+
prepare_display_grid,
|
|
66
|
+
prepare_existing_grid,
|
|
67
|
+
prepare_swath_grid,
|
|
68
|
+
)
|
|
58
69
|
from rsplot.plotting.overlay import plot_overlay
|
|
59
70
|
from rsplot.plotting.styles import setup_fonts
|
|
60
|
-
from rsplot.readers import auto_vrange
|
|
71
|
+
from rsplot.readers import auto_vrange
|
|
61
72
|
from rsplot.readers.guokong import mask_stations_to_region, read_stations
|
|
62
73
|
from rsplot.results import build_overlay_result, emit_result
|
|
63
74
|
|
|
64
75
|
cfg = AppConfig.load()
|
|
76
|
+
prod_info, use_qa = resolve_satellite(product, sensor, qa, cfg, console)
|
|
77
|
+
sensor = prod_info.sensor
|
|
78
|
+
pairing = None
|
|
79
|
+
station_request = datetime_str
|
|
80
|
+
if sensor == "gems":
|
|
81
|
+
from rsplot.readers.gems import match_station_hour
|
|
82
|
+
|
|
83
|
+
station_request, pairing = match_station_hour(
|
|
84
|
+
datetime_str, station_datetime
|
|
85
|
+
)
|
|
86
|
+
if product.lower() == "hcho" and var is None:
|
|
87
|
+
raise ValueError("GEMS HCHO 叠加需通过 --var 显式指定站点变量。")
|
|
88
|
+
elif station_datetime is not None:
|
|
89
|
+
raise ValueError("--station-datetime 在此命令中仅用于 GEMS。")
|
|
65
90
|
setup_fonts(cfg.font_dir)
|
|
66
91
|
|
|
67
92
|
if var is None:
|
|
@@ -71,7 +96,9 @@ def run_overlay(
|
|
|
71
96
|
console.print(f"[red]未知站点变量 '{var}'[/red]。可用: {available}")
|
|
72
97
|
raise typer.Exit(1)
|
|
73
98
|
|
|
74
|
-
if
|
|
99
|
+
if sensor != "gems" and (
|
|
100
|
+
not datetime_str.isdigit() or len(datetime_str) not in (8, 10)
|
|
101
|
+
):
|
|
75
102
|
console.print(
|
|
76
103
|
f"[red]datetime 参数 '{datetime_str}' 格式不正确[/red],"
|
|
77
104
|
"请使用 YYYYMMDD 或 YYYYMMDDHH。"
|
|
@@ -80,8 +107,15 @@ def run_overlay(
|
|
|
80
107
|
date = datetime_str[:8]
|
|
81
108
|
|
|
82
109
|
console.print(f"[bold]解析区域:[/bold] {region}")
|
|
83
|
-
info =
|
|
110
|
+
info = resolve_regions(region, cfg, level_override=level)
|
|
84
111
|
console.print(f" → [green]{info.name}[/green] (级别: {info.level})")
|
|
112
|
+
if info.members:
|
|
113
|
+
console.print(
|
|
114
|
+
" → 成员: "
|
|
115
|
+
+ "、".join(
|
|
116
|
+
member.qualified_name or member.name for member in info.members
|
|
117
|
+
)
|
|
118
|
+
)
|
|
85
119
|
console.print(
|
|
86
120
|
f" → 范围: {info.extent[0]:.2f}°~{info.extent[1]:.2f}°E, "
|
|
87
121
|
f"{info.extent[2]:.2f}°~{info.extent[3]:.2f}°N"
|
|
@@ -89,7 +123,6 @@ def run_overlay(
|
|
|
89
123
|
|
|
90
124
|
params = info.params
|
|
91
125
|
use_dpi = dpi if dpi is not None else cfg.dpi
|
|
92
|
-
prod_info = get_product_info(product)
|
|
93
126
|
product_defaults = prod_info.defaults_for_level(info.level)
|
|
94
127
|
use_res = (
|
|
95
128
|
res
|
|
@@ -99,7 +132,6 @@ def run_overlay(
|
|
|
99
132
|
else params.res
|
|
100
133
|
)
|
|
101
134
|
use_cmap = cmap if cmap is not None else prod_info.cmap
|
|
102
|
-
use_qa = qa if qa is not None else cfg.qa_threshold
|
|
103
135
|
use_smooth = (
|
|
104
136
|
smooth
|
|
105
137
|
if smooth is not None
|
|
@@ -129,36 +161,117 @@ def run_overlay(
|
|
|
129
161
|
)
|
|
130
162
|
|
|
131
163
|
use_raster_dir = (
|
|
132
|
-
raster_dir
|
|
164
|
+
raster_dir
|
|
165
|
+
if raster_dir is not None
|
|
166
|
+
else cfg.get_data_dir(prod_info.data_key or product)
|
|
133
167
|
)
|
|
134
168
|
reader = prod_info.reader_cls()
|
|
135
169
|
reader.cloud_fraction_max = cf
|
|
170
|
+
analysis_res = use_res
|
|
171
|
+
if sensor == "gems":
|
|
172
|
+
from rsplot.readers.gems import GEMS_ANALYSIS_RESOLUTION
|
|
173
|
+
|
|
174
|
+
analysis_res = GEMS_ANALYSIS_RESOLUTION
|
|
175
|
+
console.print(
|
|
176
|
+
f" → GEMS 分析网格 {analysis_res}°,显示网格 {use_res}°"
|
|
177
|
+
)
|
|
136
178
|
read_extent = buffered_extent(info.extent, 1.0)
|
|
137
179
|
console.print(
|
|
138
180
|
f"\n[bold]读取卫星数据:[/bold] {use_raster_dir} (产品: {product})"
|
|
139
181
|
)
|
|
140
|
-
console.print(f" 日期: {date}, QA > {use_qa}, CF <= {cf}")
|
|
141
|
-
swath = reader.read(
|
|
142
|
-
use_raster_dir,
|
|
143
|
-
date,
|
|
144
|
-
read_extent,
|
|
145
|
-
qa_threshold=use_qa,
|
|
146
|
-
)
|
|
147
182
|
console.print(
|
|
148
|
-
f"
|
|
149
|
-
f"({swath.n_files} 文件, {swath.n_broken} 损坏)"
|
|
183
|
+
f" 时间: {datetime_str}, {satellite_filter_label(sensor, use_qa, cf)}"
|
|
150
184
|
)
|
|
185
|
+
statistics_grid = None
|
|
186
|
+
if sensor == "gems":
|
|
187
|
+
from rsplot.temporal import read_scan_mean
|
|
151
188
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
batch = read_scan_mean(
|
|
190
|
+
reader=reader,
|
|
191
|
+
data_dir=use_raster_dir,
|
|
192
|
+
request=datetime_str,
|
|
193
|
+
extent=read_extent,
|
|
194
|
+
res=analysis_res,
|
|
195
|
+
)
|
|
196
|
+
prepared = prepare_existing_grid(
|
|
197
|
+
batch.LON,
|
|
198
|
+
batch.LAT,
|
|
199
|
+
batch.grid,
|
|
200
|
+
info.geometry,
|
|
201
|
+
fill_gaps=False,
|
|
202
|
+
)
|
|
203
|
+
display_source = prepared.unmasked_values
|
|
204
|
+
statistics_grid = prepared.values.copy()
|
|
205
|
+
swath_meta = {
|
|
206
|
+
"n_pixels": batch.n_pixels,
|
|
207
|
+
"n_files": batch.n_files,
|
|
208
|
+
"n_broken": batch.n_broken,
|
|
209
|
+
"resolution_deg": use_res,
|
|
210
|
+
"cloud_fraction_max": cf,
|
|
211
|
+
**batch.metadata,
|
|
212
|
+
}
|
|
213
|
+
else:
|
|
214
|
+
swath = reader.read(
|
|
215
|
+
use_raster_dir,
|
|
216
|
+
date,
|
|
217
|
+
read_extent,
|
|
218
|
+
qa_threshold=use_qa,
|
|
219
|
+
)
|
|
220
|
+
console.print(
|
|
221
|
+
f" → [green]{swath.n_pixels}[/green] 有效像元 "
|
|
222
|
+
f"({swath.n_files} 文件, {swath.n_broken} 损坏)"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
console.print(f"\n[bold]网格化:[/bold] {use_res}° 分辨率")
|
|
226
|
+
prepared = prepare_swath_grid(
|
|
227
|
+
swath.lon,
|
|
228
|
+
swath.lat,
|
|
229
|
+
swath.values,
|
|
230
|
+
read_extent,
|
|
231
|
+
use_res,
|
|
232
|
+
info.geometry,
|
|
233
|
+
smooth_sigma=use_smooth,
|
|
234
|
+
)
|
|
235
|
+
display_source = prepared.unmasked_values
|
|
236
|
+
swath_meta = {
|
|
237
|
+
"n_pixels": int(swath.n_pixels),
|
|
238
|
+
"n_files": int(swath.n_files),
|
|
239
|
+
"n_broken": int(swath.n_broken),
|
|
240
|
+
"qa_threshold": use_qa,
|
|
241
|
+
"cloud_fraction_max": cf,
|
|
242
|
+
"resolution_deg": use_res,
|
|
243
|
+
}
|
|
244
|
+
display_lon, display_lat = prepared.lon, prepared.lat
|
|
245
|
+
display_support = None
|
|
246
|
+
if sensor == "gems":
|
|
247
|
+
from rsplot.geo.footprints import prepare_footprint_display
|
|
248
|
+
|
|
249
|
+
display = prepare_footprint_display(
|
|
250
|
+
prepared.lon,
|
|
251
|
+
prepared.lat,
|
|
252
|
+
display_source,
|
|
253
|
+
info.geometry,
|
|
254
|
+
analysis_res=analysis_res,
|
|
255
|
+
display_res=use_res,
|
|
256
|
+
smooth_sigma=use_smooth,
|
|
257
|
+
)
|
|
258
|
+
display_lon, display_lat = display.lon, display.lat
|
|
259
|
+
display_grid, display_support = display.values, display.support
|
|
260
|
+
swath_meta["resolution_deg"] = analysis_res
|
|
261
|
+
swath_meta["analysis_resolution_deg"] = analysis_res
|
|
262
|
+
swath_meta["display_resolution_deg"] = use_res
|
|
263
|
+
swath_meta["display_support"] = "fixed_analysis_valid_cells"
|
|
264
|
+
else:
|
|
265
|
+
display_grid = prepare_display_grid(
|
|
266
|
+
prepared.lon,
|
|
267
|
+
prepared.lat,
|
|
268
|
+
display_source,
|
|
269
|
+
info.geometry,
|
|
270
|
+
smooth_sigma=use_smooth,
|
|
271
|
+
)
|
|
272
|
+
if not np.isfinite(display_grid).any():
|
|
273
|
+
console.print("[red]目标区域没有相交的有效卫星网格。[/red]")
|
|
274
|
+
raise typer.Exit(1)
|
|
162
275
|
console.print(
|
|
163
276
|
f" → 网格 {prepared.values.shape}, "
|
|
164
277
|
f"区域内覆盖率 {prepared.coverage_pct:.1f}% "
|
|
@@ -171,7 +284,12 @@ def run_overlay(
|
|
|
171
284
|
use_vmin = vmin if vmin is not None else prod_info.vmin
|
|
172
285
|
use_vmax = vmax if vmax is not None else prod_info.vmax
|
|
173
286
|
else:
|
|
174
|
-
auto_min, auto_max = auto_vrange(
|
|
287
|
+
auto_min, auto_max = auto_vrange(
|
|
288
|
+
prepared.values
|
|
289
|
+
if np.isfinite(prepared.values).any()
|
|
290
|
+
else display_grid,
|
|
291
|
+
prod_info,
|
|
292
|
+
)
|
|
175
293
|
use_vmin = vmin if vmin is not None else auto_min
|
|
176
294
|
use_vmax = vmax if vmax is not None else auto_max
|
|
177
295
|
try:
|
|
@@ -185,16 +303,16 @@ def run_overlay(
|
|
|
185
303
|
station_dir if station_dir is not None else cfg.get_data_dir("guokong")
|
|
186
304
|
)
|
|
187
305
|
console.print(f"\n[bold]读取站点数据:[/bold] {use_station_dir}")
|
|
188
|
-
console.print(f" 时间: {
|
|
306
|
+
console.print(f" 时间: {station_request}, 变量: {var}")
|
|
189
307
|
station_extent = buffered_extent(info.extent, 0.5)
|
|
190
308
|
data, resolved_dt = read_stations(
|
|
191
309
|
use_station_dir,
|
|
192
|
-
|
|
310
|
+
station_request,
|
|
193
311
|
var,
|
|
194
312
|
extent=station_extent,
|
|
195
313
|
)
|
|
196
314
|
data = mask_stations_to_region(data, info.geometry)
|
|
197
|
-
if resolved_dt !=
|
|
315
|
+
if resolved_dt != station_request:
|
|
198
316
|
console.print(f" → 自动选择最新时次: [cyan]{resolved_dt}[/cyan]")
|
|
199
317
|
console.print(
|
|
200
318
|
f" → [green]{data.n_valid}[/green] 有效站点 (共 {data.n_stations} 站)"
|
|
@@ -209,6 +327,17 @@ def run_overlay(
|
|
|
209
327
|
f" → 超标阈值: {threshold}, 超标站点: [red]{n_exceed}[/red]"
|
|
210
328
|
)
|
|
211
329
|
|
|
330
|
+
if output is None and sensor == "gems":
|
|
331
|
+
output = (
|
|
332
|
+
f"/tmp/rsplot_overlay_gems_{info.name}_{product}_"
|
|
333
|
+
f"{datetime_str}_{resolved_dt}.png"
|
|
334
|
+
)
|
|
335
|
+
if title is None and sensor == "gems":
|
|
336
|
+
period = "daily mean" if len(datetime_str) == 8 else "scan"
|
|
337
|
+
title = (
|
|
338
|
+
f"GEMS {product.upper()} {datetime_str} {period} CST + "
|
|
339
|
+
f"Station {var} {resolved_dt} CST - {info.name}"
|
|
340
|
+
)
|
|
212
341
|
if output is None:
|
|
213
342
|
output = f"/tmp/rsplot_overlay_{info.name}_{product}_{resolved_dt}.png"
|
|
214
343
|
unit = STATION_VAR_META[var][0]
|
|
@@ -216,9 +345,9 @@ def run_overlay(
|
|
|
216
345
|
|
|
217
346
|
console.print("\n[bold]绘图中...[/bold]")
|
|
218
347
|
plot_overlay(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
348
|
+
display_lon,
|
|
349
|
+
display_lat,
|
|
350
|
+
display_grid,
|
|
222
351
|
data,
|
|
223
352
|
info,
|
|
224
353
|
raster_cmap=use_cmap,
|
|
@@ -233,6 +362,7 @@ def run_overlay(
|
|
|
233
362
|
hcho_cmap_file=cfg.hcho_cmap_file,
|
|
234
363
|
title=title,
|
|
235
364
|
output=output,
|
|
365
|
+
**({"coverage_geometry": display_support} if sensor == "gems" else {}),
|
|
236
366
|
)
|
|
237
367
|
console.print(f"\n[bold green]✓ 已保存:[/bold green] {output}")
|
|
238
368
|
|
|
@@ -243,15 +373,14 @@ def run_overlay(
|
|
|
243
373
|
prod_info=prod_info,
|
|
244
374
|
LON=prepared.lon,
|
|
245
375
|
LAT=prepared.lat,
|
|
246
|
-
grid=
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
376
|
+
grid=statistics_grid
|
|
377
|
+
if statistics_grid is not None
|
|
378
|
+
else prepared.values,
|
|
379
|
+
swath_meta=swath_meta,
|
|
380
|
+
time_matching=pairing,
|
|
381
|
+
display_grid=display_grid,
|
|
382
|
+
display_lon=display_lon,
|
|
383
|
+
display_lat=display_lat,
|
|
255
384
|
raster_vmin=use_vmin,
|
|
256
385
|
raster_vmax=use_vmax,
|
|
257
386
|
data=data,
|