未分類

==========================================

【App A】 GPV解析・キャッシュエンジン (Ver 97.5 モダン統合版)

最新UIデザイン + ガイダンス・30分大気解析・新旧GSM完全対応

==========================================

import sys, os, glob, re, subprocess, queue
from datetime import datetime
import multiprocessing as mp

os.environ[‘QT_API’] = ‘pyqt6’
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,
QSystemTrayIcon, QMenu)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor, QAction

try: import cfgrib
except ImportError: sys.exit(1)

APP_DIR = os.getcwd()
DEFAULT_OUTPUT_DIR = os.path.join(APP_DIR, “gpv_cache_npz”)
CURRENT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR
WGRIB2_EXE = os.path.join(APP_DIR, “wgrib2_data”, “wgrib2.exe”)

os.makedirs(CURRENT_OUTPUT_DIR, exist_ok=True)

def write_syslog(msg):
log_file = os.path.join(CURRENT_OUTPUT_DIR, “system_log.txt”)
try:
with open(log_file, “a”, encoding=”utf-8″) as f:
f.write(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {msg}\n”)
except Exception: pass

def create_lightning_icon():
pixmap = QPixmap(64, 64); pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap); font = painter.font(); font.setPixelSize(50)
painter.setFont(font); painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, “⚡”)
painter.end(); return QIcon(pixmap)

==========================================

裏方ワーカー: メモリリーク対策の独立プロセス

==========================================

def extract_gpv_worker(model, mode, cache, init, target_fts, f1, f2, wgrib2_path, q):
try:
import os, sys, subprocess, warnings
import numpy as np
import cfgrib
warnings.filterwarnings(“ignore”)
os.environ[“ECCODES_MAX_VALUES”] = “5000000” if sys.platform == "win32": conda_dir = os.path.dirname(sys.executable) dll_paths = [os.path.join(conda_dir, "Library", "bin"), os.path.join(conda_dir, "bin")] for p in dll_paths: if os.path.exists(p): os.environ["PATH"] = f"{p};{os.environ.get('PATH', '')}" try: os.add_dll_directory(p) except Exception: pass def calculate_vorticity(u, v, lon, lat): try: R = 6371000.0; LON, LAT = np.meshgrid(lon, lat) if lon.ndim == 1 else (lon, lat) rad_lat = np.deg2rad(LAT); rad_lon = np.deg2rad(LON) dy = R * np.gradient(rad_lat, axis=0); dx = R * np.cos(rad_lat) * np.gradient(rad_lon, axis=1) dx[dx == 0] = 1e-10; dy[dy == 0] = 1e-10 return ((np.gradient(v, axis=1) / dx) - (np.gradient(u, axis=0) / dy)) * 1e5 except Exception: return np.zeros_like(u) d_all = {ft: {} for ft in target_fts} creationflags = 0x08000000 if sys.platform == "win32" else 0 for t_ft in target_fts: d = d_all[t_ft] def slice_with_wgrib2(fin, ft_val, suffix): if fin == "NONE": return "NONE" fout = os.path.join(cache, f"temp_{model}_{mode}_{init}_{ft_val}_{suffix}.bin") if mode == "ANAL": match_str = ":(anl):" elif ft_val == 0: match_str = ":(anl|0 hour [^:]+|[0-9]+-0 hour [^:]+):" else: match_str = f":({ft_val} hour [^:]+|[0-9]+-{ft_val} hour [^:]+):" cmd = [wgrib2_path, fin, "-match", match_str, "-grib", fout] try: subprocess.run(cmd, creationflags=creationflags, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if os.path.exists(fout) and os.path.getsize(fout) > 0: return fout except Exception: pass return "NONE" f1_mini = slice_with_wgrib2(f1, t_ft, "1") f2_mini = slice_with_wgrib2(f2, t_ft, "2") def process_file(filepath, is_pall): if filepath == "NONE": return try: dss = cfgrib.open_datasets(filepath, backend_kwargs={'indexpath': ''}) for ds in dss: lon = ds['longitude'].values if 'longitude' in ds.coords else (ds['lon'].values if 'lon' in ds.coords else None) lat = ds['latitude'].values if 'latitude' in ds.coords else (ds['lat'].values if 'lat' in ds.coords else None) for v in ds.data_vars: da = ds[v] da_step = da.isel(step=0) if 'step' in da.dims else da sName = str(da.attrs.get('GRIB_shortName', v)).lower() attrs_str = str(da.attrs).lower() # 解析ロジック (GPV/ANAL/GUID共通) val_2d = da_step.values.copy() while val_2d.ndim > 2: val_2d = val_2d[0] if mode == "GUID": if sName != 'unknown': d[sName] = val_2d if sName in ['2t', 't2m']: d['t2m'] = val_2d elif sName in ['tp', 'apcp', 'pr']: d['precip'] = val_2d else: if not is_pall and 'lon_surf' not in d: d['lon_surf'] = lon; d['lat_surf'] = lat elif is_pall and 'lon_pall' not in d: d['lon_pall'] = lon; d['lat_pall'] = lat if not is_pall: if sName in ['prmsl', 'msl', 'slp']: d['slp'] = val_2d / 100.0 if np.nanmax(val_2d) > 2000 else val_2d elif sName in ['tp', 'apcp', 'pr']: d['precip_raw'] = np.nan_to_num(val_2d, nan=0.0) elif sName in ['10u', 'u10']: d['u10'] = val_2d elif sName in ['10v', 'v10']: d['v10'] = val_2d elif sName in ['2t', 't2m']: d['t2m'] = val_2d - 273.15 if np.nanmax(val_2d) > 150 else val_2d else: lvl = int(da.attrs.get('GRIB_level', 0)) if lvl > 0: if sName in ['u', 'v', 't', 'gh', 'r', 'w']: d[f'{sName}{lvl}'] = val_2d for ds in dss: ds.close() except Exception: pass process_file(f1_mini, False) process_file(f2_mini, True) # 追加計算ロジック(厳命事項:渦度計算) if 'vort500' not in d and 'u500' in d and 'v500' in d: lon_arr = d.get('lon_pall', d.get('lon_surf')) lat_arr = d.get('lat_pall', d.get('lat_surf')) if lon_arr is not None: d['vort500'] = calculate_vorticity(d['u500'], d['v500'], lon_arr, lat_arr) pfx = "GUID_" if mode == "GUID" else "" out_file = os.path.join(cache, f"{model}_{pfx}{init}_FT{t_ft:02d}.npz") np.savez_compressed(out_file, **d) q.put(("SUCCESS", t_ft)) try: if f1_mini != "NONE": os.remove(f1_mini) if f2_mini != "NONE": os.remove(f2_mini) except: pass except Exception as e: q.put(("ERROR", str(e)))

==========================================

メインスレッド

==========================================

class DataParserThread(QThread):
log_signal = pyqtSignal(str); progress_signal = pyqtSignal(int, str); finished_signal = pyqtSignal(bool)
def init(self, folder_paths, output_dir, last_reported):
super().init()
self.folder_paths = folder_paths; self.output_dir = output_dir
self.last_reported = last_reported; self.abort = Falsedef run(self): has_new = False for folder_path in self.folder_paths: if self.abort or not folder_path or not os.path.exists(folder_path): continue # スキャン対象 (新旧GSM, MSM, ANAL, GUIDを網羅) scan_rules = [ ('*MSM*GPV*.bin', 'MSM', 'GPV', range(0, 80)), ('*GSM*GPV*Rjp*.bin', 'GSM_JP', 'GPV', range(0, 265, 3)), ('*GSM*GPV*.bin', 'GSM_JP', 'GPV', range(0, 265, 3)), # 旧加工データ用 ('*ANAL_grib2*.bin', 'ANAL', 'ANAL', [0]), ('*GSM_GUID*.bin', 'GSM', 'GUID', range(0, 265, 3)) ] for pattern, model_id, mode, ft_range in scan_rules: files = glob.glob(os.path.join(folder_path, '**', pattern), recursive=True) if not files: continue # 最新時刻の特定 times = set(re.search(r'_(\d{14})_', f).group(1) for f in files if re.search(r'_(\d{14})_', f)) if not times: continue latest_it = max(times) latest_files = [f for f in files if latest_it in f] if self.last_reported.get(f"{model_id}_{mode}") != latest_it: self.log_signal.emit(f"✅ {model_id} {mode} 新データ: {latest_it}") self.last_reported[f"{model_id}_{mode}"] = latest_it # 未処理FTの抽出 tasks = [] for ft in ft_range: pfx = "GUID_" if mode == "GUID" else "" if not os.path.exists(os.path.join(self.output_dir, f"{model_id}_{pfx}{latest_it}_FT{ft:02d}.npz")): tasks.append(ft) if tasks: has_new = True self.log_signal.emit(f"🚀 {model_id} 抽出開始 ({len(tasks)}件)") # マルチプロセス実行 (簡略化のため1件ずつ、実際はチャンク処理) ctx = mp.get_context('spawn') for ft in tasks: if self.abort: break q = ctx.Queue() f1 = latest_files[0] # 簡易的なファイル選択 p = ctx.Process(target=extract_gpv_worker, args=(model_id, mode, self.output_dir, latest_it, [ft], f1, "NONE", WGRIB2_EXE, q)) p.start(); p.join() while not q.empty(): msg = q.get() if msg[0] == "SUCCESS": self.progress_signal.emit(100, f"{model_id} FT{msg[1]} 完了") self.finished_signal.emit(has_new)

GUIクラス (Ver 96.5のデザインを適用)

class EngineStatusWindow(QWidget):
def init(self):
super().init()
self.setWindowTitle(“GPV 解析エンジン (Ver 97.5 統合版)”)
self.setFixedSize(650, 580)
self.setStyleSheet(“background-color: #0A192F; color: #E0E0E0; font-family: ‘MS Gothic’;”)
layout = QVBoxLayout(self)
self.status_label = QLabel(“状態: 待機中”); self.status_label.setStyleSheet(“font-size: 14pt; font-weight: bold; color: #64FFDA;”)
layout.addWidget(self.status_label)
self.log_list = QListWidget(); self.log_list.setStyleSheet(“background: #112240; border: 1px solid #444; color: #E0E0E0;”)
layout.addWidget(self.log_list)
self.exit_btn = QPushButton(“システムを完全に終了”); self.exit_btn.setStyleSheet(“background-color: #E63946; color: white; font-weight: bold; padding: 10px;”)
self.exit_btn.clicked.connect(QApplication.quit); layout.addWidget(self.exit_btn)def log(self, msg): self.log_list.addItem(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}"); self.log_list.scrollToBottom()

if name == ‘main‘:
mp.freeze_support()
app = QApplication(sys.argv)
win = EngineStatusWindow(); win.show()
# 起動ロジック省略 (タイマーでDataParserThreadを回す)
sys.exit(app.exec())