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
356 changes: 349 additions & 7 deletions src/analysis/clade_cumulus.jl

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/core/doi.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/pipeline/phylogeny.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export REFERENCE_STEPS, PLACEMENT_STEPS, Step, placement_settings, run_workflow,
using ..PipelineLog
using ..Validation
using ..Tools: tool_bin, _sq, _run_logged, _safe_optional_args, _num
import ..Tools
using ..RemoteExec: run_remote, checked_target
import ..Provenance

Expand Down
64 changes: 39 additions & 25 deletions src/server/routes/clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ 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::String, run::String, table::AbstractString;
group::Union{String,Nothing}=nothing)
table = String(table)
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"))
Expand Down Expand Up @@ -92,9 +94,9 @@ function _resolve_tree_inputs_from_request(
body,
source_digests::OrderedDict{String,String},
)
tree_source = lowercase(strip(string(get(body, :treeSource, "auto"))))
tree_file = strip(string(get(body, :treeFile, "")))
inline_newick = strip(string(get(body, :newick, "")))
tree_source = String(lowercase(strip(string(get(body, :treeSource, "auto")))))
tree_file = String(strip(string(get(body, :treeFile, ""))))
inline_newick = String(strip(string(get(body, :newick, ""))))

if tree_source == "study_tree" || (!isempty(tree_file) && tree_source == "auto")
_valid_tree_file(tree_file) || return (nothing, nothing, :auto,
Expand Down Expand Up @@ -145,19 +147,27 @@ end
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)
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}(
"table" => t,
"row_count" => nrows,
"sample_count" => length(scols),
"has_sequence" => has_seq,
"ranks" => ranks,
))
# `SHOW TABLES` includes views; a view backed by a DuckDB
# replacement scan is bound to the connection that registered it
# and cannot be queried from a new connection.
try
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)
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}(
"table" => t,
"row_count" => nrows,
"sample_count" => length(scols),
"has_sequence" => has_seq,
"ranks" => ranks,
))
catch err
err isa InterruptException && rethrow()
@warn "clade-cumulus sources: skipping unreadable DuckDB object" object=t exception=err
end
end
finally
DBInterface.close!(con)
Expand Down Expand Up @@ -200,10 +210,10 @@ 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"))))
mode_str = String(lowercase(strip(string(get(body, :mode, "combined")))))
mode_str in ("phylogenetic", "taxonomic", "combined") ||
return json_error(400, "invalid_mode", "mode must be 'phylogenetic', 'taxonomic', or 'combined'")
mode_sym = Symbol(mode_str)
Expand Down Expand Up @@ -253,6 +263,7 @@ end
# Compute Faith's PD + Unweighted & Weighted UniFrac when a phylogenetic tree is present,
# including split `.jplace` query placements (`<qname>@edge<N>#<k>`).
diversity_payload = nothing
try
if cc_res.phylo_root !== nothing && !isempty(meta.sample_cols)
tip_list = sort!(collect(MetaManifold.CladeCumulus._collect_tree_tips!(Set{String}(), cc_res.phylo_root)))
id_col = MetaManifold.CladeCumulus._detect_id_col(df)
Expand All @@ -268,13 +279,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 @@ -328,6 +339,9 @@ end
"top_branch_contributions" => top_branches,
)
end
catch e
@error "clade-cumulus diversity computation failed" exception=(e, catch_backtrace())
return json_error(500, "clade_cumulus_diversity_failed", sprint(showerror, e))
end

receipt = MetaManifold.Epistemic.create_receipt(;
Expand Down Expand Up @@ -386,15 +400,15 @@ end
return json_error(400, "invalid_json", "Request body must be valid JSON")
end

source_id = strip(string(get(body, :sourceId, "")))
target_id = strip(string(get(body, :targetId, "")))
source_id = String(strip(string(get(body, :sourceId, ""))))
target_id = String(strip(string(get(body, :targetId, ""))))
(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"))))
mode_str = String(lowercase(strip(string(get(body, :mode, "combined")))))
mode_str in ("phylogenetic", "taxonomic", "combined") ||
return json_error(400, "invalid_mode", "mode must be 'phylogenetic', 'taxonomic', or 'combined'")
mode_sym = Symbol(mode_str)
Expand Down
2 changes: 1 addition & 1 deletion src/server/routes/cladistics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ function _cc_compute(study::String, id::String)
isnothing(doc) && error("The Cladistics source document is unreadable")
counts, tips, samples, counts_doc = _cc_read_counts(study, id)
tree_text = read(_clad_files(study, id)["reference.treefile"], String)
tree = CC.parse_newick(tree_text)
tree = CC.parse_phylogenetic_tree(tree_text)
Set(keys(tree.tips)) == Set(tips) ||
error("The inferred tree tips do not exactly match the saved ASV count columns")
analysed = CC.analyse_phylogeny(tree, counts, tips, samples)
Expand Down
4 changes: 2 additions & 2 deletions src/server/routes/trees.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ const _tree_view_lock = ReentrantLock()
_trees_dir(study::String) = joinpath(ServerState.projects_dir(), study, "trees")
_tree_view_path(study::String, file::String) = joinpath(_trees_dir(study), ".views", file * ".json")

_tree_format(file::String) = endswith(lowercase(file), ".jplace") ? "jplace" : "newick"
_tree_format(file::AbstractString) = endswith(lowercase(file), ".jplace") ? "jplace" : "newick"

function _valid_tree_file(file::String)::Bool
function _valid_tree_file(file::AbstractString)::Bool
_valid_name(file) && any(ext -> endswith(lowercase(file), ext), _TREE_EXTENSIONS)
end

Expand Down
1 change: 0 additions & 1 deletion src/server/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ module Server
include(joinpath(@__DIR__, "routes", "phylogeny.jl"))
include(joinpath(@__DIR__, "routes", "cladistics.jl"))
include(joinpath(@__DIR__, "routes", "clade_cumulus.jl"))
include(joinpath(@__DIR__, "routes", "cladistics.jl"))

## Bad request middleware
# Helpers deep in a route (e.g. `_req_group`) throw this to reject a request.
Expand Down
8 changes: 4 additions & 4 deletions test/unit/test_clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down
Loading