diff --git a/backend/api/main.py b/backend/api/main.py index 1edd7e5..d4c740d 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -13,6 +13,7 @@ from api.routers.docmf_validation import router as validation_router from api.routers.auth import router as auth_router from api.routers.history import router as history_router from api.routers.test_mongo import router as test_mongo_router +from api.routers.docit2mf_build import router as docit2mf_router @asynccontextmanager async def lifespan(app: FastAPI): @@ -38,4 +39,5 @@ app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") app.include_router(validation_router, prefix="/api/criteria/doc-mf") app.include_router(test_mongo_router, prefix="/api") app.include_router(auth_router, prefix="/api") -app.include_router(history_router, prefix="/api") \ No newline at end of file +app.include_router(history_router, prefix="/api") +app.include_router(docit2mf_router, prefix="/api") diff --git a/backend/api/models/docit2mf_models.py b/backend/api/models/docit2mf_models.py new file mode 100644 index 0000000..d8069cb --- /dev/null +++ b/backend/api/models/docit2mf_models.py @@ -0,0 +1,69 @@ +# models/docit2mf_models.py + +from pydantic import BaseModel, field_validator +from typing import List, Tuple, Union + + +BlankCardInput = Union[int, Tuple[int, int], List[int]] + + +class DoCIT2MFRequest(BaseModel): + term: str + core: Tuple[float, float] + support: Tuple[float, float] + + left_nodes_x: List[float] + left_blank_cards: List[BlankCardInput] + + right_nodes_x: List[float] + right_blank_cards: List[BlankCardInput] + + @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 validate_cards(cls, v): + for item in v: + # Caso 1: entero + if isinstance(item, int): + if item < 0: + raise ValueError("Las cartas no pueden ser negativas.") + # Caso 2: lista o tupla [min,max] + elif isinstance(item, (list, tuple)): + if len(item) != 2: + raise ValueError("Los intervalos deben ser [min, max].") + lo, hi = item + if lo < 0 or hi < 0: + raise ValueError("Las cartas no pueden ser negativas.") + if lo > hi: + raise ValueError("Debe cumplirse min <= max.") + else: + raise ValueError("Formato inválido para cartas blancas.") + return v + + +class DoCIT2MFMultiRequest(BaseModel): + levels: List[DoCIT2MFRequest] diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py index f96e2e9..8239d45 100644 --- a/backend/api/models/docmf_models.py +++ b/backend/api/models/docmf_models.py @@ -45,3 +45,4 @@ class DoCMFRequest(BaseModel): class DoCMFMultiRequest(BaseModel): levels: List[DoCMFRequest] + diff --git a/backend/api/models/user_models.py b/backend/api/models/user_models.py index c611c6f..4b20fc0 100644 --- a/backend/api/models/user_models.py +++ b/backend/api/models/user_models.py @@ -1,8 +1,12 @@ -from typing import List, Optional +from typing import List, Optional, Union from pydantic import BaseModel, EmailStr, Field from datetime import datetime +# ----------------------------- +# MODELOS DE FUNCIONES DIFUSAS +# ----------------------------- + class FuzzyTerm(BaseModel): term: str core: List[float] @@ -11,18 +15,32 @@ class FuzzyTerm(BaseModel): right_nodes: List[List[float]] +class IT2FuzzyTerm(BaseModel): + term: str + lower: FuzzyTerm + upper: FuzzyTerm + + +# ----------------------------- +# HISTORIAL +# ----------------------------- + class HistoryItem(BaseModel): id: Optional[str] = Field(default=None, alias="_id") name: str created_at: datetime - results: List[FuzzyTerm] + results: List[Union[FuzzyTerm, IT2FuzzyTerm]] class HistoryCreateRequest(BaseModel): name: str - results: List[FuzzyTerm] + results: List[Union[FuzzyTerm, IT2FuzzyTerm]] +# ----------------------------- +# USUARIOS +# ----------------------------- + class UserCreate(BaseModel): username: str email: EmailStr @@ -41,4 +59,3 @@ class UserInDB(BaseModel): password_hash: str token: Optional[str] = None history: List[HistoryItem] = [] - diff --git a/backend/api/routers/docit2mf_build.py b/backend/api/routers/docit2mf_build.py new file mode 100644 index 0000000..7b132b4 --- /dev/null +++ b/backend/api/routers/docit2mf_build.py @@ -0,0 +1,24 @@ +# routers/docit2mf_build.py + +from fastapi import APIRouter, Depends, HTTPException +from api.models.docit2mf_models import DoCIT2MFMultiRequest +from api.services.docit2mf_build_service import build_it2mf_from_level +from api.utils.security import get_current_user + +router = APIRouter(prefix="/criteria", tags=["criteria"]) + + +@router.post("/doc-it2mf/build") +async def build_doc_it2mf( + request: DoCIT2MFMultiRequest, + current_user: dict = Depends(get_current_user) +): + results = [] + + try: + for level in request.levels: + results.append(build_it2mf_from_level(level)) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + return {"levels": results} diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py index 43a260a..0110241 100644 --- a/backend/api/routers/history.py +++ b/backend/api/routers/history.py @@ -3,7 +3,7 @@ from datetime import datetime from bson import ObjectId from api.database.mongodb import users_collection -from api.models.user_models import FuzzyTerm, HistoryCreateRequest +from api.models.user_models import FuzzyTerm, IT2FuzzyTerm, HistoryCreateRequest from api.utils.security import get_current_user router = APIRouter(prefix="/history", tags=["history"]) @@ -21,7 +21,7 @@ async def add_history_item( "_id": history_item_id, "name": data.name, "created_at": datetime.utcnow(), - "results": [r.dict() for r in data.results], + "results": [r.dict() for r in data.results], # ahora soporta IT2MF } await users_collection.update_one( @@ -35,6 +35,7 @@ async def add_history_item( } + @router.delete("/delete/{history_item_id}") async def delete_history_item( history_item_id: str, diff --git a/backend/api/services/docit2mf_build_service.py b/backend/api/services/docit2mf_build_service.py new file mode 100644 index 0000000..a18d5d8 --- /dev/null +++ b/backend/api/services/docit2mf_build_service.py @@ -0,0 +1,61 @@ +# services/docit2mf_build_service.py + +from typing import List, Union +from api.models.docit2mf_models import DoCIT2MFRequest +from api.models.docmf_models import DoCMFRequest +from api.services.docmf_build_service import build_doc_mf_level + + +def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> List[int]: + """ + Devuelve una lista de enteros: + - Si el valor es un entero → se usa tal cual para LMF y UMF + - Si es un intervalo [min,max] → se usa min o max según mode + """ + result = [] + for item in values: + if isinstance(item, int): + # valor fijo → mismo para LMF y UMF + result.append(item) + else: + lo, hi = item + result.append(lo if mode == "min" else hi) + return result + + +def build_it2mf_from_level(level: DoCIT2MFRequest): + # LMF (mínimos) + left_min = _extract_bounds(level.left_blank_cards, "min") + right_min = _extract_bounds(level.right_blank_cards, "min") + + lower_level = DoCMFRequest( + term=level.term, + core=level.core, + support=level.support, + left_nodes_x=level.left_nodes_x, + left_blank_cards=left_min, + right_nodes_x=level.right_nodes_x, + right_blank_cards=right_min, + ) + lower = build_doc_mf_level(lower_level) + + # UMF (máximos) + left_max = _extract_bounds(level.left_blank_cards, "max") + right_max = _extract_bounds(level.right_blank_cards, "max") + + upper_level = DoCMFRequest( + term=level.term, + core=level.core, + support=level.support, + left_nodes_x=level.left_nodes_x, + left_blank_cards=left_max, + right_nodes_x=level.right_nodes_x, + right_blank_cards=right_max, + ) + upper = build_doc_mf_level(upper_level) + + return { + "term": level.term, + "lower": lower, + "upper": upper + } diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py index 7c88660..0d4c406 100644 --- a/backend/api/services/docmf_build_service.py +++ b/backend/api/services/docmf_build_service.py @@ -1,4 +1,10 @@ -def build_single_docmf(request): +# services/docmf_build_service.py + +from api.models.docmf_models import DoCMFRequest +from api.models.user_models import FuzzyTerm + + +def build_single_docmf(request: DoCMFRequest): a, b = request.core c, d = request.support @@ -12,7 +18,7 @@ def build_single_docmf(request): if i == 0: left_nodes.append((x, 0.0)) else: - acc += request.left_blank_cards[i-1] + 1 + acc += request.left_blank_cards[i - 1] + 1 left_nodes.append((x, round(acc * YL, 4))) # RIGHT @@ -25,7 +31,7 @@ def build_single_docmf(request): if i == 0: right_nodes.append((x, 1.0)) else: - acc += request.right_blank_cards[i-1] + 1 + acc += request.right_blank_cards[i - 1] + 1 right_nodes.append((x, round(1 - acc * YR, 4))) return { @@ -43,3 +49,19 @@ def build_docmf_multi(request): result = build_single_docmf(level) results.append(result) return {"results": results} + + +def build_doc_mf_level(level: DoCMFRequest) -> FuzzyTerm: + """ + Adaptador para reutilizar build_single_docmf con el modelo DoCMFRequest. + Devuelve un FuzzyTerm, que es lo que espera el sistema IT2MF. + """ + result = build_single_docmf(level) + + return FuzzyTerm( + term=result["term"], + core=list(result["core"]), + support=list(result["support"]), + left_nodes=[list(p) for p in result["left_nodes"]], + right_nodes=[list(p) for p in result["right_nodes"]], + )