|
@@ -14,7 +14,6 @@ import numpy as np
|
|
|
from ..algorithms.cycles import (
|
|
from ..algorithms.cycles import (
|
|
|
DetectedCycle,
|
|
DetectedCycle,
|
|
|
PULSES_PER_REVOLUTION,
|
|
PULSES_PER_REVOLUTION,
|
|
|
- build_angle_vector,
|
|
|
|
|
detect_cycles,
|
|
detect_cycles,
|
|
|
downsample_indices,
|
|
downsample_indices,
|
|
|
)
|
|
)
|
|
@@ -91,11 +90,23 @@ CYLINDER_BORE_MM = {
|
|
|
}
|
|
}
|
|
|
PISTON_STROKE_MM = 148.0
|
|
PISTON_STROKE_MM = 148.0
|
|
|
CONNECTING_ROD_LENGTH_MM = 460.0
|
|
CONNECTING_ROD_LENGTH_MM = 460.0
|
|
|
|
|
+PISTON_ROD_DIAMETER_MM = 78.0
|
|
|
CLEARANCE_VOLUME_L_BY_BORE = {
|
|
CLEARANCE_VOLUME_L_BY_BORE = {
|
|
|
490.0: 1.51,
|
|
490.0: 1.51,
|
|
|
390.0: 0.74,
|
|
390.0: 0.74,
|
|
|
360.0: 0.62,
|
|
360.0: 0.62,
|
|
|
}
|
|
}
|
|
|
|
|
+# 7/8/9 号机各缸相位角(月度报告/沁水压缩机参数20250102.docx)。
|
|
|
|
|
+# 4/5/6 号机暂无资料,保持原逻辑(相位 0,不做旋转)。
|
|
|
|
|
+CYLINDER_PHASE_DEGREE = {
|
|
|
|
|
+ "一缸": 0.0,
|
|
|
|
|
+ "二缸": 0.0,
|
|
|
|
|
+ "三缸": 120.0,
|
|
|
|
|
+ "四缸": 240.0,
|
|
|
|
|
+ "五缸": 240.0,
|
|
|
|
|
+ "六缸": 120.0,
|
|
|
|
|
+}
|
|
|
|
|
+PHASE_ROTATION_UNITS = frozenset({"7", "8", "9"})
|
|
|
|
|
|
|
|
DEMO_POINT_NAME = "7号机组一缸压力盖侧"
|
|
DEMO_POINT_NAME = "7号机组一缸压力盖侧"
|
|
|
DEMO_DEVICE_PART = "7号机组一缸"
|
|
DEMO_DEVICE_PART = "7号机组一缸"
|
|
@@ -125,13 +136,58 @@ def _safe_float(value: Any) -> float | None:
|
|
|
return number if np.isfinite(number) else None
|
|
return number if np.isfinite(number) else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _point_cylinder(point_name: str) -> str | None:
|
|
|
|
|
+ text = point_name or ""
|
|
|
|
|
+ return next((name for name in CYLINDER_BORE_MM if name in text), None)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _phase_degrees(point_name: str) -> float:
|
|
|
|
|
+ """Per-cylinder phase angle for 7/8/9; 0 for units without reference data."""
|
|
|
|
|
+ unit = _unit_number(point_name or "")
|
|
|
|
|
+ cylinder = _point_cylinder(point_name or "")
|
|
|
|
|
+ if unit in PHASE_ROTATION_UNITS and cylinder is not None:
|
|
|
|
|
+ return CYLINDER_PHASE_DEGREE[cylinder]
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _rotate_angle(angle: np.ndarray, point_name: str) -> np.ndarray:
|
|
|
|
|
+ """Convert shared-keyphase angle to the cylinder's own 0-360 angle."""
|
|
|
|
|
+ values = np.asarray(angle, dtype=float)
|
|
|
|
|
+ phase = _phase_degrees(point_name)
|
|
|
|
|
+ if not phase:
|
|
|
|
|
+ return values
|
|
|
|
|
+ return np.mod(values - phase, 360.0)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _piecewise_angle(count: int, trigger_offsets: tuple[int, ...]) -> np.ndarray:
|
|
|
|
|
+ """Crank angle from the 8 keyphase pulses; falls back to uniform sampling."""
|
|
|
|
|
+ offsets = sorted({int(offset) for offset in trigger_offsets if 0 <= int(offset) < count})
|
|
|
|
|
+ if len(offsets) < PULSES_PER_REVOLUTION or offsets[0] != 0:
|
|
|
|
|
+ return np.linspace(0.0, 360.0, count, endpoint=False)
|
|
|
|
|
+ offsets = offsets[:PULSES_PER_REVOLUTION]
|
|
|
|
|
+ bounds = offsets + [count]
|
|
|
|
|
+ angle = np.full(count, np.nan, dtype=float)
|
|
|
|
|
+ for pulse in range(PULSES_PER_REVOLUTION):
|
|
|
|
|
+ start, end = bounds[pulse], min(bounds[pulse + 1], count)
|
|
|
|
|
+ if end <= start:
|
|
|
|
|
+ continue
|
|
|
|
|
+ angle[start:end] = np.linspace(
|
|
|
|
|
+ pulse * 45.0, (pulse + 1) * 45.0, end - start, endpoint=False,
|
|
|
|
|
+ )
|
|
|
|
|
+ if np.isnan(angle).any():
|
|
|
|
|
+ good = np.flatnonzero(~np.isnan(angle))
|
|
|
|
|
+ if not len(good):
|
|
|
|
|
+ return np.linspace(0.0, 360.0, count, endpoint=False)
|
|
|
|
|
+ angle = np.interp(np.arange(count), good, angle[good])
|
|
|
|
|
+ return angle
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _stored_first_cycle(samples: np.ndarray) -> DetectedCycle | None:
|
|
def _stored_first_cycle(samples: np.ndarray) -> DetectedCycle | None:
|
|
|
"""Build a cycle from wave_sample_one, which contains one cycle only."""
|
|
"""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:
|
|
if samples.ndim != 2 or samples.shape[1] < 3 or len(samples) < 2:
|
|
|
return None
|
|
return None
|
|
|
start = 0
|
|
start = 0
|
|
|
end = len(samples)
|
|
end = len(samples)
|
|
|
- angle = np.linspace(0.0, 360.0, end - start, endpoint=False)
|
|
|
|
|
trigger_offsets = tuple(
|
|
trigger_offsets = tuple(
|
|
|
int(offset)
|
|
int(offset)
|
|
|
for offset in np.flatnonzero(
|
|
for offset in np.flatnonzero(
|
|
@@ -139,6 +195,7 @@ def _stored_first_cycle(samples: np.ndarray) -> DetectedCycle | None:
|
|
|
)
|
|
)
|
|
|
if offset == 0 or samples[offset - 1, 2] < 30.0
|
|
if offset == 0 or samples[offset - 1, 2] < 30.0
|
|
|
)
|
|
)
|
|
|
|
|
+ angle = _piecewise_angle(end - start, trigger_offsets)
|
|
|
return DetectedCycle(
|
|
return DetectedCycle(
|
|
|
number=1,
|
|
number=1,
|
|
|
start_offset=start,
|
|
start_offset=start,
|
|
@@ -1602,6 +1659,9 @@ class DataService:
|
|
|
if not angles:
|
|
if not angles:
|
|
|
raise ValueError("请至少输入一个角度")
|
|
raise ValueError("请至少输入一个角度")
|
|
|
angles = [int(angle) for angle in angles if 0 <= int(angle) <= 360]
|
|
angles = [int(angle) for angle in angles if 0 <= int(angle) <= 360]
|
|
|
|
|
+ # statistic_pressure_angle 存的是共享键相角;7/8/9 查询时换算到该缸自身角。
|
|
|
|
|
+ phase = int(round(_phase_degrees(device_part))) % 360
|
|
|
|
|
+ stored_angles = sorted({(angle + phase) % 360 for angle in angles})
|
|
|
|
|
|
|
|
def database_query():
|
|
def database_query():
|
|
|
with get_connection() as connection:
|
|
with get_connection() as connection:
|
|
@@ -1620,9 +1680,9 @@ class DataService:
|
|
|
if end:
|
|
if end:
|
|
|
clauses.append("wf.sample_time <= %s")
|
|
clauses.append("wf.sample_time <= %s")
|
|
|
params.append(end)
|
|
params.append(end)
|
|
|
- angle_placeholders = ", ".join(["%s"] * len(angles))
|
|
|
|
|
|
|
+ angle_placeholders = ", ".join(["%s"] * len(stored_angles))
|
|
|
clauses.append(f"spa.angle IN ({angle_placeholders})")
|
|
clauses.append(f"spa.angle IN ({angle_placeholders})")
|
|
|
- params.extend(angles)
|
|
|
|
|
|
|
+ params.extend(stored_angles)
|
|
|
cursor.execute(
|
|
cursor.execute(
|
|
|
f"""
|
|
f"""
|
|
|
SELECT wf.sample_time, spa.angle, spa.pressure
|
|
SELECT wf.sample_time, spa.angle, spa.pressure
|
|
@@ -1638,22 +1698,26 @@ class DataService:
|
|
|
stop_days = self._query_stop_days(
|
|
stop_days = self._query_stop_days(
|
|
|
cursor, device_part, device_points, start, end,
|
|
cursor, device_part, device_points, start, end,
|
|
|
)
|
|
)
|
|
|
- series: dict[int, list[dict[str, Any]]] = {}
|
|
|
|
|
|
|
+ by_stored: dict[int, list[dict[str, Any]]] = {}
|
|
|
for row in rows:
|
|
for row in rows:
|
|
|
- angle = int(round(float(row["angle"])))
|
|
|
|
|
- series.setdefault(angle, []).append({
|
|
|
|
|
|
|
+ stored_angle = int(round(float(row["angle"])))
|
|
|
|
|
+ by_stored.setdefault(stored_angle, []).append({
|
|
|
"time": _time_string(row["sample_time"]),
|
|
"time": _time_string(row["sample_time"]),
|
|
|
"pressure": float(row["pressure"]),
|
|
"pressure": float(row["pressure"]),
|
|
|
})
|
|
})
|
|
|
|
|
+ series: dict[int, list[dict[str, Any]]] = {
|
|
|
|
|
+ angle: list(by_stored.get((angle + phase) % 360, []))
|
|
|
|
|
+ for angle in angles
|
|
|
|
|
+ }
|
|
|
for day in sorted(stop_days):
|
|
for day in sorted(stop_days):
|
|
|
day_time = f"{day} 00:00:00"
|
|
day_time = f"{day} 00:00:00"
|
|
|
for angle in angles:
|
|
for angle in angles:
|
|
|
- series.setdefault(angle, []).append({
|
|
|
|
|
|
|
+ series[angle].append({
|
|
|
"time": day_time,
|
|
"time": day_time,
|
|
|
"pressure": 0.0,
|
|
"pressure": 0.0,
|
|
|
})
|
|
})
|
|
|
for angle in angles:
|
|
for angle in angles:
|
|
|
- series.setdefault(angle, []).sort(key=lambda point: point["time"])
|
|
|
|
|
|
|
+ series[angle].sort(key=lambda point: point["time"])
|
|
|
return {
|
|
return {
|
|
|
"angles": angles,
|
|
"angles": angles,
|
|
|
"series": [
|
|
"series": [
|
|
@@ -2346,14 +2410,22 @@ class DataService:
|
|
|
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_source_cycles(source_samples, single_cycle)
|
|
source_detected, source_diagnostic = _detect_source_cycles(source_samples, single_cycle)
|
|
|
- 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:
|
|
|
source_angle_full[detected_cycle.start_offset:detected_cycle.end_offset] = detected_cycle.angle
|
|
source_angle_full[detected_cycle.start_offset:detected_cycle.end_offset] = detected_cycle.angle
|
|
|
|
|
+ source_angle_full = _rotate_angle(
|
|
|
|
|
+ source_angle_full, device_part + source_point,
|
|
|
|
|
+ )
|
|
|
|
|
+ source_angle_vector = np.where(
|
|
|
|
|
+ source_angle_full <= 180.0,
|
|
|
|
|
+ source_angle_full,
|
|
|
|
|
+ 360.0 - source_angle_full,
|
|
|
|
|
+ )
|
|
|
source_volume, current_volume_info = self._build_volume_vector(
|
|
source_volume, current_volume_info = self._build_volume_vector(
|
|
|
len(source_samples),
|
|
len(source_samples),
|
|
|
source_detected,
|
|
source_detected,
|
|
|
- device_part + primary,
|
|
|
|
|
|
|
+ device_part + source_point,
|
|
|
|
|
+ side=source_point,
|
|
|
)
|
|
)
|
|
|
if current_volume_info is not None:
|
|
if current_volume_info is not None:
|
|
|
volume_info = current_volume_info
|
|
volume_info = current_volume_info
|
|
@@ -2523,14 +2595,15 @@ class DataService:
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
if detected_own:
|
|
if detected_own:
|
|
|
- angle_own = build_angle_vector(len(samples), detected_own)
|
|
|
|
|
full_own = np.full(len(samples), np.nan, dtype=float)
|
|
full_own = np.full(len(samples), np.nan, dtype=float)
|
|
|
for detected_cycle in detected_own:
|
|
for detected_cycle in detected_own:
|
|
|
full_own[detected_cycle.start_offset:detected_cycle.end_offset] = detected_cycle.angle
|
|
full_own[detected_cycle.start_offset:detected_cycle.end_offset] = detected_cycle.angle
|
|
|
|
|
+ full_own = _rotate_angle(full_own, device_part + device_point)
|
|
|
own_volume, _ = self._build_volume_vector(
|
|
own_volume, _ = self._build_volume_vector(
|
|
|
len(samples),
|
|
len(samples),
|
|
|
detected_own,
|
|
detected_own,
|
|
|
device_part + device_point,
|
|
device_part + device_point,
|
|
|
|
|
+ side=device_point,
|
|
|
)
|
|
)
|
|
|
own_angle = full_own
|
|
own_angle = full_own
|
|
|
if no_sampling:
|
|
if no_sampling:
|
|
@@ -2771,11 +2844,13 @@ class DataService:
|
|
|
detected = [stored_cycle] if stored_cycle is not None else []
|
|
detected = [stored_cycle] if stored_cycle is not None else []
|
|
|
if detected:
|
|
if detected:
|
|
|
cycle = detected[0]
|
|
cycle = detected[0]
|
|
|
- angle = cycle.angle
|
|
|
|
|
|
|
+ point_name = device_part + device_point
|
|
|
|
|
+ angle = _rotate_angle(cycle.angle, point_name)
|
|
|
volume, point_volume_info = self._build_volume_vector(
|
|
volume, point_volume_info = self._build_volume_vector(
|
|
|
len(slice_arr),
|
|
len(slice_arr),
|
|
|
[cycle],
|
|
[cycle],
|
|
|
- device_part + primary,
|
|
|
|
|
|
|
+ point_name,
|
|
|
|
|
+ side=device_point,
|
|
|
)
|
|
)
|
|
|
if is_slot_source and point_volume_info is not None:
|
|
if is_slot_source and point_volume_info is not None:
|
|
|
volume_info = point_volume_info
|
|
volume_info = point_volume_info
|
|
@@ -2998,20 +3073,27 @@ class DataService:
|
|
|
sample_count: int,
|
|
sample_count: int,
|
|
|
cycles: list[DetectedCycle],
|
|
cycles: list[DetectedCycle],
|
|
|
point_name: str,
|
|
point_name: str,
|
|
|
|
|
+ side: str | None = None,
|
|
|
) -> tuple[np.ndarray, dict[str, Any] | None]:
|
|
) -> tuple[np.ndarray, dict[str, Any] | None]:
|
|
|
volume = np.full(sample_count, np.nan, dtype=float)
|
|
volume = np.full(sample_count, np.nan, dtype=float)
|
|
|
- cylinder_name = next(
|
|
|
|
|
- (name for name in CYLINDER_BORE_MM if name in point_name),
|
|
|
|
|
- None,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ cylinder_name = _point_cylinder(point_name)
|
|
|
if cylinder_name is None or not cycles:
|
|
if cylinder_name is None or not cycles:
|
|
|
return volume, None
|
|
return volume, None
|
|
|
bore_mm = CYLINDER_BORE_MM[cylinder_name]
|
|
bore_mm = CYLINDER_BORE_MM[cylinder_name]
|
|
|
clearance = CLEARANCE_VOLUME_L_BY_BORE[bore_mm]
|
|
clearance = CLEARANCE_VOLUME_L_BY_BORE[bore_mm]
|
|
|
crank_radius = PISTON_STROKE_MM / 2.0
|
|
crank_radius = PISTON_STROKE_MM / 2.0
|
|
|
- piston_area = np.pi * (bore_mm / 2.0) ** 2
|
|
|
|
|
|
|
+ phase = _phase_degrees(point_name)
|
|
|
|
|
+ rod_mm = (
|
|
|
|
|
+ PISTON_ROD_DIAMETER_MM
|
|
|
|
|
+ if side == "压力轴侧" and _unit_number(point_name) in PHASE_ROTATION_UNITS
|
|
|
|
|
+ else 0.0
|
|
|
|
|
+ )
|
|
|
|
|
+ piston_area = np.pi * ((bore_mm / 2.0) ** 2 - (rod_mm / 2.0) ** 2)
|
|
|
for cycle in cycles:
|
|
for cycle in cycles:
|
|
|
- angle_rad = np.deg2rad(cycle.angle)
|
|
|
|
|
|
|
+ angle = cycle.angle
|
|
|
|
|
+ if phase:
|
|
|
|
|
+ angle = np.mod(angle - phase, 360.0)
|
|
|
|
|
+ angle_rad = np.deg2rad(angle)
|
|
|
travel = (
|
|
travel = (
|
|
|
crank_radius * (1.0 - np.cos(angle_rad))
|
|
crank_radius * (1.0 - np.cos(angle_rad))
|
|
|
+ CONNECTING_ROD_LENGTH_MM
|
|
+ CONNECTING_ROD_LENGTH_MM
|
|
@@ -3027,6 +3109,8 @@ class DataService:
|
|
|
return volume, {
|
|
return volume, {
|
|
|
"cylinder": cylinder_name,
|
|
"cylinder": cylinder_name,
|
|
|
"boreMm": bore_mm,
|
|
"boreMm": bore_mm,
|
|
|
|
|
+ "rodDiameterMm": rod_mm,
|
|
|
|
|
+ "phaseDegree": phase,
|
|
|
"clearanceVolumeL": clearance,
|
|
"clearanceVolumeL": clearance,
|
|
|
"minVolumeL": float(np.min(finite)) if len(finite) else None,
|
|
"minVolumeL": float(np.min(finite)) if len(finite) else None,
|
|
|
"maxVolumeL": float(np.max(finite)) if len(finite) else None,
|
|
"maxVolumeL": float(np.max(finite)) if len(finite) else None,
|
|
@@ -3048,12 +3132,13 @@ class DataService:
|
|
|
if cycle is None:
|
|
if cycle is None:
|
|
|
raise ValueError(f"没有找到周期 {period_number}")
|
|
raise ValueError(f"没有找到周期 {period_number}")
|
|
|
|
|
|
|
|
|
|
+ point_name = str(metadata.get("point_name") or "")
|
|
|
|
|
+ side = metadata.get("device_point")
|
|
|
# 保留 wave_sample_one 的完整周期原始采样点,不再插值成固定 360 点。
|
|
# 保留 wave_sample_one 的完整周期原始采样点,不再插值成固定 360 点。
|
|
|
- angles360 = np.asarray(cycle.angle, dtype=float)
|
|
|
|
|
|
|
+ # 7/8/9 号机按文档相位角旋转到该缸自身 0°;4/5/6 保持原样。
|
|
|
|
|
+ angles360 = _rotate_angle(np.asarray(cycle.angle, dtype=float), point_name)
|
|
|
pressure = np.asarray(cycle.signal, dtype=float)
|
|
pressure = np.asarray(cycle.signal, dtype=float)
|
|
|
display_angles = np.where(angles360 <= 180.0, angles360, 360.0 - angles360)
|
|
display_angles = np.where(angles360 <= 180.0, angles360, 360.0 - angles360)
|
|
|
- point_name = str(metadata.get("point_name") or "")
|
|
|
|
|
- side = metadata.get("device_point")
|
|
|
|
|
if side not in PRESSURE_SIDES:
|
|
if side not in PRESSURE_SIDES:
|
|
|
side = next((item for item in PRESSURE_SIDES if point_name.endswith(item)), None)
|
|
side = next((item for item in PRESSURE_SIDES if point_name.endswith(item)), None)
|
|
|
device_part = metadata.get("device_part")
|
|
device_part = metadata.get("device_part")
|
|
@@ -3078,11 +3163,19 @@ class DataService:
|
|
|
- (crank_radius * np.sin(angle_rad)) ** 2,
|
|
- (crank_radius * np.sin(angle_rad)) ** 2,
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
- piston_area = np.pi * (bore_mm / 2.0) ** 2
|
|
|
|
|
|
|
+ # 轴侧为环形面积,需扣除活塞杆;7/8/9 按文档杆径 78mm。
|
|
|
|
|
+ rod_mm = (
|
|
|
|
|
+ PISTON_ROD_DIAMETER_MM
|
|
|
|
|
+ if side == "压力轴侧" and _unit_number(point_name) in PHASE_ROTATION_UNITS
|
|
|
|
|
+ else 0.0
|
|
|
|
|
+ )
|
|
|
|
|
+ piston_area = np.pi * ((bore_mm / 2.0) ** 2 - (rod_mm / 2.0) ** 2)
|
|
|
volume = clearance_volume + piston_area * piston_travel / 1_000_000.0
|
|
volume = clearance_volume + piston_area * piston_travel / 1_000_000.0
|
|
|
volume_info = {
|
|
volume_info = {
|
|
|
"cylinder": cylinder_name,
|
|
"cylinder": cylinder_name,
|
|
|
"boreMm": bore_mm,
|
|
"boreMm": bore_mm,
|
|
|
|
|
+ "rodDiameterMm": rod_mm,
|
|
|
|
|
+ "phaseDegree": _phase_degrees(point_name),
|
|
|
"clearanceVolumeL": clearance_volume,
|
|
"clearanceVolumeL": clearance_volume,
|
|
|
"minVolumeL": float(np.min(volume)),
|
|
"minVolumeL": float(np.min(volume)),
|
|
|
"maxVolumeL": float(np.max(volume)),
|
|
"maxVolumeL": float(np.max(volume)),
|