diff --git a/src/analysis/clade_cumulus.jl b/src/analysis/clade_cumulus.jl index a702fde2..63cbdf24 100644 --- a/src/analysis/clade_cumulus.jl +++ b/src/analysis/clade_cumulus.jl @@ -28,7 +28,11 @@ const DEFAULT_TAXONOMIC_RANKS = [ "Class", "Order", "Family", "Genus", "Species", ] -const PLACEMENT_WEIGHT_SUM_TOL = 1e-5 +# Placement tools may truncate a query's candidate list, so retained +# like-weight ratios can legitimately sum to slightly less than 1 (a single +# retained placement at 0.98 is complete evidence). Sums far from 1 indicate +# a malformed or partial record and stay undecided. +const PLACEMENT_WEIGHT_SUM_TOL = 0.05 ## Phylogenetic tree structures & Newick / jplace parsers mutable struct PhyloNode @@ -202,8 +206,12 @@ function parse_newick(src::AbstractString; warnings::Vector{String} = String[]): end end - function read_label_and_quoted()::Tuple{String,Bool} - skip_ws_and_leading_comments!() + function read_label_and_quoted(; capture_nhx::Union{Ref{Union{Float64,Nothing}},Nothing} = nothing, + capture_edge::Union{Ref{Union{Int,Nothing}},Nothing} = nothing)::Tuple{String,Bool} + # An NHX comment immediately after ')' precedes the (usually empty) + # label, so the capture refs must be threaded through this first skip + # or the support tag is consumed as an anonymous comment. + skip_ws_and_leading_comments!(capture_nhx = capture_nhx, capture_edge = capture_edge) pos[] > n && return ("", false) if bytes[pos[]] == UInt8('\'') pos[] += 1 @@ -260,7 +268,7 @@ function parse_newick(src::AbstractString; warnings::Vector{String} = String[]): nhx_sup = Ref{Union{Float64,Nothing}}(nothing) edge_num = Ref{Union{Int,Nothing}}(nothing) - raw_lbl, was_quoted = read_label_and_quoted() + raw_lbl, was_quoted = read_label_and_quoted(capture_nhx = nhx_sup, capture_edge = edge_num) skip_ws_and_leading_comments!(capture_nhx = nhx_sup, capture_edge = edge_num) branch_len = 0.0 @@ -991,7 +999,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 @@ -1000,7 +1008,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 @@ -1009,9 +1017,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 diff --git a/src/core/doi.jl b/src/core/doi.jl index 6a2f926a..f5b0bd95 100644 --- a/src/core/doi.jl +++ b/src/core/doi.jl @@ -16,6 +16,8 @@ # * Durable write-ahead journal; immutable DOI/provenance receipts # * DOI insertion never rewrites scientific objects +module DOI + using Dates, SHA, HTTP, JSON3, Logging export DOIConfig, Deposition, PublicationReceipt, diff --git a/src/server/routes/clade_cumulus.jl b/src/server/routes/clade_cumulus.jl index 45825b61..0dd6918d 100644 --- a/src/server/routes/clade_cumulus.jl +++ b/src/server/routes/clade_cumulus.jl @@ -64,7 +64,11 @@ function _probe_optional_phylo_tool(tool_key::String, fallbacks::Vector{String}) end end -function _load_run_table_df(study::String, run::String, table::String; group::Union{String,Nothing}=nothing) +function _load_run_table_df(study::AbstractString, run::AbstractString, table::AbstractString; group::Union{AbstractString,Nothing}=nothing) + # JSON3 hands request fields back as SubStrings; the DuckDB helpers are + # String-typed, so normalise once at this boundary. + study = String(study); run = String(run); table = String(table) + group = isnothing(group) ? nothing : String(group) dir = _require_duckdb(study, run; group) isnothing(dir) && return (nothing, nothing, json_error(404, "no_results", "No results for run '$run' - run the pipeline first")) @@ -142,7 +146,11 @@ end db = DuckDB.DB(db_path) con = DBInterface.connect(db) try - names_df = DataFrame(DBInterface.execute(con, "SHOW TABLES")) + # Real tables only: SHOW TABLES also lists views, and a view backed + # by a session-registered DataFrame cannot be bound (let alone + # counted) from this fresh connection. + names_df = DataFrame(DBInterface.execute(con, + "SELECT table_name AS name FROM duckdb_tables() WHERE NOT internal")) tlist = sort(String[string(r.name) for r in eachrow(names_df)]) for t in tlist cols = _duckdb_columns(con, t) diff --git a/test/unit/test_clade_cumulus.jl b/test/unit/test_clade_cumulus.jl index da09bc18..6e12a0c9 100644 --- a/test/unit/test_clade_cumulus.jl +++ b/test/unit/test_clade_cumulus.jl @@ -252,8 +252,8 @@ done echo "(MMTIP_000001:0.05,(MMTIP_000002:0.03,MMTIP_000003:0.04)0.910:0.12,MMTIP_000004:0.08);" > "$OUT" """) chmod(mock_ft, 0o755) - old_ft = get(PH.Tools._tools, "fasttree", nothing) - PH.Tools._tools["fasttree"] = mock_ft + old_ft = get(MetaManifold.Tools._tools, "fasttree", nothing) + MetaManifold.Tools._tools["fasttree"] = mock_ft try s4 = [ "ASV_1" => "ACGTACGTACGTACGTACGT", @@ -267,9 +267,9 @@ echo "(MMTIP_000001:0.05,(MMTIP_000002:0.03,MMTIP_000003:0.04)0.910:0.12,MMTIP_0 r["sha256"] == bytes2hex(open(sha256, mock_ft)), ft_res.tool_records) finally if old_ft === nothing - delete!(PH.Tools._tools, "fasttree") + delete!(MetaManifold.Tools._tools, "fasttree") else - PH.Tools._tools["fasttree"] = old_ft + MetaManifold.Tools._tools["fasttree"] = old_ft end end end