oafuncs 0.0.98.2__tar.gz → 0.0.98.4__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.
- {oafuncs-0.0.98.2/oafuncs.egg-info → oafuncs-0.0.98.4}/PKG-INFO +2 -1
- oafuncs-0.0.98.4/oafuncs/_script/netcdf_write.py +203 -0
- oafuncs-0.0.98.4/oafuncs/_script/parallel.py +214 -0
- oafuncs-0.0.98.4/oafuncs/_script/parallel_test.py +14 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_down/User_Agent-list.txt +1 -1611
- oafuncs-0.0.98.4/oafuncs/oa_down/hycom_3hourly.py +1216 -0
- oafuncs-0.0.98.2/oafuncs/oa_down/hycom_3hourly.py → oafuncs-0.0.98.4/oafuncs/oa_down/hycom_3hourly_20250416.py +16 -7
- oafuncs-0.0.98.4/oafuncs/oa_down/test_ua.py +40 -0
- oafuncs-0.0.98.4/oafuncs/oa_tool.py +207 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4/oafuncs.egg-info}/PKG-INFO +2 -1
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs.egg-info/SOURCES.txt +2 -1
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs.egg-info/requires.txt +1 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/setup.py +2 -1
- oafuncs-0.0.98.2/oafuncs/_script/netcdf_write.py +0 -116
- oafuncs-0.0.98.2/oafuncs/_script/parallel.py +0 -565
- oafuncs-0.0.98.2/oafuncs/_script/parallel_example_usage.py +0 -83
- oafuncs-0.0.98.2/oafuncs/oa_down/test_ua.py +0 -151
- oafuncs-0.0.98.2/oafuncs/oa_tool.py +0 -119
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/LICENSE.txt +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/MANIFEST.in +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/README.md +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/__init__.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_data/hycom.png +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_data/oafuncs.png +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_script/cprogressbar.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_script/email.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_script/netcdf_merge.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_script/netcdf_modify.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_script/plot_dataset.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/_script/replace_file_content.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_cmap.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_data.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_date.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_down/__init__.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_down/hycom_3hourly_20250407.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_down/idm.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_down/literature.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_down/user_agent.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_draw.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_file.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_help.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_model/__init__.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_model/roms/__init__.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_model/roms/test.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_model/wrf/__init__.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_model/wrf/little_r.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_nc.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_python.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_sign/__init__.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_sign/meteorological.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_sign/ocean.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs/oa_sign/scientific.py +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs.egg-info/dependency_links.txt +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/oafuncs.egg-info/top_level.txt +0 -0
- {oafuncs-0.0.98.2 → oafuncs-0.0.98.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: oafuncs
|
3
|
-
Version: 0.0.98.
|
3
|
+
Version: 0.0.98.4
|
4
4
|
Summary: Oceanic and Atmospheric Functions
|
5
5
|
Home-page: https://github.com/Industry-Pays/OAFuncs
|
6
6
|
Author: Kun Liu
|
@@ -25,6 +25,7 @@ Requires-Dist: rich
|
|
25
25
|
Requires-Dist: pathlib
|
26
26
|
Requires-Dist: requests
|
27
27
|
Requires-Dist: bs4
|
28
|
+
Requires-Dist: httpx
|
28
29
|
Requires-Dist: matplotlib
|
29
30
|
Requires-Dist: netCDF4
|
30
31
|
Requires-Dist: xlrd
|
@@ -0,0 +1,203 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import netCDF4 as nc
|
4
|
+
import numpy as np
|
5
|
+
import xarray as xr
|
6
|
+
import warnings
|
7
|
+
|
8
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
9
|
+
|
10
|
+
|
11
|
+
def _numpy_to_nc_type(numpy_type):
|
12
|
+
"""将 NumPy 数据类型映射到 NetCDF 数据类型"""
|
13
|
+
numpy_to_nc = {
|
14
|
+
"float32": "f4",
|
15
|
+
"float64": "f8",
|
16
|
+
"int8": "i1",
|
17
|
+
"int16": "i2",
|
18
|
+
"int32": "i4",
|
19
|
+
"int64": "i8",
|
20
|
+
"uint8": "u1",
|
21
|
+
"uint16": "u2",
|
22
|
+
"uint32": "u4",
|
23
|
+
"uint64": "u8",
|
24
|
+
}
|
25
|
+
numpy_type_str = str(numpy_type) if not isinstance(numpy_type, str) else numpy_type
|
26
|
+
return numpy_to_nc.get(numpy_type_str, "f4")
|
27
|
+
|
28
|
+
|
29
|
+
def _calculate_scale_and_offset(data, n=16):
|
30
|
+
"""
|
31
|
+
计算数值型数据的 scale_factor 与 add_offset,
|
32
|
+
将数据映射到 [0, 2**n - 1] 的范围。
|
33
|
+
|
34
|
+
要求 data 为数值型的 NumPy 数组,不允许全 NaN 值。
|
35
|
+
"""
|
36
|
+
if not isinstance(data, np.ndarray):
|
37
|
+
raise ValueError("Input data must be a NumPy array.")
|
38
|
+
|
39
|
+
data_min = np.nanmin(data)
|
40
|
+
data_max = np.nanmax(data)
|
41
|
+
|
42
|
+
if np.isnan(data_min) or np.isnan(data_max):
|
43
|
+
raise ValueError("Input data contains NaN values.")
|
44
|
+
|
45
|
+
if data_max == data_min:
|
46
|
+
scale_factor = 1.0
|
47
|
+
add_offset = data_min
|
48
|
+
else:
|
49
|
+
scale_factor = (data_max - data_min) / (2**n - 1)
|
50
|
+
add_offset = data_min + 2 ** (n - 1) * scale_factor
|
51
|
+
return scale_factor, add_offset
|
52
|
+
|
53
|
+
|
54
|
+
def _data_to_scale_offset(data, scale, offset):
|
55
|
+
"""
|
56
|
+
将数据转换为 scale_factor 和 add_offset 的形式。
|
57
|
+
此处同时替换 NaN、正无穷和负无穷为填充值 -32767,
|
58
|
+
以确保转换后的数据可安全转为 int16。
|
59
|
+
"""
|
60
|
+
if not isinstance(data, np.ndarray):
|
61
|
+
raise ValueError("Input data must be a NumPy array.")
|
62
|
+
|
63
|
+
# 先计算转换后的数据
|
64
|
+
result = np.around((data - offset) / scale)
|
65
|
+
# 替换 NaN, 正负无穷(posinf, neginf)为 -32767
|
66
|
+
result = np.nan_to_num(result, nan=-32767, posinf=-32767, neginf=-32767)
|
67
|
+
result = np.clip(result, -32767, 32767) # 限制范围在 int16 的有效范围内
|
68
|
+
result = np.where(np.isfinite(result), result, -32767) # 替换无效值为 -32767
|
69
|
+
new_data = result.astype(np.int16)
|
70
|
+
return new_data
|
71
|
+
|
72
|
+
|
73
|
+
def save_to_nc(file, data, varname=None, coords=None, mode="w", scale_offset_switch=True, compile_switch=True):
|
74
|
+
"""
|
75
|
+
保存数据到 NetCDF 文件,支持 xarray 对象(DataArray 或 Dataset)和 numpy 数组。
|
76
|
+
|
77
|
+
仅对数据变量中数值型数据进行压缩转换(利用 scale_factor/add_offset 转换后转为 int16),
|
78
|
+
非数值型数据以及所有坐标变量将禁用任何压缩,直接保存原始数据。
|
79
|
+
|
80
|
+
参数:
|
81
|
+
- file: 保存文件的路径
|
82
|
+
- data: xarray.DataArray、xarray.Dataset 或 numpy 数组
|
83
|
+
- varname: 变量名(仅适用于传入 numpy 数组或 DataArray 时)
|
84
|
+
- coords: 坐标字典(numpy 数组分支时使用),所有坐标变量均不压缩
|
85
|
+
- mode: "w"(覆盖)或 "a"(追加)
|
86
|
+
- scale_offset_switch: 是否对数值型数据变量进行压缩转换
|
87
|
+
- compile_switch: 是否启用 NetCDF4 的 zlib 压缩(仅针对数值型数据有效)
|
88
|
+
"""
|
89
|
+
# 处理 xarray 对象(DataArray 或 Dataset)的情况
|
90
|
+
if isinstance(data, (xr.DataArray, xr.Dataset)):
|
91
|
+
encoding = {} # 用于保存数据变量的编码信息
|
92
|
+
|
93
|
+
if isinstance(data, xr.DataArray):
|
94
|
+
if data.name is None:
|
95
|
+
data = data.rename("data")
|
96
|
+
varname = data.name if varname is None else varname
|
97
|
+
# 判断数据是否为数值型
|
98
|
+
if np.issubdtype(data.values.dtype, np.number) and scale_offset_switch:
|
99
|
+
scale, offset = _calculate_scale_and_offset(data.values)
|
100
|
+
new_values = _data_to_scale_offset(data.values, scale, offset)
|
101
|
+
# 生成新 DataArray,保留原坐标和属性,同时写入转换参数到属性中
|
102
|
+
new_da = data.copy(data=new_values)
|
103
|
+
new_da.attrs["scale_factor"] = float(scale)
|
104
|
+
new_da.attrs["add_offset"] = float(offset)
|
105
|
+
encoding[varname] = {
|
106
|
+
"zlib": compile_switch,
|
107
|
+
"complevel": 4,
|
108
|
+
"dtype": "int16",
|
109
|
+
"_FillValue": -32767,
|
110
|
+
}
|
111
|
+
new_da.to_dataset(name=varname).to_netcdf(file, mode=mode, encoding=encoding)
|
112
|
+
else:
|
113
|
+
data.to_dataset(name=varname).to_netcdf(file, mode=mode)
|
114
|
+
return
|
115
|
+
|
116
|
+
else:
|
117
|
+
# 处理 Dataset 的情况,仅处理 data_vars 数据变量,坐标变量保持原样
|
118
|
+
new_vars = {}
|
119
|
+
encoding = {}
|
120
|
+
for var in data.data_vars:
|
121
|
+
da = data[var]
|
122
|
+
if np.issubdtype(np.asarray(da.values).dtype, np.number) and scale_offset_switch:
|
123
|
+
scale, offset = _calculate_scale_and_offset(da.values)
|
124
|
+
new_values = _data_to_scale_offset(da.values, scale, offset)
|
125
|
+
new_da = xr.DataArray(new_values, dims=da.dims, coords=da.coords, attrs=da.attrs)
|
126
|
+
new_da.attrs["scale_factor"] = float(scale)
|
127
|
+
new_da.attrs["add_offset"] = float(offset)
|
128
|
+
new_vars[var] = new_da
|
129
|
+
encoding[var] = {
|
130
|
+
"zlib": compile_switch,
|
131
|
+
"complevel": 4,
|
132
|
+
"dtype": "int16",
|
133
|
+
"_FillValue": -32767,
|
134
|
+
}
|
135
|
+
else:
|
136
|
+
new_vars[var] = da
|
137
|
+
new_ds = xr.Dataset(new_vars, coords=data.coords)
|
138
|
+
if encoding:
|
139
|
+
new_ds.to_netcdf(file, mode=mode, encoding=encoding)
|
140
|
+
else:
|
141
|
+
new_ds.to_netcdf(file, mode=mode)
|
142
|
+
return
|
143
|
+
|
144
|
+
# 处理纯 numpy 数组情况
|
145
|
+
if mode == "w" and os.path.exists(file):
|
146
|
+
os.remove(file)
|
147
|
+
elif mode == "a" and not os.path.exists(file):
|
148
|
+
mode = "w"
|
149
|
+
data = np.asarray(data)
|
150
|
+
is_numeric = np.issubdtype(data.dtype, np.number)
|
151
|
+
try:
|
152
|
+
with nc.Dataset(file, mode, format="NETCDF4") as ncfile:
|
153
|
+
# 坐标变量直接写入,不做压缩
|
154
|
+
if coords is not None:
|
155
|
+
for dim, values in coords.items():
|
156
|
+
if dim not in ncfile.dimensions:
|
157
|
+
ncfile.createDimension(dim, len(values))
|
158
|
+
var_obj = ncfile.createVariable(dim, _numpy_to_nc_type(np.asarray(values).dtype), (dim,))
|
159
|
+
var_obj[:] = values
|
160
|
+
|
161
|
+
dims = list(coords.keys()) if coords else []
|
162
|
+
if is_numeric and scale_offset_switch:
|
163
|
+
scale, offset = _calculate_scale_and_offset(data)
|
164
|
+
new_data = _data_to_scale_offset(data, scale, offset)
|
165
|
+
var = ncfile.createVariable(varname, "i2", dims, fill_value=-32767, zlib=compile_switch)
|
166
|
+
var.scale_factor = scale
|
167
|
+
var.add_offset = offset
|
168
|
+
# Ensure no invalid values in new_data before assignment
|
169
|
+
var[:] = new_data
|
170
|
+
else:
|
171
|
+
# 非数值型数据,禁止压缩
|
172
|
+
dtype = _numpy_to_nc_type(data.dtype)
|
173
|
+
var = ncfile.createVariable(varname, dtype, dims, zlib=False)
|
174
|
+
var[:] = data
|
175
|
+
except Exception as e:
|
176
|
+
raise RuntimeError(f"netCDF4 保存失败: {str(e)}") from e
|
177
|
+
|
178
|
+
|
179
|
+
# 测试用例
|
180
|
+
if __name__ == "__main__":
|
181
|
+
# --------------------------------
|
182
|
+
# dataset
|
183
|
+
file = r"F:\roms_rst.nc"
|
184
|
+
ds = xr.open_dataset(file)
|
185
|
+
outfile = r"F:\roms_rst_test.nc"
|
186
|
+
save_to_nc(outfile, ds)
|
187
|
+
ds.close()
|
188
|
+
# --------------------------------
|
189
|
+
# dataarray
|
190
|
+
data = np.random.rand(4, 3, 2)
|
191
|
+
coords = {"x": np.arange(4), "y": np.arange(3), "z": np.arange(2)}
|
192
|
+
varname = "test_var"
|
193
|
+
data = xr.DataArray(data, dims=("x", "y", "z"), coords=coords, name=varname)
|
194
|
+
outfile = r"F:\test_dataarray.nc"
|
195
|
+
save_to_nc(outfile, data)
|
196
|
+
# --------------------------------
|
197
|
+
# numpy array
|
198
|
+
data = np.random.rand(4, 3, 2)
|
199
|
+
coords = {"x": np.arange(4), "y": np.arange(3), "z": np.arange(2)}
|
200
|
+
varname = "test_var"
|
201
|
+
outfile = r"F:\test_numpy.nc"
|
202
|
+
save_to_nc(outfile, data, varname=varname, coords=coords)
|
203
|
+
# --------------------------------
|
@@ -0,0 +1,214 @@
|
|
1
|
+
import atexit
|
2
|
+
import logging
|
3
|
+
import multiprocessing as mp
|
4
|
+
import platform
|
5
|
+
import threading
|
6
|
+
import time
|
7
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
9
|
+
|
10
|
+
import psutil
|
11
|
+
|
12
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
13
|
+
|
14
|
+
__all__ = ["ParallelExecutor"]
|
15
|
+
|
16
|
+
|
17
|
+
class ParallelExecutor:
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
max_workers: Optional[int] = None,
|
21
|
+
chunk_size: Optional[int] = None,
|
22
|
+
mem_per_process: float = 1.0, # GB
|
23
|
+
timeout_per_task: int = 3600,
|
24
|
+
max_retries: int = 3,
|
25
|
+
):
|
26
|
+
self.platform = self._detect_platform()
|
27
|
+
self.mem_per_process = mem_per_process
|
28
|
+
self.timeout_per_task = timeout_per_task
|
29
|
+
self.max_retries = max_retries
|
30
|
+
self.running = True
|
31
|
+
self.task_history = []
|
32
|
+
self._executor = None
|
33
|
+
|
34
|
+
self.mode, default_workers = self._determine_optimal_settings()
|
35
|
+
self.max_workers = max_workers or default_workers
|
36
|
+
self.chunk_size = chunk_size or self._get_default_chunk_size()
|
37
|
+
|
38
|
+
self._init_platform_settings()
|
39
|
+
self._start_resource_monitor()
|
40
|
+
atexit.register(self.shutdown)
|
41
|
+
|
42
|
+
logging.info(f"Initialized {self.__class__.__name__} on {self.platform} (mode={self.mode}, workers={self.max_workers})")
|
43
|
+
|
44
|
+
def _detect_platform(self) -> str:
|
45
|
+
system = platform.system().lower()
|
46
|
+
if system == "linux":
|
47
|
+
return "wsl" if "microsoft" in platform.release().lower() else "linux"
|
48
|
+
return system
|
49
|
+
|
50
|
+
def _init_platform_settings(self):
|
51
|
+
if self.platform in ["linux", "wsl"]:
|
52
|
+
self.mp_context = mp.get_context("fork")
|
53
|
+
elif self.platform == "windows":
|
54
|
+
mp.set_start_method("spawn", force=True)
|
55
|
+
self.mp_context = mp.get_context("spawn")
|
56
|
+
else:
|
57
|
+
self.mp_context = None
|
58
|
+
|
59
|
+
def _determine_optimal_settings(self) -> Tuple[str, int]:
|
60
|
+
logical_cores = psutil.cpu_count(logical=True) or 1
|
61
|
+
available_mem = psutil.virtual_memory().available / 1024**3 # GB
|
62
|
+
|
63
|
+
mem_limit = max(1, int(available_mem / self.mem_per_process))
|
64
|
+
return ("process", min(logical_cores, mem_limit))
|
65
|
+
|
66
|
+
def _get_default_chunk_size(self) -> int:
|
67
|
+
return max(10, 100 // (psutil.cpu_count() or 1))
|
68
|
+
|
69
|
+
def _start_resource_monitor(self):
|
70
|
+
def monitor():
|
71
|
+
threshold = self.mem_per_process * 1024**3
|
72
|
+
while self.running:
|
73
|
+
try:
|
74
|
+
if psutil.virtual_memory().available < threshold:
|
75
|
+
self._scale_down_workers()
|
76
|
+
time.sleep(1)
|
77
|
+
except Exception as e:
|
78
|
+
logging.error(f"Resource monitor error: {e}")
|
79
|
+
|
80
|
+
threading.Thread(target=monitor, daemon=True).start()
|
81
|
+
|
82
|
+
def _scale_down_workers(self):
|
83
|
+
if self.max_workers > 1:
|
84
|
+
new_count = self.max_workers - 1
|
85
|
+
logging.warning(f"Scaling down workers from {self.max_workers} to {new_count}")
|
86
|
+
self.max_workers = new_count
|
87
|
+
self._restart_executor()
|
88
|
+
|
89
|
+
def _restart_executor(self):
|
90
|
+
if self._executor:
|
91
|
+
self._executor.shutdown(wait=False)
|
92
|
+
self._executor = None
|
93
|
+
|
94
|
+
def _get_executor(self):
|
95
|
+
if not self._executor:
|
96
|
+
Executor = ThreadPoolExecutor if self.mode == "thread" else ProcessPoolExecutor
|
97
|
+
self._executor = Executor(max_workers=self.max_workers, mp_context=self.mp_context if self.mode == "process" else None)
|
98
|
+
return self._executor
|
99
|
+
|
100
|
+
def run(self, func: Callable, params: List[Tuple], chunk_size: Optional[int] = None) -> List[Any]:
|
101
|
+
chunk_size = chunk_size or self.chunk_size
|
102
|
+
for retry in range(self.max_retries + 1):
|
103
|
+
try:
|
104
|
+
start_time = time.monotonic()
|
105
|
+
results = self._execute_batch(func, params, chunk_size)
|
106
|
+
self._update_settings(time.monotonic() - start_time, len(params))
|
107
|
+
return results
|
108
|
+
except Exception as e:
|
109
|
+
logging.error(f"Attempt {retry + 1} failed: {e}")
|
110
|
+
self._handle_failure()
|
111
|
+
raise RuntimeError(f"Failed after {self.max_retries} retries")
|
112
|
+
|
113
|
+
def _execute_batch(self, func: Callable, params: List[Tuple], chunk_size: int) -> List[Any]:
|
114
|
+
if not params:
|
115
|
+
return []
|
116
|
+
|
117
|
+
if len(params) > chunk_size * 2:
|
118
|
+
return self._chunked_execution(func, params, chunk_size)
|
119
|
+
|
120
|
+
results = [None] * len(params)
|
121
|
+
with self._get_executor() as executor:
|
122
|
+
futures = {executor.submit(func, *args): idx for idx, args in enumerate(params)}
|
123
|
+
for future in as_completed(futures):
|
124
|
+
idx = futures[future]
|
125
|
+
try:
|
126
|
+
results[idx] = future.result(timeout=self.timeout_per_task)
|
127
|
+
except Exception as e:
|
128
|
+
results[idx] = self._handle_error(e, func, params[idx])
|
129
|
+
return results
|
130
|
+
|
131
|
+
def _chunked_execution(self, func: Callable, params: List[Tuple], chunk_size: int) -> List[Any]:
|
132
|
+
results = []
|
133
|
+
with self._get_executor() as executor:
|
134
|
+
futures = []
|
135
|
+
for i in range(0, len(params), chunk_size):
|
136
|
+
chunk = params[i : i + chunk_size]
|
137
|
+
futures.append(executor.submit(self._process_chunk, func, chunk))
|
138
|
+
|
139
|
+
for future in as_completed(futures):
|
140
|
+
try:
|
141
|
+
results.extend(future.result(timeout=self.timeout_per_task))
|
142
|
+
except Exception as e:
|
143
|
+
logging.error(f"Chunk failed: {e}")
|
144
|
+
results.extend([None] * chunk_size)
|
145
|
+
return results
|
146
|
+
|
147
|
+
@staticmethod
|
148
|
+
def _process_chunk(func: Callable, chunk: List[Tuple]) -> List[Any]:
|
149
|
+
return [func(*args) for args in chunk]
|
150
|
+
|
151
|
+
def _update_settings(self, duration: float, task_count: int):
|
152
|
+
self.task_history.append((duration, task_count))
|
153
|
+
self.chunk_size = max(5, min(100, self.chunk_size + (1 if duration < 5 else -1)))
|
154
|
+
|
155
|
+
def _handle_error(self, error: Exception, func: Callable, args: Tuple) -> Any:
|
156
|
+
if isinstance(error, TimeoutError):
|
157
|
+
logging.warning(f"Timeout processing {func.__name__}{args}")
|
158
|
+
elif isinstance(error, MemoryError):
|
159
|
+
logging.warning("Memory error detected")
|
160
|
+
self._scale_down_workers()
|
161
|
+
else:
|
162
|
+
logging.error(f"Error processing {func.__name__}{args}: {str(error)}")
|
163
|
+
return None
|
164
|
+
|
165
|
+
def _handle_failure(self):
|
166
|
+
if self.max_workers > 2:
|
167
|
+
self.max_workers = max(1, self.max_workers // 2)
|
168
|
+
self._restart_executor()
|
169
|
+
|
170
|
+
def shutdown(self):
|
171
|
+
self.running = False
|
172
|
+
if self._executor:
|
173
|
+
try:
|
174
|
+
self._executor.shutdown(wait=False)
|
175
|
+
except Exception as e:
|
176
|
+
logging.error(f"Shutdown error: {e}")
|
177
|
+
finally:
|
178
|
+
self._executor = None
|
179
|
+
|
180
|
+
def __enter__(self):
|
181
|
+
return self
|
182
|
+
|
183
|
+
def __exit__(self, *exc_info):
|
184
|
+
self.shutdown()
|
185
|
+
|
186
|
+
def get_stats(self) -> Dict[str, Any]:
|
187
|
+
stats = {
|
188
|
+
"platform": self.platform,
|
189
|
+
"mode": self.mode,
|
190
|
+
"workers": self.max_workers,
|
191
|
+
"chunk_size": self.chunk_size,
|
192
|
+
"total_tasks": sum(count for _, count in self.task_history),
|
193
|
+
}
|
194
|
+
if self.task_history:
|
195
|
+
total_time = sum(time for time, _ in self.task_history)
|
196
|
+
stats["avg_task_throughput"] = stats["total_tasks"] / total_time if total_time else 0
|
197
|
+
return stats
|
198
|
+
|
199
|
+
|
200
|
+
def _test_func(a, b):
|
201
|
+
time.sleep(0.01)
|
202
|
+
return a + b
|
203
|
+
|
204
|
+
|
205
|
+
if __name__ == "__main__":
|
206
|
+
params = [(i, i * 2) for i in range(1000)]
|
207
|
+
|
208
|
+
with ParallelExecutor() as executor:
|
209
|
+
results = executor.run(_test_func, params)
|
210
|
+
|
211
|
+
# print("Results:", results)
|
212
|
+
|
213
|
+
print(f"Processed {len(results)} tasks")
|
214
|
+
print("Execution stats:", executor.get_stats())
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# coding=utf-8
|
3
|
+
"""
|
4
|
+
Author: Liu Kun && 16031215@qq.com
|
5
|
+
Date: 2025-04-08 16:18:49
|
6
|
+
LastEditors: Liu Kun && 16031215@qq.com
|
7
|
+
LastEditTime: 2025-04-08 16:18:50
|
8
|
+
FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\_script\\parallel_test.py
|
9
|
+
Description:
|
10
|
+
EditPlatform: vscode
|
11
|
+
ComputerInfo: XPS 15 9510
|
12
|
+
SystemInfo: Windows 11
|
13
|
+
Python Version: 3.12
|
14
|
+
"""
|