checkpoint: real-scale 数据链 + 统计引擎 + ABLUP 稀疏性能修复

init 后首次落盘,累计工作:
- 数据侧:simulate_breeding_data real-scale 3.5万树重建 + 分层观测
- 引擎侧:ABLUP 稀疏生产档 EM-REML(blup 1.6.0/1.7.0);性能倒挂修复——
  系谱闭包收口为 BFS 可达祖先 + N_EXACT 3000 对齐 N_SUBSAMPLE(选中 200~3000 树
  不再顶进数小时精确迹尾),A/B 数值等价实证 + 守卫探针
- 前端:观测过滤 + 表单/HTTP 工具链完善
- 文档:业务链/观测梳理 + 引擎实施记录 + 规划对照总表
- gitignore:排除 Temp/调试脚本/一次性验证输出
This commit is contained in:
34047007@qq.com
2026-08-07 08:03:10 +08:00
parent b95053c52c
commit e937fc930c
16 changed files with 2293 additions and 238 deletions
@@ -26,7 +26,8 @@ import numpy as np
from datetime import date, datetime
from typing import Any
from sqlalchemy import String, case, func, or_, select, text, update
from sqlalchemy import Integer, String, case, cast, false, func, or_, select, text, update
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema
@@ -84,6 +85,19 @@ def _now() -> datetime:
return datetime.now()
def _id_in(column, ids):
"""按 IN (SELECT unnest(:arr)) 绑定大 id 列表为单个数组参数。
asyncpg 单条查询总绑定参数上限 32767常规 column.in_(ids) 会在
bre_tree 等表 id 规模过万时直接崩IN 子句分块无济于事因为是全查询
计数空列表返回恒假条件避免 SQLAlchemy 渲染出畸形的 IN (NULL)
"""
ids = list(ids)
if not ids:
return false()
return column.in_(select(func.unnest(cast(ids, ARRAY(Integer)))))
# 交配设计(配合力 GCA/SCA 的算法与 ANOVA 自由度随设计而异;与 combining.solve 分支一致)
_COMBINING_DESIGNS = combining.DESIGNS
_DESIGN_LABELS = {
@@ -438,6 +452,27 @@ class StatisticsService:
)).all()
return {int(r.tree_id) for r in rows}
@staticmethod
def _germ_ancestor_closure(tree_germs: set[int],
g_parent_of: dict[int, tuple[int | None, int | None]]) -> set[int]:
"""种质级祖先闭包:从选中树直系亲本向上取可达祖先(bre_pedigree 只追祖先链)。
旧实现把全库 bre_pedigree 子代种质无条件并进模型选中 400 n1204无关种质
MME 中零贡献无表型与选中树无亲缘EBV/ 逐位不变 n=树数+~800 一越过
N_SPARSE 就误入稀疏精确迹二分~11min/收口到真实系谱规模后常见选几百棵
候选树落稠密秒级 _germplasm_pedigree 的多源 BFS 思路一致
"""
out = set(tree_germs)
stack = list(tree_germs)
while stack:
g = stack.pop()
gd, gs = g_parent_of.get(g, (None, None))
for pp in (gd, gs):
if pp is not None and pp not in out:
out.add(pp)
stack.append(pp)
return out
async def _build_pedigree(self, tree_ids: list[int]) -> list[dict]:
"""为给定树集合构建系谱(树=子代、种质=亲本,t/g 前缀;含种质级祖先 bre_pedigree)。
@@ -456,7 +491,7 @@ class StatisticsService:
CrossCombinationModel.male_parent_id,
)
.outerjoin(CrossCombinationModel, TreeModel.combination_id == CrossCombinationModel.id)
.where(TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
.where(_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
pedigree: list[dict] = []
tree_germs: set[int] = set()
@@ -489,13 +524,7 @@ class StatisticsService:
cid = code2g.get(child_code)
if cid is not None and (gd is not None or gs is not None):
g_parent_of[cid] = (gd, gs)
all_germ_ids = set(tree_germs)
for g, (gd, gs) in g_parent_of.items():
all_germ_ids.add(g)
if gd is not None:
all_germ_ids.add(gd)
if gs is not None:
all_germ_ids.add(gs)
all_germ_ids = self._germ_ancestor_closure(tree_germs, g_parent_of)
for g in sorted(all_germ_ids):
if g in g_parent_of:
gd, gs = g_parent_of[g]
@@ -686,7 +715,7 @@ class StatisticsService:
TreeModel.germplasm_id,
)
.outerjoin(CrossCombinationModel, TreeModel.combination_id == CrossCombinationModel.id)
.where(TreeModel.id.in_(tree_ids))
.where(_id_in(TreeModel.id, tree_ids))
)).all()
pedigree: list[dict] = []
tree_germs: set[int] = set()
@@ -739,7 +768,7 @@ class StatisticsService:
pos_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.plot_id, TreeModel.block_no,
TreeModel.row_no, TreeModel.col_no)
.where(TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
.where(_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
grids: dict[tuple[int, int], list[tuple[int, int, int]]] = {}
for r in pos_rows:
@@ -776,13 +805,7 @@ class StatisticsService:
cid = code2g.get(child_code)
if cid is not None and (gd is not None or gs is not None):
g_parent_of[cid] = (gd, gs)
all_germ_ids = set(tree_germs)
for g, (gd, gs) in g_parent_of.items():
all_germ_ids.add(g)
if gd is not None:
all_germ_ids.add(gd)
if gs is not None:
all_germ_ids.add(gs)
all_germ_ids = self._germ_ancestor_closure(tree_germs, g_parent_of)
for g in sorted(all_germ_ids):
if g in g_parent_of:
gd, gs = g_parent_of[g]
@@ -846,7 +869,7 @@ class StatisticsService:
TreeModel.is_deleted.is_(False),
# 与 site 分支对齐:只取本批次选中树,避免他树混入后
# clone_of/combo_of 缺失 → 归入虚构基因型组 f{None}
TreeModel.id.in_(tree_ids),
_id_in(TreeModel.id, tree_ids),
)
.group_by(yr_obs.tree_id, yr_obs.evaluate_year)
)
@@ -1093,7 +1116,7 @@ class StatisticsService:
phenos_keyed = {f"t{tid}": v for tid, v in phenotypes.items()}
bpos_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.block_no)
.where(TreeModel.id.in_(tree_ids), TreeModel.block_no.isnot(None),
.where(_id_in(TreeModel.id, tree_ids), TreeModel.block_no.isnot(None),
TreeModel.is_deleted.is_(False))
)).all()
block_of: dict[str, int] = {}
@@ -1171,7 +1194,7 @@ class StatisticsService:
if spatial:
pos_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.row_no, TreeModel.col_no)
.where(TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
.where(_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
coords: dict[str, tuple[int, int]] = {}
missing_coord_ids: list[int] = []
@@ -1391,7 +1414,7 @@ class StatisticsService:
TreeModel.germplasm_id,
)
.outerjoin(CrossCombinationModel, TreeModel.combination_id == CrossCombinationModel.id)
.where(TreeModel.id.in_(tree_ids))
.where(_id_in(TreeModel.id, tree_ids))
)).all()
pedigree: list[dict] = []
tree_germs: set[int] = set()
@@ -1434,7 +1457,7 @@ class StatisticsService:
pos_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.plot_id, TreeModel.block_no,
TreeModel.row_no, TreeModel.col_no)
.where(TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
.where(_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
grids: dict[tuple[int, int], list[tuple[int, int, int]]] = {}
for r in pos_rows:
@@ -1470,13 +1493,7 @@ class StatisticsService:
cid = code2g.get(child_code)
if cid is not None and (gd is not None or gs is not None):
g_parent_of[cid] = (gd, gs)
all_germ_ids = set(tree_germs)
for g, (gd, gs) in g_parent_of.items():
all_germ_ids.add(g)
if gd is not None:
all_germ_ids.add(gd)
if gs is not None:
all_germ_ids.add(gs)
all_germ_ids = self._germ_ancestor_closure(tree_germs, g_parent_of)
for g in sorted(all_germ_ids):
if g in g_parent_of:
gd, gs = g_parent_of[g]
@@ -1766,7 +1783,7 @@ class StatisticsService:
rel = {r.tree_id: float(r.reliability or 0.0) for r in rows}
trees = (await self.db.execute(
select(TreeModel.id, TreeModel.trial_study_id, TreeModel.combination_id)
.where(TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
.where(_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
site_of = {t.id: t.trial_study_id for t in trees}
combo_of = {t.id: t.combination_id for t in trees}
@@ -2015,7 +2032,7 @@ class StatisticsService:
if tree_ids:
rows = (await self.db.execute(
select(TreeModel.id, TreeModel.clone_id).where(
TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False)
_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False)
)
)).all()
tree_map = {t.id: t.clone_id for t in rows}
@@ -2023,7 +2040,7 @@ class StatisticsService:
clone_map: dict[int, CloneModel] = {}
if clone_ids:
clones = (await self.db.execute(
select(CloneModel).where(CloneModel.id.in_(clone_ids), CloneModel.is_deleted.is_(False))
select(CloneModel).where(_id_in(CloneModel.id, clone_ids), CloneModel.is_deleted.is_(False))
)).scalars().all()
clone_map = {c.id: c for c in clones}
agg: dict[int, dict[str, float | int]] = {}
@@ -2533,7 +2550,7 @@ class StatisticsService:
pid2code = {pid: c for c, pid in batch_ids.items()}
pv_rows = (await self.db.execute(
select(PredictionValueModel.prediction_id, PredictionValueModel.tree_id, PredictionValueModel.reliability).where(
PredictionValueModel.prediction_id.in_(list(batch_ids.values())),
_id_in(PredictionValueModel.prediction_id, list(batch_ids.values())),
PredictionValueModel.tree_id.isnot(None),
)
)).all()
@@ -2677,7 +2694,7 @@ class StatisticsService:
"""
rows = (await self.db.execute(
select(TreeModel.id, TreeModel.clone_id, TreeModel.tree_no).where(
TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False)
_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False)
)
)).all()
clone_of = {t.id: t.clone_id for t in rows}
@@ -2687,7 +2704,7 @@ class StatisticsService:
if clone_ids:
clones = (await self.db.execute(
select(CloneModel.id, CloneModel.clone_code).where(
CloneModel.id.in_(clone_ids), CloneModel.is_deleted.is_(False)
_id_in(CloneModel.id, clone_ids), CloneModel.is_deleted.is_(False)
)
)).all()
clone_label = {c.id: c.clone_code for c in clones}
@@ -3011,7 +3028,7 @@ class StatisticsService:
if refs:
pv_rows = (await self.db.execute(
select(PredictionValueModel.tree_id, PredictionValueModel.reliability).where(
PredictionValueModel.prediction_id.in_(list(refs.values())),
_id_in(PredictionValueModel.prediction_id, list(refs.values())),
PredictionValueModel.tree_id.isnot(None),
)
)).all()
@@ -3048,11 +3065,11 @@ class StatisticsService:
all_tids = {t for u in units for t in u["tree_ids"]}
tree_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.combination_id, TreeModel.tree_no, TreeModel.stage)
.where(TreeModel.id.in_(all_tids))
.where(_id_in(TreeModel.id, all_tids))
)).all()
tree_map = {t.id: t for t in tree_rows}
existing = (await self.db.execute(
select(SelectionResultModel).where(SelectionResultModel.tree_id.in_(all_tids))
select(SelectionResultModel).where(_id_in(SelectionResultModel.tree_id, all_tids))
)).scalars().all()
existing_by_tree = {e.tree_id: e for e in existing}
@@ -3144,7 +3161,7 @@ class StatisticsService:
"""
stmt = select(TreeModel.id, TreeModel.dam_id, TreeModel.sire_id).where(TreeModel.is_deleted.is_(False))
if tree_ids:
stmt = stmt.where(TreeModel.id.in_(tree_ids))
stmt = stmt.where(_id_in(TreeModel.id, tree_ids))
rows = (await self.db.execute(stmt)).all()
if not rows:
raise CustomException(msg="无活动单株")
@@ -3203,11 +3220,35 @@ class StatisticsService:
raise CustomException(msg=f"性状不可用于统计(需数值型且存在): {trait_code}")
direction = (trait_row.direction or "desc").lower()
desirable_high = direction != "asc" # desc/None:表型越高越好;asc:越低越好(如病指)
# 1) 单株系谱 → 深层系谱 A 矩阵 → 近交系数 F
# 0) 先取目标性状每树表型(同株多次取均值)——近交只针对有该性状表型的单株;
# 全库 3.6 万树建稠密 A 会 OOM36990²×8B≈10 GiB),必须按表型树收口。
obs = TraitObservationModel
stmt = (
select(obs.tree_id, func.avg(obs.value_numeric).label("v"))
.join(TreeModel, obs.tree_id == TreeModel.id)
.where(
obs.trait_id == trait_id,
obs.value_numeric.isnot(None),
obs.tree_id.isnot(None),
obs.is_deleted.is_(False),
TreeModel.is_deleted.is_(False),
)
.group_by(obs.tree_id)
)
if year is not None:
stmt = stmt.where(obs.evaluate_year == year)
if trial_study_id is not None:
stmt = stmt.where(obs.trial_study_id == trial_study_id)
agg = (await self.db.execute(stmt)).all()
if not agg:
raise CustomException(msg="无可用表型数据(该性状为空)")
# 1) 单株系谱(仅表型树)→ 深层系谱 A 矩阵 → 近交系数 F
# 单株 dam/sire 引用种质;种质系谱经 _germplasm_pedigree 多源 BFS 解析(bre_pedigree
# 名称反查 + tree FK 兜底 + 祖先展开,与 OCS/mating 同源)——多世代项目(半同胞共享
# 祖亲)不再低估 F;被引用但未解析(软删等)种质补 founder 兜底。
stmt = select(TreeModel.id, TreeModel.dam_id, TreeModel.sire_id).where(TreeModel.is_deleted.is_(False))
stmt = (select(TreeModel.id, TreeModel.dam_id, TreeModel.sire_id)
.where(TreeModel.is_deleted.is_(False),
_id_in(TreeModel.id, [r.tree_id for r in agg])))
rows = (await self.db.execute(stmt)).all()
if not rows:
raise CustomException(msg="无活动单株")
@@ -3249,27 +3290,7 @@ class StatisticsService:
for k, v in rmat_res["inbreeding"].items():
if k.startswith("t") and k[1:].isdigit():
f_by_tree[int(k[1:])] = float(v)
# 2) 目标性状每树表型(同株多次取均值)
obs = TraitObservationModel
stmt = (
select(obs.tree_id, func.avg(obs.value_numeric).label("v"))
.join(TreeModel, obs.tree_id == TreeModel.id)
.where(
obs.trait_id == trait_id,
obs.value_numeric.isnot(None),
obs.tree_id.isnot(None),
obs.is_deleted.is_(False),
TreeModel.is_deleted.is_(False),
)
.group_by(obs.tree_id)
)
if year is not None:
stmt = stmt.where(obs.evaluate_year == year)
if trial_study_id is not None:
stmt = stmt.where(obs.trial_study_id == trial_study_id)
agg = (await self.db.execute(stmt)).all()
if not agg:
raise CustomException(msg="无可用表型数据(该性状为空)")
# 2) 表型已在上方取好(agg),直接配对 F
pairs: list[tuple[int, float, float]] = []
for r in agg:
f = f_by_tree.get(int(r.tree_id))
@@ -3329,7 +3350,7 @@ class StatisticsService:
interpretation = f"未检出近交衰退(F 对表型无{trend}影响)"
tree_ids = [p[0] for p in pairs]
trows = (await self.db.execute(
select(TreeModel.id, TreeModel.tree_no).where(TreeModel.id.in_(tree_ids))
select(TreeModel.id, TreeModel.tree_no).where(_id_in(TreeModel.id, tree_ids))
)).all()
tree_map = {tid: no for tid, no in trows}
base.update({
@@ -3470,7 +3491,7 @@ class StatisticsService:
if flagged_ids:
for tid, tno in (await self.db.execute(
select(TreeModel.id, TreeModel.tree_no)
.where(TreeModel.id.in_(flagged_ids)))).all():
.where(_id_in(TreeModel.id, flagged_ids)))).all():
tree_no_map[int(tid)] = tno or ""
excluded_ids = await self._excluded_tree_ids()
mendelian_section = {
@@ -3665,7 +3686,7 @@ class StatisticsService:
gs = (await self.db.execute(
select(BreedingGermplasmModel.id, BreedingGermplasmModel.cultivar_name,
BreedingGermplasmModel.accession_no)
.where(BreedingGermplasmModel.id.in_(gids))
.where(_id_in(BreedingGermplasmModel.id, gids))
)).all()
cand_ids = [int(g.id) for g in gs]
if not cand_ids:
@@ -3793,7 +3814,7 @@ class StatisticsService:
pvs = (await self.db.execute(
select(PredictionValueModel).where(
PredictionValueModel.prediction_id == ebv_pred.id,
PredictionValueModel.germplasm_id.in_(cand_ids),
_id_in(PredictionValueModel.germplasm_id, cand_ids),
PredictionValueModel.predicted_value.isnot(None),
)
)).scalars().all()
@@ -3819,7 +3840,7 @@ class StatisticsService:
mset = set(missing)
off_tree = (await self.db.execute(
select(TreeModel.id, TreeModel.dam_id, TreeModel.sire_id, TreeModel.combination_id).where(
or_(TreeModel.dam_id.in_(missing), TreeModel.sire_id.in_(missing)),
or_(_id_in(TreeModel.dam_id, missing), _id_in(TreeModel.sire_id, missing)),
TreeModel.is_deleted.is_(False),
)
)).all()
@@ -3828,7 +3849,7 @@ class StatisticsService:
off_pvs = (await self.db.execute(
select(PredictionValueModel.tree_id, PredictionValueModel.predicted_value).where(
PredictionValueModel.prediction_id == ebv_pred.id,
PredictionValueModel.tree_id.in_(off_ids),
_id_in(PredictionValueModel.tree_id, off_ids),
PredictionValueModel.predicted_value.isnot(None),
)
)).all()
@@ -3887,7 +3908,7 @@ class StatisticsService:
cand_ids = list(dict.fromkeys(candidate_germplasm_ids))
gs = (await self.db.execute(
select(BreedingGermplasmModel).where(
BreedingGermplasmModel.id.in_(cand_ids),
_id_in(BreedingGermplasmModel.id, cand_ids),
BreedingGermplasmModel.is_deleted.is_(False),
)
)).scalars().all()
@@ -4048,7 +4069,7 @@ class StatisticsService:
raise CustomException(msg=f"选择数量 n_select={n_select} 超过候选亲本数 {len(cand_ids)}")
gs = (await self.db.execute(
select(BreedingGermplasmModel).where(
BreedingGermplasmModel.id.in_(cand_ids),
_id_in(BreedingGermplasmModel.id, cand_ids),
BreedingGermplasmModel.is_deleted.is_(False),
)
)).scalars().all()
@@ -4210,7 +4231,7 @@ class StatisticsService:
cand_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.tree_no).where(
TreeModel.id.in_(list(dict.fromkeys(candidate_tree_ids))),
_id_in(TreeModel.id, list(dict.fromkeys(candidate_tree_ids))),
TreeModel.is_deleted.is_(False),
)
)).all()
@@ -4241,7 +4262,7 @@ class StatisticsService:
select(GenotypeCallModel.sample_id, GenotypeCallModel.marker_id,
GenotypeCallModel.allele)
.where(
GenotypeCallModel.marker_id.in_(bg_marker_ids),
_id_in(GenotypeCallModel.marker_id, bg_marker_ids),
GenotypeCallModel.allele.isnot(None),
GenotypeCallModel.is_deleted.is_(False),
)
@@ -4249,7 +4270,7 @@ class StatisticsService:
if calls:
sample_ids = sorted({c.sample_id for c in calls})
samples = (await self.db.execute(
select(GenotypeSampleModel).where(GenotypeSampleModel.id.in_(sample_ids))
select(GenotypeSampleModel).where(_id_in(GenotypeSampleModel.id, sample_ids))
)).scalars().all()
sample_tree, _ = await self._sample_to_tree_map(samples)
for c in calls:
@@ -4412,7 +4433,7 @@ class StatisticsService:
block_of[ent.id] = blk
await self.db.execute(
update(TrialStudyEntryModel)
.where(TrialStudyEntryModel.id.in_(list(block_of)))
.where(_id_in(TrialStudyEntryModel.id, list(block_of)))
.values(block_no=case(block_of, value=TrialStudyEntryModel.id, else_=None))
)
await self.db.flush()
@@ -4468,7 +4489,7 @@ class StatisticsService:
if block_of:
await self.db.execute(
update(TrialStudyEntryModel)
.where(TrialStudyEntryModel.id.in_(list(block_of)))
.where(_id_in(TrialStudyEntryModel.id, list(block_of)))
.values(block_no=case(block_of, value=TrialStudyEntryModel.id, else_=None))
)
await self.db.flush()
@@ -4577,7 +4598,7 @@ class StatisticsService:
if block_of:
await self.db.execute(
update(TrialStudyEntryModel)
.where(TrialStudyEntryModel.id.in_(list(block_of)))
.where(_id_in(TrialStudyEntryModel.id, list(block_of)))
.values(block_no=case(block_of, value=TrialStudyEntryModel.id, else_=None))
)
await self.db.flush()
@@ -4634,7 +4655,7 @@ class StatisticsService:
if block:
tree_ids = [int(r.tree_id) for r in agg]
block_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.block_no).where(TreeModel.id.in_(tree_ids))
select(TreeModel.id, TreeModel.block_no).where(_id_in(TreeModel.id, tree_ids))
)).all()
block_of = {int(tid): bn for tid, bn in block_rows}
rows = [
@@ -4998,10 +5019,10 @@ class StatisticsService:
all_tree_ids = sorted(set(by_tree) | set(ebv_map) | mas["tree_ids"])
if all_tree_ids:
tree_no_map = dict((await self.db.execute(
select(TreeModel.id, TreeModel.tree_no).where(TreeModel.id.in_(all_tree_ids))
select(TreeModel.id, TreeModel.tree_no).where(_id_in(TreeModel.id, all_tree_ids))
)).all())
combo_map = dict((await self.db.execute(
select(TreeModel.id, TreeModel.combination_id).where(TreeModel.id.in_(all_tree_ids))
select(TreeModel.id, TreeModel.combination_id).where(_id_in(TreeModel.id, all_tree_ids))
)).all())
else:
tree_no_map, combo_map = {}, {}
@@ -5168,7 +5189,7 @@ class StatisticsService:
tree_ids = sorted({r.tree_id for r in rows})
trows = (await self.db.execute(
select(TreeModel.id, TreeModel.clone_id, TreeModel.combination_id).where(
TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
clone_of = {t.id: t.clone_id for t in trows}
combo_of = {t.id: t.combination_id for t in trows}
@@ -5545,7 +5566,7 @@ class StatisticsService:
else:
tree_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.clone_id, TreeModel.tree_no).where(
TreeModel.id.in_(list(by_tree)), TreeModel.is_deleted.is_(False)
_id_in(TreeModel.id, list(by_tree)), TreeModel.is_deleted.is_(False)
)
)).all()
clone_of = {t.id: t.clone_id for t in tree_rows}
@@ -5555,7 +5576,7 @@ class StatisticsService:
if clone_ids:
clones = (await self.db.execute(
select(CloneModel.id, CloneModel.clone_code).where(
CloneModel.id.in_(clone_ids), CloneModel.is_deleted.is_(False)
_id_in(CloneModel.id, clone_ids), CloneModel.is_deleted.is_(False)
)
)).all()
clone_label = {c.id: c.clone_code for c in clones}
@@ -5807,7 +5828,7 @@ class StatisticsService:
rel = result["reliability"]
ebv = result["ebv"]
germ_of = dict((await self.db.execute(
select(TreeModel.id, TreeModel.germplasm_id).where(TreeModel.id.in_(tree_ids))
select(TreeModel.id, TreeModel.germplasm_id).where(_id_in(TreeModel.id, tree_ids))
)).all())
direction = (trait_row.direction or "desc").lower()
tree_ebv = [(tid, float(ebv.get(f"t{tid}", 0.0))) for tid in tree_ids]
@@ -5898,7 +5919,7 @@ class StatisticsService:
select(GenotypeCallModel.sample_id, GenotypeCallModel.marker_id,
GenotypeCallModel.allele)
.where(
GenotypeCallModel.sample_id.in_(sample_ids),
_id_in(GenotypeCallModel.sample_id, sample_ids),
GenotypeCallModel.marker_id.isnot(None),
GenotypeCallModel.allele.isnot(None),
GenotypeCallModel.is_deleted.is_(False),
@@ -5909,7 +5930,7 @@ class StatisticsService:
marker_ids = sorted({r.marker_id for r in call_rows})
markers = (await self.db.execute(
select(MarkerModel.id, MarkerModel.marker_name, MarkerModel.marker_type).where(
MarkerModel.id.in_(marker_ids), MarkerModel.is_deleted.is_(False))
_id_in(MarkerModel.id, marker_ids), MarkerModel.is_deleted.is_(False))
)).all()
if not markers:
raise CustomException(msg="标记不存在")
@@ -6068,7 +6089,7 @@ class StatisticsService:
select(GenotypeCallModel.sample_id, GenotypeCallModel.marker_id,
GenotypeCallModel.allele)
.where(
GenotypeCallModel.sample_id.in_(sample_ids),
_id_in(GenotypeCallModel.sample_id, sample_ids),
GenotypeCallModel.marker_id.isnot(None),
GenotypeCallModel.allele.isnot(None),
GenotypeCallModel.is_deleted.is_(False),
@@ -6081,7 +6102,7 @@ class StatisticsService:
select(MarkerModel.id, MarkerModel.marker_name,
MarkerModel.chromosome, MarkerModel.position,
MarkerModel.marker_type)
.where(MarkerModel.id.in_(marker_ids), MarkerModel.is_deleted.is_(False))
.where(_id_in(MarkerModel.id, marker_ids), MarkerModel.is_deleted.is_(False))
)).all()
if not markers:
raise CustomException(msg="标记不存在")
@@ -6594,7 +6615,7 @@ class StatisticsService:
select(GenotypeCallModel.sample_id, GenotypeCallModel.marker_id,
GenotypeCallModel.allele)
.where(
GenotypeCallModel.marker_id.in_(marker_ids),
_id_in(GenotypeCallModel.marker_id, marker_ids),
GenotypeCallModel.allele.isnot(None),
GenotypeCallModel.is_deleted.is_(False),
)
@@ -6603,7 +6624,7 @@ class StatisticsService:
return out
sample_ids = sorted({c.sample_id for c in calls})
samples = (await self.db.execute(
select(GenotypeSampleModel).where(GenotypeSampleModel.id.in_(sample_ids))
select(GenotypeSampleModel).where(_id_in(GenotypeSampleModel.id, sample_ids))
)).scalars().all()
sample_tree, _ = await self._sample_to_tree_map(samples)
tree_geno: dict[int, dict[int, str]] = {}
@@ -6744,7 +6765,7 @@ class StatisticsService:
# GXE 分支:环境维度记录 → rec_phenos(与 run_ablup 同构)
grp_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.clone_id, TreeModel.combination_id).where(
TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
clone_of = {t.id: t.clone_id for t in grp_rows}
combo_of = {t.id: t.combination_id for t in grp_rows}
@@ -6764,7 +6785,7 @@ class StatisticsService:
else:
site_rows = (await self.db.execute(
select(TreeModel.id, TreeModel.trial_study_id).where(
TreeModel.id.in_(tree_ids), TreeModel.is_deleted.is_(False))
_id_in(TreeModel.id, tree_ids), TreeModel.is_deleted.is_(False))
)).all()
site_of = {t.id: t.trial_study_id for t in site_rows}
rec_phenos = {}
@@ -12,6 +12,7 @@ from app.utils.dict_util import DictLabelResolver, dict_value_to_label
from app.utils.number_gen import NumberGenService
from .crud import BreedingTreeCRUD
from .model import TreeModel
from .schema import (
TreeCreateSchema,
TreeOutSchema,
@@ -370,8 +371,10 @@ class TreeService:
await BreedingTreeCRUD(self.auth, self.db).delete(ids=ids)
async def list_options(self) -> list[dict[str, Any]]:
"""供前端下拉选择使用:返回 [{value, label}]。"""
obj_list = await BreedingTreeCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
"""供前端下拉选择使用:返回 [{value, label}]。只取 id/tree_no 两列,避免全表列加载拖慢下拉。"""
obj_list = await BreedingTreeCRUD(self.auth, self.db).get_list(
order_by=[{"id": "asc"}], load_columns=[TreeModel.id, TreeModel.tree_no]
)
return [{"value": o.id, "label": o.tree_no} for o in obj_list]
@staticmethod