Files
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

739 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""育种统计 路由(一期 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})