Añadidos endpoints de validación, organización del backend en subcarpetas y archivos y añadido el control de errores http.

This commit is contained in:
Mireya Cueto Garrido
2026-03-25 13:42:09 +01:00
parent 9a8ddf5328
commit e79c6df2ba
18 changed files with 489 additions and 210 deletions
+25
View File
@@ -0,0 +1,25 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routers.value_function import router as value_router
from routers.docmf_build import router as docmf_build_router
from routers.docmf_evaluate import router as docmf_eval_router
from routers.docmf_simple_validation import router as simple_validation_router
from routers.docmf_validation import router as validation_router
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(value_router, prefix="/api/criteria/doc")
app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf")
app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf")
app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf")
app.include_router(validation_router, prefix="/api/criteria/doc-mf")
+43
View File
@@ -0,0 +1,43 @@
from pydantic import BaseModel, field_validator
from typing import List, Tuple
class DoCMFRequest(BaseModel):
term: str
core: Tuple[float, float]
support: Tuple[float, float]
left_nodes_x: List[float]
left_blank_cards: List[int]
right_nodes_x: List[float]
right_blank_cards: List[int]
@field_validator("term")
def term_not_empty(cls, v):
if not v.strip():
raise ValueError("El término no puede estar vacío.")
return v
@field_validator("core")
def core_valid(cls, v):
a, b = v
if a > b:
raise ValueError("El núcleo debe cumplir a <= b.")
return v
@field_validator("support")
def support_valid(cls, v, info):
c, d = v
if c >= d:
raise ValueError("El soporte debe cumplir c < d.")
core = info.data.get("core")
if core:
a, b = core
if not (c <= a < b <= d):
raise ValueError("El núcleo debe estar dentro del soporte.")
return v
@field_validator("left_blank_cards", "right_blank_cards")
def cards_valid(cls, v):
if any(c < 0 for c in v):
raise ValueError("Las cartas no pueden ser negativas.")
return v
@@ -0,0 +1,24 @@
from pydantic import BaseModel, field_validator
from typing import List, Tuple
class SimpleLevelDefinition(BaseModel):
core: Tuple[float, float]
support: Tuple[float, float]
@field_validator("core")
def validate_core(cls, v):
a, b = v
if a >= b:
raise ValueError("El núcleo debe cumplir a < b.")
return v
@field_validator("support")
def validate_support(cls, v):
c, d = v
if c >= d:
raise ValueError("El soporte debe cumplir c < d.")
return v
class SimpleValidationRequest(BaseModel):
levels: List[SimpleLevelDefinition]
@@ -0,0 +1,37 @@
from pydantic import BaseModel, field_validator
from typing import List, Tuple
class LevelDefinition(BaseModel):
core: Tuple[float, float]
support: Tuple[float, float]
left_nodes: List[Tuple[float, float]]
right_nodes: List[Tuple[float, float]]
@field_validator("core")
def validate_core(cls, v):
a, b = v
if a >= b:
raise ValueError("El núcleo debe cumplir a < b.")
return v
@field_validator("support")
def validate_support(cls, v):
c, d = v
if c >= d:
raise ValueError("El soporte debe cumplir c < d.")
return v
@field_validator("left_nodes", "right_nodes")
def validate_nodes(cls, v):
if len(v) < 2:
raise ValueError("Debe haber al menos 2 nodos.")
xs = [p[0] for p in v]
if xs != sorted(xs):
raise ValueError("Los nodos deben estar ordenados por x.")
if len(xs) != len(set(xs)):
raise ValueError("Los nodos no pueden tener valores x duplicados.")
return v
class ValidationRequest(BaseModel):
levels: List[LevelDefinition]
+35
View File
@@ -0,0 +1,35 @@
from pydantic import BaseModel, field_validator
from typing import List, Tuple
class EvaluationRequest(BaseModel):
x: float
core: Tuple[float, float]
support: Tuple[float, float]
left_nodes: List[Tuple[float, float]]
right_nodes: List[Tuple[float, float]]
@field_validator("core")
def core_valid(cls, v):
a, b = v
if a > b:
raise ValueError("El núcleo debe cumplir a <= b.")
return v
@field_validator("support")
def support_valid(cls, v, info):
c, d = v
if c >= d:
raise ValueError("El soporte debe cumplir c < d.")
core = info.data.get("core")
if core:
a, b = core
if not (c <= a < b <= d):
raise ValueError("El núcleo debe estar dentro del soporte.")
return v
@field_validator("left_nodes", "right_nodes")
def nodes_valid(cls, v):
if len(v) < 2:
raise ValueError("Debe haber al menos 2 nodos.")
return v
@@ -0,0 +1,36 @@
from pydantic import BaseModel, field_validator
from typing import List, Dict
class ValueFunctionRequest(BaseModel):
criterion_name: str
levels: List[str]
blank_cards: List[int]
references: Dict[str, float]
@field_validator("criterion_name")
def name_not_empty(cls, v):
if not v.strip():
raise ValueError("El nombre no puede estar vacío.")
return v
@field_validator("levels")
def levels_not_empty(cls, v):
if len(v) < 2:
raise ValueError("Debe haber al menos 2 niveles.")
return v
@field_validator("blank_cards")
def cards_valid(cls, v, info):
if any(c < 0 for c in v):
raise ValueError("Las cartas no pueden ser negativas.")
levels = info.data.get("levels")
if levels and len(v) != len(levels) - 1:
raise ValueError("Debe haber uno menos de número de cartas blancas que de niveles.")
return v
@field_validator("references")
def refs_valid(cls, v):
if len(v) != 2:
raise ValueError("Debe haber 2 referencias.")
return v
+12
View File
@@ -0,0 +1,12 @@
from fastapi import APIRouter, HTTPException
from models.docmf_models import DoCMFRequest
from services.docmf_build_service import build_docmf
router = APIRouter()
@router.post("/build")
def build(request: DoCMFRequest):
try:
return build_docmf(request)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
+12
View File
@@ -0,0 +1,12 @@
from fastapi import APIRouter, HTTPException
from models.evaluation_models import EvaluationRequest
from services.docmf_evaluate_service import evaluate_docmf
router = APIRouter()
@router.post("/evaluate")
def evaluate(request: EvaluationRequest):
try:
return evaluate_docmf(request)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -0,0 +1,38 @@
from fastapi import APIRouter, HTTPException
from models.docmf_simple_validation_models import SimpleValidationRequest
from services.docmf_simple_validation_service import validate_simple_levels
router = APIRouter()
@router.post("/validate-simple")
def validate_simple_docmf(request: SimpleValidationRequest):
results = validate_simple_levels(request.levels)
invalid = [r for r in results if not r["valid"]]
# Caso: un solo nivel
if len(request.levels) == 1:
if invalid:
raise HTTPException(
status_code=400,
detail={
"message": "El nivel es incorrecto.",
"errors": invalid[0]["errors"]
}
)
return {
"message": "El nivel es correcto.",
"details": results[0]
}
# Caso: varios niveles
if invalid:
return {
"message": "Validación completada.",
"valid_levels": [r for r in results if r["valid"]],
"invalid_levels": invalid
}
return {
"message": "Todos los niveles son correctos.",
"results": results
}
+39
View File
@@ -0,0 +1,39 @@
from fastapi import APIRouter, HTTPException
from models.docmf_validation_models import ValidationRequest
from services.docmf_validation_service import validate_levels
router = APIRouter()
@router.post("/validate")
def validate_docmf_levels(request: ValidationRequest):
results = validate_levels(request.levels)
invalid = [r for r in results if not r["valid"]]
if len(request.levels) == 1:
# Caso de un solo nivel
if invalid:
raise HTTPException(
status_code=400,
detail={
"message": "El nivel es incorrecto.",
"errors": invalid[0]["errors"]
}
)
return {
"message": "El nivel es correcto.",
"details": results[0]
}
# Caso de varios niveles
if invalid:
return {
"message": "Validación completada.",
"valid_levels": [r for r in results if r["valid"]],
"invalid_levels": invalid
}
return {
"message": "Todos los niveles son correctos.",
"results": results
}
+19
View File
@@ -0,0 +1,19 @@
from fastapi import APIRouter, HTTPException
from models.value_function_models import ValueFunctionRequest
from services.value_function_service import compute_value_function, compute_points
router = APIRouter()
@router.post("/value-function")
def value_function(request: ValueFunctionRequest):
try:
return compute_value_function(request)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/value-function/points")
def value_function_points(request: ValueFunctionRequest):
try:
return compute_points(request)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
-210
View File
@@ -1,210 +0,0 @@
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Dict, Tuple
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Configuración CORS para permitir peticiones desde React
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Permite cualquier origen (perfecto para desarrollo local)
allow_credentials=True,
allow_methods=["*"], # Permite POST, GET, OPTIONS, etc.
allow_headers=["*"], # Permite cualquier cabecera
)
# -----------------------------
# MODELOS
# -----------------------------
class ValueFunctionRequest(BaseModel):
criterion_name: str
levels: List[str]
blank_cards: List[int]
references: Dict[str, float]
class DoCMFRequest(BaseModel):
term: str
core: Tuple[float, float] # [a, b]
support: Tuple[float, float] # [c, d]
left_nodes_x: List[float]
left_blank_cards: List[int]
right_nodes_x: List[float]
right_blank_cards: List[int]
class EvaluationRequest(BaseModel):
x: float
left_nodes: List[Tuple[float, float]]
right_nodes: List[Tuple[float, float]]
core: Tuple[float, float]
support: Tuple[float, float]
# -----------------------------
# ENDPOINT 1: FUNCIÓN DE VALOR DoC
# -----------------------------
@app.post("/api/criteria/doc/value-function")
def calcular_value_function(request: ValueFunctionRequest):
levels = request.levels
cards = request.blank_cards
refs = request.references
ref_indices = sorted(int(k) for k in refs)
p, q = ref_indices
up, uq = refs[str(p)], refs[str(q)]
total_units = sum(cards[i] + 1 for i in range(p, q))
alpha = (uq - up) / total_units if total_units else 0
values = [0] * len(levels)
values[p] = up
for i in range(p + 1, len(levels)):
units = sum(cards[r] + 1 for r in range(p, i))
values[i] = up + alpha * units
for i in range(p - 1, -1, -1):
units = sum(cards[r] + 1 for r in range(i, p))
values[i] = up - alpha * units
return {
"criterion_name": request.criterion_name,
"values": {levels[i]: round(values[i], 4) for i in range(len(levels))}
}
# -----------------------------
# ENDPOINT 2: CONSTRUIR DoC-MF
# -----------------------------
@app.post("/api/criteria/doc-mf/build")
def build_doc_mf(request: DoCMFRequest):
a, b = request.core
c, d = request.support
# ---- LADO IZQUIERDO ----
left_x = request.left_nodes_x
left_e = request.left_blank_cards
TL = sum(e + 1 for e in left_e)
YL = 1 / TL if TL else 0
left_nodes = []
acc = 0
for i in range(len(left_x)):
if i == 0:
left_nodes.append((left_x[i], 0.0))
else:
acc += (left_e[i-1] + 1)
left_nodes.append((left_x[i], round(acc * YL, 4)))
# ---- LADO DERECHO ----
right_x = request.right_nodes_x
right_e = request.right_blank_cards
TR = sum(e + 1 for e in right_e)
YR = 1 / TR if TR else 0
right_nodes = []
acc = 0
for i in range(len(right_x)):
if i == 0:
right_nodes.append((right_x[i], 1.0))
else:
acc += (right_e[i-1] + 1)
right_nodes.append((right_x[i], round(1 - acc * YR, 4)))
return {
"term": request.term,
"core": request.core,
"support": request.support,
"left_nodes": left_nodes,
"right_nodes": right_nodes
}
# -----------------------------
# ENDPOINT 3: EVALUAR UN VALOR x
# -----------------------------
def linear_interpolation(x, nodes):
for i in range(len(nodes) - 1):
x0, y0 = nodes[i]
x1, y1 = nodes[i+1]
if x0 <= x <= x1:
t = (x - x0) / (x1 - x0)
return y0 + t * (y1 - y0)
return 0.0
@app.post("/api/criteria/doc-mf/evaluate")
def evaluate_doc_mf(request: EvaluationRequest):
x = request.x
a, b = request.core
c, d = request.support
if x < c or x > d:
return {
"membership": 0.0,
"explanation": "El valor está fuera del soporte, por eso la pertenencia es 0."
}
if a <= x <= b:
return {
"membership": 1.0,
"explanation": "El valor está dentro del núcleo, por eso la pertenencia es 1."
}
if c <= x < a:
mu = linear_interpolation(x, request.left_nodes)
return {
"membership": mu,
"explanation": "El valor está en el lado izquierdo y se interpola entre nodos."
}
if b < x <= d:
mu = linear_interpolation(x, request.right_nodes)
return {
"membership": mu,
"explanation": "El valor está en el lado derecho y se interpola entre nodos."
}
return {
"membership": 0.0,
"explanation": "No se pudo determinar la pertenencia."
}
# -----------------------------
# ENDPOINT 4: PUNTOS (x,y) DE LA FUNCIÓN DE VALOR
# -----------------------------
@app.post("/api/criteria/doc/value-function/points")
def value_function_points(request: ValueFunctionRequest):
levels = request.levels
cards = request.blank_cards
refs = request.references
ref_indices = sorted(int(k) for k in refs)
p, q = ref_indices
up, uq = refs[str(p)], refs[str(q)]
total_units = sum(cards[i] + 1 for i in range(p, q))
alpha = (uq - up) / total_units if total_units else 0
values = [0] * len(levels)
values[p] = up
for i in range(p + 1, len(levels)):
units = sum(cards[r] + 1 for r in range(p, i))
values[i] = up + alpha * units
for i in range(p - 1, -1, -1):
units = sum(cards[r] + 1 for r in range(i, p))
values[i] = up - alpha * units
points = [{"x": i, "y": round(values[i], 4)} for i in range(len(levels))]
return {"points": points}
@@ -0,0 +1,37 @@
def build_docmf(request):
a, b = request.core
c, d = request.support
# LEFT
TL = sum(e + 1 for e in request.left_blank_cards)
YL = 1 / TL
left_nodes = []
acc = 0
for i, x in enumerate(request.left_nodes_x):
if i == 0:
left_nodes.append((x, 0.0))
else:
acc += request.left_blank_cards[i-1] + 1
left_nodes.append((x, round(acc * YL, 4)))
# RIGHT
TR = sum(e + 1 for e in request.right_blank_cards)
YR = 1 / TR
right_nodes = []
acc = 0
for i, x in enumerate(request.right_nodes_x):
if i == 0:
right_nodes.append((x, 1.0))
else:
acc += request.right_blank_cards[i-1] + 1
right_nodes.append((x, round(1 - acc * YR, 4)))
return {
"term": request.term,
"core": request.core,
"support": request.support,
"left_nodes": left_nodes,
"right_nodes": right_nodes
}
@@ -0,0 +1,22 @@
from utils.interpolation import linear_interpolation
def evaluate_docmf(request):
x = request.x
a, b = request.core
c, d = request.support
if x < c or x > d:
return {"membership": 0.0, "explanation": "Fuera del soporte."}
if a <= x <= b:
return {"membership": 1.0, "explanation": "Dentro del núcleo."}
if c <= x < a:
mu = linear_interpolation(x, request.left_nodes)
return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.left_nodes} del lado izquierdo."}
if b < x <= d:
mu = linear_interpolation(x, request.right_nodes)
return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.right_nodes} del lado derecho."}
raise ValueError("No se pudo evaluar el valor.")
@@ -0,0 +1,25 @@
def validate_simple_level(level: dict):
errors = []
a, b = level["core"]
c, d = level["support"]
# Validación: núcleo dentro del soporte
if not (c <= a < b <= d):
errors.append("El núcleo debe estar completamente dentro del soporte.")
return errors
def validate_simple_levels(levels):
results = []
for idx, level in enumerate(levels):
errors = validate_simple_level(level.dict())
results.append({
"level_index": idx,
"valid": len(errors) == 0,
"errors": errors
})
return results
@@ -0,0 +1,39 @@
def validate_single_level(level: dict):
errors = []
a, b = level["core"]
c, d = level["support"]
# Core dentro del soporte
if not (c <= a < b <= d):
errors.append("El núcleo debe estar completamente dentro del soporte.")
# Nodos cubren correctamente el soporte
left = level["left_nodes"]
right = level["right_nodes"]
if left[0][0] != c:
errors.append("El primer nodo izquierdo debe coincidir con el inicio del soporte.")
if left[-1][0] != a:
errors.append("El último nodo izquierdo debe coincidir con el inicio del núcleo.")
if right[0][0] != b:
errors.append("El primer nodo derecho debe coincidir con el final del núcleo.")
if right[-1][0] != d:
errors.append("El último nodo derecho debe coincidir con el final del soporte.")
return errors
def validate_levels(levels):
results = []
for idx, level in enumerate(levels):
errors = validate_single_level(level.dict())
results.append({
"level_index": idx,
"valid": len(errors) == 0,
"errors": errors
})
return results
@@ -0,0 +1,38 @@
def compute_value_function(request):
levels = request.levels
cards = request.blank_cards
refs = request.references
p, q = sorted(int(k) for k in refs)
up, uq = refs[str(p)], refs[str(q)]
if len(levels) < 3:
raise ValueError("Mínimo debe haber 3 niveles para esta funcionalidad.")
total_units = sum(cards[i] + 1 for i in range(p, q))
if total_units == 0:
raise ValueError("Las cartas no pueden generar 0 unidades.")
alpha = (uq - up) / total_units
values = [0] * len(levels)
values[p] = up
for i in range(p + 1, len(levels)):
units = sum(cards[r] + 1 for r in range(p, i))
values[i] = up + alpha * units
for i in range(p - 1, -1, -1):
units = sum(cards[r] + 1 for r in range(i, p))
values[i] = up - alpha * units
return {
"criterion_name": request.criterion_name,
"values": {levels[i]: round(values[i], 4) for i in range(len(levels))}
}
def compute_points(request):
result = compute_value_function(request)
values = list(result["values"].values())
return {"points": [{"x": i, "y": values[i]} for i in range(len(values))]}
+8
View File
@@ -0,0 +1,8 @@
def linear_interpolation(x, nodes):
for i in range(len(nodes) - 1):
x0, y0 = nodes[i]
x1, y1 = nodes[i+1]
if x0 <= x <= x1:
t = (x - x0) / (x1 - x0)
return y0 + t * (y1 - y0)
return 0.0