Skip to content

Commit

Permalink
unit-def (#22)
Browse files Browse the repository at this point in the history
* allow defining composites with Unitful

it's now possible to define a composite by either relying on the units passed in `units` or by formatting them as units

* revert to requiring definition of units

* update docs and jldoctests
  • Loading branch information
simonsteiger authored Aug 23, 2024
1 parent f857d38 commit aaba884
Show file tree
Hide file tree
Showing 25 changed files with 65 additions and 78 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Now you're ready to start working with composite scores:

```julia
using RheumaComposites, Unitful
das28 = DAS28ESR(tjc=4, sjc=3, pga=4.1, apr=19; units=(pga=u"cm", apr=u"mm/hr"))
das28 = DAS28ESR(tjc=4, sjc=3, pga=4.1u"cm", apr=19u"mm/hr")
score(das28)
isremission(das28)
categorise(das28)
Expand Down
6 changes: 3 additions & 3 deletions docs/src/examples/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ This means that you do not have to remember that SDAI requires a 0-10 cm VAS sca
Let's try this by creating a DAS28CRP composite with patient's global assessment measured in centimeters:

````@example basics
das28_cm = DAS28CRP(tjc=1, sjc=0, pga=2.2, apr=4)
das28_cm = DAS28CRP(tjc=1, sjc=0, pga=2.2u"cm", apr=4u"mg/L")
````

As you can see, centimeters were automatically converted to millimeters.
Providing the same score in millimeters return the same result:

````@example basics
das28_mm = DAS28CRP(tjc=1, sjc=0, pga=22, apr=4)
das28_mm = DAS28CRP(tjc=1, sjc=0, pga=22u"mm", apr=4u"mg/L")
score(das28_cm) == score(das28_mm)
````

Expand All @@ -64,7 +64,7 @@ To see the docstring, first hit `?` in the REPL, then type the name of the compo
This is all we need to explore the most important aspects of many different composite scores!

````@example basics
sdai = SDAI(sjc=3, tjc=4, pga=3.4, ega=2.8, crp=21)
sdai = SDAI(sjc=3, tjc=4, pga=3.4u"cm", ega=2.8u"cm", crp=21u"mg/dL")
````

````@example basics
Expand Down
2 changes: 1 addition & 1 deletion docs/src/examples/categorisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using Unitful
As long as the composite you are defining is a continuous composite, all you need to do is to call [`categorise`](@ref).

````@example categorisation
sdai = SDAI(tjc=2, sjc=1, pga=6, ega=5.5, crp=15)
sdai = SDAI(tjc=2, sjc=1, pga=6u"cm", ega=5.5u"cm", crp=15)
sdai isa ContinuousComposite
````

Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Now you're ready to start working with composite scores:

```julia
using RheumaComposites, Unitful
das28 = DAS28ESR(tjc=4, sjc=3, pga=4.1, apr=19; units=(pga=u"cm", apr=u"mm/hr"))
das28 = DAS28ESR(tjc=4, sjc=3, pga=4.1u"cm", apr=19u"mm/hr")
score(das28)
isremission(das28)
categorise(das28)
Expand Down
6 changes: 3 additions & 3 deletions examples/basics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ This means that you do not have to remember that SDAI requires a 0-10 cm VAS sca
Let's try this by creating a DAS28CRP composite with patient's global assessment measured in centimeters:
=#

das28_cm = DAS28CRP(tjc=1, sjc=0, pga=2.2, apr=4)
das28_cm = DAS28CRP(tjc=1, sjc=0, pga=2.2u"cm", apr=4u"mg/L")

# As you can see, centimeters were automatically converted to millimeters.
# Providing the same score in millimeters return the same result:

das28_mm = DAS28CRP(tjc=1, sjc=0, pga=22, apr=4)
das28_mm = DAS28CRP(tjc=1, sjc=0, pga=22u"mm", apr=4u"mg/L")
score(das28_cm) == score(das28_mm)

#=
Expand All @@ -55,6 +55,6 @@ To see the docstring, first hit `?` in the REPL, then type the name of the compo
This is all we need to explore the most important aspects of many different composite scores!
=#

sdai = SDAI(sjc=3, tjc=4, pga=3.4, ega=2.8, crp=21)
sdai = SDAI(sjc=3, tjc=4, pga=3.4u"cm", ega=2.8u"cm", crp=21u"mg/dL")
#-
score(sdai), isremission(sdai), categorise(sdai)
2 changes: 1 addition & 1 deletion examples/categorisation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ using Unitful

# As long as the composite you are defining is a continuous composite, all you need to do is to call [`categorise`](@ref).

sdai = SDAI(tjc=2, sjc=1, pga=6, ega=5.5, crp=15)
sdai = SDAI(tjc=2, sjc=1, pga=6u"cm", ega=5.5u"cm", crp=15)
sdai isa ContinuousComposite

# Well, that was expected!
Expand Down
2 changes: 1 addition & 1 deletion src/functions/categorise.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Convert `x` to a discrete value.
# Examples
```jldoctest
julia> DAS28ESR(tjc=4, sjc=5, pga=12, apr=44) |> categorise
julia> DAS28ESR(tjc=4, sjc=5, pga=12u"mm", apr=44u"mm/hr") |> categorise
"moderate"
```
"""
Expand Down
4 changes: 2 additions & 2 deletions src/functions/decompose.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ See also [`score`](@ref).
# Examples
```jldoctest
julia> SDAI(tjc=4, sjc=5, pga=1.6, ega=1.2, crp=3) |> decompose
julia> SDAI(tjc=4, sjc=5, pga=1.6u"cm", ega=1.2u"cm", crp=3u"mg/dL") |> decompose
Dict{Symbol, Float64} with 5 entries:
:tjc => 0.27
:ega => 0.081
Expand All @@ -32,7 +32,7 @@ Return the proportion to which each facet contributes to the composite's score.
# Examples
```jldoctest
julia> root = DAS28ESR(tjc=4, sjc=5, pga=14, apr=12);
julia> root = DAS28ESR(tjc=4, sjc=5, pga=14u"mm", apr=12u"mm/hr");
julia> faceted(root, (objective=[:sjc, :apr], subjective=[:tjc, :pga])) |> decompose
Dict{Symbol, Float64} with 2 entries:
Expand Down
4 changes: 2 additions & 2 deletions src/functions/isremission.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ Check whether a composite fulfils remission criteria.
# Examples
```jldoctest
julia> DAS28ESR(tjc=4, sjc=5, pga=44, apr=23) |> isremission
julia> DAS28ESR(tjc=4, sjc=5, pga=44u"mm", apr=23u"mm/hr") |> isremission
false
julia> BooleanRemission(tjc=1, sjc=0, pga=1.4, crp=0.4) |>
julia> BooleanRemission(tjc=1, sjc=0, pga=1.4u"cm", crp=0.4u"mg/dL") |>
revised |>
isremission
true
Expand Down
2 changes: 1 addition & 1 deletion src/functions/score.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Score a composite and optionally specify the rounding precision.
# Examples
```jldoctest
julia> DAS28ESR(tjc=4, sjc=2, pga=64, apr=44) |> score
julia> DAS28ESR(tjc=4, sjc=2, pga=64u"mm", apr=44u"mm/hr") |> score
5.061
```
"""
Expand Down
2 changes: 1 addition & 1 deletion src/functions/weight.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Weight a composite score's components according to its weighting scheme.
# Example
```jldoctest
julia> DAS28CRP(tjc=2, sjc=2, pga=54, apr=19) |> weight
julia> DAS28CRP(tjc=2, sjc=2, pga=54u"mm", apr=19u"mg/L") |> weight
(0.7919595949289333, 0.39597979746446665, 0.756, 1.0784636184794367)
```
"""
Expand Down
4 changes: 2 additions & 2 deletions src/types/basdai.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ struct BASDAI <: ContinuousComposite
values::NTuple{6, Float64}
names::NTuple{6, Symbol}
units::NamedTuple
function BASDAI(; q1, q2, q3, q4, q5, q6, units=BASDAI_UNITS)
function BASDAI(; q1, q2, q3, q4, q5, q6)
ntvals = (; q1, q2, q3, q4, q5, q6)
uvals = unitfy(ntvals, units; conversions=BASDAI_UNITS)
uvals = unitfy(ntvals, BASDAI_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

valid_vas.(values(ucomponents))
Expand Down
10 changes: 5 additions & 5 deletions src/types/boolean.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ struct BooleanRemission <: BooleanComposite
values::NTuple{4, Float64}
names::NTuple{4, Symbol}
units::NamedTuple
function BooleanRemission(; tjc, sjc, pga, crp, units=BOOL_UNITS)
function BooleanRemission(; tjc, sjc, pga, crp)
ntvals = (; tjc, sjc, pga, crp)
uvals = unitfy(ntvals, units; conversions=BOOL_UNITS)
uvals = unitfy(ntvals, BOOL_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

valid_joints.([tjc, sjc])
Expand All @@ -33,13 +33,13 @@ struct BooleanRemission <: BooleanComposite
end
end

function revised(root::BooleanRemission, offsets::NamedTuple; units=BOOL_UNITS)
function revised(root::BooleanRemission, offsets::NamedTuple)
unknown_offset = findfirst((root.names), keys(offsets))
if !(unknown_offset isa Nothing)
throw(error("$(keys(offsets)[unknown_offset]) is not a component of `root`."))
end

uoffsets = unitfy(offsets, units; conversions=BOOL_UNITS)
uoffsets = unitfy(offsets, BOOL_UNITS)
vals = ustrip.(values(uoffsets))
indexes = [findfirst(==(x), root.names) for x in keys(offsets)]
offsets_w_zeros = zeros(length(root.names))
Expand Down Expand Up @@ -69,4 +69,4 @@ The values passed to `offset` will be added to the default thresholds of `root`.
See also [`isremission`](@ref).
"""
revised(root::BooleanRemission; offset=(; pga=1)) = revised(root, offset)
revised(root::BooleanRemission; offset=(; pga=1u"cm")) = revised(root, offset)
4 changes: 2 additions & 2 deletions src/types/cdai.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ struct CDAI <: ContinuousComposite
values::NTuple{4, Float64}
names::NTuple{4, Symbol}
units::NamedTuple
function CDAI(; tjc, sjc, pga, ega, units=XDAI_UNITS)
function CDAI(; tjc, sjc, pga, ega)
ntvals = (; tjc, sjc, pga, ega)
uvals = unitfy(ntvals, units; conversions=XDAI_UNITS)
uvals = unitfy(ntvals, XDAI_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

valid_joints.([tjc, sjc])
Expand Down
4 changes: 2 additions & 2 deletions src/types/dapsa.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ struct DAPSA <: ContinuousComposite
values::NTuple{5, Float64}
names::NTuple{5, Symbol}
units::NamedTuple
function DAPSA(; tjc, sjc, crp, pga, jpn, units=DAPSA_UNITS)
function DAPSA(; tjc, sjc, crp, pga, jpn)
ntvals = (; tjc, sjc, crp, pga, jpn)
uvals = unitfy(ntvals, units; conversions=DAPSA_UNITS)
uvals = unitfy(ntvals, DAPSA_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

mapreduce((jc, max) -> valid_joints(jc; max=max), &, [tjc, sjc], [66, 68])
Expand Down
8 changes: 4 additions & 4 deletions src/types/das28.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ struct DAS28CRP <: DAS28
values::NTuple{4, Float64}
names::NTuple{4, Symbol}
units::NamedTuple
function DAS28CRP(; tjc, sjc, pga, apr, units=DAS28CRP_UNITS)
function DAS28CRP(; tjc, sjc, pga, apr)
ntvals = (; tjc, sjc, pga, apr)
uvals = unitfy(ntvals, units; conversions=DAS28CRP_UNITS)
uvals = unitfy(ntvals, DAS28CRP_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

valid_joints.([tjc, sjc])
Expand Down Expand Up @@ -87,9 +87,9 @@ struct DAS28ESR <: DAS28
values::NTuple{4, Float64}
names::NTuple{4, Symbol}
units::NamedTuple
function DAS28ESR(; tjc, sjc, pga, apr, units=DAS28ESR_UNITS)
function DAS28ESR(; tjc, sjc, pga, apr)
ntvals = (; tjc, sjc, pga, apr)
uvals = unitfy(ntvals, units; conversions=DAS28ESR_UNITS)
uvals = unitfy(ntvals, DAS28ESR_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

valid_joints.([tjc, sjc])
Expand Down
4 changes: 2 additions & 2 deletions src/types/sdai.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ struct SDAI <: ContinuousComposite
values::NTuple{5, Float64}
names::NTuple{5, Symbol}
units::NamedTuple
function SDAI(; tjc, sjc, pga, ega, crp, units=XDAI_UNITS)
function SDAI(; tjc, sjc, pga, ega, crp)
ntvals = (; tjc, sjc, pga, ega, crp)
uvals = unitfy(ntvals, units; conversions=XDAI_UNITS)
uvals = unitfy(ntvals, XDAI_UNITS)
ucomponents = NamedTuple{keys(ntvals)}(uvals)

valid_joints.([tjc, sjc])
Expand Down
12 changes: 5 additions & 7 deletions src/utils/auxfuns.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ function seq_check(x::Real, conds::NamedTuple)
return string(keys(conds)[i])
end

function unitfy(vals, units; conversions)
out = map(keys(vals)) do val_key
val = getproperty(vals, val_key)
haskey(units, val_key) || return val
unit = getproperty(units, val_key)
conversion = getproperty(conversions, val_key)
return uconvert(conversion, val * unit)
function unitfy(vals, conversions) # TODO add validation here?
out = map(keys(vals), values(vals)) do k, v
v isa Real && return v
conversion = getproperty(conversions, k)
return uconvert(conversion, v)
end
return out
end
Expand Down
4 changes: 2 additions & 2 deletions src/utils/valid.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ function valid_vas(x)
end

# Check if APR value is valid
valid_apr(x) = 0 <= ustrip(x) || throw(error("Invalid APR"))
valid_apr(x, min) = min <= ustrip(x) || throw(error("Invalid APR"))
valid_apr(x) = ustrip(x) >= 0 || throw(error("Invalid APR"))
valid_apr(x, min) = ustrip(x) >= min || throw(error("Invalid APR"))
7 changes: 2 additions & 5 deletions test/types/basdai.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# Dummy CDAIs for testing
basdai = BASDAI(q1=1, q2=2, q3=2, q4=2, q5=1, q6=3)
basdai = BASDAI(q1=1u"cm", q2=2u"cm", q3=2u"cm", q4=2u"cm", q5=1u"cm", q6=3u"cm")

# Intercept
i_basdai = RheumaComposites.intercept(cdai)

# Test if different unit scales lead to same results
basdai_u1 = BASDAI(
q1=10, q2=20, q3=20, q4=20, q5=10, q6=30,
units=(q1=u"mm", q2=u"mm", q3=u"mm", q4=u"mm", q5=u"mm", q6=u"mm")
)
basdai_u1 = BASDAI(q1=10u"mm", q2=20u"mm", q3=20u"mm", q4=20u"mm", q5=10u"mm", q6=30u"mm")

basdai_ref = 1.8

Expand Down
2 changes: 1 addition & 1 deletion test/types/boolean.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
boolrem = BooleanRemission(tjc=1, sjc=0, pga=1.4, crp=0.4)
boolrem = BooleanRemission(tjc=1, sjc=0, pga=1.4u"cm", crp=0.4u"mg/dL")

@testset "Original BoolRem" begin
@test boolrem isa AbstractComposite
Expand Down
8 changes: 4 additions & 4 deletions test/types/cdai.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Dummy CDAIs for testing
cdai = CDAI(tjc=1, sjc=0, pga=1, ega=0)
cdai = CDAI(tjc=1, sjc=0, pga=1u"cm", ega=0u"cm")
cdai_ref = 2.0

# Intercept
i_cdai = RheumaComposites.intercept(cdai)

# Test if different unit scales lead to same results
cdai_u1 = CDAI(tjc=0, sjc=1, pga=10, ega=10, units=(pga=u"mm", ega=u"mm"))
cdai_u2 = CDAI(tjc=0, sjc=1, pga=1, ega=1)
cdai_u1 = CDAI(tjc=0, sjc=1, pga=10u"mm", ega=10u"mm")
cdai_u2 = CDAI(tjc=0, sjc=1, pga=1u"cm", ega=1u"cm")

@testset "Construct CDAI" begin
@test cdai isa AbstractComposite
Expand All @@ -25,7 +25,7 @@ end

@testset "CDAI Remission" begin
@test isremission(cdai)
@test !isremission(CDAI(tjc=3, sjc=4, pga=4, ega=4))
@test !isremission(CDAI(tjc=3, sjc=4, pga=4u"cm", ega=4u"cm"))
end

@testset "Categorise CDAI" begin
Expand Down
7 changes: 2 additions & 5 deletions test/types/dapsa.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# Dummy CDAIs for testing
dapsa = DAPSA(tjc=1, sjc=1, crp=3, pga=1, jpn=1)
dapsa = DAPSA(tjc=1, sjc=1, crp=3u"mg/dL", pga=1u"cm", jpn=1u"cm")

# Intercept
i_dapsa = RheumaComposites.intercept(dapsa)

# Test if different unit scales lead to same results
dapsa_ualt = DAPSA(
tjc=1, sjc=1, crp=30, pga=10, jpn=10,
units=(crp=u"mg/L", pga=u"mm", jpn=u"mm")
)
dapsa_ualt = DAPSA(tjc=1, sjc=1, crp=30u"mg/L", pga=10u"mm", jpn=10u"mm")

dapsa_ref = 7.0

Expand Down
Loading

0 comments on commit aaba884

Please sign in to comment.