rsplot 0.2.6__tar.gz → 0.2.7__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.6 → rsplot-0.2.7}/PKG-INFO +4 -1
- {rsplot-0.2.6 → rsplot-0.2.7}/README.md +3 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/pyproject.toml +1 -1
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/cli.py +194 -25
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/overlay.py +3 -3
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/results.py +24 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/.gitignore +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/LICENSE +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/__init__.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/__main__.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/config.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/fnr.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/geo/__init__.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/geo/boundaries.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/geo/gridding.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/__init__.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/colormaps.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/fnr.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/raster.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/station.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/plotting/styles.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/__init__.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/base.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/era5_wind.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/forecast.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/guokong.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/tropomi_hcho.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/tropomi_no2.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/tropomi_o3.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/readers/tropomi_o3pr.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/recent.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/src/rsplot/tiles/__init__.py +0 -0
- {rsplot-0.2.6 → rsplot-0.2.7}/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.7
|
|
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
|
|
@@ -340,6 +340,9 @@ rsplot forecast 安徽 2026063000 --lead 24 --var O3 -o forecast_anhui_o3.png
|
|
|
340
340
|
# Date-only forecast input automatically selects the latest initialized hour.
|
|
341
341
|
rsplot forecast 合肥 20260630 --lead 72 --var PM2.5 -o forecast_hefei_pm25.png
|
|
342
342
|
|
|
343
|
+
# Overlay station observations on the forecast valid time.
|
|
344
|
+
rsplot forecast 合肥 2026041409 --lead 6 --var O3 --station-overlay -o forecast_station_hefei_o3.png
|
|
345
|
+
|
|
343
346
|
# Plot national monitoring station observations.
|
|
344
347
|
rsplot station 合肥 2026041415 --var O3 -o station_hefei_o3.png
|
|
345
348
|
|
|
@@ -113,6 +113,9 @@ rsplot forecast 安徽 2026063000 --lead 24 --var O3 -o forecast_anhui_o3.png
|
|
|
113
113
|
# Date-only forecast input automatically selects the latest initialized hour.
|
|
114
114
|
rsplot forecast 合肥 20260630 --lead 72 --var PM2.5 -o forecast_hefei_pm25.png
|
|
115
115
|
|
|
116
|
+
# Overlay station observations on the forecast valid time.
|
|
117
|
+
rsplot forecast 合肥 2026041409 --lead 6 --var O3 --station-overlay -o forecast_station_hefei_o3.png
|
|
118
|
+
|
|
116
119
|
# Plot national monitoring station observations.
|
|
117
120
|
rsplot station 合肥 2026041415 --var O3 -o station_hefei_o3.png
|
|
118
121
|
|
|
@@ -445,6 +445,21 @@ def raster(
|
|
|
445
445
|
emit_result(result, output)
|
|
446
446
|
|
|
447
447
|
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# Forecast variable defaults that pair naturally with station observations.
|
|
450
|
+
# Used when the user asks for a forecast/station overlay but doesn't specify
|
|
451
|
+
# --station-var explicitly.
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
_FORECAST_STATION_PAIR: dict[str, str] = {
|
|
454
|
+
"pm25": "PM2.5",
|
|
455
|
+
"pm10": "PM10",
|
|
456
|
+
"so2": "SO2",
|
|
457
|
+
"no2": "NO2",
|
|
458
|
+
"co": "CO",
|
|
459
|
+
"o3": "O3",
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
448
463
|
@app.command()
|
|
449
464
|
def forecast(
|
|
450
465
|
region: str = typer.Argument(
|
|
@@ -476,6 +491,26 @@ def forecast(
|
|
|
476
491
|
"-d",
|
|
477
492
|
help="预报数据目录 (覆盖配置中的 forecast 路径)",
|
|
478
493
|
),
|
|
494
|
+
station_overlay: bool = typer.Option(
|
|
495
|
+
False,
|
|
496
|
+
"--station-overlay",
|
|
497
|
+
help="叠加同有效时次的国控站点散点",
|
|
498
|
+
),
|
|
499
|
+
station_var: str | None = typer.Option(
|
|
500
|
+
None,
|
|
501
|
+
"--station-var",
|
|
502
|
+
help="站点变量名;默认按预报变量自动匹配 PM2.5/PM10/SO2/NO2/CO/O3",
|
|
503
|
+
),
|
|
504
|
+
station_data_dir: str | None = typer.Option(
|
|
505
|
+
None,
|
|
506
|
+
"--station-data-dir",
|
|
507
|
+
help="国控站点数据目录 (覆盖配置中的 guokong 路径)",
|
|
508
|
+
),
|
|
509
|
+
station_datetime: str | None = typer.Option(
|
|
510
|
+
None,
|
|
511
|
+
"--station-datetime",
|
|
512
|
+
help="站点时次 YYYYMMDDHH 或 YYYYMMDD;默认使用预报有效时次",
|
|
513
|
+
),
|
|
479
514
|
output: str | None = typer.Option(
|
|
480
515
|
None,
|
|
481
516
|
"--output",
|
|
@@ -502,16 +537,23 @@ def forecast(
|
|
|
502
537
|
示例:
|
|
503
538
|
rsplot forecast 安徽 2026063000 --lead 24 --var O3
|
|
504
539
|
rsplot forecast 合肥 20260630 --lead 72 --var PM2.5
|
|
540
|
+
rsplot forecast 合肥 2026041409 --lead 6 --var O3 --station-overlay
|
|
505
541
|
"""
|
|
506
542
|
from pathlib import Path
|
|
507
543
|
|
|
508
|
-
from rsplot.
|
|
544
|
+
from rsplot.config import (
|
|
545
|
+
STATION_VAR_LABELS,
|
|
546
|
+
STATION_VAR_META,
|
|
547
|
+
get_exceedance_threshold,
|
|
548
|
+
)
|
|
549
|
+
from rsplot.geo.boundaries import _load_city, resolve_region
|
|
509
550
|
from rsplot.geo.gridding import (
|
|
510
551
|
grid_coverage_in_region,
|
|
511
552
|
mask_to_region,
|
|
512
553
|
smooth_grid,
|
|
513
554
|
)
|
|
514
555
|
from rsplot.plotting.colormaps import STATION_CONCENTRATION_CMAP
|
|
556
|
+
from rsplot.plotting.overlay import plot_overlay
|
|
515
557
|
from rsplot.plotting.raster import plot_raster
|
|
516
558
|
from rsplot.plotting.styles import setup_fonts
|
|
517
559
|
from rsplot.readers.forecast import (
|
|
@@ -519,6 +561,7 @@ def forecast(
|
|
|
519
561
|
get_forecast_var_info,
|
|
520
562
|
read_forecast,
|
|
521
563
|
)
|
|
564
|
+
from rsplot.readers.guokong import mask_stations_to_region, read_stations
|
|
522
565
|
from rsplot.results import build_forecast_result, emit_result
|
|
523
566
|
|
|
524
567
|
# --- 1. Load config ---
|
|
@@ -644,42 +687,159 @@ def forecast(
|
|
|
644
687
|
raise typer.Exit(1)
|
|
645
688
|
console.print(f" → 色标范围: {use_vmin:.1f} ~ {use_vmax:.1f}")
|
|
646
689
|
|
|
647
|
-
# --- 6.
|
|
690
|
+
# --- 6. Optional station overlay ---
|
|
691
|
+
station_data = None
|
|
692
|
+
station_resolved_dt = None
|
|
693
|
+
station_threshold = None
|
|
694
|
+
station_unit = ""
|
|
695
|
+
station_colorbar_label = ""
|
|
696
|
+
use_station_overlay = (
|
|
697
|
+
station_overlay
|
|
698
|
+
or station_var is not None
|
|
699
|
+
or station_data_dir is not None
|
|
700
|
+
or station_datetime is not None
|
|
701
|
+
)
|
|
702
|
+
if use_station_overlay:
|
|
703
|
+
use_station_var = station_var or _FORECAST_STATION_PAIR[var_info.name]
|
|
704
|
+
if use_station_var not in STATION_VAR_META:
|
|
705
|
+
available = ", ".join(STATION_VAR_META)
|
|
706
|
+
console.print(
|
|
707
|
+
f"[red]未知站点变量 '{use_station_var}'[/red]。"
|
|
708
|
+
f"可用: {available}"
|
|
709
|
+
)
|
|
710
|
+
raise typer.Exit(1)
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
use_station_data_dir = (
|
|
714
|
+
station_data_dir
|
|
715
|
+
if station_data_dir is not None
|
|
716
|
+
else cfg.get_data_dir("guokong")
|
|
717
|
+
)
|
|
718
|
+
except ValueError as e:
|
|
719
|
+
console.print(f"[red]{e}[/red]")
|
|
720
|
+
raise typer.Exit(1) from e
|
|
721
|
+
|
|
722
|
+
use_station_datetime = station_datetime or forecast_data.valid_datetime
|
|
723
|
+
console.print(
|
|
724
|
+
f"\n[bold]读取站点数据:[/bold] {use_station_data_dir}"
|
|
725
|
+
)
|
|
726
|
+
console.print(
|
|
727
|
+
f" 时间: {use_station_datetime}, 变量: {use_station_var}"
|
|
728
|
+
)
|
|
729
|
+
try:
|
|
730
|
+
station_data, station_resolved_dt = read_stations(
|
|
731
|
+
use_station_data_dir,
|
|
732
|
+
use_station_datetime,
|
|
733
|
+
use_station_var,
|
|
734
|
+
extent=read_extent,
|
|
735
|
+
)
|
|
736
|
+
except (FileNotFoundError, KeyError, ValueError) as e:
|
|
737
|
+
console.print(f"[red]{e}[/red]")
|
|
738
|
+
raise typer.Exit(1) from e
|
|
739
|
+
station_data = mask_stations_to_region(station_data, info.geometry)
|
|
740
|
+
if station_resolved_dt != use_station_datetime:
|
|
741
|
+
console.print(
|
|
742
|
+
f" → 自动选择最新站点时次: [cyan]{station_resolved_dt}[/cyan]"
|
|
743
|
+
)
|
|
744
|
+
console.print(
|
|
745
|
+
f" → [green]{station_data.n_valid}[/green] 有效站点 "
|
|
746
|
+
f"(共 {station_data.n_stations} 站)"
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
station_threshold = get_exceedance_threshold(use_station_var)
|
|
750
|
+
if station_threshold is not None:
|
|
751
|
+
n_exceed = int(
|
|
752
|
+
np.sum(
|
|
753
|
+
station_data.values[np.isfinite(station_data.values)]
|
|
754
|
+
> station_threshold
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
console.print(
|
|
758
|
+
f" → 超标阈值: {station_threshold}, "
|
|
759
|
+
f"超标站点: [red]{n_exceed}[/red]"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
station_unit = STATION_VAR_META[use_station_var][0]
|
|
763
|
+
station_display_var = STATION_VAR_LABELS.get(
|
|
764
|
+
use_station_var, use_station_var
|
|
765
|
+
)
|
|
766
|
+
station_colorbar_label = (
|
|
767
|
+
f"{station_display_var} ({station_unit})"
|
|
768
|
+
if station_unit
|
|
769
|
+
else station_display_var
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# --- 7. Plot ---
|
|
648
773
|
if output is None:
|
|
774
|
+
station_suffix = (
|
|
775
|
+
f"_station{station_data.var_name}"
|
|
776
|
+
if station_data is not None
|
|
777
|
+
else ""
|
|
778
|
+
)
|
|
649
779
|
output = (
|
|
650
780
|
f"/tmp/rsplot_forecast_{info.name}_{forecast_data.variable}_"
|
|
651
|
-
f"{forecast_data.init_datetime}_f{forecast_data.lead_hour}
|
|
781
|
+
f"{forecast_data.init_datetime}_f{forecast_data.lead_hour}"
|
|
782
|
+
f"{station_suffix}.png"
|
|
652
783
|
)
|
|
653
784
|
Path(output).parent.mkdir(parents=True, exist_ok=True)
|
|
654
785
|
|
|
655
786
|
if title is None:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
787
|
+
if station_data is None:
|
|
788
|
+
title = (
|
|
789
|
+
f"Forecast {forecast_data.label} - {info.name} "
|
|
790
|
+
f"(valid {forecast_data.valid_datetime}, "
|
|
791
|
+
f"+{forecast_data.lead_hour}h)"
|
|
792
|
+
)
|
|
793
|
+
else:
|
|
794
|
+
title = (
|
|
795
|
+
f"Forecast {forecast_data.label} + Station "
|
|
796
|
+
f"{station_data.var_name} - {info.name} "
|
|
797
|
+
f"(valid {forecast_data.valid_datetime}, "
|
|
798
|
+
f"+{forecast_data.lead_hour}h)"
|
|
799
|
+
)
|
|
661
800
|
|
|
662
801
|
console.print("\n[bold]绘图中...[/bold]")
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
802
|
+
if station_data is None:
|
|
803
|
+
plot_raster(
|
|
804
|
+
forecast_data.lon,
|
|
805
|
+
forecast_data.lat,
|
|
806
|
+
grid_masked,
|
|
807
|
+
info,
|
|
808
|
+
dpi=use_dpi,
|
|
809
|
+
cmap=use_cmap,
|
|
810
|
+
vmin=use_vmin,
|
|
811
|
+
vmax=use_vmax,
|
|
812
|
+
basemap=use_basemap,
|
|
813
|
+
tianditu_key=cfg.tianditu_key,
|
|
814
|
+
hcho_cmap_file=cfg.hcho_cmap_file,
|
|
815
|
+
title=title,
|
|
816
|
+
output=output,
|
|
817
|
+
colorbar_label=var_info.colorbar_label,
|
|
818
|
+
)
|
|
819
|
+
else:
|
|
820
|
+
plot_overlay(
|
|
821
|
+
forecast_data.lon,
|
|
822
|
+
forecast_data.lat,
|
|
823
|
+
grid_masked,
|
|
824
|
+
station_data,
|
|
825
|
+
info,
|
|
826
|
+
raster_cmap=use_cmap,
|
|
827
|
+
raster_vmin=use_vmin,
|
|
828
|
+
raster_vmax=use_vmax,
|
|
829
|
+
raster_label=var_info.colorbar_label,
|
|
830
|
+
station_threshold=station_threshold,
|
|
831
|
+
station_label=station_colorbar_label,
|
|
832
|
+
dpi=use_dpi,
|
|
833
|
+
basemap=use_basemap,
|
|
834
|
+
tianditu_key=cfg.tianditu_key,
|
|
835
|
+
hcho_cmap_file=cfg.hcho_cmap_file,
|
|
836
|
+
title=title,
|
|
837
|
+
output=output,
|
|
838
|
+
)
|
|
679
839
|
|
|
680
840
|
console.print(f"\n[bold green]✓ 已保存:[/bold green] {output}")
|
|
681
841
|
|
|
682
|
-
# ---
|
|
842
|
+
# --- 8. Emit structured result for downstream agents ---
|
|
683
843
|
result = build_forecast_result(
|
|
684
844
|
region=info,
|
|
685
845
|
data=forecast_data,
|
|
@@ -689,6 +849,15 @@ def forecast(
|
|
|
689
849
|
vmin=use_vmin,
|
|
690
850
|
vmax=use_vmax,
|
|
691
851
|
output=output,
|
|
852
|
+
station_data=station_data,
|
|
853
|
+
station_datetime=station_resolved_dt,
|
|
854
|
+
station_threshold=station_threshold,
|
|
855
|
+
station_unit=station_unit,
|
|
856
|
+
cities_gdf=(
|
|
857
|
+
_load_city(cfg.require_geojson_dir())
|
|
858
|
+
if station_data is not None
|
|
859
|
+
else None
|
|
860
|
+
),
|
|
692
861
|
)
|
|
693
862
|
emit_result(result, output)
|
|
694
863
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Combined raster + station overlay plotting.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
on top as colored dots. Each
|
|
5
|
-
quantity scales stay independent.
|
|
3
|
+
A gridded raster layer (satellite or forecast) is drawn as the background
|
|
4
|
+
colored field, with 国控 station observations on top as colored dots. Each
|
|
5
|
+
layer keeps its own colorbar so the two quantity scales stay independent.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -201,6 +201,11 @@ def build_forecast_result(
|
|
|
201
201
|
vmin: float,
|
|
202
202
|
vmax: float,
|
|
203
203
|
output: str,
|
|
204
|
+
station_data: StationData | None = None,
|
|
205
|
+
station_datetime: str | None = None,
|
|
206
|
+
station_threshold: float | None = None,
|
|
207
|
+
station_unit: str = "",
|
|
208
|
+
cities_gdf: gpd.GeoDataFrame | None = None,
|
|
204
209
|
) -> dict[str, Any]:
|
|
205
210
|
"""Build the JSON sidecar payload for a forecast run."""
|
|
206
211
|
from rsplot.geo.gridding import grid_coverage_in_region
|
|
@@ -211,6 +216,11 @@ def build_forecast_result(
|
|
|
211
216
|
|
|
212
217
|
result: dict[str, Any] = {
|
|
213
218
|
"command": "forecast",
|
|
219
|
+
"mode": (
|
|
220
|
+
"forecast_station_overlay"
|
|
221
|
+
if station_data is not None
|
|
222
|
+
else "forecast"
|
|
223
|
+
),
|
|
214
224
|
"image": output,
|
|
215
225
|
"region": {
|
|
216
226
|
"name": region.name,
|
|
@@ -239,6 +249,20 @@ def build_forecast_result(
|
|
|
239
249
|
},
|
|
240
250
|
}
|
|
241
251
|
|
|
252
|
+
if station_data is not None:
|
|
253
|
+
station_block = build_station_result(
|
|
254
|
+
region=region,
|
|
255
|
+
data=station_data,
|
|
256
|
+
datetime_str=station_datetime or data.valid_datetime,
|
|
257
|
+
threshold=station_threshold,
|
|
258
|
+
unit=station_unit,
|
|
259
|
+
cities_gdf=cities_gdf,
|
|
260
|
+
output=output,
|
|
261
|
+
)
|
|
262
|
+
for key in ("command", "image", "region"):
|
|
263
|
+
station_block.pop(key, None)
|
|
264
|
+
result["station"] = station_block
|
|
265
|
+
|
|
242
266
|
sub_gdf = region.sub_boundary_gdf
|
|
243
267
|
if sub_gdf is not None and len(sub_gdf) > 0:
|
|
244
268
|
result["spatial_summary"] = _grid_per_city_stats(LON, LAT, grid, sub_gdf)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|