feat(sprint4): completar optimizador heuristico concurrente, integracion Spring Boot, correccion de login y fixes visuales
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
import time
|
||||
import uuid
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Dict, Any, Optional
|
||||
from flask import current_app
|
||||
|
||||
from app.models.genetic_algorithm import (
|
||||
ReservationOptimizer,
|
||||
GeneticAlgorithm,
|
||||
ReservationRequest,
|
||||
Classroom
|
||||
)
|
||||
from app.models.subject import Commission
|
||||
from app.schemas.optimizer_dto import SpringBootSyncPayloadDTO
|
||||
|
||||
|
||||
class OptimizerJobStatus(str, Enum):
|
||||
PENDING = "PENDING"
|
||||
RUNNING = "RUNNING"
|
||||
COMPLETED = "COMPLETED"
|
||||
FAILED = "FAILED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class OptimizerJob:
|
||||
"""Representa un trabajo asíncrono en la cola del optimizador heurístico."""
|
||||
|
||||
def __init__(self, job_id: str, params: Dict[str, Any]):
|
||||
self.job_id = job_id
|
||||
self.status = OptimizerJobStatus.PENDING
|
||||
self.progress = 0
|
||||
self.generation = 0
|
||||
self.total_generations = params.get("generations", 80)
|
||||
self.fitness_score = 0.0
|
||||
self.total_requests = 0
|
||||
self.assigned_reservations = 0
|
||||
self.reservations: List[Dict[str, Any]] = []
|
||||
self.conflicts: List[Dict[str, Any]] = []
|
||||
self.error: Optional[str] = None
|
||||
self.created_at = datetime.utcnow()
|
||||
self.started_at: Optional[datetime] = None
|
||||
self.completed_at: Optional[datetime] = None
|
||||
self.execution_time_ms: Optional[float] = None
|
||||
self.params = params
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"job_id": self.job_id,
|
||||
"status": self.status.value,
|
||||
"progress": self.progress,
|
||||
"generation": self.generation,
|
||||
"total_generations": self.total_generations,
|
||||
"fitness_score": round(self.fitness_score, 2),
|
||||
"total_requests": self.total_requests,
|
||||
"assigned_reservations": self.assigned_reservations,
|
||||
"reservations": self.reservations,
|
||||
"conflict_count": len(self.conflicts),
|
||||
"conflicts": self.conflicts,
|
||||
"error": self.error,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
||||
"execution_time_ms": self.execution_time_ms,
|
||||
"params": {k: v for k, v in self.params.items() if k not in ("app",)}
|
||||
}
|
||||
|
||||
|
||||
class OptimizerService:
|
||||
"""
|
||||
Servicio de alto rendimiento para el Optimizador Heurístico y Cola de Trabajos Asíncronos.
|
||||
Soporta evaluación concurrente multi-hilo y sincronización interoperable con Spring Boot.
|
||||
"""
|
||||
|
||||
_jobs: Dict[str, OptimizerJob] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def submit_job(
|
||||
cls,
|
||||
commission_ids: List[int],
|
||||
admin_user_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
generations: int = 80,
|
||||
population_size: int = 40,
|
||||
workers: int = 4,
|
||||
app = None
|
||||
) -> OptimizerJob:
|
||||
"""
|
||||
Registra y despacha un trabajo de optimización en segundo plano (asíncrono).
|
||||
"""
|
||||
job_id = str(uuid.uuid4())
|
||||
params = {
|
||||
"commission_ids": commission_ids,
|
||||
"admin_user_id": admin_user_id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"generations": generations,
|
||||
"population_size": population_size,
|
||||
"workers": workers
|
||||
}
|
||||
|
||||
job = OptimizerJob(job_id, params)
|
||||
with cls._lock:
|
||||
cls._jobs[job_id] = job
|
||||
|
||||
# Si no se pasó app explícitamente, intentar obtener current_app
|
||||
if app is None:
|
||||
try:
|
||||
app = current_app._get_current_object()
|
||||
except Exception:
|
||||
app = None
|
||||
|
||||
thread = threading.Thread(
|
||||
target=cls._run_job_worker,
|
||||
args=(job_id, app),
|
||||
daemon=True,
|
||||
name=f"optimizer-worker-{job_id[:8]}"
|
||||
)
|
||||
thread.start()
|
||||
return job
|
||||
|
||||
@classmethod
|
||||
def _run_job_worker(cls, job_id: str, app):
|
||||
"""Worker en segundo plano para ejecutar el algoritmo genético."""
|
||||
job = cls.get_job(job_id)
|
||||
if not job:
|
||||
return
|
||||
|
||||
def _execute():
|
||||
t_start = time.perf_counter()
|
||||
with cls._lock:
|
||||
job.status = OptimizerJobStatus.RUNNING
|
||||
job.started_at = datetime.utcnow()
|
||||
|
||||
def progress_callback(gen: int, total_gen: int, pct: int, best_fit: float):
|
||||
with cls._lock:
|
||||
job.generation = gen
|
||||
job.total_generations = total_gen
|
||||
job.progress = pct
|
||||
job.fitness_score = best_fit
|
||||
|
||||
optimizer = ReservationOptimizer(
|
||||
population_size=job.params.get("population_size", 40),
|
||||
generations=job.params.get("generations", 80),
|
||||
workers=job.params.get("workers", 4)
|
||||
)
|
||||
|
||||
result = optimizer.optimize_schedule(
|
||||
commission_ids=job.params["commission_ids"],
|
||||
admin_user_id=job.params["admin_user_id"],
|
||||
start_date=job.params.get("start_date"),
|
||||
end_date=job.params.get("end_date"),
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
|
||||
t_end = time.perf_counter()
|
||||
exec_time_ms = round((t_end - t_start) * 1000, 2)
|
||||
|
||||
with cls._lock:
|
||||
job.status = OptimizerJobStatus.COMPLETED
|
||||
job.progress = 100
|
||||
job.fitness_score = result.get("fitness_score", 0.0)
|
||||
job.total_requests = result.get("total_requests", 0)
|
||||
job.assigned_reservations = result.get("assigned_reservations", 0)
|
||||
job.reservations = result.get("reservations", [])
|
||||
job.conflicts = result.get("conflicts", [])
|
||||
job.completed_at = datetime.utcnow()
|
||||
job.execution_time_ms = exec_time_ms
|
||||
|
||||
try:
|
||||
if app:
|
||||
with app.app_context():
|
||||
_execute()
|
||||
else:
|
||||
_execute()
|
||||
except Exception as ex:
|
||||
with cls._lock:
|
||||
job.status = OptimizerJobStatus.FAILED
|
||||
job.error = str(ex)
|
||||
job.completed_at = datetime.utcnow()
|
||||
|
||||
@classmethod
|
||||
def get_job(cls, job_id: str) -> Optional[OptimizerJob]:
|
||||
"""Obtiene un trabajo por su identificador único."""
|
||||
with cls._lock:
|
||||
return cls._jobs.get(job_id)
|
||||
|
||||
@classmethod
|
||||
def list_jobs(cls, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""Lista los trabajos más recientes ordenados por fecha de creación desc."""
|
||||
with cls._lock:
|
||||
sorted_jobs = sorted(cls._jobs.values(), key=lambda j: j.created_at, reverse=True)
|
||||
return [j.to_dict() for j in sorted_jobs[:limit]]
|
||||
|
||||
@classmethod
|
||||
def sync_with_spring_boot(
|
||||
cls,
|
||||
payload: SpringBootSyncPayloadDTO,
|
||||
admin_user_id: int = 1
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Procesa una carga de optimización interoperable proveniente de microservicios Spring Boot.
|
||||
Garantiza respuesta conforme al contrato JSON del microservicio.
|
||||
"""
|
||||
t_start = time.perf_counter()
|
||||
|
||||
# 1. Resolver aulas disponibles
|
||||
if payload.classrooms and len(payload.classrooms) > 0:
|
||||
classrooms = []
|
||||
for c_dto in payload.classrooms:
|
||||
room = Classroom(
|
||||
room_number=c_dto.roomNumber,
|
||||
capacity=c_dto.capacity,
|
||||
is_virtual=c_dto.isVirtual,
|
||||
is_active=True
|
||||
)
|
||||
room.id = c_dto.classroomId
|
||||
classrooms.append(room)
|
||||
else:
|
||||
classrooms = Classroom.query.filter_by(is_active=True).all()
|
||||
|
||||
# 2. Generar ReservationRequests desde el DTO de Spring Boot
|
||||
requests: List[ReservationRequest] = []
|
||||
for c in payload.commissions:
|
||||
req = ReservationRequest(
|
||||
commission_id=c.commissionId,
|
||||
expected_attendees=c.expectedAttendees,
|
||||
purpose=f"Spring Boot Class: {c.subjectCode} - {c.subjectName}",
|
||||
preferred_start_time=c.preferredStart,
|
||||
preferred_end_time=c.preferredEnd,
|
||||
priority=1,
|
||||
flexibility_hours=2
|
||||
)
|
||||
requests.append(req)
|
||||
|
||||
# 3. Opciones de configuración de algoritmo
|
||||
options = payload.options or {}
|
||||
generations = options.get("generations", 60)
|
||||
pop_size = options.get("populationSize", 30)
|
||||
workers = options.get("workers", 4)
|
||||
|
||||
ga = GeneticAlgorithm(
|
||||
population_size=pop_size,
|
||||
generations=generations,
|
||||
workers=workers
|
||||
)
|
||||
|
||||
start_date = min(c.preferredStart for c in payload.commissions)
|
||||
end_date = max(c.preferredEnd for c in payload.commissions)
|
||||
|
||||
best_individual = ga.optimize_reservations(requests, classrooms, start_date, end_date)
|
||||
|
||||
t_end = time.perf_counter()
|
||||
exec_ms = round((t_end - t_start) * 1000, 2)
|
||||
|
||||
scheduled_assignments = []
|
||||
for gene in best_individual.genes:
|
||||
scheduled_assignments.append({
|
||||
"commissionId": gene.commission_id,
|
||||
"classroomId": gene.classroom_id,
|
||||
"startTime": gene.start_time.isoformat() if gene.start_time else None,
|
||||
"endTime": gene.end_time.isoformat() if gene.end_time else None,
|
||||
"purpose": gene.purpose,
|
||||
"expectedAttendees": gene.expected_attendees,
|
||||
"status": "SCHEDULED"
|
||||
})
|
||||
|
||||
return {
|
||||
"requestId": payload.requestId,
|
||||
"status": "SUCCESS",
|
||||
"academicTerm": payload.academicTerm,
|
||||
"campusCode": payload.campusCode,
|
||||
"totalCommissions": len(payload.commissions),
|
||||
"scheduledAssignments": scheduled_assignments,
|
||||
"conflictCount": len(best_individual.conflicts),
|
||||
"fitnessScore": round(best_individual.fitness, 2),
|
||||
"executionTimeMs": exec_ms,
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z"
|
||||
}
|
||||
Reference in New Issue
Block a user