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:
@@ -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 树 → n≈1204),无关种质
|
||||
在 MME 中零贡献(无表型、与选中树无亲缘),EBV/h² 逐位不变,但 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 会 OOM(36990²×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
|
||||
|
||||
Reference in New Issue
Block a user