diff --git a/backend/api/main.py b/backend/api/main.py new file mode 100644 index 0000000..f7df1ae --- /dev/null +++ b/backend/api/main.py @@ -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") diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py new file mode 100644 index 0000000..a13b69c --- /dev/null +++ b/backend/api/models/docmf_models.py @@ -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 diff --git a/backend/api/models/docmf_simple_validation_models.py b/backend/api/models/docmf_simple_validation_models.py new file mode 100644 index 0000000..246e5d5 --- /dev/null +++ b/backend/api/models/docmf_simple_validation_models.py @@ -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] diff --git a/backend/api/models/docmf_validation_models.py b/backend/api/models/docmf_validation_models.py new file mode 100644 index 0000000..e62907d --- /dev/null +++ b/backend/api/models/docmf_validation_models.py @@ -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] diff --git a/backend/api/models/evaluation_models.py b/backend/api/models/evaluation_models.py new file mode 100644 index 0000000..ca18221 --- /dev/null +++ b/backend/api/models/evaluation_models.py @@ -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 diff --git a/backend/api/models/value_function_models.py b/backend/api/models/value_function_models.py new file mode 100644 index 0000000..66aaed5 --- /dev/null +++ b/backend/api/models/value_function_models.py @@ -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 diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py new file mode 100644 index 0000000..f3169b2 --- /dev/null +++ b/backend/api/routers/docmf_build.py @@ -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)) diff --git a/backend/api/routers/docmf_evaluate.py b/backend/api/routers/docmf_evaluate.py new file mode 100644 index 0000000..f2aae4a --- /dev/null +++ b/backend/api/routers/docmf_evaluate.py @@ -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)) diff --git a/backend/api/routers/docmf_simple_validation.py b/backend/api/routers/docmf_simple_validation.py new file mode 100644 index 0000000..800ec93 --- /dev/null +++ b/backend/api/routers/docmf_simple_validation.py @@ -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 + } diff --git a/backend/api/routers/docmf_validation.py b/backend/api/routers/docmf_validation.py new file mode 100644 index 0000000..a1bb518 --- /dev/null +++ b/backend/api/routers/docmf_validation.py @@ -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 + } diff --git a/backend/api/routers/value_function.py b/backend/api/routers/value_function.py new file mode 100644 index 0000000..c30022e --- /dev/null +++ b/backend/api/routers/value_function.py @@ -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)) diff --git a/backend/api/routes.py b/backend/api/routes.py deleted file mode 100644 index 10544bc..0000000 --- a/backend/api/routes.py +++ /dev/null @@ -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} diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py new file mode 100644 index 0000000..0cdc6eb --- /dev/null +++ b/backend/api/services/docmf_build_service.py @@ -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 + } diff --git a/backend/api/services/docmf_evaluate_service.py b/backend/api/services/docmf_evaluate_service.py new file mode 100644 index 0000000..37a3562 --- /dev/null +++ b/backend/api/services/docmf_evaluate_service.py @@ -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.") diff --git a/backend/api/services/docmf_simple_validation_service.py b/backend/api/services/docmf_simple_validation_service.py new file mode 100644 index 0000000..253f910 --- /dev/null +++ b/backend/api/services/docmf_simple_validation_service.py @@ -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 diff --git a/backend/api/services/docmf_validation_service.py b/backend/api/services/docmf_validation_service.py new file mode 100644 index 0000000..97ccfa5 --- /dev/null +++ b/backend/api/services/docmf_validation_service.py @@ -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 diff --git a/backend/api/services/value_function_service.py b/backend/api/services/value_function_service.py new file mode 100644 index 0000000..07039b0 --- /dev/null +++ b/backend/api/services/value_function_service.py @@ -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))]} diff --git a/backend/api/utils/interpolation.py b/backend/api/utils/interpolation.py new file mode 100644 index 0000000..ccfc35f --- /dev/null +++ b/backend/api/utils/interpolation.py @@ -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