391 lines
14 KiB
Python
391 lines
14 KiB
Python
def groupTrackersToTransformers(transformer_power, max_distance):
|
|
import numpy as np
|
|
from scipy.spatial import KDTree
|
|
import FreeCAD
|
|
from collections import deque
|
|
|
|
# 1. Obtener todos los trackers válidos
|
|
valid_trackers = []
|
|
valid_points = []
|
|
valid_power = []
|
|
|
|
for tracker in FreeCAD.ActiveDocument.Objects:
|
|
if hasattr(tracker, 'Proxy') and (tracker.Proxy.Type == "Tracker"):
|
|
base = tracker.Placement.Base
|
|
if all(np.isfinite([base.x, base.y, base.z])):
|
|
valid_trackers.append(tracker)
|
|
valid_points.append([base.x, base.y])
|
|
valid_power.append(tracker.Setup.TotalPower)
|
|
|
|
if not valid_trackers:
|
|
FreeCAD.Console.PrintWarning("No se encontraron trackers válidos para agrupar\n")
|
|
return
|
|
|
|
# 2. Obtener parámetros de los CTs
|
|
target_power = transformer_power * 1.2
|
|
points = np.array(valid_points)
|
|
power_values = np.array(valid_power)
|
|
|
|
# 3. Determinar dirección de barrido (oeste a este por defecto)
|
|
min_x, min_y = np.min(points, axis=0)
|
|
max_x, max_y = np.max(points, axis=0)
|
|
|
|
# 4. Ordenar trackers de oeste a este (menor X a mayor X)
|
|
'''
|
|
Norte a Sur: sorted_indices = np.argsort(-points[:, 1]) (Y descendente)
|
|
Sur a Norte: sorted_indices = np.argsort(points[:, 1]) (Y ascendente)
|
|
Este a Oeste: sorted_indices = np.argsort(-points[:, 0]) (X descendente)
|
|
'''
|
|
sorted_indices = np.argsort(points[:, 0])
|
|
sorted_points = points[sorted_indices]
|
|
sorted_power = power_values[sorted_indices]
|
|
sorted_trackers = [valid_trackers[i] for i in sorted_indices]
|
|
|
|
# 5. Crear KDTree para búsquedas rápidas
|
|
kdtree = KDTree(sorted_points)
|
|
|
|
# 6. Algoritmo de barrido espacial
|
|
transformer_groups = []
|
|
used_indices = set()
|
|
|
|
# Función para expandir un grupo desde un punto inicial
|
|
def expand_group(start_idx):
|
|
group = []
|
|
total_power = 0
|
|
queue = deque([start_idx])
|
|
|
|
while queue and total_power < target_power:
|
|
idx = queue.popleft()
|
|
if idx in used_indices:
|
|
continue
|
|
|
|
# Añadir tracker al grupo si no excede la potencia
|
|
tracker_power = sorted_power[idx]
|
|
if total_power + tracker_power > target_power * 1.05:
|
|
continue
|
|
|
|
group.append(sorted_trackers[idx])
|
|
total_power += tracker_power
|
|
used_indices.add(idx)
|
|
|
|
# Buscar vecinos cercanos
|
|
neighbors = kdtree.query_ball_point(
|
|
sorted_points[idx],
|
|
max_distance
|
|
)
|
|
|
|
# Filtrar vecinos no usados y ordenar por proximidad al punto inicial
|
|
neighbors = [n for n in neighbors if n not in used_indices]
|
|
neighbors.sort(key=lambda n: abs(n - start_idx))
|
|
queue.extend(neighbors)
|
|
|
|
return group, total_power
|
|
|
|
# 7. Barrido principal de oeste a este
|
|
for i in range(len(sorted_points)):
|
|
if i in used_indices:
|
|
continue
|
|
|
|
group, total_power = expand_group(i)
|
|
|
|
if group:
|
|
# Calcular centro del grupo
|
|
group_points = np.array([t.Placement.Base[:2] for t in group])
|
|
center = np.mean(group_points, axis=0)
|
|
|
|
transformer_groups.append({
|
|
'trackers': group,
|
|
'total_power': total_power,
|
|
'center': center
|
|
})
|
|
|
|
# 8. Manejar grupos residuales (si los hay)
|
|
unused_indices = set(range(len(sorted_points))) - used_indices
|
|
if unused_indices:
|
|
# Intentar añadir trackers residuales a grupos existentes
|
|
for idx in unused_indices:
|
|
point = sorted_points[idx]
|
|
tracker_power = sorted_power[idx]
|
|
|
|
# Buscar el grupo más cercano que pueda aceptar este tracker
|
|
best_group = None
|
|
min_distance = float('inf')
|
|
|
|
for group in transformer_groups:
|
|
if group['total_power'] + tracker_power <= target_power * 1.05:
|
|
dist = np.linalg.norm(point - group['center'])
|
|
if dist < min_distance and dist < max_distance * 1.5:
|
|
min_distance = dist
|
|
best_group = group
|
|
|
|
# Añadir al grupo si se encontró uno adecuado
|
|
if best_group:
|
|
best_group['trackers'].append(sorted_trackers[idx])
|
|
best_group['total_power'] += tracker_power
|
|
# Actualizar centro del grupo
|
|
group_points = np.array([t.Placement.Base[:2] for t in best_group['trackers']])
|
|
best_group['center'] = np.mean(group_points, axis=0)
|
|
else:
|
|
# Crear un nuevo grupo con este tracker residual
|
|
group = [sorted_trackers[idx]]
|
|
center = point
|
|
transformer_groups.append({
|
|
'trackers': group,
|
|
'total_power': tracker_power,
|
|
'center': center
|
|
})
|
|
|
|
# 9. Crear los grupos en FreeCAD
|
|
transformer_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Transformers")
|
|
transformer_group.Label = "Centros de Transformación"
|
|
|
|
for i, group in enumerate(transformer_groups):
|
|
# Crear la esfera que representará el CT
|
|
ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}")
|
|
ct_shape.Length = 6058
|
|
ct_shape.Width = 2438
|
|
ct_shape.Height = 2591
|
|
ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0)
|
|
|
|
# Añadir propiedades personalizadas
|
|
ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT",
|
|
"Lista de trackers asociados a este CT")
|
|
ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT",
|
|
"Potencia total del grupo (W)")
|
|
ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT",
|
|
"Potencia nominal del transformador (W)")
|
|
ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT",
|
|
"Porcentaje de utilización (Total/Nominal)")
|
|
|
|
# Establecer valores de las propiedades
|
|
ct_shape.Trackers = group['trackers']
|
|
ct_shape.TotalPower = group['total_power'].Value
|
|
ct_shape.NominalPower = transformer_power
|
|
ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100
|
|
|
|
# Configurar visualización
|
|
# Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%)
|
|
utilization = ct_shape.Utilization
|
|
if utilization <= 100:
|
|
color = (0.0, 1.0, 0.0) # Verde
|
|
elif utilization <= 110:
|
|
color = (1.0, 1.0, 0.0) # Amarillo
|
|
else:
|
|
color = (1.0, 0.0, 0.0) # Rojo
|
|
|
|
ct_shape.ViewObject.ShapeColor = color
|
|
ct_shape.ViewObject.Transparency = 40 # 40% de transparencia
|
|
|
|
# Añadir etiqueta con información
|
|
ct_shape.ViewObject.DisplayMode = "Shaded"
|
|
ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)"
|
|
|
|
# Añadir al grupo principal
|
|
transformer_group.addObject(ct_shape)
|
|
|
|
FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n")
|
|
onSelectGatePoint()
|
|
|
|
|
|
import FreeCAD, FreeCADGui, Part
|
|
import numpy as np
|
|
from scipy.stats import linregress
|
|
from PySide import QtGui
|
|
|
|
class InternalPathCreator:
|
|
def __init__(self, gate_point, strategy=1, path_width=4000):
|
|
self.gate_point = gate_point
|
|
self.strategy = strategy
|
|
self.path_width = path_width
|
|
self.ct_shapes = []
|
|
self.ct_positions = []
|
|
|
|
def get_transformers(self):
|
|
transformers_group = FreeCAD.ActiveDocument.getObject("Transformers")
|
|
if not transformers_group:
|
|
FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n")
|
|
return False
|
|
|
|
self.ct_shapes = transformers_group.Group
|
|
if not self.ct_shapes:
|
|
FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n")
|
|
return False
|
|
|
|
# Obtener las posiciones de los CTs
|
|
for sphere in self.ct_shapes:
|
|
base = sphere.Placement.Base
|
|
self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0))
|
|
return True
|
|
|
|
def create_paths(self):
|
|
if not self.get_transformers():
|
|
return []
|
|
|
|
if self.strategy == 1:
|
|
return self.create_direct_paths()
|
|
elif self.strategy == 2:
|
|
return self.create_unified_path()
|
|
else:
|
|
FreeCAD.Console.PrintError("Estrategia no válida. Use 1 o 2.\n")
|
|
return []
|
|
|
|
def create_direct_paths(self):
|
|
"""Estrategia 1: Caminos independientes desde cada CT hasta la puerta"""
|
|
paths = []
|
|
for ct in self.ct_positions:
|
|
paths.append([ct, self.gate_point])
|
|
return paths
|
|
|
|
def create_unified_path(self):
|
|
"""Estrategia 2: Único camino que une todos los CTs y la puerta usando regresión lineal"""
|
|
if not self.ct_positions:
|
|
return []
|
|
|
|
all_points = self.ct_positions + [self.gate_point]
|
|
x = [p.x for p in all_points]
|
|
y = [p.y for p in all_points]
|
|
|
|
# Manejar caso de puntos alineados verticalmente
|
|
if np.std(x) < 1e-6:
|
|
sorted_points = sorted(all_points, key=lambda p: p.y)
|
|
paths = []
|
|
for i in range(len(sorted_points) - 1):
|
|
paths.append([sorted_points[i], sorted_points[i + 1]])
|
|
return paths
|
|
|
|
# Calcular regresión lineal
|
|
slope, intercept, _, _, _ = linregress(x, y)
|
|
|
|
# Función para proyectar puntos
|
|
def project_point(point):
|
|
x0, y0 = point.x, point.y
|
|
if abs(slope) > 1e6:
|
|
return FreeCAD.Vector(x0, intercept, 0)
|
|
x_proj = (x0 + slope * (y0 - intercept)) / (1 + slope ** 2)
|
|
y_proj = slope * x_proj + intercept
|
|
return FreeCAD.Vector(x_proj, y_proj, 0)
|
|
|
|
# return slope * x + intercept --> desde placement
|
|
|
|
projected_points = [project_point(p) for p in all_points]
|
|
|
|
# Calcular distancias a lo largo de la línea
|
|
ref_point = projected_points[0]
|
|
direction_vector = FreeCAD.Vector(1, slope).normalize()
|
|
distances = []
|
|
for p in projected_points:
|
|
vec_to_point = p - ref_point
|
|
distance = vec_to_point.dot(direction_vector)
|
|
distances.append(distance)
|
|
|
|
# Ordenar por distancia
|
|
sorted_indices = np.argsort(distances)
|
|
sorted_points = [all_points[i] for i in sorted_indices]
|
|
|
|
# Crear caminos
|
|
paths = []
|
|
for i in range(len(sorted_points) - 1):
|
|
paths.append([sorted_points[i], sorted_points[i + 1]])
|
|
return paths
|
|
|
|
def create_3d_path(self, path_poly):
|
|
"""Crea geometría 3D para el camino adaptada a orientación norte-sur"""
|
|
segments = []
|
|
for i in range(len(path_poly.Vertexes) - 1):
|
|
start = path_poly.Vertexes[i].Point
|
|
end = path_poly.Vertexes[i + 1].Point
|
|
direction = end - start
|
|
|
|
# Determinar orientación predominante
|
|
if abs(direction.x) > abs(direction.y):
|
|
normal = FreeCAD.Vector(0, 1, 0) # Norte-sur
|
|
else:
|
|
normal = FreeCAD.Vector(1, 0, 0) # Este-oeste
|
|
|
|
offset = normal * self.path_width / 2
|
|
|
|
# Crear puntos para la sección transversal
|
|
p1 = start + offset
|
|
p2 = start - offset
|
|
p3 = end - offset
|
|
p4 = end + offset
|
|
|
|
# Crear cara
|
|
wire = Part.makePolygon([p1, p2, p3, p4, p1])
|
|
face = Part.Face(wire)
|
|
segments.append(face)
|
|
|
|
if segments:
|
|
'''road_shape = segments[0].fuse(segments[1:])
|
|
return road_shape.removeSplitter()'''
|
|
return Part.makeCompound(segments)
|
|
return Part.Shape()
|
|
|
|
def build(self):
|
|
paths = self.create_paths()
|
|
if not paths:
|
|
return
|
|
|
|
path_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "InternalPaths")
|
|
path_group.Label = f"Caminos Internos (Estrategia {self.strategy})"
|
|
|
|
for i, path in enumerate(paths):
|
|
poly = Part.makePolygon(path)
|
|
|
|
# Objeto para la línea central
|
|
path_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Path_{i + 1}")
|
|
path_obj.Shape = poly
|
|
path_obj.ViewObject.LineWidth = 3.0
|
|
path_obj.ViewObject.LineColor = (0.0, 0.0, 1.0)
|
|
|
|
# Objeto para la superficie 3D
|
|
road_shape = self.create_3d_path(poly)
|
|
road_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Road_{i + 1}")
|
|
road_obj.Shape = road_shape
|
|
road_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
|
|
|
|
path_group.addObject(path_obj)
|
|
path_group.addObject(road_obj)
|
|
|
|
FreeCAD.Console.PrintMessage(f"Se crearon {len(paths)} segmentos de caminos internos\n")
|
|
|
|
|
|
# Función para mostrar el diálogo de estrategia
|
|
def show_path_strategy_dialog(gate_point):
|
|
dialog = QtGui.QDialog()
|
|
dialog.setWindowTitle("Seleccionar Estrategia de Caminos")
|
|
layout = QtGui.QVBoxLayout(dialog)
|
|
|
|
label = QtGui.QLabel("Seleccione la estrategia para crear los caminos internos:")
|
|
layout.addWidget(label)
|
|
|
|
rb1 = QtGui.QRadioButton("Estrategia 1: Caminos independientes desde cada CT hasta la puerta")
|
|
rb1.setChecked(True)
|
|
layout.addWidget(rb1)
|
|
|
|
rb2 = QtGui.QRadioButton("Estrategia 2: Único camino que une todos los CTs y la puerta")
|
|
layout.addWidget(rb2)
|
|
|
|
btn_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
|
|
layout.addWidget(btn_box)
|
|
|
|
def on_accept():
|
|
strategy = 1 if rb1.isChecked() else 2
|
|
dialog.accept()
|
|
creator = InternalPathCreator(gate_point, strategy)
|
|
creator.build()
|
|
|
|
btn_box.accepted.connect(on_accept)
|
|
btn_box.rejected.connect(dialog.reject)
|
|
|
|
dialog.exec_()
|
|
|
|
|
|
# Uso: seleccionar un punto para la puerta de entrada
|
|
def onSelectGatePoint():
|
|
'''gate = FreeCAD.ActiveDocument.findObjects(Name="FenceGate")[0]
|
|
gate_point = gate.Placement.Base
|
|
show_path_strategy_dialog(gate_point)'''
|
|
|
|
sel = FreeCADGui.Selection.getSelectionEx()[0]
|
|
show_path_strategy_dialog(sel.SubObjects[0].CenterOfMass) |