Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,26 @@ jobs:
status="${PIPESTATUS[0]}"
if [ "$status" -ne 0 ]; then
enc() { head -c 3500 | sed -e 's/%/%25/g' -e 's/\r/%0D/g' | awk '{printf "%s%%0A", $0}'; }
echo "::error title=Validated-statistics tests failed::$(tail -n 120 /tmp/validated-statistics-tests.log | enc)"
awk '
/Test Summary:/ { left = 0 }
/(Error: (validated run statistics|numeric input did not fail closed|exponent numeric token did not fail closed)|Test Failed at|Error During Test at)/ || (left == 0 && /Got exception outside of a @test/) { n++; left = 60 }
left > 0 { print > ("/tmp/validated-failure-" n ".txt"); left-- }
' /tmp/validated-statistics-tests.log
shown=0
for f in $(ls /tmp/validated-failure-*.txt 2>/dev/null | sort -t- -k3 -n); do
shown=$((shown + 1))
[ "$shown" -gt 8 ] && break
echo "::error title=Validated-statistics failure ${shown}::$(enc < "$f")"
done
awk '
/Test Summary:/ { print; p = index($0, "Pass"); pe = (p ? p + 3 : 0); t = index($0, "Total"); next }
pe && t && index($0, "|") && substr($0, pe + 1, t - pe - 1) ~ /[0-9]/ { print; next }
/ERROR:|Some tests did not pass/ { print }
' /tmp/validated-statistics-tests.log | tail -n 60 > /tmp/validated-statistics-summary.txt
echo "::error title=Validated-statistics tests failed (summary)::$(enc < /tmp/validated-statistics-summary.txt)"
if [ "$shown" -eq 0 ]; then
echo "::error title=Validated-statistics tests failed (tail)::$(tail -c 3500 /tmp/validated-statistics-tests.log | enc)"
fi
fi
exit "$status"

Expand Down
30 changes: 20 additions & 10 deletions src/analysis/clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ function parse_newick(src::AbstractString; warnings::Vector{String} = String[]):
end

function read_label_and_quoted()::Tuple{String,Bool}
skip_ws_and_leading_comments!()
while pos[] <= n && bytes[pos[]] in (UInt8(' '), UInt8('\t'), UInt8('\r'), UInt8('\n'))
pos[] += 1
end
pos[] > n && return ("", false)
if bytes[pos[]] == UInt8('\'')
pos[] += 1
Expand Down Expand Up @@ -260,6 +262,9 @@ function parse_newick(src::AbstractString; warnings::Vector{String} = String[]):

nhx_sup = Ref{Union{Float64,Nothing}}(nothing)
edge_num = Ref{Union{Int,Nothing}}(nothing)
# Comments directly after ')' may carry NHX support for the internal node;
# consume them with capture enabled before reading its optional label.
skip_ws_and_leading_comments!(capture_nhx = nhx_sup, capture_edge = edge_num)
raw_lbl, was_quoted = read_label_and_quoted()
skip_ws_and_leading_comments!(capture_nhx = nhx_sup, capture_edge = edge_num)

Expand Down Expand Up @@ -991,7 +996,7 @@ function _graft_jplace_queries(

for q in jd.queries
q.status === :valid && !isempty(q.candidates) || continue
total_w = sum((c.weight for c in q.candidates); init = 0.0)
total_w = sum((c.like_weight for c in q.candidates); init = 0.0)
total_w > 0.0 || continue
multi = length(q.candidates) > 1
for (qname, mult) in q.names
Expand All @@ -1000,7 +1005,7 @@ function _graft_jplace_queries(
tx = get(asv_deepest_taxon, qname, nothing)
tconf = get(asv_tax_conf, qname, 1.0)
for (ci, cand) in enumerate(q.candidates)
share = cand.weight / total_w
share = cand.like_weight / total_w
tip_id = multi ? string(qname, "@edge", cand.edge_num, "#", ci) : qname
if multi
asv_reads[tip_id] = get(asv_reads, tip_id, 0.0) + base_cnt * share
Expand All @@ -1009,9 +1014,9 @@ function _graft_jplace_queries(
elseif !haskey(asv_reads, tip_id)
asv_reads[tip_id] = base_cnt
end
query_lwr[tip_id] = cand.weight
query_lwr[tip_id] = cand.like_weight
push!(get!(by_edge, cand.edge_num, _GraftedQuerySpec[]),
_GraftedQuerySpec(tip_id, qname, share, max(0.0, cand.distal_length), max(0.0, cand.pendant_length), cand.weight))
_GraftedQuerySpec(tip_id, qname, share, max(0.0, cand.distal_len), max(0.0, cand.pendant_len), cand.like_weight))
end
end
end
Expand All @@ -1021,8 +1026,8 @@ function _graft_jplace_queries(

function walk(u::PhyloNode)::PhyloNode
rebuilt_kids = PhyloNode[walk(ch) for ch in u.children]
cur = PhyloNode(u.id, u.name, u.branch_length, u.support, u.edge_num, rebuilt_kids)
specs = u.edge_num === nothing ? nothing : get(by_edge, u.edge_num, nothing)
cur = PhyloNode(u.id, u.name, u.branch_length, u.support, u.edge_number, rebuilt_kids)
specs = u.edge_number === nothing ? nothing : get(by_edge, u.edge_number, nothing)
(specs === nothing || isempty(specs)) && return cur

# Order grafts from most distal (closest to `cur`, smallest `distal`) to most proximal.
Expand All @@ -1040,7 +1045,7 @@ function _graft_jplace_queries(
sub = PhyloNode(string("jplace_clade#", seq_auto[]), "", 0.0, 1.0, nothing, PhyloNode[sub, qtip])
end
rem_len = max(0.0, total_len - prev_distal)
PhyloNode(sub.id, sub.name, rem_len, cur.support, cur.edge_num, sub.children)
PhyloNode(sub.id, sub.name, rem_len, cur.support, cur.edge_number, sub.children)
end

walk(base_root)
Expand Down Expand Up @@ -1783,11 +1788,12 @@ function _reaggregate_after_move(
mono = get(total_tips_per_taxon, u.dominant_taxon, 0) == 1
end
active_here = u.cumulative_count > 0.0
sub_pd = active_here && !is_root ? max(0.0, u.branch_length) : 0.0
st = Epistemic.epistemic_status(case_obj, u.id)
leaf_node = CladeNode(
u.id, u.name, u.rank, u.direct_count, u.cumulative_count, freq,
u.residual_count, Epistemic.cloud_size_by_residual(u.residual_count),
u.branch_length, u.support, 0.0, u.dominant_taxon, u.taxon_purity, mono,
u.branch_length, u.support, sub_pd, u.dominant_taxon, u.taxon_purity, mono,
st, Epistemic.epistemic_colour(st), CladeNode[],
)
return (leaf_node, tmap, 1, active_here)
Expand All @@ -1809,13 +1815,17 @@ function _reaggregate_after_move(
n_sub_tips += nt
if ch_active
any_active = true
sub_pd += max(0.0, rk.branch_length) + rk.subtree_pd
sub_pd += rk.subtree_pd
end
for (k, v) in ct
merged_taxa[k] = get(merged_taxa, k, 0) + v
end
end

# Match `clade_faith_pd`: an active non-root clade contributes its own
# incoming branch in addition to the branches accumulated below it.
!is_root && any_active && (sub_pd += max(0.0, u.branch_length))

Comment thread
coderabbitai[bot] marked this conversation as resolved.
dom_tx = nothing
dom_n = 0
for (tx, c) in merged_taxa
Expand Down
35 changes: 14 additions & 21 deletions src/server/routes/clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,14 @@ function _load_run_table_df(study::String, run::String, table::String; group::Un
db_path = joinpath(dir, "results.duckdb")
isfile(db_path) || return (nothing, nothing, json_error(404, "no_results",
"No results database for run '$run'"))
db = DuckDB.DB(db_path)
con = DBInterface.connect(db)
try
with_results_db(dir) do con
cols = _duckdb_columns(con, table)
isempty(cols) && return (nothing, nothing, json_error(404, "table_not_found",
"Table '$table' not found in run '$run'"))
scols = _sample_count_columns(con, table)
order = _stable_order_clause(cols)
df = DataFrame(DBInterface.execute(con, "SELECT * FROM \"$table\" $order"))
return (df, (db_path = db_path, sample_cols = scols, columns = cols), nothing)
finally
DBInterface.close!(con)
close(db)
(df, (db_path = db_path, sample_cols = scols, columns = cols), nothing)
end
end

Expand All @@ -93,7 +88,7 @@ function _resolve_tree_inputs_from_request(
source_digests::OrderedDict{String,String},
)
tree_source = lowercase(strip(string(get(body, :treeSource, "auto"))))
tree_file = strip(string(get(body, :treeFile, "")))
tree_file = String(strip(string(get(body, :treeFile, ""))))
inline_newick = strip(string(get(body, :newick, "")))

if tree_source == "study_tree" || (!isempty(tree_file) && tree_source == "auto")
Expand Down Expand Up @@ -138,30 +133,28 @@ end
dir = _require_duckdb(study, run; group)
db_path = isnothing(dir) ? nothing : joinpath(dir, "results.duckdb")
tables_info = OrderedDict{String,Any}[]
if !isnothing(db_path) && isfile(db_path)
db = DuckDB.DB(db_path)
con = DBInterface.connect(db)
try
if !isnothing(dir) && !isnothing(db_path) && isfile(db_path)
tables_info = with_results_db(dir) do con
available = OrderedDict{String,Any}[]
names_df = DataFrame(DBInterface.execute(con, "SHOW TABLES"))
tlist = sort(String[string(r.name) for r in eachrow(names_df)])
for t in tlist
cols = _duckdb_columns(con, t)
scols = _sample_count_columns(con, t)
isempty(scols) && continue
nrows = Int(only(DataFrame(DBInterface.execute(con, "SELECT COUNT(*) AS n FROM \"$t\""))).n)
nrows = Int(only(DataFrame(DBInterface.execute(con,
"SELECT COUNT(*) AS n FROM \"$t\""))).n)
has_seq = any(c -> c in ("Sequence", "sequence", "seq", "DNA"), cols)
ranks = [r for r in MetaManifold.CladeCumulus.DEFAULT_TAXONOMIC_RANKS if r in cols]
push!(tables_info, OrderedDict{String,Any}(
push!(available, OrderedDict{String,Any}(
"table" => t,
"row_count" => nrows,
"sample_count" => length(scols),
"has_sequence" => has_seq,
"ranks" => ranks,
))
end
finally
DBInterface.close!(con)
close(db)
available
end
end

Expand Down Expand Up @@ -200,7 +193,7 @@ end
return json_error(400, "invalid_json", "Request body must be valid JSON")
end

table = strip(string(get(body, :table, "merged")))
table = String(strip(string(get(body, :table, "merged"))))
_valid_name(table) || return json_error(400, "invalid_table", "Invalid table name '$table'")

mode_str = lowercase(strip(string(get(body, :mode, "combined"))))
Expand Down Expand Up @@ -268,13 +261,13 @@ end
jd_div = MetaManifold.CladeCumulus.parse_jplace(jplace_str)
for q in jd_div.queries
q.status === :valid && !isempty(q.candidates) || continue
tw = sum((c.weight for c in q.candidates); init = 0.0)
tw = sum((c.like_weight for c in q.candidates); init = 0.0)
tw > 0.0 || continue
multi = length(q.candidates) > 1
for (qname, _) in q.names
for (ci, cand) in enumerate(q.candidates)
tid = multi ? string(qname, "@edge", cand.edge_num, "#", ci) : qname
tip_weights[tid] = cand.weight / tw
tip_weights[tid] = cand.like_weight / tw
end
end
end
Expand Down Expand Up @@ -391,7 +384,7 @@ end
(isempty(source_id) || isempty(target_id)) &&
return json_error(400, "missing_clade_ids", "Both sourceId and targetId are required")

table = strip(string(get(body, :table, "merged")))
table = String(strip(string(get(body, :table, "merged"))))
_valid_name(table) || return json_error(400, "invalid_table", "Invalid table name '$table'")

mode_str = lowercase(strip(string(get(body, :mode, "combined"))))
Expand Down
Loading
Loading