[migrate]:[FL-13][迁移防雷计算规程法公式]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-07 16:17:49 +08:00
parent 632d2a2d17
commit 6244534582
3 changed files with 1292 additions and 210 deletions
File diff suppressed because it is too large Load Diff
+21 -3
View File
@@ -315,7 +315,7 @@ def execute_job(job_id: str) -> None:
latitude=tower.latitude,
altitude_m=tower.altitude_m,
terrain=tower.terrain,
base_tower_json=_build_base_tower_json(tower),
base_tower_json=_build_base_tower_json(tower, job.line),
profile_json=_build_profile_json(profile),
create_date=utcnow(),
)
@@ -497,11 +497,15 @@ def _mark_job_failed_without_run(db: Session, *, job_id: str, error_message: str
)
def _build_base_tower_json(tower: LineTower) -> dict[str, Any]:
def _build_base_tower_json(tower: LineTower, line: Line | None) -> dict[str, Any]:
return {
"tower_id": tower.id,
"line_id": tower.line_id,
"line_voltage_kv": tower.line.voltage_kv if tower.line else None,
"line_name": line.name if line else None,
"line_voltage_kv": line.voltage_kv if line else None,
"line_phase_sequence_json": (line.phase_sequence_json or {}) if line else {},
"line_arrester_install_json": (line.arrester_install_json or {}) if line else {},
"line_lightning_param_json": (line.lightning_param_json or {}) if line else {},
"seq_no": tower.seq_no,
"tower_no": tower.tower_no,
"tower_model": tower.tower_model,
@@ -552,6 +556,10 @@ def _build_profile_json(profile: TowerProfile | None) -> dict[str, Any]:
}
def _grade_snapshot_payload(payload: dict[str, Any]) -> dict[str, Any]:
return grade_snapshot_payload(payload)
def _new_result_summary() -> dict[str, Any]:
return {
"risk_counts": {"high": 0, "medium": 0, "low": 0},
@@ -688,6 +696,16 @@ def _resolve_source_run_id(job: FlAnalysisJob) -> str | None:
return None
def _as_int(value: Any) -> int | None:
parsed = _as_float(value)
if parsed is None:
return None
try:
return int(parsed)
except (TypeError, ValueError):
return None
def _placeholder_message_for_adapter(adapter: str) -> str:
if adapter == "wine":
return "Wine 外部程序适配器已预留,真实执行链路尚未接入"
+346 -163
View File
@@ -1,187 +1,370 @@
from __future__ import annotations
import unittest
import pytest
from app.services.fl_analysis_rules import grade_mitigation_snapshot_payload, grade_snapshot_payload
class FlAnalysisRulesTest(unittest.TestCase):
def test_grade_snapshot_payload_marks_high_risk_and_actions(self) -> None:
payload = {
"base_tower_json": {
"tower_no": "T001",
"tower_type": "耐张",
"ground_resistance_ohm": 35.0,
"lightning_density": 6.5,
"span_large_m": 620.0,
"line_voltage_kv": 110,
"slope_1": 18.0,
"slope_2": 6.0,
"circuit_geometry_json": {
"I": {
"phase_height_m": {"upper": 28.0, "middle": 24.0, "lower": 20.0},
},
"lightning_wire": {
"left_mid_distance_m": 9.0,
"right_mid_distance_m": 9.0,
"height_m": 32.0,
},
def _build_circuit_geometry(
*,
shield_left_m: float,
shield_right_m: float,
shield_height_m: float,
insulator_length_mm: float,
circuit_i_upper_m: float,
circuit_i_middle_m: float,
circuit_i_lower_m: float,
circuit_i_upper_h_m: float,
circuit_i_middle_h_m: float,
circuit_i_lower_h_m: float,
circuit_ii_upper_m: float | None = None,
circuit_ii_middle_m: float | None = None,
circuit_ii_lower_m: float | None = None,
circuit_ii_upper_h_m: float | None = None,
circuit_ii_middle_h_m: float | None = None,
circuit_ii_lower_h_m: float | None = None,
) -> dict[str, object]:
return {
"I": {
"phase_spacing_m": {
"upper": circuit_i_upper_m,
"middle": circuit_i_middle_m,
"lower": circuit_i_lower_m,
},
"phase_height_m": {
"upper": circuit_i_upper_h_m,
"middle": circuit_i_middle_h_m,
"lower": circuit_i_lower_h_m,
},
},
"II": {
"phase_spacing_m": {
"upper": circuit_ii_upper_m,
"middle": circuit_ii_middle_m,
"lower": circuit_ii_lower_m,
},
"phase_height_m": {
"upper": circuit_ii_upper_h_m,
"middle": circuit_ii_middle_h_m,
"lower": circuit_ii_lower_h_m,
},
},
"lightning_wire": {
"left_mid_distance_m": shield_left_m,
"right_mid_distance_m": shield_right_m,
"height_m": shield_height_m,
},
"insulator_length_mm": insulator_length_mm,
}
def test_grade_snapshot_payload_marks_high_risk_and_actions() -> None:
payload = {
"base_tower_json": {
"tower_no": "T001",
"tower_type": "耐张",
"ground_resistance_ohm": 35.0,
"lightning_density": 6.5,
"span_large_m": 620.0,
"line_voltage_kv": 110,
"slope_1": 18.0,
"slope_2": 6.0,
"circuit_geometry_json": {
"I": {
"phase_height_m": {"upper": 28.0, "middle": 24.0, "lower": 20.0},
},
"lightning_wire": {
"left_mid_distance_m": 9.0,
"right_mid_distance_m": 9.0,
"height_m": 32.0,
},
},
"profile_json": {
"structure_kind": "耐张",
"stroke_mode": "反击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 1200.0,
},
}
},
"profile_json": {
"structure_kind": "耐张",
"stroke_mode": "反击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 1200.0,
},
}
result = grade_snapshot_payload(payload)
result = grade_snapshot_payload(payload)
self.assertEqual(result["risk_level"], "high")
self.assertGreaterEqual(result["score"], 80)
self.assertIn("接地电阻偏高", result["cause_analysis"])
self.assertIn("避雷器", result["mitigation_recommendation"])
self.assertTrue(any(item["code"] == "insulator_length" for item in result["reason_details"]))
assert result["risk_level"] == "high"
assert result["score"] >= 80
assert "接地电阻偏高" in result["cause_analysis"]
assert "避雷器" in result["mitigation_recommendation"]
assert any(item["code"] == "insulator_length" for item in result["reason_details"])
def test_grade_snapshot_payload_marks_low_risk_when_inputs_are_good(self) -> None:
payload = {
"base_tower_json": {
"tower_no": "T002",
"tower_type": "直线",
"ground_resistance_ohm": 4.0,
"lightning_density": 1.5,
"span_large_m": 180.0,
"line_voltage_kv": 110,
},
"profile_json": {
"structure_kind": "直线",
"stroke_mode": "绕击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 1800.0,
},
}
result = grade_snapshot_payload(payload)
def test_grade_snapshot_payload_marks_low_risk_when_inputs_are_good() -> None:
payload = {
"base_tower_json": {
"tower_no": "T002",
"tower_type": "直线",
"ground_resistance_ohm": 4.0,
"lightning_density": 1.5,
"span_large_m": 180.0,
"line_voltage_kv": 110,
},
"profile_json": {
"structure_kind": "直线",
"stroke_mode": "绕击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 1800.0,
},
}
self.assertEqual(result["risk_level"], "low")
self.assertLess(result["score"], 40)
self.assertTrue(result["cause_analysis"])
self.assertTrue(result["mitigation_recommendation"])
result = grade_snapshot_payload(payload)
def test_grade_mitigation_snapshot_payload_builds_actions_and_reduces_expected_risk(self) -> None:
payload = {
"base_tower_json": {
"tower_no": "T003",
"tower_type": "耐张",
"ground_resistance_ohm": 28.0,
"lightning_density": 5.0,
"span_large_m": 420.0,
"line_voltage_kv": 220,
"slope_1": 12.0,
"slope_2": 8.0,
"circuit_geometry_json": {
"I": {
"phase_height_m": {"upper": 36.0, "middle": 32.0, "lower": 28.0},
},
"lightning_wire": {
"left_mid_distance_m": 8.5,
"right_mid_distance_m": 8.5,
"height_m": 40.0,
},
assert result["risk_level"] == "low"
assert result["score"] < 40
assert result["cause_analysis"]
assert result["mitigation_recommendation"]
def test_grade_mitigation_snapshot_payload_builds_actions_and_reduces_expected_risk() -> None:
payload = {
"base_tower_json": {
"tower_no": "T003",
"tower_type": "耐张",
"ground_resistance_ohm": 28.0,
"lightning_density": 5.0,
"span_large_m": 420.0,
"line_voltage_kv": 220,
"slope_1": 12.0,
"slope_2": 8.0,
"circuit_geometry_json": {
"I": {
"phase_height_m": {"upper": 36.0, "middle": 32.0, "lower": 28.0},
},
"lightning_wire": {
"left_mid_distance_m": 8.5,
"right_mid_distance_m": 8.5,
"height_m": 40.0,
},
},
"profile_json": {
"structure_kind": "耐张",
"stroke_mode": "反击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 2000.0,
},
}
},
"profile_json": {
"structure_kind": "耐张",
"stroke_mode": "反击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 2000.0,
},
}
result = grade_mitigation_snapshot_payload(payload, non_construction=False)
result = grade_mitigation_snapshot_payload(payload, non_construction=False)
self.assertIn(result["recommendation_result"], {"需要安装避雷器", "不需要安装避雷器"})
self.assertTrue(result["mitigation_actions"])
self.assertLess(result["expected_score"], result["current_score"])
self.assertTrue(any(action["code"] == "grounding_upgrade" for action in result["mitigation_actions"]))
assert result["recommendation_result"] in {"需要安装避雷器", "不需要安装避雷器"}
assert result["mitigation_actions"]
assert result["expected_score"] < result["current_score"]
assert any(action["code"] == "grounding_upgrade" for action in result["mitigation_actions"])
def test_grade_mitigation_snapshot_payload_includes_non_construction_action(self) -> None:
payload = {
"base_tower_json": {
"tower_no": "T004",
"tower_model": "s220guxing",
"tower_type": "直线",
"ground_resistance_ohm": 16.0,
"lightning_density": 3.5,
"span_large_m": 260.0,
"line_voltage_kv": 220,
"circuit_geometry_json": {
"I": {
"phase_height_m": {"upper": 30.0, "middle": 26.0, "lower": 22.0},
},
"lightning_wire": {
"left_mid_distance_m": 10.0,
"right_mid_distance_m": 10.0,
"height_m": 33.0,
},
def test_grade_mitigation_snapshot_payload_includes_non_construction_action() -> None:
payload = {
"base_tower_json": {
"tower_no": "T004",
"tower_model": "s220guxing",
"tower_type": "直线",
"ground_resistance_ohm": 16.0,
"lightning_density": 3.5,
"span_large_m": 260.0,
"line_voltage_kv": 220,
"circuit_geometry_json": {
"I": {
"phase_height_m": {"upper": 30.0, "middle": 26.0, "lower": 22.0},
},
"lightning_wire": {
"left_mid_distance_m": 10.0,
"right_mid_distance_m": 10.0,
"height_m": 33.0,
},
},
"profile_json": {
"structure_kind": "直线",
"stroke_mode": "绕击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 2500.0,
},
"profile_json": {
"structure_kind": "直线",
"stroke_mode": "绕击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 2500.0,
},
}
result = grade_mitigation_snapshot_payload(payload, non_construction=True)
assert result["non_construction"] is True
assert any(action["code"] == "shielding_geometry" for action in result["mitigation_actions"])
def test_grade_mitigation_snapshot_payload_prefers_source_risk_result() -> None:
payload = {
"base_tower_json": {
"tower_no": "T005",
"tower_type": "直线",
"ground_resistance_ohm": 4.0,
"lightning_density": 1.5,
"span_large_m": 180.0,
"line_voltage_kv": 110,
},
"profile_json": {
"structure_kind": "直线",
"stroke_mode": "绕击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 1800.0,
},
"source_result_json": {
"risk_level": "high",
"score": 92,
"cause_analysis": "沿用前驱风险结果",
"reason_details": [{"code": "source_reason", "label": "前驱原因"}],
"inputs": {"ground_resistance_ohm": 35.0},
},
}
result = grade_mitigation_snapshot_payload(payload, non_construction=False)
assert result["current_risk_level"] == "high"
assert result["current_score"] == 92
assert result["cause_analysis"] == "沿用前驱风险结果"
assert result["reason_details"][0]["code"] == "source_reason"
def test_grade_snapshot_payload_uses_source_formula_and_converts_mm_to_m() -> None:
geometry = _build_circuit_geometry(
shield_left_m=12.4,
shield_right_m=15.9,
shield_height_m=73.5,
insulator_length_mm=5945.0,
circuit_i_upper_m=14.5,
circuit_i_middle_m=10.5,
circuit_i_lower_m=12.0,
circuit_i_upper_h_m=53.0,
circuit_i_middle_h_m=65.5,
circuit_i_lower_h_m=42.0,
circuit_ii_upper_m=14.5,
circuit_ii_middle_m=10.5,
circuit_ii_lower_m=12.0,
circuit_ii_upper_h_m=53.0,
circuit_ii_middle_h_m=65.5,
circuit_ii_lower_h_m=42.0,
)
payload = {
"base_tower_json": {
"tower_no": "P1",
"tower_type": "耐张",
"tower_model": "500-MC31S-DJC2-42",
"line_name": "交流500kV雁船线(双回段)",
"line_voltage_kv": 500,
"ground_resistance_ohm": 20.0,
"lightning_density": 5.42528304182587,
"slope_1": -5.70339674677262,
"slope_2": -1.42923539492821,
"circuit_geometry_json": geometry,
"lightning_result_json": {},
},
"profile_json": {
"structure_kind": "耐张",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"shield_wire_height_m": 73.5,
"insulator_length_m": 5945.0,
"current_a": 25.109,
"current_b": 3.62,
},
}
result = grade_snapshot_payload(payload)
assert result["risk_level"] == "high"
assert result["risk_grade"] == 3
assert result["inputs"]["insulator_length_m"] == pytest.approx(5.945, abs=1e-6)
assert result["counterstrike_withstand_ka"] == pytest.approx(75.8070, rel=1e-4)
assert result["counterstrike_trip_rate"] == pytest.approx(0.529876, rel=1e-4)
assert result["shielding_withstand_ka"] == pytest.approx(31.2860, rel=1e-4)
assert result["shielding_trip_rate"] == pytest.approx(0.000003, abs=1e-6)
assert "接地电阻" in result["cause_analysis"]
assert "规程法等级 3" in result["summary_text"]
def test_grade_snapshot_payload_marks_low_risk_for_good_inputs() -> None:
geometry = _build_circuit_geometry(
shield_left_m=8.0,
shield_right_m=8.0,
shield_height_m=40.0,
insulator_length_mm=3500.0,
circuit_i_upper_m=10.0,
circuit_i_middle_m=3.0,
circuit_i_lower_m=8.0,
circuit_i_upper_h_m=28.0,
circuit_i_middle_h_m=31.0,
circuit_i_lower_h_m=24.0,
)
payload = {
"base_tower_json": {
"tower_no": "L1",
"tower_type": "直线",
"tower_model": "220-TEST-ZX",
"line_name": "交流220kV示例线",
"line_voltage_kv": 220,
"ground_resistance_ohm": 5.0,
"lightning_density": 1.2,
"slope_1": 0.0,
"slope_2": 0.0,
"circuit_geometry_json": geometry,
"lightning_result_json": {},
},
"profile_json": {
"structure_kind": "直线",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"shield_wire_height_m": 40.0,
"insulator_length_m": 3500.0,
"current_a": 31.0,
"current_b": 2.6,
},
}
result = grade_snapshot_payload(payload)
assert result["risk_level"] == "low"
assert result["risk_grade"] == 1
assert result["score"] < 30
assert result["counterstrike_trip_rate"] == pytest.approx(0.109404, rel=1e-4)
assert result["shielding_trip_rate"] == pytest.approx(0.006737, rel=1e-4)
assert "高风险" not in result["summary_text"]
def test_grade_snapshot_payload_falls_back_to_legacy_result_when_geometry_is_missing() -> None:
payload = {
"base_tower_json": {
"tower_no": "F1",
"lightning_result_json": {
"counterstroke_withstand_ka": 52.3,
"counterstroke_trip_rate": 0.12,
"shielding_withstand_ka": 18.6,
"shielding_trip_rate": 0.03,
"risk_level": 2,
},
}
},
"profile_json": {},
}
result = grade_mitigation_snapshot_payload(payload, non_construction=True)
result = grade_snapshot_payload(payload)
self.assertTrue(result["non_construction"])
self.assertTrue(any(action["code"] == "shielding_geometry" for action in result["mitigation_actions"]))
def test_grade_mitigation_snapshot_payload_prefers_source_risk_result(self) -> None:
payload = {
"base_tower_json": {
"tower_no": "T005",
"tower_type": "直线",
"ground_resistance_ohm": 4.0,
"lightning_density": 1.5,
"span_large_m": 180.0,
"line_voltage_kv": 110,
},
"profile_json": {
"structure_kind": "直线",
"stroke_mode": "绕击",
"arrester_a": "",
"arrester_b": "",
"arrester_c": "",
"insulator_length_m": 1800.0,
},
"source_result_json": {
"risk_level": "high",
"score": 92,
"cause_analysis": "沿用前驱风险结果",
"reason_details": [{"code": "source_reason", "label": "前驱原因"}],
"inputs": {"ground_resistance_ohm": 35.0},
},
}
result = grade_mitigation_snapshot_payload(payload, non_construction=False)
self.assertEqual(result["current_risk_level"], "high")
self.assertEqual(result["current_score"], 92)
self.assertEqual(result["cause_analysis"], "沿用前驱风险结果")
self.assertEqual(result["reason_details"][0]["code"], "source_reason")
if __name__ == "__main__":
unittest.main()
assert result["used_legacy_fallback"] is True
assert result["risk_level"] == "medium"
assert result["counterstrike_withstand_ka"] == pytest.approx(52.3)
assert result["shielding_trip_rate"] == pytest.approx(0.03)
assert "历史结果" in result["cause_analysis"]