|
@@ -104,6 +104,43 @@ def _safe_float(value: Any) -> float | None:
|
|
|
return number if np.isfinite(number) else None
|
|
return number if np.isfinite(number) else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _stored_first_cycle(samples: np.ndarray) -> DetectedCycle | None:
|
|
|
|
|
+ """Build a cycle from wave_sample_one, which contains one cycle only."""
|
|
|
|
|
+ if samples.ndim != 2 or samples.shape[1] < 3 or len(samples) < 2:
|
|
|
|
|
+ return None
|
|
|
|
|
+ start = 0
|
|
|
|
|
+ end = len(samples)
|
|
|
|
|
+ angle = np.linspace(0.0, 360.0, end - start, endpoint=False)
|
|
|
|
|
+ trigger_offsets = tuple(
|
|
|
|
|
+ int(offset)
|
|
|
|
|
+ for offset in np.flatnonzero(
|
|
|
|
|
+ np.nan_to_num(samples[:, 2], nan=-np.inf) >= 30.0,
|
|
|
|
|
+ )
|
|
|
|
|
+ if offset == 0 or samples[offset - 1, 2] < 30.0
|
|
|
|
|
+ )
|
|
|
|
|
+ return DetectedCycle(
|
|
|
|
|
+ number=1,
|
|
|
|
|
+ start_offset=start,
|
|
|
|
|
+ end_offset=end,
|
|
|
|
|
+ angle=angle,
|
|
|
|
|
+ signal=samples[start:end, 1].copy(),
|
|
|
|
|
+ trigger_offsets=trigger_offsets,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _detect_source_cycles(
|
|
|
|
|
+ samples: np.ndarray,
|
|
|
|
|
+ single_cycle: bool,
|
|
|
|
|
+) -> tuple[list[DetectedCycle], dict[str, Any]]:
|
|
|
|
|
+ """Detect cycles: wave_sample_one holds exactly one stored cycle already."""
|
|
|
|
|
+ if single_cycle:
|
|
|
|
|
+ stored = _stored_first_cycle(samples)
|
|
|
|
|
+ if stored is None:
|
|
|
|
|
+ return [], {"completeCycleCount": 0}
|
|
|
|
|
+ return [stored], {"completeCycleCount": 1, "storedFirstCycle": 1}
|
|
|
|
|
+ return detect_cycles(samples)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _series_extent(items: list[dict[str, Any]]) -> tuple[float, float]:
|
|
def _series_extent(items: list[dict[str, Any]]) -> tuple[float, float]:
|
|
|
"""Whole-window min/max over a series' finite raw values."""
|
|
"""Whole-window min/max over a series' finite raw values."""
|
|
|
if not items:
|
|
if not items:
|
|
@@ -636,6 +673,63 @@ class DataService:
|
|
|
"notice": self._source_notice(source),
|
|
"notice": self._source_notice(source),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ def faults(
|
|
|
|
|
+ self,
|
|
|
|
|
+ device_part: str,
|
|
|
|
|
+ min_time: str | None,
|
|
|
|
|
+ max_time: str | None,
|
|
|
|
|
+ ) -> dict[str, Any]:
|
|
|
|
|
+ """Return compressor_fault rows for the unit of ``device_part``.
|
|
|
|
|
+
|
|
|
|
|
+ ``compressor_fault.unit_name`` ("7号机组") is matched against the unit
|
|
|
|
|
+ prefix of ``wave_file.device_part`` ("7号机组一缸"). Only faults whose
|
|
|
|
|
+ ``fault_date`` falls inside the requested time range are returned.
|
|
|
|
|
+ """
|
|
|
|
|
+ unit = _unit_number(device_part)
|
|
|
|
|
+ start = _parse_time(min_time)
|
|
|
|
|
+ end = _parse_time(max_time)
|
|
|
|
|
+
|
|
|
|
|
+ def database_query():
|
|
|
|
|
+ if unit is None:
|
|
|
|
|
+ return []
|
|
|
|
|
+ clauses = ["(unit_name = %s OR unit_name LIKE %s)"]
|
|
|
|
|
+ params: list[Any] = [f"{unit}号机组", f"{unit}号机组%"]
|
|
|
|
|
+ if start:
|
|
|
|
|
+ clauses.append("fault_date >= DATE(%s)")
|
|
|
|
|
+ params.append(start)
|
|
|
|
|
+ if end:
|
|
|
|
|
+ clauses.append("fault_date <= DATE(%s)")
|
|
|
|
|
+ params.append(end)
|
|
|
|
|
+ with get_connection() as connection:
|
|
|
|
|
+ with connection.cursor() as cursor:
|
|
|
|
|
+ cursor.execute(
|
|
|
|
|
+ f"""
|
|
|
|
|
+ SELECT id, unit_name, fault_date, fault_category
|
|
|
|
|
+ FROM compressor_fault
|
|
|
|
|
+ WHERE {' AND '.join(clauses)}
|
|
|
|
|
+ ORDER BY fault_date ASC, id ASC
|
|
|
|
|
+ """,
|
|
|
|
|
+ params,
|
|
|
|
|
+ )
|
|
|
|
|
+ rows = cursor.fetchall()
|
|
|
|
|
+ return [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": int(row["id"]),
|
|
|
|
|
+ "unitName": row["unit_name"] or "",
|
|
|
|
|
+ "faultDate": _time_string(row["fault_date"])[:10],
|
|
|
|
|
+ "faultCategory": row["fault_category"] or "",
|
|
|
|
|
+ }
|
|
|
|
|
+ for row in rows
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ result, source = self._run_with_fallback(database_query, lambda: [])
|
|
|
|
|
+ return {
|
|
|
|
|
+ "source": source,
|
|
|
|
|
+ "unit": unit,
|
|
|
|
|
+ "faults": result,
|
|
|
|
|
+ "notice": self._source_notice(source),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
def _group_time_points(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
def _group_time_points(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
grouped: OrderedDict[Any, dict[str, Any]] = OrderedDict()
|
|
grouped: OrderedDict[Any, dict[str, Any]] = OrderedDict()
|
|
@@ -889,6 +983,8 @@ class DataService:
|
|
|
def period_detail(self, wave_file_id: int, period_number: int) -> dict[str, Any]:
|
|
def period_detail(self, wave_file_id: int, period_number: int) -> dict[str, Any]:
|
|
|
if wave_file_id <= 0 or period_number <= 0:
|
|
if wave_file_id <= 0 or period_number <= 0:
|
|
|
raise ValueError("wave_file_id 和周期编号必须为正整数")
|
|
raise ValueError("wave_file_id 和周期编号必须为正整数")
|
|
|
|
|
+ if period_number != 1:
|
|
|
|
|
+ raise ValueError("当前数据仅保留第一个周期")
|
|
|
|
|
|
|
|
def database_query():
|
|
def database_query():
|
|
|
metadata, samples = self._load_db_wave(wave_file_id)
|
|
metadata, samples = self._load_db_wave(wave_file_id)
|
|
@@ -1102,6 +1198,7 @@ class DataService:
|
|
|
primary_type = DEVICE_POINT_TO_TYPE[primary]
|
|
primary_type = DEVICE_POINT_TO_TYPE[primary]
|
|
|
pressure_points = [point for point in device_points if DEVICE_POINT_TO_TYPE[point] == "压力"]
|
|
pressure_points = [point for point in device_points if DEVICE_POINT_TO_TYPE[point] == "压力"]
|
|
|
load_cache: dict[int, tuple[dict[str, Any], np.ndarray]] = {}
|
|
load_cache: dict[int, tuple[dict[str, Any], np.ndarray]] = {}
|
|
|
|
|
+ single_cycle = loader.__name__ != "_load_demo_wave"
|
|
|
|
|
|
|
|
for slot, point in enumerate(points):
|
|
for slot, point in enumerate(points):
|
|
|
target_per_file = max(256, int(np.ceil(max_points / max(len(points), 1))))
|
|
target_per_file = max(256, int(np.ceil(max_points / max(len(points), 1))))
|
|
@@ -1142,7 +1239,7 @@ class DataService:
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
source_samples = None
|
|
source_samples = None
|
|
|
if source_samples is not None and len(source_samples):
|
|
if source_samples is not None and len(source_samples):
|
|
|
- source_detected, source_diagnostic = detect_cycles(source_samples)
|
|
|
|
|
|
|
+ source_detected, source_diagnostic = _detect_source_cycles(source_samples, single_cycle)
|
|
|
source_angle_vector = build_angle_vector(len(source_samples), source_detected)
|
|
source_angle_vector = build_angle_vector(len(source_samples), source_detected)
|
|
|
source_angle_full = np.full(len(source_samples), np.nan, dtype=float)
|
|
source_angle_full = np.full(len(source_samples), np.nan, dtype=float)
|
|
|
for detected_cycle in source_detected:
|
|
for detected_cycle in source_detected:
|
|
@@ -1292,7 +1389,7 @@ class DataService:
|
|
|
if source_required is not None:
|
|
if source_required is not None:
|
|
|
file_required.update(source_required)
|
|
file_required.update(source_required)
|
|
|
else:
|
|
else:
|
|
|
- detected_own, _ = detect_cycles(samples)
|
|
|
|
|
|
|
+ detected_own, _ = _detect_source_cycles(samples, single_cycle)
|
|
|
for cycle in detected_own:
|
|
for cycle in detected_own:
|
|
|
file_required.update(
|
|
file_required.update(
|
|
|
{
|
|
{
|
|
@@ -1513,15 +1610,11 @@ class DataService:
|
|
|
ws.sample_index,
|
|
ws.sample_index,
|
|
|
CAST(ws.signal_value AS FLOAT) AS sig,
|
|
CAST(ws.signal_value AS FLOAT) AS sig,
|
|
|
CAST(ws.second_value AS FLOAT) AS sec
|
|
CAST(ws.second_value AS FLOAT) AS sec
|
|
|
- FROM wave_sample ws
|
|
|
|
|
- JOIN wave_file wf ON wf.id = ws.wave_file_id
|
|
|
|
|
|
|
+ FROM wave_sample_one ws
|
|
|
WHERE ws.wave_file_id IN ({source_placeholders})
|
|
WHERE ws.wave_file_id IN ({source_placeholders})
|
|
|
- AND wf.cycle_start >= 0
|
|
|
|
|
- AND ws.sample_index >= wf.cycle_start
|
|
|
|
|
- AND ws.sample_index < wf.cycle_end + %s
|
|
|
|
|
ORDER BY ws.wave_file_id, ws.sample_index
|
|
ORDER BY ws.wave_file_id, ws.sample_index
|
|
|
""",
|
|
""",
|
|
|
- (*tuple(source_ids), FIRST_CYCLE_PAD),
|
|
|
|
|
|
|
+ tuple(source_ids),
|
|
|
)
|
|
)
|
|
|
raw: dict[int, list[tuple[float, float, float]]] = {}
|
|
raw: dict[int, list[tuple[float, float, float]]] = {}
|
|
|
for row in cursor.fetchall():
|
|
for row in cursor.fetchall():
|
|
@@ -1534,11 +1627,12 @@ class DataService:
|
|
|
)
|
|
)
|
|
|
for file_id, rows in raw.items():
|
|
for file_id, rows in raw.items():
|
|
|
meta = metas.get(file_id)
|
|
meta = metas.get(file_id)
|
|
|
- if meta is None or meta.get("cycle_start") is None:
|
|
|
|
|
|
|
+ if meta is None or not rows:
|
|
|
continue
|
|
continue
|
|
|
|
|
+ # wave_sample_one 只保留第一个周期,返回的数组本身就是该周期。
|
|
|
slices[file_id] = (
|
|
slices[file_id] = (
|
|
|
- int(meta["cycle_start"]),
|
|
|
|
|
- int(meta["cycle_end"]),
|
|
|
|
|
|
|
+ int(rows[0][0]),
|
|
|
|
|
+ int(rows[-1][0]) + 1,
|
|
|
np.asarray(rows, dtype=float),
|
|
np.asarray(rows, dtype=float),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -1561,13 +1655,14 @@ class DataService:
|
|
|
slice_arr: np.ndarray | None = None
|
|
slice_arr: np.ndarray | None = None
|
|
|
if cycle_bounds is not None:
|
|
if cycle_bounds is not None:
|
|
|
cycle_start, cycle_end, slice_arr = cycle_bounds
|
|
cycle_start, cycle_end, slice_arr = cycle_bounds
|
|
|
- cycle_len = max(cycle_end - cycle_start, 0)
|
|
|
|
|
- has_cycle = slice_arr is not None and 1 < cycle_len <= len(slice_arr)
|
|
|
|
|
|
|
+ cycle_len = len(slice_arr) if slice_arr is not None else 0
|
|
|
|
|
+ has_cycle = slice_arr is not None and 1 < cycle_len
|
|
|
if has_cycle:
|
|
if has_cycle:
|
|
|
angle: np.ndarray | None = None
|
|
angle: np.ndarray | None = None
|
|
|
volume: np.ndarray | None = None
|
|
volume: np.ndarray | None = None
|
|
|
point_volume_info: dict[str, Any] | None = None
|
|
point_volume_info: dict[str, Any] | None = None
|
|
|
- detected, _ = detect_cycles(slice_arr)
|
|
|
|
|
|
|
+ stored_cycle = _stored_first_cycle(slice_arr)
|
|
|
|
|
+ detected = [stored_cycle] if stored_cycle is not None else []
|
|
|
if detected:
|
|
if detected:
|
|
|
cycle = detected[0]
|
|
cycle = detected[0]
|
|
|
angle = cycle.angle
|
|
angle = cycle.angle
|
|
@@ -1837,7 +1932,12 @@ class DataService:
|
|
|
samples: np.ndarray,
|
|
samples: np.ndarray,
|
|
|
period_number: int,
|
|
period_number: int,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
- detected, diagnostics = detect_cycles(samples)
|
|
|
|
|
|
|
+ stored_cycle = _stored_first_cycle(samples) if metadata.get("single_cycle") else None
|
|
|
|
|
+ if stored_cycle is not None:
|
|
|
|
|
+ detected = [stored_cycle]
|
|
|
|
|
+ diagnostics = {"completeCycleCount": 1, "storedFirstCycle": 1}
|
|
|
|
|
+ else:
|
|
|
|
|
+ detected, diagnostics = detect_cycles(samples)
|
|
|
cycle = next((item for item in detected if item.number == period_number), None)
|
|
cycle = next((item for item in detected if item.number == period_number), None)
|
|
|
if cycle is None:
|
|
if cycle is None:
|
|
|
raise ValueError(f"没有找到周期 {period_number}")
|
|
raise ValueError(f"没有找到周期 {period_number}")
|
|
@@ -1937,7 +2037,8 @@ class DataService:
|
|
|
cursor.execute(
|
|
cursor.execute(
|
|
|
"""
|
|
"""
|
|
|
SELECT id, point_name, measurement_type, sample_frequency_hz,
|
|
SELECT id, point_name, measurement_type, sample_frequency_hz,
|
|
|
- sample_count, sample_time, rpm, file_name, tspluse_status
|
|
|
|
|
|
|
+ sample_count, sample_time, rpm, file_name, tspluse_status,
|
|
|
|
|
+ cycle_start, cycle_end
|
|
|
FROM wave_file
|
|
FROM wave_file
|
|
|
WHERE id = %s
|
|
WHERE id = %s
|
|
|
""",
|
|
""",
|
|
@@ -1946,10 +2047,11 @@ class DataService:
|
|
|
metadata = cursor.fetchone()
|
|
metadata = cursor.fetchone()
|
|
|
if metadata is None:
|
|
if metadata is None:
|
|
|
raise ValueError(f"wave_file.id={file_id} 不存在")
|
|
raise ValueError(f"wave_file.id={file_id} 不存在")
|
|
|
|
|
+ # wave_sample_one 只保留第一个周期,直接读取该文件全部样本。
|
|
|
cursor.execute(
|
|
cursor.execute(
|
|
|
"""
|
|
"""
|
|
|
SELECT sample_index, signal_value, second_value
|
|
SELECT sample_index, signal_value, second_value
|
|
|
- FROM wave_sample
|
|
|
|
|
|
|
+ FROM wave_sample_one
|
|
|
WHERE wave_file_id = %s
|
|
WHERE wave_file_id = %s
|
|
|
ORDER BY sample_index ASC
|
|
ORDER BY sample_index ASC
|
|
|
""",
|
|
""",
|
|
@@ -1958,6 +2060,7 @@ class DataService:
|
|
|
rows = cursor.fetchall()
|
|
rows = cursor.fetchall()
|
|
|
if not rows:
|
|
if not rows:
|
|
|
raise ValueError(f"wave_file.id={file_id} 没有采样数据")
|
|
raise ValueError(f"wave_file.id={file_id} 没有采样数据")
|
|
|
|
|
+ metadata["single_cycle"] = True
|
|
|
samples = np.asarray(
|
|
samples = np.asarray(
|
|
|
[
|
|
[
|
|
|
(
|
|
(
|