Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.

Commit dc45521

Browse files
Add tagging system for WorkPrecision and filtering helpers
Add a `tags` field to `WorkPrecision` allowing algorithms to be categorized with symbolic tags (e.g., `:rosenbrock`, `:stiff`, `:reference`). Tags are specified via the `:tags` key in setups dicts and propagated through all WorkPrecisionSet constructors (ODE, BVP, RODE, Ensemble, Nonlinear). New exported functions: - `filter_by_tags(wp_set, tags...)` - filter by ALL tags (AND logic) - `exclude_by_tags(wp_set, tags...)` - exclude entries with ANY tag (OR logic) - `get_tags(wp_set)` - get tags for each entry - `unique_tags(wp_set)` - all unique tags in the set - `merge_wp_sets(sets...)` - combine multiple WorkPrecisionSets Plot recipe extended with `tags`, `include_tags`, and `exclude_tags` kwargs for tag-based filtering directly in plot calls. Fully backward compatible - tags default to empty, all existing APIs unchanged. Closes #177 (Phase 1) Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent acccfd3 commit dc45521

File tree

5 files changed

+298
-11
lines changed

5 files changed

+298
-11
lines changed

src/DiffEqDevTools.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export test_convergence, analyticless_test_convergence, appxtrue!, appxtrue, def
5454

5555
export get_sample_errors
5656

57+
#Tagging and Filtering
58+
export filter_by_tags, exclude_by_tags, get_tags, unique_tags, merge_wp_sets
59+
5760
#Tab Functions
5861
export stability_region, residual_order_condition, check_tableau,
5962
imaginary_stability_interval

src/benchmark.jl

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ relates to error (precision) across different tolerance settings.
226226
- `name`: Name of the algorithm
227227
- `error_estimate`: Error metric used (e.g., `:final`, `:l2`)
228228
- `N`: Number of tolerance settings tested
229+
- `tags`: Symbolic tags for categorizing this algorithm (e.g., `[:rosenbrock, :stiff, :4th_order]`)
229230
230231
# Example
231232
```julia
@@ -248,6 +249,7 @@ mutable struct WorkPrecision
248249
name::Any
249250
error_estimate::Any
250251
N::Int
252+
tags::Vector{Symbol}
251253
end
252254

253255
"""
@@ -294,7 +296,8 @@ end
294296
function WorkPrecision(
295297
prob, alg, abstols, reltols, dts = nothing;
296298
name = nothing, appxsol = nothing, error_estimate = :final,
297-
numruns = 20, seconds = 2, reduction = default_reduction, kwargs...
299+
numruns = 20, seconds = 2, reduction = default_reduction,
300+
tags::Vector{Symbol} = Symbol[], kwargs...
298301
)
299302
N = length(abstols)
300303
errors = Vector{Dict{Symbol, Float64}}(undef, N)
@@ -417,15 +420,16 @@ function WorkPrecision(
417420
end
418421
return WorkPrecision(
419422
prob, abstols, reltols, _dicts_to_structarray(errors),
420-
times, dts, stats, name, error_estimate, N
423+
times, dts, stats, name, error_estimate, N, tags
421424
)
422425
end
423426

424427
# Work precision information for a BVP
425428
function WorkPrecision(
426429
prob::AbstractBVProblem, alg, abstols, reltols, dts = nothing;
427430
name = nothing, appxsol = nothing, error_estimate = :final,
428-
numruns = 20, seconds = 2, reduction = default_reduction, kwargs...
431+
numruns = 20, seconds = 2, reduction = default_reduction,
432+
tags::Vector{Symbol} = Symbol[], kwargs...
429433
)
430434
N = length(abstols)
431435
errors = Vector{Dict{Symbol, Float64}}(undef, N)
@@ -547,15 +551,15 @@ function WorkPrecision(
547551
end
548552
return WorkPrecision(
549553
prob, abstols, reltols, _dicts_to_structarray(errors),
550-
times, dts, stats, name, error_estimate, N
554+
times, dts, stats, name, error_estimate, N, tags
551555
)
552556
end
553557

554558
# Work precision information for a nonlinear problem.
555559
function WorkPrecision(
556560
prob::NonlinearProblem, alg, abstols, reltols, dts = nothing; name = nothing,
557561
appxsol = nothing, error_estimate = :l2, numruns = 20, seconds = 2,
558-
reduction = default_reduction, kwargs...
562+
reduction = default_reduction, tags::Vector{Symbol} = Symbol[], kwargs...
559563
)
560564
N = length(abstols)
561565
errors = Vector{Dict{Symbol, Float64}}(undef, N)
@@ -612,7 +616,7 @@ function WorkPrecision(
612616

613617
return WorkPrecision(
614618
prob, abstols, reltols, _dicts_to_structarray(errors),
615-
times, dts, stats, name, error_estimate, N
619+
times, dts, stats, name, error_estimate, N, tags
616620
)
617621
end
618622

@@ -636,11 +640,13 @@ function WorkPrecisionSet(
636640
_dts = get(setups[i], :dts, nothing)
637641
filtered_setup = filter(p -> p.first in DiffEqBase.allowedkeywords, setups[i])
638642

643+
_tags = get(setups[i], :tags, Symbol[])
639644
wps[i] = WorkPrecision(
640645
prob, setups[i][:alg], _abstols, _reltols, _dts;
641646
appxsol = appxsol,
642647
error_estimate = error_estimate,
643-
name = names[i], reduction = reduction, kwargs..., filtered_setup...
648+
name = names[i], reduction = reduction,
649+
tags = _tags, kwargs..., filtered_setup...
644650
)
645651
end
646652
return WorkPrecisionSet(
@@ -792,7 +798,8 @@ function WorkPrecisionSet(
792798
wps = [WorkPrecision(
793799
prob, _abstols[i], _reltols[i],
794800
_dicts_to_structarray(errors[i]),
795-
times[:, i], _dts[i], stats, names[i], error_estimate, N
801+
times[:, i], _dts[i], stats, names[i], error_estimate, N,
802+
get(setups[i], :tags, Symbol[])
796803
)
797804
for i in 1:N]
798805
return WorkPrecisionSet(
@@ -922,7 +929,8 @@ function WorkPrecisionSet(
922929
stats = nothing
923930
wps = [WorkPrecision(
924931
prob, _abstols[i], _reltols[i], errors[i], times[:, i],
925-
_dts[i], stats, names[i], error_estimate, N
932+
_dts[i], stats, names[i], error_estimate, N,
933+
get(setups[i], :tags, Symbol[])
926934
)
927935
for i in 1:N]
928936
return WorkPrecisionSet(
@@ -951,11 +959,13 @@ function WorkPrecisionSet(
951959
_dts = get(setups[i], :dts, nothing)
952960
filtered_setup = filter(p -> p.first in DiffEqBase.allowedkeywords, setups[i])
953961

962+
_tags = get(setups[i], :tags, Symbol[])
954963
wps[i] = WorkPrecision(
955964
prob, setups[i][:alg], _abstols, _reltols, _dts;
956965
appxsol = appxsol,
957966
error_estimate = error_estimate,
958-
name = names[i], reduction = reduction, kwargs..., filtered_setup...
967+
name = names[i], reduction = reduction,
968+
tags = _tags, kwargs..., filtered_setup...
959969
)
960970
end
961971
return WorkPrecisionSet(
@@ -1040,6 +1050,97 @@ function get_sample_errors(
10401050
end
10411051
end
10421052

1053+
"""
1054+
filter_by_tags(wp_set::WorkPrecisionSet, tags::Symbol...) -> WorkPrecisionSet
1055+
1056+
Return a new `WorkPrecisionSet` containing only entries whose tags include
1057+
ALL of the specified tags (AND logic).
1058+
1059+
# Example
1060+
```julia
1061+
setups = [
1062+
Dict(:alg => Rosenbrock23(), :tags => [:rosenbrock, :2nd_order]),
1063+
Dict(:alg => Rodas5P(), :tags => [:rosenbrock, :5th_order]),
1064+
Dict(:alg => TRBDF2(), :tags => [:bdf, :2nd_order]),
1065+
]
1066+
wp_set = WorkPrecisionSet(prob, abstols, reltols, setups)
1067+
rosenbrock_only = filter_by_tags(wp_set, :rosenbrock)
1068+
second_order_rosenbrock = filter_by_tags(wp_set, :rosenbrock, :2nd_order)
1069+
```
1070+
"""
1071+
function filter_by_tags(wp_set::WorkPrecisionSet, tags::Symbol...)
1072+
isempty(tags) && return wp_set
1073+
indices = findall(wp -> all(t -> t in wp.tags, tags), wp_set.wps)
1074+
_subset_wps(wp_set, indices)
1075+
end
1076+
1077+
"""
1078+
exclude_by_tags(wp_set::WorkPrecisionSet, tags::Symbol...) -> WorkPrecisionSet
1079+
1080+
Return a new `WorkPrecisionSet` excluding entries that have ANY of the specified tags.
1081+
1082+
# Example
1083+
```julia
1084+
no_reference = exclude_by_tags(wp_set, :reference)
1085+
```
1086+
"""
1087+
function exclude_by_tags(wp_set::WorkPrecisionSet, tags::Symbol...)
1088+
isempty(tags) && return wp_set
1089+
indices = findall(wp -> !any(t -> t in wp.tags, tags), wp_set.wps)
1090+
_subset_wps(wp_set, indices)
1091+
end
1092+
1093+
"""
1094+
get_tags(wp_set::WorkPrecisionSet) -> Vector{Vector{Symbol}}
1095+
1096+
Return the tags for each entry in the `WorkPrecisionSet`.
1097+
"""
1098+
get_tags(wp_set::WorkPrecisionSet) = [wp.tags for wp in wp_set.wps]
1099+
1100+
"""
1101+
unique_tags(wp_set::WorkPrecisionSet) -> Vector{Symbol}
1102+
1103+
Return all unique tags present across entries in the `WorkPrecisionSet`.
1104+
"""
1105+
function unique_tags(wp_set::WorkPrecisionSet)
1106+
alltags = Symbol[]
1107+
for wp in wp_set.wps
1108+
append!(alltags, wp.tags)
1109+
end
1110+
return unique!(sort!(alltags))
1111+
end
1112+
1113+
"""
1114+
merge_wp_sets(sets::WorkPrecisionSet...) -> WorkPrecisionSet
1115+
1116+
Merge multiple `WorkPrecisionSet`s into a single set. All entries are combined.
1117+
The metadata (abstols, reltols, prob, error_estimate) is taken from the first set.
1118+
"""
1119+
function merge_wp_sets(sets::WorkPrecisionSet...)
1120+
isempty(sets) && throw(ArgumentError("At least one WorkPrecisionSet is required"))
1121+
wps = vcat([s.wps for s in sets]...)
1122+
all_setups = vcat([s.setups for s in sets]...)
1123+
all_names = vcat([s.names for s in sets]...)
1124+
N = length(wps)
1125+
first_set = first(sets)
1126+
return WorkPrecisionSet(
1127+
wps, N, first_set.abstols, first_set.reltols, first_set.prob,
1128+
all_setups, all_names, first_set.error_estimate, first_set.numruns
1129+
)
1130+
end
1131+
1132+
function _subset_wps(wp_set::WorkPrecisionSet, indices::Vector{Int})
1133+
isempty(indices) &&
1134+
@warn "No entries match the specified tags. Returning empty WorkPrecisionSet."
1135+
wps = wp_set.wps[indices]
1136+
setups = wp_set.setups isa AbstractVector ? wp_set.setups[indices] : wp_set.setups
1137+
names = wp_set.names isa AbstractVector ? wp_set.names[indices] : wp_set.names
1138+
return WorkPrecisionSet(
1139+
wps, length(wps), wp_set.abstols, wp_set.reltols, wp_set.prob,
1140+
setups, names, wp_set.error_estimate, wp_set.numruns
1141+
)
1142+
end
1143+
10431144
Base.length(wp::WorkPrecision) = wp.N
10441145
Base.size(wp::WorkPrecision) = length(wp)
10451146
Base.getindex(wp::WorkPrecision, i::Int) = wp.times[i]

src/plotrecipes.jl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,38 @@ end
102102
x = wp_set.error_estimate,
103103
y = :times,
104104
view = :benchmark,
105-
color = nothing
105+
color = nothing,
106+
tags = nothing,
107+
include_tags = nothing,
108+
exclude_tags = nothing
106109
)
110+
# Apply tag-based filtering if specified
111+
if tags !== nothing || include_tags !== nothing || exclude_tags !== nothing
112+
filtered = wp_set
113+
if tags !== nothing
114+
filtered = filter_by_tags(filtered, tags...)
115+
end
116+
if include_tags !== nothing
117+
extra = filter_by_tags(wp_set, include_tags...)
118+
# Merge: filtered + extra (deduplicated)
119+
seen = Set(wp.name for wp in filtered.wps)
120+
extra_wps = [wp for wp in extra.wps if wp.name seen]
121+
if !isempty(extra_wps)
122+
all_wps = vcat(filtered.wps, extra_wps)
123+
all_names = [wp.name for wp in all_wps]
124+
filtered = WorkPrecisionSet(
125+
all_wps, length(all_wps), filtered.abstols, filtered.reltols,
126+
filtered.prob, filtered.setups, all_names, filtered.error_estimate,
127+
filtered.numruns
128+
)
129+
end
130+
end
131+
if exclude_tags !== nothing
132+
filtered = exclude_by_tags(filtered, exclude_tags...)
133+
end
134+
wp_set = filtered
135+
end
136+
107137
if view == :benchmark
108138
seriestype --> :path
109139
linewidth --> 3

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ end
3232
@time @testset "Error Reduction Function Tests" begin
3333
include("error_reduction_tests.jl")
3434
end
35+
@time @testset "Tagging Tests" begin
36+
include("tagging_tests.jl")
37+
end

0 commit comments

Comments
 (0)