Files
windfarm/mip.py

346 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
import pandas as pd
from scipy.spatial import distance_matrix
from scipy.sparse.csgraph import minimum_spanning_tree
from collections import defaultdict
import random
try:
import pulp
pulp_available = True
except ImportError:
pulp = None
pulp_available = False
try:
import pyomo.environ as pyo_env
pyomo_available = True
except (ImportError, AttributeError):
pyomo_available = False
print("Pyomo not available, falling back to PuLP")
def design_with_pyomo(
turbines,
substation,
cable_specs=None,
voltage=66000,
power_factor=0.95,
system_params=None,
max_clusters=None,
time_limit=300,
evaluate_func=None,
total_invest_func=None,
get_max_capacity_func=None,
):
"""
使用Pyomo求解器优化集电线路布局
:param turbines: 风机DataFrame
:param substation: 升压站坐标
:param cable_specs: 电缆规格
:param system_params: 系统参数用于NPV计算
:param max_clusters: 最大簇数,默认基于功率计算
:param time_limit: 求解时间限制(秒)
:param evaluate_func: 评估函数
:param total_invest_func: 总投资计算函数
:param get_max_capacity_func: 获取最大容量函数
:return: 连接列表和带有簇信息的turbines
"""
if get_max_capacity_func:
max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
else:
max_mw = 100.0
total_power = turbines["power"].sum()
if max_clusters is None:
max_clusters = int(np.ceil(total_power / max_mw))
n_turbines = len(turbines)
all_coords = np.vstack([substation, turbines[["x", "y"]].values])
dist_matrix_full = distance_matrix(all_coords, all_coords)
# Simple fallback for now - use PuLP instead
print("Pyomo not fully implemented, falling back to PuLP")
return design_with_mip(
turbines,
substation,
cable_specs,
voltage,
power_factor,
system_params,
max_clusters,
time_limit,
evaluate_func,
total_invest_func,
get_max_capacity_func,
)
def design_with_mip(
turbines,
substation,
cable_specs=None,
voltage=66000,
power_factor=0.95,
system_params=None,
max_clusters=None,
time_limit=300,
evaluate_func=None,
total_invest_func=None,
get_max_capacity_func=None,
):
"""
使用混合整数规划(MIP)优化集电线路布局
:param turbines: 风机DataFrame
:param substation: 升压站坐标
:param cable_specs: 电缆规格
:param system_params: 系统参数用于NPV计算
:param max_clusters: 最大簇数,默认基于功率计算
:param time_limit: 求解时间限制(秒)
:param evaluate_func: 评估函数
:param total_invest_func: 总投资计算函数
:param get_max_capacity_func: 获取最大容量函数
:return: 连接列表和带有簇信息的turbines
"""
if not pulp_available:
print(
"WARNING: PuLP library not available. MIP optimization skipped, falling back to MST."
)
from main import design_with_mst
connections = design_with_mst(turbines, substation)
return connections, turbines
if get_max_capacity_func:
max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
else:
max_mw = 100.0
if max_clusters is None:
max_clusters = int(np.ceil(turbines["power"].sum() / max_mw))
n_turbines = len(turbines)
print(
f"MIP Model Setup: n_turbines={n_turbines}, max_clusters={max_clusters}, max_mw={max_mw:.2f} MW"
)
all_coords = np.vstack([substation, turbines[["x", "y"]].values])
dist_matrix_full = distance_matrix(all_coords, all_coords)
prob = pulp.LpProblem("WindFarmCollectorMIP", pulp.LpMinimize)
def assign_var(i, k):
return pulp.LpVariable(f"assign_{i}_{k}", cat="Binary")
def cluster_var(k):
return pulp.LpVariable(f"cluster_{k}", cat="Binary")
def cluster_connection_var(k):
return pulp.LpVariable(f"cluster_connection_{k}", cat="Binary")
turbine_coords = turbines[["x", "y"]].values
turbine_powers = turbines["power"].values
# 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):
prob += pulp.lpSum([assign_var(i, k) for k in range(max_clusters)]) == 1
for k in range(max_clusters):
cluster_power = pulp.lpSum(
[turbines.iloc[i]["power"] * assign_var(i, k) for i in range(n_turbines)]
)
prob += cluster_power <= max_mw * 1.0 * cluster_var(k)
for k in range(max_clusters):
for i in range(n_turbines):
prob += assign_var(i, k) <= cluster_var(k)
print(
f"MIP Model: {len(prob.variables())} variables, {len(prob.constraints)} constraints"
)
print("MIP: Starting to solve...")
solver = pulp.PULP_CBC_CMD(timeLimit=time_limit, msg=0, warmStart=False, path=None)
try:
status = prob.solve(solver)
print(
f"MIP: Solver status={pulp.LpStatus[prob.status]}, Objective value={pulp.value(prob.objective):.4f}"
)
except Exception as e:
print(f"MIP: Solver execution failed: {e}, falling back to MST")
from main import design_with_mst
connections = design_with_mst(turbines, substation)
return connections, turbines
if pulp.LpStatus[prob.status] != "Optimal":
print(
f"MIP solver status: {pulp.LpStatus[prob.status]}, solution not found, falling back to MST"
)
print("Model feasibility check:")
print(f"Total power: {turbines['power'].sum():.2f} MW")
print(f"Max cluster capacity: {max_mw:.2f} MW")
print(f"Number of clusters: {max_clusters}, Number of turbines: {n_turbines}")
for k in range(max_clusters):
cluster_power = pulp.value(
pulp.lpSum(
[
turbines.iloc[i]["power"] * assign_var(i, k)
for i in range(n_turbines)
]
)
)
cluster_used = pulp.value(cluster_var(k))
print(
f"Cluster {k}: Power={cluster_power:.2f} MW (max {max_mw * 1.2:.2f}), Used={cluster_used}"
)
from main import design_with_mst
connections = design_with_mst(turbines, substation)
return connections, turbines
cluster_assign = [-1] * n_turbines
active_clusters = []
for k in range(max_clusters):
if pulp.value(cluster_var(k)) > 0.5:
active_clusters.append(k)
for i in range(n_turbines):
assigned = False
for k in active_clusters:
if pulp.value(assign_var(i, k)) > 0.5:
cluster_assign[i] = k
assigned = True
break
if not assigned:
dists = [dist_matrix_full[0, i + 1] for k in active_clusters]
cluster_assign[i] = active_clusters[np.argmin(dists)]
clusters = defaultdict(list)
for i, c in enumerate(cluster_assign):
clusters[c].append(i)
connections = []
for c, members in clusters.items():
if len(members) == 0:
continue
coords = turbines.iloc[members][["x", "y"]].values
if len(members) > 1:
dm = distance_matrix(coords, coords)
mst = minimum_spanning_tree(dm).toarray()
for i in range(len(members)):
for j in range(len(members)):
if mst[i, j] > 0:
connections.append(
(
f"turbine_{members[i]}",
f"turbine_{members[j]}",
mst[i, j],
)
)
dists = [dist_matrix_full[0, m + 1] for m in members]
closest = members[np.argmin(dists)]
connections.append((f"turbine_{closest}", "substation", min(dists)))
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(
f"MIP optimization completed successfully, {len(connections)} connections generated"
)
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