Improve MIP optimization and add log export feature

This commit is contained in:
dmy
2026-01-08 15:08:04 +08:00
parent ebd5883dbf
commit 04a5e19451
4 changed files with 215 additions and 9 deletions

62
gui.py
View File

@@ -92,6 +92,7 @@ def index():
"info_container": None, # 新增信息展示容器 "info_container": None, # 新增信息展示容器
"ga_switch": None, # 遗传算法开关 "ga_switch": None, # 遗传算法开关
"mip_switch": None, # MIP开关 "mip_switch": None, # MIP开关
"log_content": "", # 存储计算日志内容
} }
def update_info_panel(): def update_info_panel():
@@ -632,6 +633,63 @@ def index():
"icon=folder_zip color=secondary" "icon=folder_zip color=secondary"
) )
# --- 导出计算日志 ---
async def on_click_export_log(e):
# 尝试多种方式获取日志内容
log_content = ""
method_used = "unknown"
# 方法1: 首先尝试从保存的日志内容获取
if refs.get("log_content") and refs["log_content"].strip():
log_content = refs["log_content"]
method_used = "saved_memory"
# 方法2: 如果保存的日志为空,尝试使用 JavaScript 获取 log 组件的内容
if not log_content.strip() and refs["log_box"]:
try:
log_id = refs["log_box"].id
js_code = f"""
(function() {{
const logElement = document.querySelector("#c{log_id}");
if (logElement) {{
console.log("Found log element:", logElement);
return logElement.innerText || logElement.textContent || "";
}}
console.log("Log element not found for ID: c{log_id}");
return "";
}})()
"""
result = await ui.run_javascript(js_code)
if result and result.strip():
log_content = result
method_used = "javascript"
except Exception as js_error:
print(f"JavaScript method failed: {js_error}")
if not log_content.strip():
ui.notify(
"没有可导出的日志内容。请先运行计算任务。", type="warning"
)
print(f"Log export failed. Method tried: {method_used}")
return
print(
f"Successfully exported log using method: {method_used}, length: {len(log_content)}"
)
default_name = f"{file_prefix}_calculation_log.txt"
async def save_log(path):
with open(path, "w", encoding="utf-8") as f:
f.write(log_content)
await save_file_with_dialog(
default_name, save_log, "Text Files (*.txt)", sender=e.sender
)
ui.button("导出计算日志", on_click=on_click_export_log).props(
"icon=description color=info"
)
def update_plot(result): def update_plot(result):
if refs["plot_container"]: if refs["plot_container"]:
refs["plot_container"].clear() refs["plot_container"].clear()
@@ -686,6 +744,8 @@ def index():
return return
if refs["log_box"]: if refs["log_box"]:
refs["log_box"].clear() refs["log_box"].clear()
# 重置日志内容
refs["log_content"] = ""
log_queue = queue.Queue() log_queue = queue.Queue()
# 获取开关状态 # 获取开关状态
@@ -706,6 +766,8 @@ def index():
try: try:
msg = log_queue.get_nowait() msg = log_queue.get_nowait()
refs["log_box"].push(msg) refs["log_box"].push(msg)
# 同时保存到日志内容中
refs["log_content"] += msg + "\n"
new_msg = True new_msg = True
if msg.startswith("--- Scenario"): if msg.startswith("--- Scenario"):
scenario_name = msg.replace("---", "").strip() scenario_name = msg.replace("---", "").strip()

110
mip.py
View File

@@ -135,14 +135,51 @@ def design_with_mip(
def cluster_var(k): def cluster_var(k):
return pulp.LpVariable(f"cluster_{k}", cat="Binary") return pulp.LpVariable(f"cluster_{k}", cat="Binary")
# 目标函数:最小化总投资(近似为风机到升压站的总距离) def cluster_connection_var(k):
prob += pulp.lpSum( return pulp.LpVariable(f"cluster_connection_{k}", cat="Binary")
[
dist_matrix_full[0, i + 1] * assign_var(i, k) turbine_coords = turbines[["x", "y"]].values
for i in range(n_turbines) turbine_powers = turbines["power"].values
for k in range(max_clusters)
] # Calculate cost per meter for cluster-to-substation connections
) # Higher power clusters need thicker cables = higher cost
cost_per_meter_per_mw = 1000 # Base cost per MW per meter (can be adjusted)
# Objective function: minimize total investment including:
# 1. Intra-cluster connections (estimated using pairwise distances)
# 2. Cluster-to-substation connections (based on distance and power)
objective_terms = []
# Intra-cluster connection costs (estimated)
for k in range(max_clusters):
for i in range(n_turbines):
for j in range(i + 1, n_turbines):
# Only count if both turbines are in the same cluster
# This is a simplified approximation of MST cost
both_in_cluster = assign_var(i, k) + assign_var(j, k) - 1
distance_ij = np.linalg.norm(turbine_coords[i] - turbine_coords[j])
objective_terms.append(distance_ij * both_in_cluster * 0.5)
# Cluster-to-substation connection costs
for k in range(max_clusters):
cluster_power = pulp.lpSum(
[turbine_powers[i] * assign_var(i, k) for i in range(n_turbines)]
)
cluster_to_substation_distance = dist_matrix_full[
0, :
] # Distance from each turbine to substation
# Use minimum distance from any turbine in cluster to substation
for i in range(n_turbines):
objective_terms.append(
cluster_to_substation_distance[i + 1]
* assign_var(i, k)
* cost_per_meter_per_mw
* turbine_powers[i]
* 0.001
)
prob += pulp.lpSum(objective_terms)
for i in range(n_turbines): for i in range(n_turbines):
prob += pulp.lpSum([assign_var(i, k) for k in range(max_clusters)]) == 1 prob += pulp.lpSum([assign_var(i, k) for k in range(max_clusters)]) == 1
@@ -151,7 +188,7 @@ def design_with_mip(
cluster_power = pulp.lpSum( cluster_power = pulp.lpSum(
[turbines.iloc[i]["power"] * assign_var(i, k) for i in range(n_turbines)] [turbines.iloc[i]["power"] * assign_var(i, k) for i in range(n_turbines)]
) )
prob += cluster_power <= max_mw * 1.2 * cluster_var(k) prob += cluster_power <= max_mw * 1.0 * cluster_var(k)
for k in range(max_clusters): for k in range(max_clusters):
for i in range(n_turbines): for i in range(n_turbines):
@@ -247,7 +284,62 @@ def design_with_mip(
connections.append((f"turbine_{closest}", "substation", min(dists))) connections.append((f"turbine_{closest}", "substation", min(dists)))
turbines["cluster"] = cluster_assign turbines["cluster"] = cluster_assign
# Check cluster distances
min_cluster_distance = check_cluster_distances(clusters, turbines)
if min_cluster_distance is not None:
print(
f"Cluster validation: Minimum distance between clusters = {min_cluster_distance:.2f} m"
)
if min_cluster_distance < 1000:
print(
f"WARNING: Clusters are very close to each other ({min_cluster_distance:.2f} m < 1000 m)"
)
elif min_cluster_distance < 2000:
print(
f"NOTICE: Clusters are relatively close ({min_cluster_distance:.2f} m)"
)
print( print(
f"MIP optimization completed successfully, {len(connections)} connections generated" f"MIP optimization completed successfully, {len(connections)} connections generated"
) )
return connections, turbines return connections, turbines
def calculate_cluster_centroids(clusters, turbines):
"""Calculate the centroid coordinates for each cluster."""
centroids = {}
for c, members in clusters.items():
if len(members) == 0:
centroids[c] = (0, 0)
else:
coords = turbines.iloc[members][["x", "y"]].values
centroid_x = np.mean(coords[:, 0])
centroid_y = np.mean(coords[:, 1])
centroids[c] = (centroid_x, centroid_y)
return centroids
def check_cluster_distances(clusters, turbines, min_distance_threshold=1000):
"""Check if any clusters are too close to each other."""
if len(clusters) < 2:
return None
centroids = calculate_cluster_centroids(clusters, turbines)
active_clusters = [c for c, members in clusters.items() if len(members) > 0]
min_distance = float("inf")
min_pair = None
for i in range(len(active_clusters)):
for j in range(i + 1, len(active_clusters)):
c1, c2 = active_clusters[i], active_clusters[j]
centroid1 = np.array(centroids[c1])
centroid2 = np.array(centroids[c2])
distance = np.linalg.norm(centroid1 - centroid2)
if distance < min_distance:
min_distance = distance
min_pair = (c1, c2)
return min_distance

View File

@@ -12,6 +12,8 @@ dependencies = [
"numpy>=2.4.0", "numpy>=2.4.0",
"openpyxl>=3.1.5", "openpyxl>=3.1.5",
"pandas>=2.3.3", "pandas>=2.3.3",
"pulp>=3.3.0",
"pyomo>=6.9.5",
"pywebview>=6.1", "pywebview>=6.1",
"scikit-learn>=1.8.0", "scikit-learn>=1.8.0",
"scipy>=1.16.3", "scipy>=1.16.3",

50
uv.lock generated
View File

@@ -1243,6 +1243,15 @@ wheels = [
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831" }, { url = "https://mirrors.pku.edu.cn/pypi/web/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831" },
] ]
[[package]]
name = "ply"
version = "3.11"
source = { registry = "https://mirrors.pku.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.pku.edu.cn/pypi/web/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3" }
wheels = [
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce" },
]
[[package]] [[package]]
name = "propcache" name = "propcache"
version = "0.4.1" version = "0.4.1"
@@ -1333,6 +1342,15 @@ version = "0.1.0"
source = { registry = "https://mirrors.pku.edu.cn/pypi/web/simple" } source = { registry = "https://mirrors.pku.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.pku.edu.cn/pypi/web/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010" } sdist = { url = "https://mirrors.pku.edu.cn/pypi/web/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010" }
[[package]]
name = "pulp"
version = "3.3.0"
source = { registry = "https://mirrors.pku.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.pku.edu.cn/pypi/web/packages/16/1c/d880b739b841a8aa81143091c9bdda5e72e226a660aa13178cb312d4b27f/pulp-3.3.0.tar.gz", hash = "sha256:7eb99b9ce7beeb8bbb7ea9d1c919f02f003ab7867e0d1e322f2f2c26dd31c8ba" }
wheels = [
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/99/6c/64cafaceea3f99927e84b38a362ec6a8f24f33061c90bda77dfe1cd4c3c6/pulp-3.3.0-py3-none-any.whl", hash = "sha256:dd6ad2d63f196d1254eddf9dcff5cd224912c1f046120cb7c143c5b0eda63fae" },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.23" version = "2.23"
@@ -1571,6 +1589,34 @@ wheels = [
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8" }, { url = "https://mirrors.pku.edu.cn/pypi/web/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8" },
] ]
[[package]]
name = "pyomo"
version = "6.9.5"
source = { registry = "https://mirrors.pku.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "ply" },
]
sdist = { url = "https://mirrors.pku.edu.cn/pypi/web/packages/87/d8/f32e0dcacc8219694709200d4402c86a6e28d3af50380a5ccf7f7e15ffae/pyomo-6.9.5.tar.gz", hash = "sha256:0734020fcd5cc03ee200fd3f79d143fbfc14e6be116e0d16bab79f3f89609879" }
wheels = [
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/f8/63/5f163b231a924ba7a5f6c58466c751f70be88568fa446524b6e806c98e4b/pyomo-6.9.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:549ee4226cab6e2ff6efe5b3b9891ce1dfd866d38a024715315ea850fa1bf0ec" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/2d/bf/0cebcfce70be04d6d7aa19fbcbdeecdd5843caac617424f34ab3feb8e96e/pyomo-6.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b382cc8c3728199c8332024d64eed8622dabb3f8aebe5874c86a036489064f7a" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/14/27/967545514a2d0f4ca5ac6b595661cb0927cdcd10c3bb2832c5aa0ee15990/pyomo-6.9.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43c6e425ca5231b530cd23460e371b7ca9119224dd57237c34580e15f31e4d72" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/85/fe/691e5eb26f58ee4a072add6cc484756d9e3c367901ec6701d2c6789b394d/pyomo-6.9.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1923c358e1e8009a05ada911fc72e615c9e2ce6988f0979ec1ecc75880ee1f7" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/c7/b3/ae47340790f2f1f92f76b176acf475890717f0cb7def073e504b9857a057/pyomo-6.9.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:694262dc2eb53ca1ab245261f432a5ed1ec30cf3e651b5a6a1c276bc2dd81076" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/29/e9/7f782864afd28a9eb53057c9d046541be6535b2da35e11c2bcb80839c6bd/pyomo-6.9.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f99ce91f2710d60b380a3a519288282d2183c44e1d66c131909313a3b63e7a2" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/cd/4d/9ca17a602e31a1c3f3148c455a5739fcbe23c102b80a12ec3e6d3bf5e847/pyomo-6.9.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d22f99e0ba8e2fb7d0e806bf630b8ce9b0a41d777c51f22711adbcb905f7486e" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/44/2e/78c3ac876791b59c836338b73dc49317b01cef574b01af061999a04a064a/pyomo-6.9.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5953e490b9e9ea42d28804dd0358a9d3ef82560022c2b538e70a638790bc392" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/3c/27/3eb3db8e9ed6a01dee63219389aec761d5cc29b6dc5015b32f826f2a9225/pyomo-6.9.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:058eddde05b4354307975f1ecd25cfda9f8a282ad2e3b4f168ff8fee3c3623a1" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/9a/31/7f4750fc9bb0ec18a9534549e4c80ea63f1267aa828d495a48bbf0018f49/pyomo-6.9.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:105a073c47a2d2d6e74e48ed6fc82c6f6d19027488d5003aabb7ed5d10271483" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/f8/67/639d0006eddab30cf415b0154763ccc51f3c15b934e866eb4fb07bc2b6ed/pyomo-6.9.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2c636c2c640b33dde3b119f6f0941a1bbde39397c392dba55351b0438d8600f" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/67/ef/023b74b8f161f15a51febdd160354f1e3fd7e1475abbe5ccfb3d7588cf1f/pyomo-6.9.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e02813b4021eeed7214a1ca5d7daecbdc78d3db7059962553a57fd138d747c22" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/a0/ca/edab1b532fd5e2d146d0cb96836eb5ae387b8a5bd255213e306793f6168e/pyomo-6.9.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:83789ce89271da31e0ff5bbef692af1621ab1747798183a5603b6577b7074277" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/a9/3c/2745386f57030bc60b626adba002b68db3f9538d5b52900f48026a4a17d7/pyomo-6.9.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f449aaceac5078daaecc21d19b96a15529f9ac8aa90f6472e8811cc07112ecc" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/f1/93/2058af0890b13f7e1a26e4925ff8d681c23d9cbdc2ecc9db17c744941617/pyomo-6.9.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f94d03f122fcf04a769c28ad48c423cd7b6d3d2c40da20bc8ea1a41bb20d0c36" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/de/30/c808931fc034851a16d3f8360d045b087ac743ea97bfe96cdb4b1df47c21/pyomo-6.9.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96ff300e96cdab75e2e6983c99e3a61eaff2a6d0f5ed83acd939e74e361de537" },
{ url = "https://mirrors.pku.edu.cn/pypi/web/packages/68/29/394967f7df51788cbdf1b4aedfb7c5a3a62e11b85b4c9d806b86cc576be4/pyomo-6.9.5-py3-none-any.whl", hash = "sha256:60326f7d3143ee7d0f5c5c4a3cbf871b53e08cc6c2b0c9e6d25568880233472f" },
]
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "3.3.1" version = "3.3.1"
@@ -2106,6 +2152,8 @@ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "openpyxl" }, { name = "openpyxl" },
{ name = "pandas" }, { name = "pandas" },
{ name = "pulp" },
{ name = "pyomo" },
{ name = "pywebview" }, { name = "pywebview" },
{ name = "scikit-learn" }, { name = "scikit-learn" },
{ name = "scipy" }, { name = "scipy" },
@@ -2125,6 +2173,8 @@ requires-dist = [
{ name = "numpy", specifier = ">=2.4.0" }, { name = "numpy", specifier = ">=2.4.0" },
{ name = "openpyxl", specifier = ">=3.1.5" }, { name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pandas", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2.3.3" },
{ name = "pulp", specifier = ">=3.3.0" },
{ name = "pyomo", specifier = ">=6.9.5" },
{ name = "pywebview", specifier = ">=6.1" }, { name = "pywebview", specifier = ">=6.1" },
{ name = "scikit-learn", specifier = ">=1.8.0" }, { name = "scikit-learn", specifier = ">=1.8.0" },
{ name = "scipy", specifier = ">=1.16.3" }, { name = "scipy", specifier = ">=1.16.3" },