timesat-cli 1.0.0__tar.gz → 1.3.1__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.
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/PKG-INFO +2 -17
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/README.md +0 -13
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/pyproject.toml +1 -6
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/src/timesat_cli/__init__.py +1 -1
- timesat_cli-1.3.1/src/timesat_cli/__main__.py +28 -0
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/src/timesat_cli/config.py +2 -4
- timesat_cli-1.3.1/src/timesat_cli/dateutils.py +95 -0
- timesat_cli-1.3.1/src/timesat_cli/fsutils.py +64 -0
- timesat_cli-1.3.1/src/timesat_cli/processing.py +206 -0
- timesat_cli-1.3.1/src/timesat_cli/readers.py +159 -0
- timesat_cli-1.3.1/src/timesat_cli/writers.py +47 -0
- timesat_cli-1.0.0/src/timesat_cli/__main__.py +0 -12
- timesat_cli-1.0.0/src/timesat_cli/fsutils.py +0 -25
- timesat_cli-1.0.0/src/timesat_cli/parallel.py +0 -32
- timesat_cli-1.0.0/src/timesat_cli/processing.py +0 -167
- timesat_cli-1.0.0/src/timesat_cli/readers.py +0 -204
- timesat_cli-1.0.0/src/timesat_cli/writers.py +0 -35
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/.gitignore +0 -0
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/LICENSE +0 -0
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/src/timesat_cli/csvutils.py +0 -0
- {timesat_cli-1.0.0 → timesat_cli-1.3.1}/src/timesat_cli/qa.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: timesat-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Python-based command line interface for TIMESAT
|
|
5
5
|
Author: Zhanzhang Cai
|
|
6
6
|
License: GPL-3.0-only
|
|
@@ -16,9 +16,7 @@ Requires-Python: >=3.10
|
|
|
16
16
|
Requires-Dist: numpy
|
|
17
17
|
Requires-Dist: pandas
|
|
18
18
|
Requires-Dist: rasterio
|
|
19
|
-
Requires-Dist: timesat
|
|
20
|
-
Provides-Extra: parallel
|
|
21
|
-
Requires-Dist: ray; extra == 'parallel'
|
|
19
|
+
Requires-Dist: timesat>=4.1.11
|
|
22
20
|
Description-Content-Type: text/markdown
|
|
23
21
|
|
|
24
22
|
# TIMESAT CLI
|
|
@@ -91,19 +89,6 @@ pip install timesat-cli
|
|
|
91
89
|
|
|
92
90
|
---
|
|
93
91
|
|
|
94
|
-
## ⚙️ Optional: Parallel Processing Support
|
|
95
|
-
|
|
96
|
-
`timesat-cli` provides an optional extra for **parallel execution** using [`ray`](https://www.ray.io/).
|
|
97
|
-
|
|
98
|
-
To install with parallel-processing support:
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
pip install timesat-cli[parallel]
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
This installs the base package plus the ray dependency.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
92
|
|
|
108
93
|
## Running the Application
|
|
109
94
|
|
|
@@ -68,19 +68,6 @@ pip install timesat-cli
|
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
71
|
-
## ⚙️ Optional: Parallel Processing Support
|
|
72
|
-
|
|
73
|
-
`timesat-cli` provides an optional extra for **parallel execution** using [`ray`](https://www.ray.io/).
|
|
74
|
-
|
|
75
|
-
To install with parallel-processing support:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
pip install timesat-cli[parallel]
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
This installs the base package plus the ray dependency.
|
|
82
|
-
|
|
83
|
-
---
|
|
84
71
|
|
|
85
72
|
## Running the Application
|
|
86
73
|
|
|
@@ -23,17 +23,12 @@ dependencies = [
|
|
|
23
23
|
"numpy",
|
|
24
24
|
"rasterio",
|
|
25
25
|
"pandas",
|
|
26
|
-
"timesat",
|
|
26
|
+
"timesat>=4.1.11",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[dependency-groups]
|
|
30
30
|
dev = ["pre-commit", "ruff", "matplotlib"]
|
|
31
31
|
|
|
32
|
-
[project.optional-dependencies]
|
|
33
|
-
parallel = [
|
|
34
|
-
'ray',
|
|
35
|
-
]
|
|
36
|
-
|
|
37
32
|
[tool.hatch.version]
|
|
38
33
|
source = "vcs" # <-- tell Hatch to use Git tags
|
|
39
34
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(description="Run TIMESAT processing pipeline.")
|
|
7
|
+
parser.add_argument("settings_json", help="Path to the JSON configuration file.")
|
|
8
|
+
args = parser.parse_args()
|
|
9
|
+
|
|
10
|
+
# read config first (pure Python)
|
|
11
|
+
with open(args.settings_json) as f:
|
|
12
|
+
s = json.load(f)
|
|
13
|
+
|
|
14
|
+
# thread control BEFORE heavy imports
|
|
15
|
+
threads = s.get("threads")
|
|
16
|
+
if threads:
|
|
17
|
+
threads = str(int(threads))
|
|
18
|
+
os.environ["OMP_NUM_THREADS"] = threads
|
|
19
|
+
os.environ.setdefault("OPENBLAS_NUM_THREADS", threads)
|
|
20
|
+
os.environ.setdefault("MKL_NUM_THREADS", threads)
|
|
21
|
+
os.environ.setdefault("NUMEXPR_NUM_THREADS", threads)
|
|
22
|
+
|
|
23
|
+
# late import of processing (safe)
|
|
24
|
+
from .processing import run
|
|
25
|
+
run(args.settings_json)
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
@@ -39,13 +39,12 @@ class Settings:
|
|
|
39
39
|
p_outlier: int
|
|
40
40
|
p_printflag: int
|
|
41
41
|
max_memory_gb: float
|
|
42
|
-
para_check: int
|
|
43
|
-
ray_dir: str
|
|
44
42
|
scale: float
|
|
45
43
|
offset: float
|
|
46
44
|
p_hrvppformat: int
|
|
47
45
|
p_nclasses: int
|
|
48
46
|
classes: List[ClassParams]
|
|
47
|
+
outputvariables: int
|
|
49
48
|
|
|
50
49
|
|
|
51
50
|
@dataclass
|
|
@@ -104,11 +103,10 @@ def load_config(jsfile: str) -> Config:
|
|
|
104
103
|
p_outlier=int(s["p_outlier"]["value"]),
|
|
105
104
|
p_printflag=int(s["p_printflag"]["value"]),
|
|
106
105
|
max_memory_gb=float(s["max_memory_gb"]["value"]),
|
|
107
|
-
para_check=int(s["para_check"]["value"]),
|
|
108
|
-
ray_dir=s["ray_dir"]["value"],
|
|
109
106
|
scale=float(s["scale"]["value"]),
|
|
110
107
|
offset=float(s["offset"]["value"]),
|
|
111
108
|
p_hrvppformat=int(s["p_hrvppformat"]["value"]),
|
|
109
|
+
outputvariables=int(s["outputvariables"]["value"]),
|
|
112
110
|
p_nclasses=nclasses,
|
|
113
111
|
classes=classes,
|
|
114
112
|
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for handling date operations in TIMESAT processing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
__all__ = ["date_with_ignored_day", "build_monthly_sample_indices"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_leap_year(y: int) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Return True if year y is a Gregorian leap year, False otherwise.
|
|
16
|
+
"""
|
|
17
|
+
return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def date_with_ignored_day(yrstart: int, i_tv: int, p_ignoreday: int) -> datetime.date:
|
|
21
|
+
"""
|
|
22
|
+
Convert a synthetic TIMESAT time index (1-based, assuming 365 days/year)
|
|
23
|
+
into a real calendar date while skipping one day in leap years.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ---- Step 1: synthetic 365-day calendar ----
|
|
27
|
+
i = int(i_tv)
|
|
28
|
+
year_offset, doy_365 = divmod(i - 1, 365)
|
|
29
|
+
doy_365 += 1
|
|
30
|
+
year = yrstart + year_offset
|
|
31
|
+
|
|
32
|
+
jan1 = datetime.date(year, 1, 1)
|
|
33
|
+
|
|
34
|
+
if is_leap_year(year):
|
|
35
|
+
if not (1 <= p_ignoreday <= 366):
|
|
36
|
+
raise ValueError("p_ignoreday must be in [1, 366] for leap years")
|
|
37
|
+
|
|
38
|
+
if p_ignoreday == 1:
|
|
39
|
+
real_ordinal = doy_365 + 1
|
|
40
|
+
elif p_ignoreday == 366:
|
|
41
|
+
real_ordinal = doy_365
|
|
42
|
+
else:
|
|
43
|
+
real_ordinal = doy_365 if doy_365 < p_ignoreday else doy_365 + 1
|
|
44
|
+
else:
|
|
45
|
+
real_ordinal = doy_365
|
|
46
|
+
|
|
47
|
+
return jan1 + datetime.timedelta(days=real_ordinal - 1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_monthly_sample_indices(yrstart: int, yr: int) -> np.ndarray:
|
|
51
|
+
"""
|
|
52
|
+
Build a synthetic time index (1-based) for sampling the 1st, 11th, and 21st
|
|
53
|
+
of each month across multiple years.
|
|
54
|
+
|
|
55
|
+
The synthetic timeline always uses 365 days per year.
|
|
56
|
+
In leap years we:
|
|
57
|
+
- keep Feb 29
|
|
58
|
+
- drop Dec 31
|
|
59
|
+
so that each year still has 365 synthetic days.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
yrstart : int
|
|
64
|
+
Starting year of the period.
|
|
65
|
+
|
|
66
|
+
yr : int
|
|
67
|
+
Number of years to include.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
np.ndarray
|
|
72
|
+
A 1D array of indices into the synthetic timeline (1-based).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
indices: list[int] = []
|
|
76
|
+
year_offset = 0 # offset of each synthetic year start (0, 365, 730, ...)
|
|
77
|
+
|
|
78
|
+
for year in range(yrstart, yrstart + yr):
|
|
79
|
+
if is_leap_year(year):
|
|
80
|
+
# Include Feb 29, drop Dec 31
|
|
81
|
+
days_in_month = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 30]
|
|
82
|
+
else:
|
|
83
|
+
days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
84
|
+
|
|
85
|
+
cum = 0 # cumulative day count within the current year
|
|
86
|
+
|
|
87
|
+
for dim in days_in_month:
|
|
88
|
+
for d in (1, 11, 21):
|
|
89
|
+
if d <= dim:
|
|
90
|
+
indices.append(year_offset + cum + d)
|
|
91
|
+
cum += dim
|
|
92
|
+
|
|
93
|
+
year_offset += 365
|
|
94
|
+
|
|
95
|
+
return np.array(indices, dtype=int)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
__all__ = ["create_output_folders", "close_all"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_output_folders(outfolder: str) -> tuple[str, str]:
|
|
9
|
+
vpp_folder = os.path.join(outfolder, "VPP")
|
|
10
|
+
st_folder = os.path.join(outfolder, "ST")
|
|
11
|
+
os.makedirs(vpp_folder, exist_ok=True)
|
|
12
|
+
os.makedirs(st_folder, exist_ok=True)
|
|
13
|
+
return st_folder, vpp_folder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def memory_plan(
|
|
17
|
+
dx: int,
|
|
18
|
+
dy: int,
|
|
19
|
+
z: int,
|
|
20
|
+
p_outindex_num: int,
|
|
21
|
+
yr: int,
|
|
22
|
+
max_memory_gb: float,
|
|
23
|
+
) -> tuple[int, int]:
|
|
24
|
+
num_layers = (
|
|
25
|
+
2 * z # VI + QA
|
|
26
|
+
+ 2 * p_outindex_num # yfit + yfit QA
|
|
27
|
+
+ 2 * 13 * 2 * yr # VPP + VPP QA
|
|
28
|
+
+ yr # nseason
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
bytes_per = 4 # float32
|
|
32
|
+
safety = 0.8 # keep 60% margin for overhead
|
|
33
|
+
max_bytes = max_memory_gb * (2 ** 30) * safety
|
|
34
|
+
|
|
35
|
+
dy_max = max_bytes / (dx * num_layers * bytes_per) if num_layers > 0 else dy
|
|
36
|
+
y_slice_size = int(min(math.floor(dy_max), dy)) if dy_max > 0 else dy
|
|
37
|
+
y_slice_size = max(1, y_slice_size)
|
|
38
|
+
num_block = int(math.ceil(dy / y_slice_size))
|
|
39
|
+
return y_slice_size, num_block
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def close_all(*items):
|
|
43
|
+
"""
|
|
44
|
+
Close datasets or other objects that have a .close() method.
|
|
45
|
+
Accepts individual objects and iterables (lists/tuples/etc).
|
|
46
|
+
Ignores None safely.
|
|
47
|
+
"""
|
|
48
|
+
for obj in items:
|
|
49
|
+
if obj is None:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
# If it's an iterable of objects (e.g. list of datasets)
|
|
53
|
+
if isinstance(obj, (list, tuple, set)):
|
|
54
|
+
for x in obj:
|
|
55
|
+
if x is None:
|
|
56
|
+
continue
|
|
57
|
+
close = getattr(x, "close", None)
|
|
58
|
+
if callable(close):
|
|
59
|
+
close()
|
|
60
|
+
else:
|
|
61
|
+
# Single object
|
|
62
|
+
close = getattr(obj, "close", None)
|
|
63
|
+
if callable(close):
|
|
64
|
+
close()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import math, os, datetime
|
|
3
|
+
|
|
4
|
+
from .config import load_config, build_param_array
|
|
5
|
+
from .readers import read_file_lists, open_image_data
|
|
6
|
+
from .fsutils import create_output_folders, memory_plan, close_all
|
|
7
|
+
from .writers import prepare_profiles, write_layers
|
|
8
|
+
from .dateutils import date_with_ignored_day, build_monthly_sample_indices
|
|
9
|
+
|
|
10
|
+
VPP_NAMES = ["SOSD","SOSV","LSLOPE","EOSD","EOSV","RSLOPE","LENGTH",
|
|
11
|
+
"MINV","MAXD","MAXV","AMPL","TPROD","SPROD"]
|
|
12
|
+
|
|
13
|
+
def _build_output_filenames(st_folder: str, vpp_folder: str, p_outindex, yrstart: int, yrend: int, p_ignoreday: int):
|
|
14
|
+
outyfitfn = []
|
|
15
|
+
outyfitqafn = []
|
|
16
|
+
for i_tv in p_outindex:
|
|
17
|
+
yfitdate = date_with_ignored_day(yrstart, int(i_tv), p_ignoreday)
|
|
18
|
+
outyfitfn.append(os.path.join(st_folder, f"TIMESAT_{yfitdate.strftime('%Y%m%d')}.tif"))
|
|
19
|
+
outyfitqafn.append(os.path.join(st_folder, f"TIMESAT_{yfitdate.strftime('%Y%m%d')}_QA.tif"))
|
|
20
|
+
|
|
21
|
+
outvppfn = []
|
|
22
|
+
outvppqafn = []
|
|
23
|
+
outnsfn = []
|
|
24
|
+
for i_yr in range(yrstart, yrend + 1):
|
|
25
|
+
for i_seas in range(2):
|
|
26
|
+
for name in VPP_NAMES:
|
|
27
|
+
outvppfn.append(os.path.join(vpp_folder, f"TIMESAT_{name}_{i_yr}_season_{i_seas+1}.tif"))
|
|
28
|
+
outvppqafn.append(os.path.join(vpp_folder, f"TIMESAT_QA_{i_yr}_season_{i_seas+1}.tif"))
|
|
29
|
+
outnsfn.append(os.path.join(vpp_folder, f"TIMESAT_{i_yr}_numseason.tif"))
|
|
30
|
+
return outyfitfn, outyfitqafn, outvppfn, outvppqafn, outnsfn
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(jsfile: str) -> None:
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
import rasterio
|
|
37
|
+
import timesat # external dependency
|
|
38
|
+
|
|
39
|
+
print(jsfile)
|
|
40
|
+
cfg = load_config(jsfile)
|
|
41
|
+
s = cfg.settings
|
|
42
|
+
|
|
43
|
+
if s.outputfolder == '':
|
|
44
|
+
print('Nothing to do...')
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Precompute arrays once per block to pass into timesat
|
|
48
|
+
landuse_arr = build_param_array(s, 'landuse', 'uint8')
|
|
49
|
+
p_fitmethod_arr = build_param_array(s, 'p_fitmethod', 'uint8')
|
|
50
|
+
p_smooth_arr = build_param_array(s, 'p_smooth', 'double')
|
|
51
|
+
p_nenvi_arr = build_param_array(s, 'p_nenvi', 'uint8')
|
|
52
|
+
p_wfactnum_arr = build_param_array(s, 'p_wfactnum', 'double')
|
|
53
|
+
p_startmethod_arr = build_param_array(s, 'p_startmethod', 'uint8')
|
|
54
|
+
p_startcutoff_arr = build_param_array(s, 'p_startcutoff', 'double', shape=(2,), fortran_2d=True)
|
|
55
|
+
p_low_percentile_arr = build_param_array(s, 'p_low_percentile', 'double')
|
|
56
|
+
p_fillbase_arr = build_param_array(s, 'p_fillbase', 'uint8')
|
|
57
|
+
p_seasonmethod_arr = build_param_array(s, 'p_seasonmethod', 'uint8')
|
|
58
|
+
p_seapar_arr = build_param_array(s, 'p_seapar', 'double')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
timevector, flist, qlist, yr, yrstart, yrend = read_file_lists(s.tv_list, s.image_file_list, s.quality_file_list)
|
|
62
|
+
|
|
63
|
+
z = len(flist)
|
|
64
|
+
print(f'num of images: {z}')
|
|
65
|
+
print('First image: ' + os.path.basename(flist[0]))
|
|
66
|
+
print('Last image: ' + os.path.basename(flist[-1]))
|
|
67
|
+
print(yrstart)
|
|
68
|
+
|
|
69
|
+
if int(s.p_st_timestep)>0:
|
|
70
|
+
p_outindex = np.arange(1, yr * 365 + 1)[:: int(s.p_st_timestep)]
|
|
71
|
+
elif int(s.p_st_timestep)<0:
|
|
72
|
+
p_outindex = build_monthly_sample_indices(yrstart, yr)
|
|
73
|
+
elif int(s.p_st_timestep)==0:
|
|
74
|
+
p_outindex = np.arange(1, yr * 365 + 1)[:: int(9999)]
|
|
75
|
+
p_outindex_num = len(p_outindex)
|
|
76
|
+
|
|
77
|
+
with rasterio.open(flist[0], 'r') as temp:
|
|
78
|
+
img_profile = temp.profile
|
|
79
|
+
|
|
80
|
+
if sum(s.imwindow) == 0:
|
|
81
|
+
dx, dy = img_profile['width'], img_profile['height']
|
|
82
|
+
else:
|
|
83
|
+
dx, dy = int(s.imwindow[2]), int(s.imwindow[3])
|
|
84
|
+
|
|
85
|
+
st_folder, vpp_folder = create_output_folders(s.outputfolder)
|
|
86
|
+
|
|
87
|
+
outyfitfn, outyfitqafn, outvppfn, outvppqafn, outnsfn = _build_output_filenames(st_folder, vpp_folder, p_outindex, yrstart, yrend, s.p_ignoreday)
|
|
88
|
+
|
|
89
|
+
img_profile_st, img_profile_vpp, img_profile_qa, img_profile_ns = prepare_profiles(img_profile, s.p_nodata, s.scale, s.offset)
|
|
90
|
+
# Open output datasets once and reuse them for all blocks
|
|
91
|
+
st_datasets = []
|
|
92
|
+
stqa_datasets = []
|
|
93
|
+
vpp_datasets = []
|
|
94
|
+
vppqa_datasets = []
|
|
95
|
+
ns_dataset = []
|
|
96
|
+
|
|
97
|
+
# VPP outputs
|
|
98
|
+
if s.outputvariables == 1:
|
|
99
|
+
for path in outvppfn:
|
|
100
|
+
ds = rasterio.open(path, "w", **img_profile_vpp)
|
|
101
|
+
vpp_datasets.append(ds)
|
|
102
|
+
for path in outvppqafn:
|
|
103
|
+
ds = rasterio.open(path, "w", **img_profile_qa)
|
|
104
|
+
vppqa_datasets.append(ds)
|
|
105
|
+
for path in outnsfn:
|
|
106
|
+
ds = rasterio.open(path, "w", **img_profile_ns)
|
|
107
|
+
ns_dataset.append(ds)
|
|
108
|
+
|
|
109
|
+
# ST (yfit) outputs
|
|
110
|
+
for path in outyfitfn:
|
|
111
|
+
ds = rasterio.open(path, "w", **img_profile_st)
|
|
112
|
+
st_datasets.append(ds)
|
|
113
|
+
for path in outyfitqafn:
|
|
114
|
+
ds = rasterio.open(path, "w", **img_profile_qa)
|
|
115
|
+
stqa_datasets.append(ds)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# compute memory blocks
|
|
119
|
+
y_slice_size, num_block = memory_plan(dx, dy, z, p_outindex_num, yr, s.max_memory_gb)
|
|
120
|
+
y_slice_end = dy % y_slice_size if (dy % y_slice_size) > 0 else y_slice_size
|
|
121
|
+
print('y_slice_size = ' + str(y_slice_size))
|
|
122
|
+
|
|
123
|
+
s3_opts = getattr(s, "s3", None)
|
|
124
|
+
|
|
125
|
+
if s3_opts:
|
|
126
|
+
# Open all files inside a single S3 environment
|
|
127
|
+
with rasterio.Env(**s3_opts):
|
|
128
|
+
data_datasets = [rasterio.open(p, "r") for p in flist]
|
|
129
|
+
qa_datasets = [rasterio.open(p, "r") for p in qlist] if qlist else []
|
|
130
|
+
lc_dataset = rasterio.open(s.lc_file, "r") if s.lc_file else None
|
|
131
|
+
else:
|
|
132
|
+
# Local files
|
|
133
|
+
data_datasets = [rasterio.open(p, "r") for p in flist]
|
|
134
|
+
qa_datasets = [rasterio.open(p, "r") for p in qlist] if qlist else []
|
|
135
|
+
lc_dataset = rasterio.open(s.lc_file, "r") if s.lc_file else None
|
|
136
|
+
|
|
137
|
+
for iblock in range(num_block):
|
|
138
|
+
print(f'Processing block: {iblock + 1}/{num_block} starttime: {datetime.datetime.now()}')
|
|
139
|
+
x = dx
|
|
140
|
+
y = int(y_slice_size) if iblock != num_block - 1 else int(y_slice_end)
|
|
141
|
+
x_map = int(s.imwindow[0])
|
|
142
|
+
y_map = int(iblock * y_slice_size + s.imwindow[1])
|
|
143
|
+
|
|
144
|
+
vi, qa, lc = open_image_data(
|
|
145
|
+
x_map, y_map, x, y,
|
|
146
|
+
data_datasets,
|
|
147
|
+
qa_datasets,
|
|
148
|
+
lc_dataset,
|
|
149
|
+
img_profile['dtype'],
|
|
150
|
+
s.p_a,
|
|
151
|
+
s.p_band_id
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
print('--- start TIMESAT processing --- starttime: ' + str(datetime.datetime.now()))
|
|
155
|
+
|
|
156
|
+
if s.scale != 1 or s.offset != 0:
|
|
157
|
+
vi = vi * s.scale + s.offset
|
|
158
|
+
|
|
159
|
+
vpp, vppqa, nseason, yfit, yfitqa, seasonfit, tseq = timesat.tsfprocess(
|
|
160
|
+
yr, vi, qa, timevector, lc, s.p_nclasses, landuse_arr, p_outindex,
|
|
161
|
+
s.p_ignoreday, s.p_ylu, s.p_printflag, p_fitmethod_arr, p_smooth_arr,
|
|
162
|
+
s.p_nodata, s.p_davailwin, s.p_outlier,
|
|
163
|
+
p_nenvi_arr, p_wfactnum_arr, p_startmethod_arr, p_startcutoff_arr,
|
|
164
|
+
p_low_percentile_arr, p_fillbase_arr, s.p_hrvppformat,
|
|
165
|
+
p_seasonmethod_arr, p_seapar_arr, s.outputvariables)
|
|
166
|
+
|
|
167
|
+
print('--- start writing geotif --- starttime: ' + str(datetime.datetime.now()))
|
|
168
|
+
window = (x_map, y_map, x, y)
|
|
169
|
+
|
|
170
|
+
if s.outputvariables == 1:
|
|
171
|
+
vpp = np.moveaxis(vpp, -1, 0)
|
|
172
|
+
write_layers(vpp_datasets, vpp, window)
|
|
173
|
+
|
|
174
|
+
vppqa = np.moveaxis(vppqa, -1, 0)
|
|
175
|
+
write_layers(vppqa_datasets, vppqa, window)
|
|
176
|
+
|
|
177
|
+
nseason = np.moveaxis(nseason, -1, 0)
|
|
178
|
+
write_layers(ns_dataset, nseason, window)
|
|
179
|
+
|
|
180
|
+
if s.scale == 1 and s.offset == 0:
|
|
181
|
+
yfit = np.moveaxis(yfit, -1, 0).astype(img_profile['dtype'])
|
|
182
|
+
else:
|
|
183
|
+
yfit = np.moveaxis(yfit, -1, 0).astype('float32')
|
|
184
|
+
write_layers(st_datasets, yfit, window)
|
|
185
|
+
|
|
186
|
+
yfitqa = np.moveaxis(yfitqa, -1, 0)
|
|
187
|
+
write_layers(stqa_datasets, yfitqa, window)
|
|
188
|
+
|
|
189
|
+
print(f'Block: {iblock + 1}/{num_block} finishedtime: {datetime.datetime.now()}')
|
|
190
|
+
|
|
191
|
+
close_all(
|
|
192
|
+
data_datasets,
|
|
193
|
+
qa_datasets,
|
|
194
|
+
lc_dataset,
|
|
195
|
+
st_datasets,
|
|
196
|
+
stqa_datasets,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if s.outputvariables == 1:
|
|
200
|
+
close_all(
|
|
201
|
+
vpp_datasets,
|
|
202
|
+
vppqa_datasets,
|
|
203
|
+
ns_dataset,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import rasterio
|
|
9
|
+
from rasterio.windows import Window
|
|
10
|
+
|
|
11
|
+
from .qa import assign_qa_weight
|
|
12
|
+
|
|
13
|
+
__all__ = ["read_file_lists", "open_image_data"]
|
|
14
|
+
|
|
15
|
+
def _parse_dates_from_name(name: str) -> tuple[int, int, int]:
|
|
16
|
+
date_regex1 = r"\d{4}-\d{2}-\d{2}"
|
|
17
|
+
date_regex2 = r"\d{4}\d{2}\d{2}"
|
|
18
|
+
try:
|
|
19
|
+
dates = re.findall(date_regex1, name)
|
|
20
|
+
position = name.find(dates[0])
|
|
21
|
+
y = int(name[position : position + 4])
|
|
22
|
+
m = int(name[position + 5 : position + 7])
|
|
23
|
+
d = int(name[position + 8 : position + 10])
|
|
24
|
+
return y, m, d
|
|
25
|
+
except Exception:
|
|
26
|
+
try:
|
|
27
|
+
dates = re.findall(date_regex2, name)
|
|
28
|
+
position = name.find(dates[0])
|
|
29
|
+
y = int(name[position : position + 4])
|
|
30
|
+
m = int(name[position + 4 : position + 6])
|
|
31
|
+
d = int(name[position + 6 : position + 8])
|
|
32
|
+
return y, m, d
|
|
33
|
+
except Exception as e:
|
|
34
|
+
raise ValueError(f"No date found in filename: {name}") from e
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _read_time_vector(tlist: str, filepaths: list[str]):
|
|
38
|
+
"""Return (timevector, yr, yrstart, yrend) in YYYYDOY format."""
|
|
39
|
+
flist = [os.path.basename(p) for p in filepaths]
|
|
40
|
+
timevector = np.ndarray(len(flist), order="F", dtype="uint32")
|
|
41
|
+
if tlist == "":
|
|
42
|
+
for i, fname in enumerate(flist):
|
|
43
|
+
y, m, d = _parse_dates_from_name(fname)
|
|
44
|
+
doy = (datetime.date(y, m, d) - datetime.date(y, 1, 1)).days + 1
|
|
45
|
+
timevector[i] = y * 1000 + doy
|
|
46
|
+
else:
|
|
47
|
+
with open(tlist, "r") as f:
|
|
48
|
+
lines = f.read().splitlines()
|
|
49
|
+
for idx, val in enumerate(lines):
|
|
50
|
+
n = len(val)
|
|
51
|
+
if n == 8: # YYYYMMDD
|
|
52
|
+
dt = datetime.datetime.strptime(val, "%Y%m%d")
|
|
53
|
+
timevector[idx] = int(f"{dt.year}{dt.timetuple().tm_yday:03d}")
|
|
54
|
+
elif n == 7: # YYYYDOY
|
|
55
|
+
_ = datetime.datetime.strptime(val, "%Y%j")
|
|
56
|
+
timevector[idx] = int(val)
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"Unrecognized date format: {val}")
|
|
59
|
+
|
|
60
|
+
yrstart = int(np.floor(timevector.min() / 1000))
|
|
61
|
+
yrend = int(np.floor(timevector.max() / 1000))
|
|
62
|
+
yr = yrend - yrstart + 1
|
|
63
|
+
return timevector, yr, yrstart, yrend
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _unique_by_timevector(flist: list[str], qlist: list[str], timevector):
|
|
67
|
+
tv_unique, indices = np.unique(timevector, return_index=True)
|
|
68
|
+
flist2 = [flist[i] for i in indices]
|
|
69
|
+
qlist2 = [qlist[i] for i in indices] if qlist else []
|
|
70
|
+
return tv_unique, flist2, qlist2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_file_lists(
|
|
74
|
+
tlist: str, data_list: str, qa_list: str
|
|
75
|
+
) -> tuple[np.ndarray, list[str], list[str], int, int, int]:
|
|
76
|
+
qlist: list[str] | str = ""
|
|
77
|
+
with open(data_list, "r") as f:
|
|
78
|
+
flist = f.read().splitlines()
|
|
79
|
+
if qa_list != "":
|
|
80
|
+
with open(qa_list, "r") as f:
|
|
81
|
+
qlist = f.read().splitlines()
|
|
82
|
+
if len(flist) != len(qlist):
|
|
83
|
+
raise ValueError("No. of Data and QA are not consistent")
|
|
84
|
+
|
|
85
|
+
timevector, yr, yrstart, yrend = _read_time_vector(tlist, flist)
|
|
86
|
+
timevector, flist, qlist = _unique_by_timevector(flist, qlist, timevector)
|
|
87
|
+
return (
|
|
88
|
+
timevector,
|
|
89
|
+
flist,
|
|
90
|
+
(qlist if isinstance(qlist, list) else []),
|
|
91
|
+
yr,
|
|
92
|
+
yrstart,
|
|
93
|
+
yrend,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def open_image_data(
|
|
97
|
+
x_map: int,
|
|
98
|
+
y_map: int,
|
|
99
|
+
x: int,
|
|
100
|
+
y: int,
|
|
101
|
+
data_datasets: list,
|
|
102
|
+
qa_datasets: list,
|
|
103
|
+
lc_dataset,
|
|
104
|
+
data_type: str,
|
|
105
|
+
p_a,
|
|
106
|
+
layer: int,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Read VI, QA, and LC blocks using already-open rasterio datasets.
|
|
110
|
+
This is fast because we do NOT call rasterio.open() for each block.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
z = len(data_datasets)
|
|
114
|
+
|
|
115
|
+
# allocate arrays
|
|
116
|
+
vi = np.ndarray((y, x, z), order="F", dtype=data_type)
|
|
117
|
+
qa = np.ndarray((y, x, z), order="F", dtype=data_type)
|
|
118
|
+
lc = np.ndarray((y, x), order="F", dtype=np.uint8)
|
|
119
|
+
|
|
120
|
+
win = Window(x_map, y_map, x, y)
|
|
121
|
+
|
|
122
|
+
# -----------------------------------------------------------
|
|
123
|
+
# 1) Read VI stack
|
|
124
|
+
# -----------------------------------------------------------
|
|
125
|
+
for i, ds in enumerate(data_datasets):
|
|
126
|
+
arr = ds.read(layer, window=win)
|
|
127
|
+
if arr.ndim == 3:
|
|
128
|
+
arr = arr[0, :, :]
|
|
129
|
+
vi[:, :, i] = arr
|
|
130
|
+
|
|
131
|
+
# -----------------------------------------------------------
|
|
132
|
+
# 2) Read QA stack (or fill with ones)
|
|
133
|
+
# -----------------------------------------------------------
|
|
134
|
+
if len(qa_datasets) == 0:
|
|
135
|
+
qa[:] = 1
|
|
136
|
+
else:
|
|
137
|
+
for i, ds in enumerate(qa_datasets):
|
|
138
|
+
arr = ds.read(layer, window=win)
|
|
139
|
+
if arr.ndim == 3:
|
|
140
|
+
arr = arr[0, :, :]
|
|
141
|
+
qa[:, :, i] = arr
|
|
142
|
+
|
|
143
|
+
# Weight QA
|
|
144
|
+
from .qa import assign_qa_weight
|
|
145
|
+
qa = assign_qa_weight(p_a, qa)
|
|
146
|
+
|
|
147
|
+
# -----------------------------------------------------------
|
|
148
|
+
# 3) Land cover (single layer)
|
|
149
|
+
# -----------------------------------------------------------
|
|
150
|
+
if lc_dataset is None:
|
|
151
|
+
lc[:] = 1
|
|
152
|
+
else:
|
|
153
|
+
arr = lc_dataset.read(1, window=win)
|
|
154
|
+
if arr.ndim == 3:
|
|
155
|
+
arr = arr[0, :, :]
|
|
156
|
+
lc[:, :] = arr.astype(np.uint8)
|
|
157
|
+
|
|
158
|
+
return vi, qa, lc
|
|
159
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import rasterio
|
|
7
|
+
from rasterio.windows import Window
|
|
8
|
+
|
|
9
|
+
__all__ = ["prepare_profiles", "write_layers"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def prepare_profiles(img_profile, p_nodata: float, scale: float, offset: float):
|
|
13
|
+
img_profile_st = copy.deepcopy(img_profile)
|
|
14
|
+
img_profile_st.update(compress="lzw")
|
|
15
|
+
if scale != 1 or offset != 0:
|
|
16
|
+
img_profile_st.update(dtype=rasterio.float32)
|
|
17
|
+
|
|
18
|
+
img_profile_vpp = copy.deepcopy(img_profile)
|
|
19
|
+
img_profile_vpp.update(nodata=p_nodata, dtype=rasterio.float32, compress="lzw")
|
|
20
|
+
|
|
21
|
+
img_profile_qa = copy.deepcopy(img_profile)
|
|
22
|
+
img_profile_qa.update(nodata=0, dtype=rasterio.uint8, compress="lzw")
|
|
23
|
+
|
|
24
|
+
img_profile_ns = copy.deepcopy(img_profile)
|
|
25
|
+
img_profile_ns.update(nodata=255, dtype=rasterio.uint8, compress="lzw")
|
|
26
|
+
|
|
27
|
+
return img_profile_st, img_profile_vpp, img_profile_qa, img_profile_ns
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_layers(
|
|
31
|
+
datasets: list[rasterio.io.DatasetWriter],
|
|
32
|
+
arrays: np.ndarray,
|
|
33
|
+
window: tuple[int, int, int, int],
|
|
34
|
+
) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Write a block (window) for each array into the corresponding open dataset.
|
|
37
|
+
|
|
38
|
+
datasets : list of open rasterio DatasetWriter objects
|
|
39
|
+
arrays : np.ndarray with shape (n_layers, y, x) or iterable of 2D arrays
|
|
40
|
+
window : (x_map, y_map, x, y)
|
|
41
|
+
"""
|
|
42
|
+
x_map, y_map, x, y = window
|
|
43
|
+
win = Window(x_map, y_map, x, y)
|
|
44
|
+
|
|
45
|
+
for i, arr in enumerate(arrays, 1):
|
|
46
|
+
dst = datasets[i - 1]
|
|
47
|
+
dst.write(arr, window=win, indexes=1)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# src/timesat_cli/__main__.py
|
|
2
|
-
import argparse
|
|
3
|
-
from .processing import run
|
|
4
|
-
|
|
5
|
-
def main():
|
|
6
|
-
parser = argparse.ArgumentParser(description="Run TIMESAT processing pipeline.")
|
|
7
|
-
parser.add_argument("settings_json", help="Path to the JSON configuration file.")
|
|
8
|
-
args = parser.parse_args()
|
|
9
|
-
run(args.settings_json)
|
|
10
|
-
|
|
11
|
-
if __name__ == "__main__":
|
|
12
|
-
main()
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import os
|
|
3
|
-
import math
|
|
4
|
-
from typing import Tuple
|
|
5
|
-
|
|
6
|
-
__all__ = ["create_output_folders"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def create_output_folders(outfolder: str) -> Tuple[str, str]:
|
|
10
|
-
vpp_folder = os.path.join(outfolder, "VPP")
|
|
11
|
-
st_folder = os.path.join(outfolder, "ST")
|
|
12
|
-
os.makedirs(vpp_folder, exist_ok=True)
|
|
13
|
-
os.makedirs(st_folder, exist_ok=True)
|
|
14
|
-
return st_folder, vpp_folder
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def memory_plan(dx: int, dy: int, z: int, p_outindex_num: int, yr: int, max_memory_gb: float) -> Tuple[int, int]:
|
|
18
|
-
num_layers = p_outindex_num + z * 2 + (13 * 2) * yr
|
|
19
|
-
bytes_per = 4 # float32
|
|
20
|
-
max_bytes = max_memory_gb * (2 ** 30)
|
|
21
|
-
dy_max = max_bytes / (dx * num_layers * bytes_per)
|
|
22
|
-
y_slice_size = int(min(math.floor(dy_max), dy)) if dy_max > 0 else dy
|
|
23
|
-
y_slice_size = max(1, y_slice_size)
|
|
24
|
-
num_block = int(math.ceil(dy / y_slice_size))
|
|
25
|
-
return y_slice_size, num_block
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
__all__ = ["maybe_init_ray"]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def maybe_init_ray(para_check: int, ray_dir: str | None = None) -> bool:
|
|
7
|
-
"""
|
|
8
|
-
Initialize Ray if parallelism is requested.
|
|
9
|
-
|
|
10
|
-
Parameters
|
|
11
|
-
----------
|
|
12
|
-
para_check : int
|
|
13
|
-
Number of CPUs to use (if >1, Ray will be initialized).
|
|
14
|
-
ray_dir : str or None, optional
|
|
15
|
-
Temporary directory for Ray logs/state. If None or empty,
|
|
16
|
-
Ray will use its default temp location.
|
|
17
|
-
|
|
18
|
-
Returns
|
|
19
|
-
-------
|
|
20
|
-
bool
|
|
21
|
-
True if Ray was initialized, False otherwise.
|
|
22
|
-
"""
|
|
23
|
-
if para_check > 1:
|
|
24
|
-
import ray
|
|
25
|
-
kwargs = {"num_cpus": para_check}
|
|
26
|
-
if ray_dir: # only include if user provided a valid path
|
|
27
|
-
kwargs["_temp_dir"] = ray_dir
|
|
28
|
-
|
|
29
|
-
ray.init(**kwargs)
|
|
30
|
-
return True
|
|
31
|
-
|
|
32
|
-
return False
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import math, os, datetime
|
|
3
|
-
from typing import List, Tuple
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
import rasterio
|
|
7
|
-
|
|
8
|
-
import timesat # external dependency
|
|
9
|
-
|
|
10
|
-
from .config import load_config, build_param_array
|
|
11
|
-
from .readers import read_file_lists, open_image_data
|
|
12
|
-
from .fsutils import create_output_folders, memory_plan
|
|
13
|
-
from .writers import prepare_profiles, write_vpp_layers, write_st_layers
|
|
14
|
-
from .parallel import maybe_init_ray
|
|
15
|
-
|
|
16
|
-
VPP_NAMES = ["SOSD","SOSV","LSLOPE","EOSD","EOSV","RSLOPE","LENGTH",
|
|
17
|
-
"MINV","MAXD","MAXV","AMPL","TPROD","SPROD"]
|
|
18
|
-
|
|
19
|
-
def _build_output_filenames(st_folder: str, vpp_folder: str, p_outindex, yrstart: int, yrend: int):
|
|
20
|
-
outyfitfn = []
|
|
21
|
-
for i_tv in p_outindex:
|
|
22
|
-
yfitdate = datetime.date(yrstart, 1, 1) + datetime.timedelta(days=int(i_tv)) - datetime.timedelta(days=1)
|
|
23
|
-
outyfitfn.append(os.path.join(st_folder, f"TIMESAT_{yfitdate.strftime('%Y%m%d')}.tif"))
|
|
24
|
-
|
|
25
|
-
outvppfn = []
|
|
26
|
-
for i_yr in range(yrstart, yrend + 1):
|
|
27
|
-
for i_seas in range(2):
|
|
28
|
-
for name in VPP_NAMES:
|
|
29
|
-
outvppfn.append(os.path.join(vpp_folder, f"TIMESAT_{name}_{i_yr}_season_{i_seas+1}.tif"))
|
|
30
|
-
outnsfn = os.path.join(vpp_folder, 'TIMESAT_nsperyear.tif')
|
|
31
|
-
return outyfitfn, outvppfn, outnsfn
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def run(jsfile: str) -> None:
|
|
35
|
-
print(jsfile)
|
|
36
|
-
cfg = load_config(jsfile)
|
|
37
|
-
s = cfg.settings
|
|
38
|
-
|
|
39
|
-
if s.outputfolder == '':
|
|
40
|
-
print('Nothing to do...')
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
# Precompute arrays once per block to pass into timesat
|
|
44
|
-
landuse_arr = build_param_array(s, 'landuse', 'uint8')
|
|
45
|
-
p_fitmethod_arr = build_param_array(s, 'p_fitmethod', 'uint8')
|
|
46
|
-
p_smooth_arr = build_param_array(s, 'p_smooth', 'double')
|
|
47
|
-
p_nenvi_arr = build_param_array(s, 'p_nenvi', 'uint8')
|
|
48
|
-
p_wfactnum_arr = build_param_array(s, 'p_wfactnum', 'double')
|
|
49
|
-
p_startmethod_arr = build_param_array(s, 'p_startmethod', 'uint8')
|
|
50
|
-
p_startcutoff_arr = build_param_array(s, 'p_startcutoff', 'double', shape=(2,), fortran_2d=True)
|
|
51
|
-
p_low_percentile_arr = build_param_array(s, 'p_low_percentile', 'double')
|
|
52
|
-
p_fillbase_arr = build_param_array(s, 'p_fillbase', 'uint8')
|
|
53
|
-
p_seasonmethod_arr = build_param_array(s, 'p_seasonmethod', 'uint8')
|
|
54
|
-
p_seapar_arr = build_param_array(s, 'p_seapar', 'double')
|
|
55
|
-
|
|
56
|
-
ray_inited = maybe_init_ray(s.para_check, s.ray_dir)
|
|
57
|
-
|
|
58
|
-
timevector, flist, qlist, yr, yrstart, yrend = read_file_lists(s.tv_list, s.image_file_list, s.quality_file_list)
|
|
59
|
-
|
|
60
|
-
z = len(flist)
|
|
61
|
-
print(f'num of images: {z}')
|
|
62
|
-
print('First image: ' + os.path.basename(flist[0]))
|
|
63
|
-
print('Last image: ' + os.path.basename(flist[-1]))
|
|
64
|
-
print(yrstart)
|
|
65
|
-
|
|
66
|
-
p_outindex = np.arange(
|
|
67
|
-
(datetime.datetime(yrstart, 1, 1) - datetime.datetime(yrstart, 1, 1)).days + 1,
|
|
68
|
-
(datetime.datetime(yrstart + yr - 1, 12, 31) - datetime.datetime(yrstart, 1, 1)).days + 1
|
|
69
|
-
)[:: int(s.p_st_timestep)]
|
|
70
|
-
p_outindex_num = len(p_outindex)
|
|
71
|
-
|
|
72
|
-
with rasterio.open(flist[0], 'r') as temp:
|
|
73
|
-
img_profile = temp.profile
|
|
74
|
-
|
|
75
|
-
if sum(s.imwindow) == 0:
|
|
76
|
-
dx, dy = img_profile['width'], img_profile['height']
|
|
77
|
-
else:
|
|
78
|
-
dx, dy = int(s.imwindow[2]), int(s.imwindow[3])
|
|
79
|
-
|
|
80
|
-
imgprocessing = not (s.imwindow[2] + s.imwindow[3] == 2)
|
|
81
|
-
|
|
82
|
-
if imgprocessing:
|
|
83
|
-
st_folder, vpp_folder = create_output_folders(s.outputfolder)
|
|
84
|
-
outyfitfn, outvppfn, outnsfn = _build_output_filenames(st_folder, vpp_folder, p_outindex, yrstart, yrend)
|
|
85
|
-
img_profile_st, img_profile_vpp, img_profile_ns = prepare_profiles(img_profile, s.p_nodata, s.scale, s.offset)
|
|
86
|
-
# pre-create files
|
|
87
|
-
for path in outvppfn:
|
|
88
|
-
with rasterio.open(path, 'w', **img_profile_vpp):
|
|
89
|
-
pass
|
|
90
|
-
for path in outyfitfn:
|
|
91
|
-
with rasterio.open(path, 'w', **img_profile_st):
|
|
92
|
-
pass
|
|
93
|
-
|
|
94
|
-
# compute memory blocks
|
|
95
|
-
y_slice_size, num_block = memory_plan(dx, dy, z, p_outindex_num, yr, s.max_memory_gb)
|
|
96
|
-
y_slice_end = dy % y_slice_size if (dy % y_slice_size) > 0 else y_slice_size
|
|
97
|
-
print('y_slice_size = ' + str(y_slice_size))
|
|
98
|
-
|
|
99
|
-
for iblock in range(num_block):
|
|
100
|
-
print(f'Processing block: {iblock + 1}/{num_block} starttime: {datetime.datetime.now()}')
|
|
101
|
-
x = dx
|
|
102
|
-
y = int(y_slice_size) if iblock != num_block - 1 else int(y_slice_end)
|
|
103
|
-
x_map = int(s.imwindow[0])
|
|
104
|
-
y_map = int(iblock * y_slice_size + s.imwindow[1])
|
|
105
|
-
|
|
106
|
-
vi, qa, lc = open_image_data(
|
|
107
|
-
x_map, y_map, x, y, flist, qlist if qlist else '', s.lc_file,
|
|
108
|
-
img_profile['dtype'], s.p_a, s.para_check, s.p_band_id
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
print('--- start TIMESAT processing --- starttime: ' + str(datetime.datetime.now()))
|
|
112
|
-
|
|
113
|
-
if s.scale != 1 or s.offset != 0:
|
|
114
|
-
vi = vi * s.scale + s.offset
|
|
115
|
-
|
|
116
|
-
if s.para_check > 1 and ray_inited:
|
|
117
|
-
import ray
|
|
118
|
-
|
|
119
|
-
@ray.remote
|
|
120
|
-
def runtimesat(vi_temp, qa_temp, lc_temp):
|
|
121
|
-
vpp_para, vppqa, nseason_para, yfit_para, yfitqa, seasonfit, tseq = timesat.tsf2py(
|
|
122
|
-
yr, vi_temp, qa_temp, timevector, lc_temp, s.p_nclasses,landuse_arr, p_outindex,
|
|
123
|
-
s.p_ignoreday, s.p_ylu, s.p_printflag, p_fitmethod_arr, p_smooth_arr,
|
|
124
|
-
s.p_nodata, s.p_davailwin, s.p_outlier,
|
|
125
|
-
p_nenvi_arr, p_wfactnum_arr, p_startmethod_arr, p_startcutoff_arr,
|
|
126
|
-
p_low_percentile_arr, p_fillbase_arr, s.p_hrvppformat,
|
|
127
|
-
p_seasonmethod_arr, p_seapar_arr,
|
|
128
|
-
1, x, len(flist), p_outindex_num
|
|
129
|
-
)
|
|
130
|
-
vpp_para = vpp_para[0, :, :]
|
|
131
|
-
yfit_para = yfit_para[0, :, :]
|
|
132
|
-
nseason_para = nseason_para[0, :]
|
|
133
|
-
return vpp_para, yfit_para, nseason_para
|
|
134
|
-
|
|
135
|
-
futures = [
|
|
136
|
-
runtimesat.remote(
|
|
137
|
-
np.expand_dims(vi[i, :, :], axis=0),
|
|
138
|
-
np.expand_dims(qa[i, :, :], axis=0),
|
|
139
|
-
np.expand_dims(lc[i, :], axis=0)
|
|
140
|
-
) for i in range(y)
|
|
141
|
-
]
|
|
142
|
-
results = ray.get(futures)
|
|
143
|
-
vpp = np.stack([r[0] for r in results], axis=0)
|
|
144
|
-
yfit = np.stack([r[1] for r in results], axis=0)
|
|
145
|
-
nseason = np.stack([r[2] for r in results], axis=0)
|
|
146
|
-
else:
|
|
147
|
-
vpp, vppqa, nseason, yfit, yfitqa, seasonfit, tseq = timesat.tsf2py(
|
|
148
|
-
yr, vi, qa, timevector, lc, s.p_nclasses, landuse_arr, p_outindex,
|
|
149
|
-
s.p_ignoreday, s.p_ylu, s.p_printflag, p_fitmethod_arr, p_smooth_arr,
|
|
150
|
-
s.p_nodata, s.p_davailwin, s.p_outlier,
|
|
151
|
-
p_nenvi_arr, p_wfactnum_arr, p_startmethod_arr, p_startcutoff_arr,
|
|
152
|
-
p_low_percentile_arr, p_fillbase_arr, s.p_hrvppformat,
|
|
153
|
-
p_seasonmethod_arr, p_seapar_arr,
|
|
154
|
-
y, x, len(flist), p_outindex_num)
|
|
155
|
-
|
|
156
|
-
vpp = np.moveaxis(vpp, -1, 0)
|
|
157
|
-
if s.scale == 0 and s.offset == 0:
|
|
158
|
-
yfit = np.moveaxis(yfit, -1, 0).astype(img_profile['dtype'])
|
|
159
|
-
else:
|
|
160
|
-
yfit = np.moveaxis(yfit, -1, 0).astype('float32')
|
|
161
|
-
|
|
162
|
-
print('--- start writing geotif --- starttime: ' + str(datetime.datetime.now()))
|
|
163
|
-
window = (x_map, y_map, x, y)
|
|
164
|
-
write_vpp_layers(outvppfn, vpp, window, img_profile_vpp)
|
|
165
|
-
write_st_layers(outyfitfn, yfit, window, img_profile_st)
|
|
166
|
-
|
|
167
|
-
print(f'Block: {iblock + 1}/{num_block} finishedtime: {datetime.datetime.now()}')
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import datetime
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
from typing import List, Tuple
|
|
7
|
-
|
|
8
|
-
import numpy as np
|
|
9
|
-
import rasterio
|
|
10
|
-
from rasterio.windows import Window
|
|
11
|
-
|
|
12
|
-
from .qa import assign_qa_weight
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
import ray
|
|
16
|
-
except Exception: # optional
|
|
17
|
-
ray = None
|
|
18
|
-
|
|
19
|
-
__all__ = ["read_file_lists", "open_image_data"]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _parse_dates_from_name(name: str) -> Tuple[int, int, int]:
|
|
23
|
-
date_regex1 = r"\d{4}-\d{2}-\d{2}"
|
|
24
|
-
date_regex2 = r"\d{4}\d{2}\d{2}"
|
|
25
|
-
try:
|
|
26
|
-
dates = re.findall(date_regex1, name)
|
|
27
|
-
position = name.find(dates[0])
|
|
28
|
-
y = int(name[position : position + 4])
|
|
29
|
-
m = int(name[position + 5 : position + 7])
|
|
30
|
-
d = int(name[position + 8 : position + 10])
|
|
31
|
-
return y, m, d
|
|
32
|
-
except Exception:
|
|
33
|
-
try:
|
|
34
|
-
dates = re.findall(date_regex2, name)
|
|
35
|
-
position = name.find(dates[0])
|
|
36
|
-
y = int(name[position : position + 4])
|
|
37
|
-
m = int(name[position + 4 : position + 6])
|
|
38
|
-
d = int(name[position + 6 : position + 8])
|
|
39
|
-
return y, m, d
|
|
40
|
-
except Exception as e:
|
|
41
|
-
raise ValueError(f"No date found in filename: {name}") from e
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _read_time_vector(tlist: str, filepaths: List[str]):
|
|
45
|
-
"""Return (timevector, yr, yrstart, yrend) in YYYYDOY format."""
|
|
46
|
-
flist = [os.path.basename(p) for p in filepaths]
|
|
47
|
-
timevector = np.ndarray(len(flist), order="F", dtype="uint32")
|
|
48
|
-
if tlist == "":
|
|
49
|
-
for i, fname in enumerate(flist):
|
|
50
|
-
y, m, d = _parse_dates_from_name(fname)
|
|
51
|
-
doy = (datetime.date(y, m, d) - datetime.date(y, 1, 1)).days + 1
|
|
52
|
-
timevector[i] = y * 1000 + doy
|
|
53
|
-
else:
|
|
54
|
-
with open(tlist, "r") as f:
|
|
55
|
-
lines = f.read().splitlines()
|
|
56
|
-
for idx, val in enumerate(lines):
|
|
57
|
-
n = len(val)
|
|
58
|
-
if n == 8: # YYYYMMDD
|
|
59
|
-
dt = datetime.datetime.strptime(val, "%Y%m%d")
|
|
60
|
-
timevector[idx] = int(f"{dt.year}{dt.timetuple().tm_yday:03d}")
|
|
61
|
-
elif n == 7: # YYYYDOY
|
|
62
|
-
_ = datetime.datetime.strptime(val, "%Y%j")
|
|
63
|
-
timevector[idx] = int(val)
|
|
64
|
-
else:
|
|
65
|
-
raise ValueError(f"Unrecognized date format: {val}")
|
|
66
|
-
|
|
67
|
-
yrstart = int(np.floor(timevector.min() / 1000))
|
|
68
|
-
yrend = int(np.floor(timevector.max() / 1000))
|
|
69
|
-
yr = yrend - yrstart + 1
|
|
70
|
-
return timevector, yr, yrstart, yrend
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _unique_by_timevector(flist: List[str], qlist: List[str], timevector):
|
|
74
|
-
tv_unique, indices = np.unique(timevector, return_index=True)
|
|
75
|
-
flist2 = [flist[i] for i in indices]
|
|
76
|
-
qlist2 = [qlist[i] for i in indices] if qlist else []
|
|
77
|
-
return tv_unique, flist2, qlist2
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def read_file_lists(
|
|
81
|
-
tlist: str, data_list: str, qa_list: str
|
|
82
|
-
) -> Tuple[np.ndarray, List[str], List[str], int, int, int]:
|
|
83
|
-
qlist: List[str] | str = ""
|
|
84
|
-
with open(data_list, "r") as f:
|
|
85
|
-
flist = f.read().splitlines()
|
|
86
|
-
if qa_list != "":
|
|
87
|
-
with open(qa_list, "r") as f:
|
|
88
|
-
qlist = f.read().splitlines()
|
|
89
|
-
if len(flist) != len(qlist):
|
|
90
|
-
raise ValueError("No. of Data and QA are not consistent")
|
|
91
|
-
|
|
92
|
-
timevector, yr, yrstart, yrend = _read_time_vector(tlist, flist)
|
|
93
|
-
timevector, flist, qlist = _unique_by_timevector(flist, qlist, timevector)
|
|
94
|
-
return (
|
|
95
|
-
timevector,
|
|
96
|
-
flist,
|
|
97
|
-
(qlist if isinstance(qlist, list) else []),
|
|
98
|
-
yr,
|
|
99
|
-
yrstart,
|
|
100
|
-
yrend,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def open_image_data(
|
|
105
|
-
x_map: int,
|
|
106
|
-
y_map: int,
|
|
107
|
-
x: int,
|
|
108
|
-
y: int,
|
|
109
|
-
yflist: List[str],
|
|
110
|
-
wflist: List[str] | str,
|
|
111
|
-
lcfile: str,
|
|
112
|
-
data_type: str,
|
|
113
|
-
p_a,
|
|
114
|
-
para_check: int,
|
|
115
|
-
layer: int,
|
|
116
|
-
s3: dict | None = None,
|
|
117
|
-
):
|
|
118
|
-
"""Read VI, QA, and LC blocks as arrays."""
|
|
119
|
-
z = len(yflist)
|
|
120
|
-
vi = np.ndarray((y, x, z), order="F", dtype=data_type)
|
|
121
|
-
qa = np.ndarray((y, x, z), order="F", dtype=data_type)
|
|
122
|
-
lc = np.ndarray((y, x, z), order="F", dtype=np.uint8)
|
|
123
|
-
|
|
124
|
-
# VI stack
|
|
125
|
-
if para_check > 1 and ray is not None:
|
|
126
|
-
vi_para = np.ndarray((y, x), order="F", dtype=data_type)
|
|
127
|
-
|
|
128
|
-
@ray.remote
|
|
129
|
-
def _readimgpara_(yfname, s3=s3):
|
|
130
|
-
if s3 is not None:
|
|
131
|
-
with rasterio.Env(**s3):
|
|
132
|
-
with rasterio.open(yfname, "r") as temp:
|
|
133
|
-
vi_para[:, :] = temp.read(
|
|
134
|
-
layer, window=Window(x_map, y_map, x, y)
|
|
135
|
-
)
|
|
136
|
-
else:
|
|
137
|
-
with rasterio.open(yfname, "r") as temp:
|
|
138
|
-
vi_para[:, :] = temp.read(layer, window=Window(x_map, y_map, x, y))
|
|
139
|
-
return vi_para
|
|
140
|
-
|
|
141
|
-
futures = [_readimgpara_.remote(i) for i in yflist]
|
|
142
|
-
vi = np.stack(ray.get(futures), axis=2)
|
|
143
|
-
else:
|
|
144
|
-
for i, yfname in enumerate(yflist):
|
|
145
|
-
if s3 is not None:
|
|
146
|
-
with rasterio.Env(**s3):
|
|
147
|
-
with rasterio.open(yfname, "r") as temp:
|
|
148
|
-
vi[:, :, i] = temp.read(
|
|
149
|
-
layer, window=Window(x_map, y_map, x, y)
|
|
150
|
-
)
|
|
151
|
-
else:
|
|
152
|
-
with rasterio.open(yfname, "r") as temp:
|
|
153
|
-
vi[:, :, i] = temp.read(layer, window=Window(x_map, y_map, x, y))
|
|
154
|
-
|
|
155
|
-
# QA stack
|
|
156
|
-
if wflist == "" or wflist == []:
|
|
157
|
-
qa = np.ones((y, x, z))
|
|
158
|
-
else:
|
|
159
|
-
if para_check > 1 and ray is not None:
|
|
160
|
-
qa_para = np.ndarray((y, x), order="F", dtype=data_type)
|
|
161
|
-
|
|
162
|
-
@ray.remote
|
|
163
|
-
def _readqapara_(wfname, s3=s3):
|
|
164
|
-
if s3 is not None:
|
|
165
|
-
with rasterio.Env(**s3):
|
|
166
|
-
with rasterio.open(wfname, "r") as temp:
|
|
167
|
-
qa_para[:, :] = temp.read(
|
|
168
|
-
layer, window=Window(x_map, y_map, x, y)
|
|
169
|
-
)
|
|
170
|
-
else:
|
|
171
|
-
with rasterio.open(wfname, "r") as temp:
|
|
172
|
-
qa_para[:, :] = temp.read(
|
|
173
|
-
layer, window=Window(x_map, y_map, x, y)
|
|
174
|
-
)
|
|
175
|
-
return qa_para
|
|
176
|
-
|
|
177
|
-
futures = [_readqapara_.remote(i) for i in wflist]
|
|
178
|
-
qa = np.stack(ray.get(futures), axis=2)
|
|
179
|
-
else:
|
|
180
|
-
for i, wfname in enumerate(wflist):
|
|
181
|
-
if s3 is not None:
|
|
182
|
-
with rasterio.Env(**s3):
|
|
183
|
-
with rasterio.open(wfname, "r") as temp2:
|
|
184
|
-
qa[:, :, i] = temp2.read(
|
|
185
|
-
1, window=Window(x_map, y_map, x, y)
|
|
186
|
-
)
|
|
187
|
-
else:
|
|
188
|
-
with rasterio.open(wfname, "r") as temp2:
|
|
189
|
-
qa[:, :, i] = temp2.read(1, window=Window(x_map, y_map, x, y))
|
|
190
|
-
qa = assign_qa_weight(p_a, qa)
|
|
191
|
-
|
|
192
|
-
# LC
|
|
193
|
-
if lcfile == "":
|
|
194
|
-
lc = np.ones((y, x))
|
|
195
|
-
else:
|
|
196
|
-
if s3 is not None:
|
|
197
|
-
with rasterio.Env(**s3):
|
|
198
|
-
with rasterio.open(lcfile, "r") as temp3:
|
|
199
|
-
lc = temp3.read(1, window=Window(x_map, y_map, x, y))
|
|
200
|
-
else:
|
|
201
|
-
with rasterio.open(lcfile, "r") as temp3:
|
|
202
|
-
lc = temp3.read(1, window=Window(x_map, y_map, x, y))
|
|
203
|
-
|
|
204
|
-
return vi, qa, lc
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from typing import List, Tuple
|
|
3
|
-
import rasterio
|
|
4
|
-
from rasterio.windows import Window
|
|
5
|
-
|
|
6
|
-
__all__ = ["prepare_profiles", "write_vpp_layers", "write_st_layers"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def prepare_profiles(img_profile, p_nodata: float, scale: float, offset: float):
|
|
10
|
-
import copy
|
|
11
|
-
img_profile_st = copy.deepcopy(img_profile)
|
|
12
|
-
img_profile_st.update(compress='lzw')
|
|
13
|
-
if scale != 0 or offset != 0:
|
|
14
|
-
img_profile_st.update(dtype=rasterio.float32)
|
|
15
|
-
|
|
16
|
-
img_profile_vpp = copy.deepcopy(img_profile)
|
|
17
|
-
img_profile_vpp.update(nodata=p_nodata, dtype=rasterio.float32, compress='lzw')
|
|
18
|
-
|
|
19
|
-
img_profile_ns = copy.deepcopy(img_profile)
|
|
20
|
-
img_profile_ns.update(nodata=255, compress='lzw')
|
|
21
|
-
return img_profile_st, img_profile_vpp, img_profile_ns
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def write_vpp_layers(paths: List[str], arrays, window: Tuple[int, int, int, int], img_profile_vpp):
|
|
25
|
-
x_map, y_map, x, y = window
|
|
26
|
-
for i, arr in enumerate(arrays, 1):
|
|
27
|
-
with rasterio.open(paths[i - 1], 'r+', **img_profile_vpp) as outvppfile:
|
|
28
|
-
outvppfile.write(arr, window=Window(x_map, y_map, x, y), indexes=1)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def write_st_layers(paths: List[str], arrays, window: Tuple[int, int, int, int], img_profile_st):
|
|
32
|
-
x_map, y_map, x, y = window
|
|
33
|
-
for i, arr in enumerate(arrays, 1):
|
|
34
|
-
with rasterio.open(paths[i - 1], 'r+', **img_profile_st) as outstfile:
|
|
35
|
-
outstfile.write(arr, window=Window(x_map, y_map, x, y), indexes=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|