346 lines
12 KiB
Python
346 lines
12 KiB
Python
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
|