init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
@@ -0,0 +1 @@
"""育种统计模块(一期 2 个最基础:ABLUP·EBV + 配合力 GCA/SCA"""
@@ -0,0 +1,738 @@
"""育种统计 路由(一期 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})
@@ -0,0 +1,62 @@
"""育种统计 CRUD"""
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import (
AnovaResultModel,
CombiningAbilityModel,
GeneticCorrResultModel,
PredictionModel,
PredictionValueModel,
SelectionIndexModel,
StabilityResultModel,
StatisticsJobModel,
TypeBResultModel,
)
class PredictionCRUD(CRUDBase[PredictionModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(PredictionModel, auth, db)
class PredictionValueCRUD(CRUDBase[PredictionValueModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(PredictionValueModel, auth, db)
class CombiningAbilityCRUD(CRUDBase[CombiningAbilityModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(CombiningAbilityModel, auth, db)
class StatisticsJobCRUD(CRUDBase[StatisticsJobModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(StatisticsJobModel, auth, db)
class SelectionIndexCRUD(CRUDBase[SelectionIndexModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(SelectionIndexModel, auth, db)
class AnovaResultCRUD(CRUDBase[AnovaResultModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(AnovaResultModel, auth, db)
class GeneticCorrResultCRUD(CRUDBase[GeneticCorrResultModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(GeneticCorrResultModel, auth, db)
class StabilityResultCRUD(CRUDBase[StabilityResultModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(StabilityResultModel, auth, db)
class TypeBResultCRUD(CRUDBase[TypeBResultModel, dict, dict]):
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(TypeBResultModel, auth, db)
@@ -0,0 +1,445 @@
"""育种统计 数据模型(一期 2 个最基础:ABLUP·EBV + 配合力 GCA/SCA"""
from datetime import datetime
from sqlalchemy import (
JSON,
Boolean,
Date,
DateTime,
Float,
ForeignKey,
Index,
Integer,
Numeric,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import MappedBase, ModelMixin, UserMixin
class PredictionModel(ModelMixin, UserMixin, MappedBase):
"""育种值模型/批次元数据(§3.12)。"""
__tablename__ = "bre_prediction"
model_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="模型名称")
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
method: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="方法(ABLUP/COMBINING)")
stage: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="测定阶段(juvenile幼龄/evaluation成株)")
accuracy: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="模型精度(批次PA=√均值可靠性)")
train_n: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="训练样本量")
heritability: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="遗传力 h²")
predict_date: Mapped[datetime | None] = mapped_column(Date, nullable=True, comment="预测日期")
data_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="数据版本(输入快照版本)")
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="输入快照SHA256(表型+系谱)")
engine_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="统计引擎版本")
is_active: Mapped[bool | None] = mapped_column(Boolean, nullable=True, default=False, comment="当前生效版本(同性状最近批次,供版本回滚)")
note: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_prediction_created_deleted", "created_time", "is_deleted"),
)
class PredictionValueModel(ModelMixin, UserMixin, MappedBase):
"""个体预测值(EBV 持久化,V1.1 建表)。"""
__tablename__ = "bre_prediction_value"
prediction_id: Mapped[int] = mapped_column(
Integer, ForeignKey("bre_prediction.id", ondelete="CASCADE"),
index=True, nullable=False, comment="关联模型"
)
germplasm_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_germplasm.id", ondelete="CASCADE"),
index=True, nullable=True, comment="种质(亲本级)"
)
tree_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_tree.id", ondelete="CASCADE"),
index=True, nullable=True, comment="单株(树级)"
)
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
predicted_value: Mapped[float | None] = mapped_column(Float, nullable=True, comment="预测值/EBV")
reliability: Mapped[float | None] = mapped_column(Float, nullable=True, comment="可靠性")
pa: Mapped[float | None] = mapped_column(Float, nullable=True, comment="预测准确度 PA=√可靠性")
rank: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="排名")
__table_args__ = (
Index("ix_bre_prediction_value_created_deleted", "created_time", "is_deleted"),
)
class CombiningAbilityModel(ModelMixin, UserMixin, MappedBase):
"""配合力分析结果(§3.12;GCA 按亲本、SCA 按组合)。"""
__tablename__ = "bre_combining_ability"
model_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="模型名称")
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="方法")
design_type: Mapped[str | None] = mapped_column(
String(32), nullable=True,
comment="分析采用交配设计(full_diallel/partial_diallel/line_tester/nciii,结果快照)",
)
gca_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="亲本一般配合力 {parent: gca}")
sca_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="组合特殊配合力 {combo: {p1,p2,sca}}")
anova_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="配合力方差分解(F_gca/p_gca)")
__table_args__ = (
Index("ix_bre_combining_ability_created_deleted", "created_time", "is_deleted"),
)
class SelectionIndexModel(ModelMixin, UserMixin, MappedBase):
"""选择指数 批次(加权 Z 综合;可选接入 ABLUP 批次 EBV 与 h² 加权,结果持久化)。"""
__tablename__ = "bre_selection_index"
model_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="批次名称")
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="SI_EBV/SI_PHENO")
weights_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{trait_code: 用户权重}")
batch_refs_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{trait_code: ABLUP批次id}")
heritability_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{trait_code: h²}")
top_n: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="返回前 N")
result_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{traits, top:[{tree_id,index,detail}]}")
__table_args__ = (
Index("ix_bre_selection_index_created_deleted", "created_time", "is_deleted"),
)
class AnovaResultModel(ModelMixin, UserMixin, MappedBase):
"""单因素 ANOVA 结果(家系方差组分 + 广义遗传力 H²)。"""
__tablename__ = "bre_anova_result"
model_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="批次名称")
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="ANOVA/H2")
result_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="方差组分/H²/F/p")
__table_args__ = (
Index("ix_bre_anova_result_created_deleted", "created_time", "is_deleted"),
)
class CvResultModel(ModelMixin, UserMixin, MappedBase):
"""k-fold 交叉验证结果(外部验证预测准确度,区别于 PEV 可靠性)。"""
__tablename__ = "bre_cv_result"
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
trait_code: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="性状编码")
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="KFCV/ABLUP 或 KFCV/GXE")
k: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="折数")
n_total: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="总记录数")
n_individuals: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="加性个体数")
mean_pearson: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="有效折预测-观测 pearson 均值")
mean_rmse: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="有效折 RMSE 均值")
pooled_pearson: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="合并全部折 pearson")
pooled_rmse: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="合并全部折 RMSE")
cv_accuracy: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="预测准确度(=mean_pearson)")
h2: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="有效折遗传力均值")
folds_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="折明细快照")
data_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="数据版本")
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="输入快照SHA256")
engine_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="统计引擎版本")
note: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_cv_result_created_deleted", "created_time", "is_deleted"),
)
class CvFoldModel(ModelMixin, UserMixin, MappedBase):
"""k-fold CV 折明细。"""
__tablename__ = "bre_cv_fold"
cv_result_id: Mapped[int] = mapped_column(
Integer, ForeignKey("bre_cv_result.id", ondelete="CASCADE"),
index=True, nullable=False, comment="关联 CV 结果"
)
fold_idx: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="折号(1-based)")
n_train: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="训练记录数")
n_test: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="测试个体数")
n_eval: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="有效评估个体数")
pearson: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="折内预测-观测 pearson")
rmse: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="折内 RMSE")
error: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="折失败原因")
__table_args__ = (
Index("ix_bre_cv_fold_created_deleted", "created_time", "is_deleted"),
)
class StatisticsJobModel(ModelMixin, UserMixin, MappedBase):
"""统计异步任务(R 引擎;ABLUP/COMBINING 提交后异步执行并回写结果)。"""
__tablename__ = "bre_statistics_job"
job_type: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="ABLUP/COMBINING")
params_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="入参")
status: Mapped[str] = mapped_column(String(16), nullable=False, default="PENDING", comment="PENDING/RUNNING/SUCCESS/FAILED")
result_ref: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="结果主表 id")
error_msg: Mapped[str | None] = mapped_column(Text, nullable=True, comment="错误信息")
started_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
finished_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
__table_args__ = (
Index("ix_bre_statistics_job_created_deleted", "created_time", "is_deleted"),
)
class GeneticCorrResultModel(ModelMixin, UserMixin, MappedBase):
"""成对双性状 BLUP 遗传相关矩阵结果(MT-BLUP,§8.18)。
逐对 bivariate 估计 σ²a1/σ²a2/σ²a12(固定单性状方差、仅对遗传相关 ρ 作
1-D 剖面 REML golden-max),输出遗传相关矩阵 r_g 供 Smith-Hazel 指数
G 矩阵非对角替换 Calo 近似。
"""
__tablename__ = "bre_genetic_corr_result"
trait_ids_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{trait_code: trait_id}")
matrix_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="遗传相关矩阵 {code: {code: r_g}}")
sigma_a_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="加性方差矩阵 {code: {code: σa}}")
heritability_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{code: h²}")
pairs_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="逐对详情 {pair: {r_g, va1, va2, ve1, ve2, n_common, converged, warning}}")
n_common: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="成对共有树数(最小)")
data_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="数据版本")
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="输入快照SHA256(表型+系谱)")
engine_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="统计引擎版本")
note: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_genetic_corr_result_created_deleted", "created_time", "is_deleted"),
)
class TypeBResultModel(ModelMixin, UserMixin, MappedBase):
"""Type-B 多环境遗传相关结果(§8.27)。
同一性状在多个环境(site=研究点 / year=年份)的表型,以环境互为"性状"逐对
REML 双性状 BLUP 估计遗传相关 r_g(复用 mtblup.solve_bivariate);r_g 均值
即 Type-B 遗传相关,衡量基因型×环境互作强度与跨环境遗传稳定性。
"""
__tablename__ = "bre_type_b_result"
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
trait_code: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="性状编码")
env_dim: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="环境维度(site/year)")
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="方法(reml/calocalo 无环境级EBV时回退reml)")
envs_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="{env_code: env_label}")
matrix_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="环境间遗传相关矩阵 {env: {env: r_g}}")
pairs_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="逐对详情 {pair: {r_g, va1, va2, ve1, ve2, n_common, converged, warning}}")
n_common: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="成对共有树数(最小)")
data_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="数据版本")
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="输入快照SHA256")
engine_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="统计引擎版本")
note: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_type_b_result_created_deleted", "created_time", "is_deleted"),
)
class StabilityResultModel(ModelMixin, UserMixin, MappedBase):
"""AMMI / Finlay-Wilkinson 稳定性分析结果(§8.18)。
两因素(基因型×环境)均值表 → AMMI SVD(IPC1/IPC2/ASV/ecovalence) 与
Finlay-Wilkinson 回归斜率 b,输出稳定性排名。
"""
__tablename__ = "bre_stability_result"
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
trait_code: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="性状编码")
env_dim: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="环境维度(site/year)")
methods: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="方法(ammi,finlay)")
detail_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="稳定性分析明细快照")
data_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="数据版本")
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="输入快照SHA256")
engine_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="统计引擎版本")
note: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_stability_result_created_deleted", "created_time", "is_deleted"),
)
class GwasResultModel(ModelMixin, UserMixin, MappedBase):
"""GWAS 关联分析批次结果(§8.22)。
GLM+PC 逐标记单标记回归(纯 numpy + fdist),输出显著标记集与 QTL 区间。
"""
__tablename__ = "bre_gwas_result"
dataset_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_genotyping_dataset.id", ondelete="CASCADE"),
index=True, nullable=True, comment="基因型数据集"
)
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
trait_code: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="性状编码")
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="方法(gwas/ssgwas)")
n_individuals: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="样本数(表型∩基因型)")
n_markers: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="标记总数")
m_after_maf: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="MAF 过滤后标记数")
maf_min: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="MAF 过滤下限")
n_pc: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="群体结构主成分数")
sig_level: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="显著性水平")
threshold_bonf: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="Bonferroni 阈值")
n_sig_bonf: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="Bonferroni 显著标记数")
n_sig_fdr: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="FDR q<α 标记数")
n_qtl: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="QTL 区间数")
data_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="数据版本")
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="输入快照SHA256(表型+系谱+基因型)")
engine_version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="统计引擎版本")
remark: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_gwas_result_created_deleted", "created_time", "is_deleted"),
)
class GwasSnpModel(ModelMixin, UserMixin, MappedBase):
"""GWAS 逐标记关联结果(§8.22)。"""
__tablename__ = "bre_gwas_snp"
gwas_result_id: Mapped[int] = mapped_column(
Integer, ForeignKey("bre_gwas_result.id", ondelete="CASCADE"),
index=True, nullable=False, comment="关联 GWAS 批次"
)
marker_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_marker.id", ondelete="SET NULL"),
index=True, nullable=True, comment="标记"
)
marker_name: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="标记名称")
chromosome: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="染色体")
position: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="物理位置(bp)")
maf: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="次要等位频率")
effect: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="加性效应")
se: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="效应标准误")
t_value: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="t 值")
p_value: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="p 值")
neg_log10p: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="log10(p)")
q_value: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="BH-FDR q 值")
sig_bonf: Mapped[bool | None] = mapped_column(Boolean, nullable=True, comment="Bonferroni 显著")
sig_fdr: Mapped[bool | None] = mapped_column(Boolean, nullable=True, comment="FDR 显著")
__table_args__ = (
Index("ix_bre_gwas_snp_created_deleted", "created_time", "is_deleted"),
)
class QtlModel(ModelMixin, UserMixin, MappedBase):
"""QTL 区间(§8.22)。
source=known 为人工录入已知 QTL(桃成熟期/果重等位点);source=gwas 为 GWAS
显著标记聚类自动定位(gwas_result_id 关联批次)。
"""
__tablename__ = "bre_qtl"
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="性状"
)
chromosome: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="染色体")
start_bp: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="区间起点(bp)")
end_bp: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="区间终点(bp)")
peak_marker_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_marker.id", ondelete="SET NULL"),
index=True, nullable=True, comment="峰标记"
)
peak_marker_name: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="峰标记名称")
peak_p: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="峰标记 p 值")
n_markers: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="区间显著标记数")
effect: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="区间峰效应")
source: Mapped[str | None] = mapped_column(String(8), nullable=True, default="known", comment="来源(known/gwas)")
gwas_result_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_gwas_result.id", ondelete="CASCADE"),
index=True, nullable=True, comment="来源 GWAS 批次(source=gwas 时)"
)
remark: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_qtl_created_deleted", "created_time", "is_deleted"),
)
class MasPanelModel(ModelMixin, UserMixin, MappedBase):
"""MAS 标记辅助选择面板(§8.22)。
一组与目标性状关联的标记(QTL 峰标记),供决策预览按有利剂量命中童期幼苗。
"""
__tablename__ = "bre_mas_panel"
panel_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="面板名称")
trait_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_trait.id", ondelete="CASCADE"),
index=True, nullable=True, comment="目标性状"
)
n_markers: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="标记数")
remark: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注")
__table_args__ = (
Index("ix_bre_mas_panel_created_deleted", "created_time", "is_deleted"),
)
class MasPanelMarkerModel(ModelMixin, UserMixin, MappedBase):
"""MAS 面板标记(§8.22)。"""
__tablename__ = "bre_mas_panel_marker"
panel_id: Mapped[int] = mapped_column(
Integer, ForeignKey("bre_mas_panel.id", ondelete="CASCADE"),
index=True, nullable=False, comment="关联面板"
)
marker_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_marker.id", ondelete="SET NULL"),
index=True, nullable=True, comment="标记"
)
favorable_dose: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="有利剂量(0/1/2)")
effect: Mapped[float | None] = mapped_column(Numeric, nullable=True, comment="效应(绝对值参考)")
direction: Mapped[str | None] = mapped_column(String(8), nullable=True, default="high", comment="方向(high=剂量≥favorable_dose命中 / low=剂量≤命中)")
mode: Mapped[str | None] = mapped_column(
String(16), nullable=True, default="additive",
comment="基因作用模式:additive加性(剂量≥fav)/dominance显性(剂量≥1高/≤1低)/recessive隐性(剂量=2高/0低)/allele等位存在(有利等位)/haplotype单倍型(同组全命中)")
favorable_allele: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="有利等位(allele/haplotype 模式用,SSR 等位索引)")
haplotype_group: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="单倍型组(haplotype 模式:同组标记全命中才计 1)")
__table_args__ = (
Index("ix_bre_mas_panel_marker_created_deleted", "created_time", "is_deleted"),
)
@@ -0,0 +1,673 @@
# -*- coding: utf-8 -*-
"""育种统计 Schema(一期 2 个最基础:ABLUP·EBV + 配合力 GCA/SCA"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class RunStatsIn(BaseModel):
"""提交统计任务的入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码(bre_trait.trait_code,如 avg_fruit_weight")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
fixed_effects: list[str] | None = Field(
None, description="固定效应因子:支持 trial_study(试验/站点) / rootstock(砧木)"
)
covariate: str | None = Field(
None, description="协变量(BLUP 校正):crop_load(负载量) / competition(空间竞争:相邻株数)"
)
data_gate: bool = Field(
True, description="数据就绪门禁(G1):每 clone/家系最小 n、系谱完整率、缺失率任一不达标则拒绝运行"
)
min_clone_n: int = Field(
2, ge=0, description="每 clone/家系最小样本量(观测树数低于该值的组列入未达标)"
)
min_pedigree_rate: float = Field(
0.3, ge=0.0, le=1.0, description="系谱完整率下限(有父母或组合亲本的观测树占比)"
)
max_missing_rate: float = Field(
0.8, ge=0.0, le=1.0, description="表型缺失率上限(缺失=有评价记录但无该性状数值)"
)
gxe: bool = Field(
False, description="启用 G×E 交互随机效应(clone×site / 组合×siteyear 同理)"
)
gxe_env: str = Field(
"site", description="G×E 环境维度:site(trial_study_id) / year(evaluate_year)"
)
gxr: bool = Field(
False, description="启用砧木×接穗随机互作(G×R):rootstock 从固定效应移到随机效应,与 gxe 互斥"
)
spatial: bool = Field(
False,
description=(
"启用 AR1×AR1 空间协方差(残差 e~N(0,σ²e·R)R_ij=ρ^(|Δrow|+|Δcol|)):"
"需观测株均有 bre_tree.row_no/col_no;与 gxe/gxr 互斥"
),
)
spatial_aniso: bool = Field(
False,
description=(
"空间协方差各向异性双参数(需 spatial=True):R_ij=ρ_row^|Δrow|·ρ_col^|Δcol|"
"方法 AR1×AR1(aniso),输出 ρ_row/ρ_colFalse 时用单参 ρ(v1 兼容)"
),
)
block: bool = Field(
False,
description=(
"启用区组随机效应(不完全区组/增广/α-格子):bre_tree.block_no 作第二随机效应,"
"精度收益进入遗传评估;需 ≥2 个区组;与 gxe/gxr/spatial 互斥"
),
)
stage: str | None = Field(
None, description="发育阶段拆分:juvenile(童期)/evaluation(成株);给定则只取该阶段观测建模并拆独立批次"
)
design_type: str = Field(
"full_diallel",
description=(
"交配设计(配合力分析用):full_diallel=完全双列(Griffing) / "
"partial_diallel=部分双列 / line_tester=line×tester(NCII 两因素模型) / "
"nciii=NCIII 测交"
),
)
model_config = ConfigDict(extra="ignore")
class PredictionOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
model_name: str | None = None
trait_id: int | None = None
method: str | None = None
stage: str | None = None
accuracy: float | None = None
train_n: int | None = None
heritability: float | None = None
data_version: str | None = None
input_hash: str | None = None
engine_version: str | None = None
is_active: bool | None = None
note: str | None = None
created_time: datetime | None = None
class PredictionValueOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
prediction_id: int
germplasm_id: int | None = None
tree_id: int | None = None
trait_id: int | None = None
predicted_value: float | None = None
reliability: float | None = None
pa: float | None = None
rank: int | None = None
class CombiningAbilityOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
model_name: str | None = None
trait_id: int | None = None
method: str | None = None
design_type: str | None = None
gca_json: dict | None = None
sca_json: dict | None = None
anova_json: dict | None = None
class StatisticsJobOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
job_type: str | None = None
status: str
params_json: dict | None = None
result_ref: int | None = None
error_msg: str | None = None
started_time: datetime | None = None
finished_time: datetime | None = None
# ---------- 纯 Python 统计分析(不依赖 R ----------
class DescribeStatsIn(BaseModel):
"""描述性统计入参。"""
trait_codes: list[str] | None = Field(None, description="数值性状列名;为空则用全部数值列")
group_by: str = Field("none", description="分组维度:none/year/combination")
year: int | None = Field(None, description="评价年份过滤")
model_config = ConfigDict(extra="ignore")
class CorrelationIn(BaseModel):
"""相关分析入参。"""
trait_codes: list[str] | None = Field(None, description="参与相关的数值性状;至少2个")
mode: str = Field(
"pheno",
description="相关类型:pheno=表型相关(皮尔逊,纯Python);genetic=遗传相关(成对双性状BLUP REML)",
)
g_method: str = Field(
"mtblup",
description="遗传相关 r_g 来源(仅 mode=genetic):mtblup=成对双性状BLUP(REML精确)calo=可靠性校正EBV相关,无生效EBV批次时回退mtblup",
)
year: int | None = Field(None, description="评价年份过滤")
model_config = ConfigDict(extra="ignore")
class SelectionIndexIn(BaseModel):
"""选择指数入参。"""
weights: dict[str, float] | None = Field(None, description="性状->权重,缺省等权;自动归一化")
year: int | None = Field(None, description="评价年份过滤(仅表型回退路径使用)")
top_n: int = Field(50, description="返回排名前 N 的单株")
batch_ids: dict[str, int] | None = Field(None, description="性状->ABLUP批次id,指定则用该批次的EBV作为指数输入")
use_h2: bool = Field(True, description="有效权重 = 用户权重 × 遗传力(h²) 加权(仅 zsum 模式生效)")
method: str = Field(
"zsum",
description=(
"zsum=加权Z综合(轻量,可表型回退);"
"smith_hazel=真Smith-Hazel b=P⁻¹Ga(需各性状指定EBV批次且含h²);"
"restricted=约束指数 b=P⁻¹G(IM)aKempthorne-Nordskog,受限性状 ΔG=0,需 restricted_traits"
),
)
aggregate: str = Field(
"clone",
description=(
"聚合粒度:clone=无性系级(同系多株可靠加权聚合为一个遗传实体再排名,桃无性繁殖默认);"
"tree=单株级(旧行为)"
),
)
stage: str | None = Field(
None, description="发育阶段过滤:juvenile(童期)/evaluation(成株);不传且性状横跨两阶段时返回 stage_warning"
)
g_method: str = Field(
"calo", description="遗传相关 r_g 来源(smith_hazel / restricted 生效):calo=可靠性校正EBV相关;mtblup=成对双性状BLUP(REML精确估计,单对不收敛回退calo)"
)
auto_weights: bool = Field(
False,
description="自动权重:以各性状 default_h2(实测h²优先,兜底0.1)为权重;强制 use_h2=False 防双重相乘",
)
restricted_traits: list[str] | None = Field(
None, description="受限性状列表(仅 method=restricted 生效):这些性状的遗传增益被约束为 0,其余性状自由响应(Kempthorne-Nordskog 闭式投影)"
)
economic_weights: dict[str, float] | None = Field(
None, description="经济权重(经济价值,绝对尺度不归一化;smith_hazel / restricted 生效):直接作为聚合基因型系数 a,未列出性状按 0 处理"
)
model_config = ConfigDict(extra="ignore")
class SelectionIndexOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
model_name: str | None = None
method: str | None = None
weights_json: dict | None = None
batch_refs_json: dict | None = None
heritability_json: dict | None = None
top_n: int | None = None
result_json: dict | None = None
created_time: datetime | None = None
class SelectionIndexApplyIn(BaseModel):
"""选择指数 -> 决选 入参。"""
top_n: int = Field(50, description="写入前 N 名单株")
selection_year: int | None = Field(None, description="入选年份;为空用当年")
rule_id: int | None = Field(None, description="来源选择规则(决策预览选定,用于溯源 rule_id/from_stage/to_stage)")
min_reliability: float = Field(
0.2, ge=0.0, le=1.0,
description="EBV 可靠性硬门槛:指数引用批次的 EBV 可靠性低于该值的单株跳过(证据不足不入选)",
)
model_config = ConfigDict(extra="ignore")
class KinshipIn(BaseModel):
"""亲缘/近交分析入参。"""
threshold: float = Field(0.25, description="亲缘系数预警阈值(r>A 预警)")
tree_ids: list[int] | None = Field(None, description="限定单株范围;为空用全部活动单株")
model_config = ConfigDict(extra="ignore")
class DataQualityIn(BaseModel):
"""数据质量/异常值诊断入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
trial_study_id: int | None = Field(None, description="限定试验/站点(MET);为空则全部")
dataset_id: int | None = Field(
None, description="基因型数据集:提供时附加孟德尔检验段(最新批次计数+flag树+隔离状态)")
model_config = ConfigDict(extra="ignore")
class GeneticGainIn(BaseModel):
"""ΔG 遗传增益投影入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
prediction_id: int | None = Field(None, description="指定 EBV 批次;为空用该性状最新批次")
top_p: float | None = Field(None, gt=0, lt=1, description="入选比例(0-1,与 top_n 二选一)")
top_n: int | None = Field(None, ge=1, description="入选株数(与 top_p 二选一)")
generation_interval: float = Field(1.0, gt=0, description="世代间隔(年);年增益=ΔG/L")
model_config = ConfigDict(extra="ignore")
class InbreedingDepressionIn(BaseModel):
"""近交衰退分析入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
trial_study_id: int | None = Field(None, description="限定试验/站点(MET);为空则全部")
min_n: int = Field(10, ge=2, description="回归最小样本量(同时具备 F 与表型的树)")
model_config = ConfigDict(extra="ignore")
class TrialDesignIn(BaseModel):
"""试验设计生成入参(rcbd / augmented / alpha)。"""
trial_study_id: int = Field(..., description="试验研究点 idbre_trial_study")
design_type: str = Field("rcbd", description="设计类型:rcbd=随机完全区组、augmented=增广、alpha=α-格子")
seed: int | None = Field(None, description="随机种子;固定 seed 可复现同一设计")
check_germplasm_ids: list[int] = Field(default_factory=list,
description="增广设计对照种质 id 列表(每区组重复)")
block_size: int | None = Field(None, description="α-格子区组大小 k(每重复区组块大小)")
reps: int | None = Field(None, description="α-格子重复数 r(平方格子缺省 k+1)")
model_config = ConfigDict(extra="ignore")
class MatingRecommendIn(BaseModel):
"""主动选配推荐入参。"""
candidate_germplasm_ids: list[int] = Field(..., description="候选亲本种质 id 集合(≥2")
prediction_id: int | None = Field(None, description="EBV 批次;为空用最新 ABLUP/GBLUP 批次")
kinship_threshold: float = Field(0.25, description="亲缘惩罚阈值(r>阈值开始扣分)")
w_ebv: float = Field(1.0, ge=0, description="EBV 互补权重")
w_kin: float = Field(1.0, ge=0, description="近交惩罚权重")
max_pairs: int = Field(20, ge=1, le=500, description="返回 top N 配对")
model_config = ConfigDict(extra="ignore")
class OcsIn(BaseModel):
"""最优贡献选择(OCS)入参。"""
candidate_germplasm_ids: list[int] = Field(..., description="候选亲本种质 id 集合")
n_select: int = Field(5, ge=1, description="选择数量(贡献总和 Σc=n_select)")
lam: float = Field(0.1, ge=0, description="近交约束权重 λ:越大越压低子代平均亲缘(avg_kinship 单调非增),0=无近交约束(满额投最高 EBV 单亲)")
prediction_id: int | None = Field(None, description="EBV 批次;为空用最新 ABLUP/GBLUP 批次")
model_config = ConfigDict(extra="ignore")
class MabcIn(BaseModel):
"""MABC 标记辅助回交进度入参。"""
candidate_tree_ids: list[int] = Field(..., description="候选回交单株树 id 集合(BC 世代分离群体)")
foreground_panel_ids: list[int] = Field(..., description="前景选择面板 id(目标性状 MAS 面板)")
background_panel_ids: list[int] = Field(..., description="背景恢复面板 id(全基因组标记面板)")
recurrent_parent_tree_id: int = Field(..., description="轮回亲本树 id(背景纯合一致对照)")
foreground_min_hits: int = Field(1, ge=1, description="前景通过阈值(有利剂量命中标记数 ≥ 此值,stage 无关、童期可用)")
generation: str = Field("BC1", description="候选当前回交代(F1/BC1/BC2/BC3),用于晋级建议")
background_target: float = Field(90.0, gt=0, le=100, description="背景恢复目标 %(达到则建议晋级下一回交代)")
model_config = ConfigDict(extra="ignore")
class AnovaIn(BaseModel):
"""ANOVA / 广义遗传力 H² 入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
block: bool = Field(
False, description="RCBD 设计基 ANOVATrue=从残差析出区组效应(SS_block/F_block/p_block),遗传差异为区组校正后;需观测株 bre_tree.block_no")
model_config = ConfigDict(extra="ignore")
class AnovaOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
model_name: str | None = None
trait_id: int | None = None
method: str | None = None
result_json: dict | None = None
created_time: datetime | None = None
class CvRunIn(BaseModel):
"""k-fold 交叉验证入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
fixed_effects: list[str] | None = Field(
None, description="固定效应因子:trial_study(试验/站点) / rootstock(砧木)"
)
covariate: str | None = Field(
None, description="协变量:crop_load(负载量) / competition(空间竞争)"
)
gxe: bool = Field(False, description="启用 G×E 交互随机效应")
gxe_env: str = Field("site", description="G×E 环境维度:site / year")
k: int = Field(5, ge=2, le=10, description="折数")
dataset_id: int | None = Field(
None, description="基因型数据集 id;给定则走 GS 交叉验证(GBLUP/ssGBLUP/rrBLUP/BayesB"
)
method: str | None = Field(
None, description="GS 方法:gblup / ssgblup / rrblup / bayesb(仅 dataset_id 给定时生效)"
)
maf_min: float = Field(0.05, description="MAF 下限(GS 交叉验证用)")
split: str = Field(
"random", description="GS 折划分:random=固定种子随机分层(默认)/ family=家系阻塞折(同父半同胞同折,防亲缘泄漏)"
)
seed: int = Field(20260805, description="BayesB 随机种子(折划分与 Gibbs 共用,固定可复现)")
model_config = ConfigDict(extra="ignore")
class CvResultOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
trait_id: int | None = None
trait_code: str | None = None
method: str | None = None
k: int | None = None
n_total: int | None = None
n_individuals: int | None = None
mean_pearson: float | None = None
mean_rmse: float | None = None
pooled_pearson: float | None = None
pooled_rmse: float | None = None
cv_accuracy: float | None = None
h2: float | None = None
data_version: str | None = None
input_hash: str | None = None
engine_version: str | None = None
note: str | None = None
created_time: datetime | None = None
class CvFoldOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
cv_result_id: int
fold_idx: int | None = None
n_train: int | None = None
n_test: int | None = None
n_eval: int | None = None
pearson: float | None = None
rmse: float | None = None
error: str | None = None
class DecisionPreviewIn(BaseModel):
"""选择规则 × 表型/EBV/标记(MAS) 决策预览入参。"""
rule_ids: list[int] | None = Field(None, description="指定规则;为空用全部启用规则")
prediction_id: int | None = Field(None, description="EBV 源批次;为空用最新 ABLUP 批次")
year: int | None = Field(None, description="表型均值年份过滤")
min_reliability: float = Field(
0.2, ge=0.0, le=1.0,
description="EBV 可靠性门槛:单株 EBV 可靠性低于该值视为证据不足,不参与晋级/淘汰判定;"
"ebv 条件内可配 min_reliability 逐性状覆盖",
)
stage: str | None = Field(
None, description="发育阶段过滤:juvenile(童期)/evaluation(成株);不传且规则引用性状横跨两阶段时返回 stage_warning"
)
model_config = ConfigDict(extra="ignore")
class DecisionPreviewOut(BaseModel):
rule_id: int | None = None
rule_name: str | None = None
action: str | None = None
stage: str | None = None
matched: bool | None = None
reasons: list[str] | None = None
tree_id: int | None = None
tree_no: str | None = None
combination_id: int | None = None
# ---------- AMMI / Finlay-Wilkinson 稳定性(§8.18 ----------
class StabilityRunIn(BaseModel):
"""稳定性分析入参。"""
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
gxe_env: str = Field("site", description="环境维度:site(trial_study_id) / year(evaluate_year)")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
methods: list[str] = Field(
["ammi", "finlay"], description="分析方法:ammi / finlay,可多选"
)
model_config = ConfigDict(extra="ignore")
class StabilityOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
trait_id: int | None = None
trait_code: str | None = None
env_dim: str | None = None
methods: str | None = None
detail_json: dict | None = None
data_version: str | None = None
input_hash: str | None = None
engine_version: str | None = None
note: str | None = None
created_time: datetime | None = None
# ---------- 遗传相关矩阵(MT-BLUP,§8.18 ----------
class GeneticCorrIn(BaseModel):
"""遗传相关矩阵入参。"""
trait_ids: list[int] = Field(..., description="参与遗传相关的性状 id(≥2")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
full_mtblup: bool = Field(
False, description="True=全多变量 EM-REML(一次估计完整 G0⊗A,替代逐对 bivariate")
model_config = ConfigDict(extra="ignore")
class GeneticCorrOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
trait_ids_json: dict | None = None
matrix_json: dict | None = None
sigma_a_json: dict | None = None
heritability_json: dict | None = None
pairs_json: dict | None = None
n_common: int | None = None
data_version: str | None = None
input_hash: str | None = None
engine_version: str | None = None
note: str | None = None
created_time: datetime | None = None
# ---------- Type-B 多环境遗传相关(§8.27 ----------
class TypeBIn(BaseModel):
"""Type-B 多环境遗传相关入参。"""
trait_id: int = Field(..., description="性状 idbre_trait,数值型)")
trait_code: str = Field(..., description="性状编码")
env_dim: str = Field("site", description="环境维度:site(研究点 trial_study_id) / year(评价年份) / stage(发育阶段:juvenile童期 vs evaluation成株,估幼年-成年遗传相关)")
method: str = Field(
"reml", description="方法:reml=环境互为性状逐对REML双性状BLUP(默认)calo=分环境EBV相关/√rel(无环境级EBV时回退reml)"
)
year: int | None = Field(None, description="评价年份过滤;为空则全部")
model_config = ConfigDict(extra="ignore")
class TypeBOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
trait_id: int | None = None
trait_code: str | None = None
env_dim: str | None = None
method: str | None = None
envs_json: dict | None = None
matrix_json: dict | None = None
pairs_json: dict | None = None
n_common: int | None = None
data_version: str | None = None
input_hash: str | None = None
engine_version: str | None = None
note: str | None = None
created_time: datetime | None = None
# ---------- UPGMA 聚类(§8.27,计算端点不落库) ----------
class ClusterIn(BaseModel):
"""UPGMA 聚类入参(实体=单株或无性系,按多性状轮廓聚类)。"""
trait_ids: list[int] = Field(..., description="参与聚类的数值性状 id(≥2")
entity_type: str = Field("tree", description="聚类实体:tree=单株;clone=无性系(同系多株均值聚合)")
mode: str = Field("pheno", description="值来源:pheno=表型均值;genetic=EBV(无生效批次回退表型)")
distance: str = Field(
"corr", description="实体距离:corr=1-|皮尔逊r|(轮廓相似)euclidean=标准化欧氏距离"
)
k: int | None = Field(None, description="聚成 k 类;为空按合并距离最大跳变自动选择")
model_config = ConfigDict(extra="ignore")
# ---------- GBLUP / ssGBLUP 基因组选择(§8.18 ----------
class GblupRunIn(BaseModel):
"""GBLUP/ssGBLUP/rrBLUP/BayesB 基因组选择入参(§8.18)。"""
dataset_id: int = Field(..., description="基因型数据集 idbre_genotyping_dataset")
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
method: str = Field("gblup", description="gblup(仅基因型株)/ ssgblup(单步法,含非基因型亲属)/ rrblup(岭回归逐标记,输出 marker effects/ bayesb(贝叶斯可变选择,固定 seed 可复现)")
maf_min: float = Field(0.05, ge=0.0, lt=0.5, description="MAF 过滤下限")
seed: int = Field(20260805, description="BayesB 随机种子(固定保证可复现;MLOps 铁律)")
model_config = ConfigDict(extra="ignore")
class GenotypingDatasetOut(BaseModel):
id: int
dataset_name: str | None = None
platform: str | None = None
panel: str | None = None
purpose: str | None = None
run_date: datetime | None = None
n_samples: int | None = None
# ---------- GWAS / QTL / MAS 标记辅助选择(§8.22 ----------
class GwasRunIn(BaseModel):
"""GWAS 关联分析入参(GLM+PC 首版)。"""
dataset_id: int = Field(..., description="基因型数据集 idbre_genotyping_dataset")
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
method: str = Field("gwas", description="gwasGLM+PC/ emmax(混合模型 EMMAX/ ssgwas(单步 GWAS")
maf_min: float = Field(0.05, ge=0.0, lt=0.5, description="MAF 过滤下限")
n_pc: int = Field(3, ge=1, le=10, description="群体结构主成分数")
sig_level: float = Field(0.05, gt=0.0, le=1.0, description="显著性水平")
qtl_window: int = Field(1_000_000, ge=1, description="QTL 合并窗口(bp)")
model_config = ConfigDict(extra="ignore")
class GwasQtlXEIn(BaseModel):
"""QTL×E 入参(按环境分层 GWAS + 稳定性判定,计算端点不建表)。"""
dataset_id: int = Field(..., description="基因型数据集 idbre_genotyping_dataset")
trait_id: int = Field(..., description="性状 idbre_trait")
trait_code: str = Field(..., description="性状编码")
year: int | None = Field(None, description="评价年份过滤;为空则全部")
method: str = Field("gwas", description="gwasGLM+PC/ emmax(混合模型 EMMAX")
maf_min: float = Field(0.05, ge=0.0, lt=0.5, description="MAF 过滤下限")
n_pc: int = Field(3, ge=1, le=10, description="群体结构主成分数")
sig_level: float = Field(0.05, gt=0.0, le=1.0, description="显著性水平")
qtl_window: int = Field(1_000_000, ge=1, description="QTL 合并窗口(bp)")
env_dim: str = Field("site", description="环境维度:site(trial_study_id) / year(evaluate_year)")
model_config = ConfigDict(extra="ignore")
class GwasResultOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
dataset_id: int | None = None
trait_id: int | None = None
trait_code: str | None = None
method: str | None = None
n_individuals: int | None = None
n_markers: int | None = None
m_after_maf: int | None = None
maf_min: float | None = None
n_pc: int | None = None
sig_level: float | None = None
threshold_bonf: float | None = None
n_sig_bonf: int | None = None
n_sig_fdr: int | None = None
n_qtl: int | None = None
data_version: str | None = None
input_hash: str | None = None
engine_version: str | None = None
remark: str | None = None
created_time: datetime | None = None
class GwasSnpOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
gwas_result_id: int
marker_id: int | None = None
marker_name: str | None = None
chromosome: str | None = None
position: int | None = None
maf: float | None = None
effect: float | None = None
se: float | None = None
t_value: float | None = None
p_value: float | None = None
neg_log10p: float | None = None
q_value: float | None = None
sig_bonf: bool | None = None
sig_fdr: bool | None = None
class QtlIn(BaseModel):
"""已知 QTL 录入(source=known)入参。"""
trait_id: int | None = Field(None, description="性状 idbre_trait")
chromosome: str | None = Field(None, description="染色体")
start_bp: int | None = Field(None, description="区间起点(bp)")
end_bp: int | None = Field(None, description="区间终点(bp)")
peak_marker_id: int | None = Field(None, description="峰标记 idbre_marker")
peak_p: float | None = Field(None, description="峰标记 p 值")
n_markers: int | None = Field(None, description="区间显著标记数")
effect: float | None = Field(None, description="峰效应")
remark: str | None = Field(None, description="备注")
model_config = ConfigDict(extra="ignore")
class QtlOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
trait_id: int | None = None
chromosome: str | None = None
start_bp: int | None = None
end_bp: int | None = None
peak_marker_id: int | None = None
peak_marker_name: str | None = None
peak_p: float | None = None
n_markers: int | None = None
effect: float | None = None
source: str | None = None
gwas_result_id: int | None = None
remark: str | None = None
created_time: datetime | None = None
class MasPanelIn(BaseModel):
"""MAS 标记辅助选择面板入参。"""
panel_name: str = Field(..., description="面板名称")
trait_id: int | None = Field(None, description="目标性状 idbre_trait")
remark: str | None = Field(None, description="备注")
model_config = ConfigDict(extra="ignore")
class MasPanelOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
panel_name: str | None = None
trait_id: int | None = None
n_markers: int | None = None
remark: str | None = None
created_time: datetime | None = None
class MasPanelMarkerIn(BaseModel):
"""面板标记(有利剂量 + 方向 + 基因作用模式)。"""
marker_id: int | None = Field(None, description="标记 idbre_marker")
favorable_dose: int | None = Field(None, description="有利剂量(0/1/2)")
effect: float | None = Field(None, description="效应(参考)")
direction: str | None = Field("high", description="high=剂量≥favorable_dose命中 / low=剂量≤命中")
mode: str | None = Field("additive", description="基因作用模式:additive/dominance/recessive/allele/haplotype")
favorable_allele: str | None = Field(None, description="有利等位(allele/haplotype 模式用,SSR 等位索引)")
haplotype_group: str | None = Field(None, description="单倍型组(haplotype 模式:同组标记全命中才计 1)")
model_config = ConfigDict(extra="ignore")
class MasPanelSetMarkersIn(BaseModel):
"""面板标记全量替换入参。"""
markers: list[MasPanelMarkerIn] = Field(default_factory=list, description="面板标记列表")
model_config = ConfigDict(extra="ignore")
File diff suppressed because it is too large Load Diff