Añadida funcionalidad de intervalos en cartas blancas

This commit is contained in:
Mireya Cueto Garrido
2026-04-06 10:42:42 +02:00
parent 111acc632e
commit 5d3de4e27f
8 changed files with 207 additions and 10 deletions
+3 -1
View File
@@ -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.auth import router as auth_router
from api.routers.history import router as history_router from api.routers.history import router as history_router
from api.routers.test_mongo import router as test_mongo_router from api.routers.test_mongo import router as test_mongo_router
from api.routers.docit2mf_build import router as docit2mf_router
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): 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(validation_router, prefix="/api/criteria/doc-mf")
app.include_router(test_mongo_router, prefix="/api") app.include_router(test_mongo_router, prefix="/api")
app.include_router(auth_router, prefix="/api") app.include_router(auth_router, prefix="/api")
app.include_router(history_router, prefix="/api") app.include_router(history_router, prefix="/api")
app.include_router(docit2mf_router, prefix="/api")
+69
View File
@@ -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]
+1
View File
@@ -45,3 +45,4 @@ class DoCMFRequest(BaseModel):
class DoCMFMultiRequest(BaseModel): class DoCMFMultiRequest(BaseModel):
levels: List[DoCMFRequest] levels: List[DoCMFRequest]
+21 -4
View File
@@ -1,8 +1,12 @@
from typing import List, Optional from typing import List, Optional, Union
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field
from datetime import datetime from datetime import datetime
# -----------------------------
# MODELOS DE FUNCIONES DIFUSAS
# -----------------------------
class FuzzyTerm(BaseModel): class FuzzyTerm(BaseModel):
term: str term: str
core: List[float] core: List[float]
@@ -11,18 +15,32 @@ class FuzzyTerm(BaseModel):
right_nodes: List[List[float]] right_nodes: List[List[float]]
class IT2FuzzyTerm(BaseModel):
term: str
lower: FuzzyTerm
upper: FuzzyTerm
# -----------------------------
# HISTORIAL
# -----------------------------
class HistoryItem(BaseModel): class HistoryItem(BaseModel):
id: Optional[str] = Field(default=None, alias="_id") id: Optional[str] = Field(default=None, alias="_id")
name: str name: str
created_at: datetime created_at: datetime
results: List[FuzzyTerm] results: List[Union[FuzzyTerm, IT2FuzzyTerm]]
class HistoryCreateRequest(BaseModel): class HistoryCreateRequest(BaseModel):
name: str name: str
results: List[FuzzyTerm] results: List[Union[FuzzyTerm, IT2FuzzyTerm]]
# -----------------------------
# USUARIOS
# -----------------------------
class UserCreate(BaseModel): class UserCreate(BaseModel):
username: str username: str
email: EmailStr email: EmailStr
@@ -41,4 +59,3 @@ class UserInDB(BaseModel):
password_hash: str password_hash: str
token: Optional[str] = None token: Optional[str] = None
history: List[HistoryItem] = [] history: List[HistoryItem] = []
+24
View File
@@ -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}
+3 -2
View File
@@ -3,7 +3,7 @@ from datetime import datetime
from bson import ObjectId from bson import ObjectId
from api.database.mongodb import users_collection 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 from api.utils.security import get_current_user
router = APIRouter(prefix="/history", tags=["history"]) router = APIRouter(prefix="/history", tags=["history"])
@@ -21,7 +21,7 @@ async def add_history_item(
"_id": history_item_id, "_id": history_item_id,
"name": data.name, "name": data.name,
"created_at": datetime.utcnow(), "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( await users_collection.update_one(
@@ -35,6 +35,7 @@ async def add_history_item(
} }
@router.delete("/delete/{history_item_id}") @router.delete("/delete/{history_item_id}")
async def delete_history_item( async def delete_history_item(
history_item_id: str, history_item_id: str,
@@ -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
}
+25 -3
View File
@@ -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 a, b = request.core
c, d = request.support c, d = request.support
@@ -12,7 +18,7 @@ def build_single_docmf(request):
if i == 0: if i == 0:
left_nodes.append((x, 0.0)) left_nodes.append((x, 0.0))
else: 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))) left_nodes.append((x, round(acc * YL, 4)))
# RIGHT # RIGHT
@@ -25,7 +31,7 @@ def build_single_docmf(request):
if i == 0: if i == 0:
right_nodes.append((x, 1.0)) right_nodes.append((x, 1.0))
else: 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))) right_nodes.append((x, round(1 - acc * YR, 4)))
return { return {
@@ -43,3 +49,19 @@ def build_docmf_multi(request):
result = build_single_docmf(level) result = build_single_docmf(level)
results.append(result) results.append(result)
return {"results": results} 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"]],
)