Files
dpb/backend/app/api/v1/module_bre/statistics/controller.py
T

739 lines
30 KiB
Python
Raw Normal View History

"""育种统计 路由(一期 2 个最基础:ABLUP·EBV + 配合力 GCA/SCA"""
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_bre.statistics.schema import (
AnovaIn,
AnovaOut,
ClusterIn,
CombiningAbilityOut,
CorrelationIn,
CvFoldOut,
CvResultOut,
CvRunIn,
DataQualityIn,
DecisionPreviewIn,
DescribeStatsIn,
GblupRunIn,
GeneticCorrIn,
GeneticCorrOut,
GeneticGainIn,
GenotypingDatasetOut,
GwasQtlXEIn,
GwasResultOut,
GwasRunIn,
GwasSnpOut,
InbreedingDepressionIn,
KinshipIn,
MatingRecommendIn,
MabcIn,
MasPanelIn,
MasPanelOut,
MasPanelSetMarkersIn,
OcsIn,
PredictionOut,
PredictionValueOut,
QtlIn,
QtlOut,
RunStatsIn,
SelectionIndexApplyIn,
SelectionIndexIn,
SelectionIndexOut,
StabilityOut,
StabilityRunIn,
StatisticsJobOut,
TrialDesignIn,
TypeBIn,
TypeBOut,
)
from app.api.v1.module_bre.statistics.service import StatisticsService
from app.core.base_schema import AuthSchema
from app.core.router_class import OperationLogRoute
from app.core.dependencies import db_getter
from app.core.dependencies import AuthPermission
from app.common.response import SuccessResponse
StatisticsRouter = APIRouter(route_class=OperationLogRoute, prefix="/statistics")
@StatisticsRouter.post("/ablup/run", summary="运行 ABLUP/EBV 估计")
async def run_ablup(
data: RunStatsIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_ablup(
data.trait_id, data.trait_code, data.year, data.fixed_effects, data.covariate,
data.data_gate, data.min_clone_n, data.min_pedigree_rate, data.max_missing_rate,
data.gxe, data.gxe_env, data.stage, data.gxr, data.spatial, data.spatial_aniso,
data.block,
))
@StatisticsRouter.post("/combining/run", summary="运行配合力 GCA/SCA 分析")
async def run_combining(
data: RunStatsIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(
await StatisticsService(auth, db).run_combining(
data.trait_id, data.trait_code, data.year, data.design_type,
)
)
@StatisticsRouter.get("/predictions", summary="育种值模型列表")
async def list_predictions(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_predictions()
return SuccessResponse([PredictionOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/predictions/{prediction_id}/values", summary="EBV 排行")
async def ebv_ranking(
prediction_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).ebv_ranking(prediction_id)
return SuccessResponse([PredictionValueOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/predictions/{prediction_id}/clones", summary="无性系级 EBV 排行")
async def clone_ranking(
prediction_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
result = await StatisticsService(auth, db).clone_ranking(prediction_id)
return SuccessResponse(result)
@StatisticsRouter.get("/combining", summary="配合力结果列表")
async def list_combining(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_combining()
return SuccessResponse([CombiningAbilityOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/combining/{ca_id}", summary="配合力结果详情(GCA/SCA)")
async def combining_detail(
ca_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).combining_detail(ca_id)
return SuccessResponse(CombiningAbilityOut.model_validate(obj))
@StatisticsRouter.get("/jobs", summary="统计任务列表")
async def list_jobs(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_jobs()
return SuccessResponse([StatisticsJobOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/jobs/{job_id}", summary="统计任务状态")
async def job_status(
job_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).job_status(job_id)
return SuccessResponse(StatisticsJobOut.model_validate(obj))
# ---------- k-fold 交叉验证(外部验证) ----------
@StatisticsRouter.post("/cv/run", summary="运行 k-fold 交叉验证(外部验证预测准确度)")
async def run_cv(
data: CvRunIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_cv(
data.trait_id, data.trait_code, data.year, data.fixed_effects, data.covariate,
data.gxe, data.gxe_env, data.k,
data.dataset_id, data.method, data.maf_min,
data.split, data.seed,
))
@StatisticsRouter.get("/cv", summary="k-fold CV 结果批次列表")
async def list_cv(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_cv()
return SuccessResponse([CvResultOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/cv/{cv_id}", summary="k-fold CV 结果详情(含折明细)")
async def cv_detail(
cv_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
result = await StatisticsService(auth, db).cv_detail(cv_id)
return SuccessResponse({
"result": CvResultOut.model_validate(result["result"]),
"folds": [CvFoldOut.model_validate(f) for f in result["folds"]],
})
# ---------- 纯 Python 统计分析(不依赖 R ----------
@StatisticsRouter.get("/describe", summary="描述性统计(无需R)")
async def describe_stats(
trait_codes: list[str] | None = Query(None),
group_by: str = Query("none"),
year: int | None = Query(None),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).describe_stats(trait_codes, group_by, year))
@StatisticsRouter.get("/correlation", summary="性状相关性矩阵(无需Rmode=pheno表型/genetic遗传)")
async def correlation(
trait_codes: list[str] | None = Query(None),
mode: str = Query("pheno", description="pheno=皮尔逊表型相关;genetic=成对双性状BLUP遗传相关"),
g_method: str = Query("mtblup", description="genetic 模式遗传相关来源:mtblup / calo(缺批次回退)"),
year: int | None = Query(None, description="年份过滤(仅 genetic 模式生效)"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).correlation_matrix(
trait_codes, mode, g_method, year))
@StatisticsRouter.post("/selection-index", summary="选择指数排名(EBV或表型加权,落库)")
async def selection_index(
data: SelectionIndexIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).selection_index(
data.weights, data.year, data.top_n, data.batch_ids, data.use_h2, data.method, data.aggregate,
data.stage, data.g_method, data.auto_weights,
data.restricted_traits, data.economic_weights,
))
@StatisticsRouter.get("/selection-index", summary="选择指数批次列表")
async def list_index_batches(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_index_batches()
return SuccessResponse([SelectionIndexOut.model_validate(o) for o in objs])
@StatisticsRouter.post("/selection-index/{index_id}/apply", summary="选择指数前N名写入决选")
async def apply_selection(
index_id: int,
data: SelectionIndexApplyIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(
await StatisticsService(auth, db).apply_selection(
index_id, data.top_n, data.selection_year, data.rule_id, data.min_reliability)
)
@StatisticsRouter.post("/kinship", summary="亲缘/近交分析(复用系谱A矩阵)")
async def kinship(
data: KinshipIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).kinship_matrix(data.threshold, data.tree_ids))
@StatisticsRouter.post("/inbreeding-depression", summary="近交衰退分析(F→表型线性回归,纯计算)")
async def inbreeding_depression(
data: InbreedingDepressionIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).inbreeding_depression(
data.trait_id, data.trait_code, data.year, data.trial_study_id, data.min_n))
@StatisticsRouter.post("/data-quality", summary="数据质量/异常值诊断(IQR+MAD稳健z)")
async def data_quality(
data: DataQualityIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).data_quality_report(
data.trait_id, data.trait_code, data.year, data.trial_study_id, data.dataset_id))
@StatisticsRouter.post("/genetic-gain", summary="ΔG 遗传增益投影(截断选择强度×PA×σ_A)")
async def genetic_gain(
data: GeneticGainIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).genetic_gain(
data.trait_id, data.trait_code, data.prediction_id,
data.top_p, data.top_n, data.generation_interval))
@StatisticsRouter.post("/mating-recommend", summary="主动选配推荐(EBV互补−近交惩罚,S-等位硬过滤)")
async def mating_recommend(
data: MatingRecommendIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).mating_recommend(
data.candidate_germplasm_ids, data.prediction_id, data.kinship_threshold,
data.w_ebv, data.w_kin, data.max_pairs))
@StatisticsRouter.post("/ocs", summary="最优贡献选择 OCS(群体配种贡献优化,纯计算)")
async def ocs(
data: OcsIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).ocs(
data.candidate_germplasm_ids, data.n_select, data.lam, data.prediction_id))
@StatisticsRouter.post("/mabc-progress", summary="MABC 标记辅助回交进度(前景MAS+背景恢复率+回交代建议,纯计算)")
async def mabc_progress(
data: MabcIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).mabc_progress(
data.candidate_tree_ids, data.foreground_panel_ids, data.background_panel_ids,
data.recurrent_parent_tree_id, data.foreground_min_hits, data.generation,
data.background_target))
@StatisticsRouter.post("/trial-design", summary="试验设计生成:RCBD/增广/α-格子(落库 block_no)")
async def trial_design(
data: TrialDesignIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).trial_design(
data.trial_study_id, data.design_type, data.seed,
data.check_germplasm_ids, data.block_size, data.reps))
@StatisticsRouter.post("/anova/run", summary="运行 ANOVA / 广义遗传力 H²")
async def run_anova(
data: AnovaIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_anova(
data.trait_id, data.trait_code, data.year, data.block))
@StatisticsRouter.get("/anova", summary="ANOVA 结果列表")
async def list_anova(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_anova()
return SuccessResponse([AnovaOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/anova/{anova_id}", summary="ANOVA 结果详情")
async def anova_detail(
anova_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).anova_detail(anova_id)
return SuccessResponse(AnovaOut.model_validate(obj))
@StatisticsRouter.get("/trait-values", summary="统一性状值视图(长表)")
async def trait_values(
trait_codes: list[str] | None = Query(None),
group_by: str = Query("none"),
year: int | None = Query(None),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).trait_values(trait_codes, group_by, year))
@StatisticsRouter.get("/fairness", summary="按site EBV偏差/公平性报告(只读)")
async def fairness_report(
prediction_id: int = Query(..., description="EBV 预测批次 id"),
group_by: str = Query("site", description="分组维度(当前仅支持 site"),
threshold_sd: float = Query(1.0, description="偏差标记阈值:|site均值-全体均值| > threshold_sd×全体sd 标记站点"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).fairness_report(
prediction_id, group_by, threshold_sd))
@StatisticsRouter.get("/predictions/{prediction_id}/compare", summary="模型健康监控(相邻两轮ABLUP对比)")
async def compare_predictions(
prediction_id: int,
prev_prediction_id: int | None = Query(None),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).compare_predictions(prediction_id, prev_prediction_id))
@StatisticsRouter.post("/decision-preview", summary="选择规则决策预览(表型+EBV)")
async def decision_preview(
data: DecisionPreviewIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(
auth, db).decision_preview(data.rule_ids, data.prediction_id, data.year, data.min_reliability,
data.stage))
@StatisticsRouter.get("/combination-funnel", summary="组合得失漏斗(花→果→种→苗→定植→树→入选,只读)")
async def combination_funnel(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).combination_funnel())
@StatisticsRouter.get("/traits", summary="统计可用性状下拉(只读bre_trait)")
async def list_traits(
core_only: bool = Query(True, description="仅返回核心性状(对应 tree_evaluation 列)"),
selection_only: bool = Query(False, description="仅返回选种目标性状(into_ebv=1,供指数/EBV排行/决策候选)"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).list_traits(core_only, selection_only))
# ---------- AMMI / Finlay-Wilkinson 稳定性(§8.18 ----------
@StatisticsRouter.post("/stability/run", summary="运行 AMMI/Finlay-Wilkinson 稳定性分析")
async def run_stability(
data: StabilityRunIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_stability(
data.trait_id, data.trait_code, data.gxe_env, data.year, tuple(data.methods),
))
@StatisticsRouter.get("/stability", summary="稳定性分析结果列表")
async def list_stability(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_stability()
return SuccessResponse([StabilityOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/stability/{sid}", summary="稳定性分析结果详情(AMMI/FW明细)")
async def stability_detail(
sid: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).stability_detail(sid)
return SuccessResponse(StabilityOut.model_validate(obj))
# ---------- 遗传相关矩阵(MT-BLUP 成对双性状,§8.18 ----------
@StatisticsRouter.post("/genetic-corr/run", summary="运行遗传相关矩阵(MT-BLUP成对双性状)")
async def run_genetic_corr(
data: GeneticCorrIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_genetic_corr(
data.trait_ids, data.year, data.full_mtblup))
@StatisticsRouter.get("/genetic-corr", summary="遗传相关结果列表")
async def list_genetic_corr(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_genetic_corr()
return SuccessResponse([GeneticCorrOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/genetic-corr/{cid}", summary="遗传相关结果详情")
async def genetic_corr_detail(
cid: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).genetic_corr_detail(cid)
return SuccessResponse(GeneticCorrOut.model_validate(obj))
# ---------- Type-B 多环境遗传相关 / UPGMA 聚类(§8.27 ----------
@StatisticsRouter.post("/type-b-heredity", summary="运行 Type-B 多环境遗传相关(环境当性状逐对BLUP,落库)")
async def run_type_b_heredity(
data: TypeBIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_type_b_heredity(
data.trait_id, data.trait_code, data.env_dim, data.method, data.year))
@StatisticsRouter.get("/type-b", summary="Type-B 多环境遗传相关结果列表")
async def list_type_b(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_type_b()
return SuccessResponse([TypeBOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/type-b/{rid}", summary="Type-B 多环境遗传相关结果详情")
async def type_b_detail(
rid: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).type_b_detail(rid)
return SuccessResponse(TypeBOut.model_validate(obj))
@StatisticsRouter.post("/cluster", summary="UPGMA 层次聚类(计算端点,不落库)")
async def run_cluster(
data: ClusterIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_cluster(
data.trait_ids, data.entity_type, data.mode, data.distance, data.k))
# ---------- GBLUP / ssGBLUP 基因组选择(§8.18 ----------
@StatisticsRouter.post("/gblup/run", summary="运行 GBLUP/ssGBLUP 基因组选择")
async def run_gblup(
data: GblupRunIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_gblup(
data.dataset_id, data.trait_id, data.trait_code, data.year, data.method, data.maf_min,
data.seed,
))
@StatisticsRouter.get("/gblup/datasets", summary="基因型数据集列表(含样本数)")
async def list_genotyping_datasets(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
datasets = await StatisticsService(auth, db).list_genotyping_datasets()
return SuccessResponse([GenotypingDatasetOut(**d) for d in datasets])
# ---------- MLOps 漂移检测 / 手动重训 / 版本回滚(§8.18 ----------
@StatisticsRouter.get("/model/drift", summary="模型漂移检测(输入快照哈希比对)")
async def model_drift(
prediction_id: int = Query(..., description="预测批次 id"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).model_drift(prediction_id))
@StatisticsRouter.post("/model/retrain", summary="手动重训(漂移才重跑run_ablupactive自动转移)")
async def model_retrain(
prediction_id: int = Query(..., description="预测批次 id"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).model_retrain(prediction_id))
@StatisticsRouter.post("/model/activate/{prediction_id}", summary="版本回滚(该批次设为当前生效版本)")
async def model_activate(
prediction_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).model_activate(prediction_id))
# ---------- GWAS / QTL / MAS 标记辅助选择(§8.22 ----------
@StatisticsRouter.post("/gwas/run", summary="运行 GWAS 关联分析(GLM+PC)")
async def run_gwas(
data: GwasRunIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).run_gwas(
data.dataset_id, data.trait_id, data.trait_code, data.year,
data.method, data.maf_min, data.n_pc, data.sig_level, data.qtl_window,
))
@StatisticsRouter.get("/gwas", summary="GWAS 批次列表")
async def list_gwas(
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).list_gwas()
return SuccessResponse([GwasResultOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/gwas/{gwas_id}", summary="GWAS 批次详情(Manhattan/QQ 数据 + QTL)")
async def gwas_detail(
gwas_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
result = await StatisticsService(auth, db).gwas_detail(gwas_id)
return SuccessResponse({
"result": GwasResultOut.model_validate(result["result"]),
"snps": [GwasSnpOut.model_validate(s) for s in result["snps"]],
"qtls": [QtlOut.model_validate(q) for q in result["qtls"]],
})
@StatisticsRouter.post("/gwas-qtl-x-e", summary="QTL×E 分层 GWAS + 稳定性判定(计算端点不建表)")
async def gwas_qtl_x_e(
data: GwasQtlXEIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).gwas_qtl_x_e(
data.dataset_id, data.trait_id, data.trait_code, data.year,
data.method, data.maf_min, data.n_pc, data.sig_level, data.qtl_window,
data.env_dim,
))
@StatisticsRouter.post("/qtl", summary="录入已知 QTL")
async def qtl_create(
data: QtlIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).qtl_create(data))
@StatisticsRouter.put("/qtl/{qtl_id}", summary="更新 QTL")
async def qtl_update(
qtl_id: int,
data: QtlIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).qtl_update(qtl_id, data)
return SuccessResponse(QtlOut.model_validate(obj))
@StatisticsRouter.delete("/qtl/delete", summary="删除 QTL")
async def qtl_delete(
ids: Annotated[list[int], Body(description="ID列表")],
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:delete"])),
db: AsyncSession = Depends(db_getter),
):
await StatisticsService(auth, db).qtl_delete(ids)
return SuccessResponse()
@StatisticsRouter.get("/qtl", summary="QTL 列表(可按性状)")
async def qtl_list(
trait_id: int | None = Query(None, description="按性状过滤"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).qtl_list(trait_id)
return SuccessResponse([QtlOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/qtl/{qtl_id}", summary="QTL 详情")
async def qtl_detail(
qtl_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).qtl_detail(qtl_id)
return SuccessResponse(QtlOut.model_validate(obj))
@StatisticsRouter.post("/mas-panel", summary="创建 MAS 标记辅助选择面板")
async def mas_panel_create(
data: MasPanelIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
return SuccessResponse(await StatisticsService(auth, db).mas_panel_create(data))
@StatisticsRouter.put("/mas-panel/{panel_id}", summary="更新 MAS 面板")
async def mas_panel_update(
panel_id: int,
data: MasPanelIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
obj = await StatisticsService(auth, db).mas_panel_update(panel_id, data)
return SuccessResponse(MasPanelOut.model_validate(obj))
@StatisticsRouter.delete("/mas-panel/delete", summary="删除 MAS 面板")
async def mas_panel_delete(
ids: Annotated[list[int], Body(description="ID列表")],
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:delete"])),
db: AsyncSession = Depends(db_getter),
):
await StatisticsService(auth, db).mas_panel_delete(ids)
return SuccessResponse()
@StatisticsRouter.get("/mas-panel", summary="MAS 面板列表(可按性状)")
async def mas_panel_list(
trait_id: int | None = Query(None, description="按性状过滤"),
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
objs = await StatisticsService(auth, db).mas_panel_list(trait_id)
return SuccessResponse([MasPanelOut.model_validate(o) for o in objs])
@StatisticsRouter.get("/mas-panel/{panel_id}", summary="MAS 面板详情(含标记)")
async def mas_panel_detail(
panel_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
db: AsyncSession = Depends(db_getter),
):
result = await StatisticsService(auth, db).mas_panel_detail(panel_id)
return SuccessResponse({
"panel": MasPanelOut.model_validate(result["panel"]),
"markers": [m for m in result["markers"]],
})
@StatisticsRouter.post("/mas-panel/{panel_id}/markers", summary="设置面板标记(全量替换)")
async def mas_panel_set_markers(
panel_id: int,
data: MasPanelSetMarkersIn,
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
db: AsyncSession = Depends(db_getter),
):
n = await StatisticsService(auth, db).mas_panel_set_markers(panel_id, data.markers)
return SuccessResponse({"panel_id": panel_id, "n_markers": n})