"""育种统计 路由(一期 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="性状相关性矩阵(无需R;mode=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_ablup,active自动转移)") 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})