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.
Files changed (31) hide show
  1. {rsplot-0.2.2 → rsplot-0.2.3}/PKG-INFO +4 -1
  2. {rsplot-0.2.2 → rsplot-0.2.3}/README.md +3 -0
  3. {rsplot-0.2.2 → rsplot-0.2.3}/pyproject.toml +1 -1
  4. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/cli.py +44 -10
  5. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/config.py +1 -0
  6. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/__init__.py +35 -1
  7. rsplot-0.2.3/src/rsplot/readers/tropomi_o3pr.py +103 -0
  8. {rsplot-0.2.2 → rsplot-0.2.3}/.gitignore +0 -0
  9. {rsplot-0.2.2 → rsplot-0.2.3}/LICENSE +0 -0
  10. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/__init__.py +0 -0
  11. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/__main__.py +0 -0
  12. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/fnr.py +0 -0
  13. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/geo/__init__.py +0 -0
  14. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/geo/boundaries.py +0 -0
  15. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/geo/gridding.py +0 -0
  16. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/__init__.py +0 -0
  17. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/colormaps.py +0 -0
  18. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/fnr.py +0 -0
  19. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/overlay.py +0 -0
  20. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/raster.py +0 -0
  21. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/station.py +0 -0
  22. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/plotting/styles.py +0 -0
  23. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/base.py +0 -0
  24. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/guokong.py +0 -0
  25. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/tropomi_hcho.py +0 -0
  26. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/tropomi_no2.py +0 -0
  27. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/readers/tropomi_o3.py +0 -0
  28. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/recent.py +0 -0
  29. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/results.py +0 -0
  30. {rsplot-0.2.2 → rsplot-0.2.3}/src/rsplot/tiles/__init__.py +0 -0
  31. {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.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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rsplot"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "CLI tool for plotting Remote Sensing data, designed for Agent."
5
5
  authors = [
6
6
  { name = "Yaoyao Hu", email = "yaoyhu@mail.ustc.edu.cn" },
@@ -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=max(3, 天数//2), "
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 = smooth if smooth is not None else params.smooth_sigma
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 = smooth if smooth is not None else params.smooth_sigma
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
- use_res = res if res is not None else info.params.res
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