diff --git a/docs/Project.toml b/docs/Project.toml index d7b8ccfd..dc288c65 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,7 +1,13 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" +RecursiveArrayToolsRaggedArrays = "13072b0f-2c55-5437-9ae7-d433b7a33950" + +[sources] +RecursiveArrayTools = {path = ".."} +RecursiveArrayToolsRaggedArrays = {path = "../lib/RecursiveArrayToolsRaggedArrays"} [compat] Documenter = "1.3" RecursiveArrayTools = "4" +RecursiveArrayToolsRaggedArrays = "1" diff --git a/docs/make.jl b/docs/make.jl index 2093aa0c..87628932 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,4 @@ -using Documenter, RecursiveArrayTools +using Documenter, RecursiveArrayTools, RecursiveArrayToolsRaggedArrays cp("./docs/Manifest.toml", "./docs/src/assets/Manifest.toml", force = true) cp("./docs/Project.toml", "./docs/src/assets/Project.toml", force = true) @@ -8,7 +8,7 @@ include("pages.jl") makedocs( sitename = "RecursiveArrayTools.jl", authors = "Chris Rackauckas", - modules = [RecursiveArrayTools], + modules = [RecursiveArrayTools, RecursiveArrayToolsRaggedArrays], clean = true, doctest = false, linkcheck = true, warnonly = [:missing_docs], format = Documenter.HTML( diff --git a/docs/src/array_types.md b/docs/src/array_types.md index d3fb25b0..19f712a3 100644 --- a/docs/src/array_types.md +++ b/docs/src/array_types.md @@ -15,3 +15,11 @@ DiffEqArray ArrayPartition NamedArrayPartition ``` + +## Convenience Constructor Types + +```@docs +VA +AP +AllObserved +``` diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 56beb125..dc288c65 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -1,7 +1,13 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" +RecursiveArrayToolsRaggedArrays = "13072b0f-2c55-5437-9ae7-d433b7a33950" + +[sources] +RecursiveArrayTools = {path = ".."} +RecursiveArrayToolsRaggedArrays = {path = "../lib/RecursiveArrayToolsRaggedArrays"} [compat] Documenter = "1.3" -RecursiveArrayTools = "3" +RecursiveArrayTools = "4" +RecursiveArrayToolsRaggedArrays = "1" diff --git a/docs/src/plotting.md b/docs/src/plotting.md index 16e4f600..fb23b5ff 100644 --- a/docs/src/plotting.md +++ b/docs/src/plotting.md @@ -152,3 +152,16 @@ additional features: These advanced features are defined in SciMLBase and activate automatically when plotting solution objects. + +## Plot Helper API + +```@docs +DEFAULT_PLOT_FUNC +plottable_indices +plot_indices +getindepsym_defaultt +interpret_vars +add_labels! +diffeq_to_arrays +solplot_vecs_and_labels +``` diff --git a/docs/src/ragged_arrays.md b/docs/src/ragged_arrays.md index 0a3ed7e6..d2a3c89f 100644 --- a/docs/src/ragged_arrays.md +++ b/docs/src/ragged_arrays.md @@ -184,4 +184,6 @@ structure and access elements without implicit zeros. ```@docs RecursiveArrayTools.AbstractRaggedVectorOfArray RecursiveArrayTools.AbstractRaggedDiffEqArray +RecursiveArrayToolsRaggedArrays.RaggedVectorOfArray +RecursiveArrayToolsRaggedArrays.RaggedDiffEqArray ``` diff --git a/docs/src/recursive_array_functions.md b/docs/src/recursive_array_functions.md index 48eae61a..48d1e79e 100644 --- a/docs/src/recursive_array_functions.md +++ b/docs/src/recursive_array_functions.md @@ -9,6 +9,15 @@ and do not require that the RecursiveArrayTools types are used. recursivecopy recursivecopy! recursivecopyto! +recursivefill! vecvecapply copyat_or_push! +vecvec_to_mat +recursive_one +recursive_mean +recursive_bottom_eltype +recursive_unitless_bottom_eltype +recursive_unitless_eltype +vecarr_to_vectors +tuples ``` diff --git a/src/utils.jl b/src/utils.jl index b96224cb..2f538f10 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -267,7 +267,12 @@ for type in [AbstractArray, AbstractVectorOfArray] end end -# Deprecated +""" + vecvec_to_mat(vecvec) + +Convert a vector of equal-length vectors into a dense matrix whose rows are the +input vectors. +""" function vecvec_to_mat(vecvec) mat = Matrix{eltype(eltype(vecvec))}(undef, length(vecvec), length(vecvec[1])) for i in 1:length(vecvec) @@ -351,6 +356,12 @@ Calls `one` on the bottom container to get the "true element one type". recursive_one(a) = recursive_one(a[1]) recursive_one(a::T) where {T <: Number} = one(a) +""" + recursive_bottom_eltype(x) + +Return the scalar element type at the bottom of nested array-like element +types. +""" recursive_bottom_eltype(a) = a == eltype(a) ? a : recursive_bottom_eltype(eltype(a)) """ @@ -395,6 +406,12 @@ end recursive_unitless_eltype(a::Type{T}) where {T <: Number} = typeof(one(eltype(a))) recursive_unitless_eltype(::Type{<:Enum{T}}) where {T} = T +""" + recursive_mean(x) + +Compute a mean value through recursive array containers without requiring +`Statistics.mean`. +""" function recursive_mean(vecvec::Vector{T}) where {T <: AbstractArray} out = zero(vecvec[1]) for i in eachindex(vecvec) diff --git a/src/vector_of_array.jl b/src/vector_of_array.jl index ac673613..39c9f752 100644 --- a/src/vector_of_array.jl +++ b/src/vector_of_array.jl @@ -74,6 +74,13 @@ mutable struct DiffEqArray{ dense::Bool end ### Abstract Interface + +""" + AllObserved() + +Sentinel used by symbolic indexing paths to request all observed variables from +an `AbstractDiffEqArray`. +""" struct AllObserved end @@ -936,6 +943,11 @@ function Base.:(==)(A::AbstractVectorOfArray, B::AbstractVectorOfArray) end # Comparison with plain arrays uses AbstractArray element-wise comparison via default +""" + tuples(A::DiffEqArray) + +Return the saved time/state pairs of `A` as `(t, u)` tuples. +""" tuples(VA::DiffEqArray) = tuple.(VA.t, VA.u) # Growing the array simply adds to the container vector @@ -1132,7 +1144,11 @@ function Base.fill!(VA::AbstractVectorOfArray, x) end return VA end -# conversion tools +""" + vecarr_to_vectors(A::AbstractVectorOfArray) + +Collect the component time series of `A` as one vector per component. +""" vecarr_to_vectors(VA::AbstractVectorOfArray) = [VA[i, :] for i in eachindex(VA.u[1])] # linear algebra ArrayInterface.issingular(va::AbstractVectorOfArray) = ArrayInterface.issingular(Matrix(va)) @@ -1170,11 +1186,30 @@ end ## Plotting helper functions (shared with SciMLBase) +""" + DEFAULT_PLOT_FUNC(x, y) + DEFAULT_PLOT_FUNC(x, y, z) + +Default transformation used by plotting helpers when no custom plotting +function is supplied. +""" DEFAULT_PLOT_FUNC(x, y) = (x, y) DEFAULT_PLOT_FUNC(x, y, z) = (x, y, z) +""" + plottable_indices(x) + +Return the default component indices to plot for `x`. +""" plottable_indices(x::AbstractArray) = 1:length(x) plottable_indices(x::Number) = 1 + +""" + plot_indices(A::AbstractArray) + +Return the index iterator used when expanding array-valued data into plot +series. +""" plot_indices(A::AbstractArray) = eachindex(A) """ @@ -1266,6 +1301,11 @@ function _var_label(A, x) end end +""" + add_labels!(labels, x, dims, A, strs) + +Append the plot label for the variable tuple `x` to `labels`. +""" function add_labels!(labels, x, dims, A, strs) if ((x[2] isa Integer && x[2] == 0) || isequal(x[2], getindepsym_defaultt(A))) && dims == 2 @@ -1319,6 +1359,12 @@ function diffeq_to_arrays( return solplot_vecs_and_labels(dims, vars, plott, A) end +""" + solplot_vecs_and_labels(dims, vars, plott, A) + +Build plot vectors and labels for interpreted plotting variables over the +sample points `plott`. +""" function solplot_vecs_and_labels(dims, vars, plott, A) plot_vecs = [] labels = String[] diff --git a/test/QA/qa.jl b/test/QA/qa.jl index e41650eb..983c9073 100644 --- a/test/QA/qa.jl +++ b/test/QA/qa.jl @@ -1,5 +1,168 @@ using SciMLTesting, RecursiveArrayTools, Test, Pkg +const PACKAGE_ROOT = normpath(joinpath(@__DIR__, "..", "..")) + +function source_roots() + roots = [joinpath(PACKAGE_ROOT, "src")] + lib_root = joinpath(PACKAGE_ROOT, "lib") + if isdir(lib_root) + append!(roots, joinpath.(lib_root, readdir(lib_root), "src")) + end + return filter(isdir, roots) +end + +function source_files() + files = String[] + for root in source_roots() + for (dir, _, filenames) in walkdir(root), filename in filenames + endswith(filename, ".jl") && push!(files, joinpath(dir, filename)) + end + end + return sort(files) +end + +count_triple_quotes(line) = length(collect(eachmatch(r"\"\"\"", line))) +strip_line_comment(line) = replace(line, r"#.*" => "") + +function public_names_from_statement(statement) + statement = replace(statement, r"^\s*(export|public|@public)\s+" => "") + statement = replace(statement, r"^\s*SciMLPublic\.@public\s+" => "") + ignored = Set(["Expr", "SciMLPublic", "eval", "export", "public", "return"]) + names = Symbol[] + for match in eachmatch(r"[A-Za-z_][A-Za-z0-9_!]*", statement) + name = match.match + name in ignored && continue + push!(names, Symbol(name)) + end + return names +end + +function public_api_names() + names = Set{Symbol}() + for file in source_files() + lines = readlines(file) + in_docstring = false + index = 1 + while index <= length(lines) + line = lines[index] + raw = in_docstring ? "" : strip_line_comment(line) + if occursin(r"^\s*(export|public|@public)\b", raw) || + occursin(r"^\s*SciMLPublic\.", raw) + statement = raw + parens = count(==('('), statement) - count(==(')'), statement) + while (endswith(rstrip(statement), ",") || parens > 0) && + index < length(lines) + index += 1 + statement *= " " * strip_line_comment(lines[index]) + parens = count(==('('), statement) - count(==(')'), statement) + end + union!(names, public_names_from_statement(statement)) + end + isodd(count_triple_quotes(line)) && (in_docstring = !in_docstring) + index += 1 + end + end + return names +end + +is_doc_macro(x) = x == Symbol("@doc") || + (x isa GlobalRef && x.name == Symbol("@doc")) + +function definition_head_name(x) + x isa Symbol && return x + x isa Expr || return nothing + if x.head in (:curly, :where, :call, :<:) + return definition_head_name(x.args[1]) + elseif x.head == :. + last = x.args[end] + return last isa QuoteNode ? last.value : last + else + return nothing + end +end + +function documented_definition_names(x) + names = Set{Symbol}() + x isa Expr || return names + name = if x.head == :struct + definition_head_name(x.args[2]) + elseif x.head == :abstract + definition_head_name(x.args[1]) + elseif x.head in (:function, :(=)) + definition_head_name(x.args[1]) + elseif x.head == :const + return documented_definition_names(x.args[1]) + else + nothing + end + name isa Symbol && push!(names, name) + return names +end + +function collect_docstring_names!(names, x) + x isa Expr || return names + if x.head == :macrocall && length(x.args) >= 4 && is_doc_macro(x.args[1]) + union!(names, documented_definition_names(x.args[end])) + end + foreach(arg -> collect_docstring_names!(names, arg), x.args) + return names +end + +function source_docstring_names() + names = Set{Symbol}() + for file in source_files() + collect_docstring_names!(names, Meta.parseall(read(file, String))) + end + return names +end + +function rendered_docs_pages() + docs_root = joinpath(PACKAGE_ROOT, "docs") + pages_file = joinpath(docs_root, "pages.jl") + pages = String[] + for match in eachmatch(r"\"([^\"]+\.md)\"", read(pages_file, String)) + path = joinpath(docs_root, "src", match.captures[1]) + isfile(path) && push!(pages, path) + end + return unique(pages) +end + +function docs_entry_name(line) + item = split(strip_line_comment(line))[1] + component = split(item, '.')[end] + match = Base.match(r"^([A-Za-z_][A-Za-z0-9_!]*)", component) + return match === nothing ? nothing : Symbol(match.captures[1]) +end + +function rendered_docs_entries() + entries = Set{Symbol}() + for file in rendered_docs_pages() + in_docs_block = false + for line in eachline(file) + stripped = strip(line) + if startswith(stripped, "```@docs") + in_docs_block = true + elseif in_docs_block && startswith(stripped, "```") + in_docs_block = false + elseif in_docs_block && !isempty(stripped) && !startswith(stripped, "#") + name = docs_entry_name(stripped) + name isa Symbol && push!(entries, name) + end + end + end + return entries +end + +@testset "Public API documentation coverage" begin + public_api = public_api_names() + + missing_source_docstrings = sort!(collect(setdiff(public_api, source_docstring_names()))) + @test isempty(missing_source_docstrings) + + missing_rendered_docs = sort!(collect(setdiff(public_api, rendered_docs_entries()))) + @test isempty(missing_rendered_docs) +end + # yes this is horrible, we'll fix it when Pkg or Base provides a decent API manifest = Pkg.Types.EnvCache().manifest # these are good sentinels to test whether someone has added a heavy SciML package to the test deps