Skip to content

Commit

Permalink
Add lod loq, bug fix
Browse files Browse the repository at this point in the history
  • Loading branch information
yufongpeng committed Mar 26, 2024
1 parent 36970b1 commit 40095d3
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 11 deletions.
10 changes: 8 additions & 2 deletions src/ChemistryQuantitativeAnalysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ export ColumnDataTable, RowDataTable,
accuracy, set_accuracy, set_accuracy!, update_accuracy!,
isdof, isisd,
findanalyte, getanalyte, findsample, getsample, eachanalyte, eachsample,
dynamic_range, lloq, uloq, signal_range, signal_lloq, signal_uloq,
dynamic_range, lloq, uloq, signal_range, signal_lloq, signal_uloq, lod, loq,
formula_repr, weight_repr, weight_value, formula_repr_utf8, weight_repr_utf8, format_number, mkbatch,
typedmap
typedmap,
ui_init

import Base: getproperty, propertynames, show, write, eltype, length, iterate,
getindex, setindex!, insert!, get!, delete!, get,
Expand Down Expand Up @@ -444,4 +445,9 @@ RowDataTable(analytecol::Symbol, tablesink::TypeOrFn, tbl::ColumnDataTable) =
RowDataTable(analytecol::Symbol, tbl::ColumnDataTable) =
RowDataTable(tbl, analytecol)

function ui_init()
@eval ChemistryQuantitativeAnalysis include(joinpath(@__DIR__(), "ui", "ui.jl"))
@info "Use function `icalibrate!` to start a UI for a batch."
end

end
6 changes: 3 additions & 3 deletions src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Base.pairs(at::AnalysisTable) = pairs(tables(at))
Base.keys(at::AnalysisTable) = keys(tables(at))
Base.values(at::AnalysisTable) = values(tables(at))
Base.haskey(at::AnalysisTable, i) = haskey(tables(at), i)
Base.iterate(at::AnalysisTable, st = 1) = st > length(at) ? nothing : (first(Base.iterate(tables(at), st)), st + 1)
Base.iterate(at::AnalysisTable, s...) = Base.iterate(tables(at), s...)
Base.length(at::AnalysisTable) = length(tables(at))
Base.copy(at::AnalysisTable) = AnalysisTable(copy(analyteobj(at)), copy(sampleobj(at)), Dictionary(collect(keys(tables(at))), values(tables(at))))
function Base.getproperty(tbl::AnalysisMethod, p::Symbol)
Expand All @@ -65,9 +65,9 @@ function Base.getproperty(tbl::AnalysisMethod, p::Symbol)
getfield(tbl, :analytetable).analyte[getfield(tbl, :analytetable).isd .>= 0]
elseif p == :point
s = getfield(tbl, :signaltable)
isnothing(s) ? s : s.sample
isnothing(s) ? s : sampleobj(s)
elseif p == :level
getfield(tbl, :conctable).sample
sampleobj(getfield(tbl, :conctable))
else
getfield(tbl, p)
end
Expand Down
37 changes: 36 additions & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,35 @@ Return theoretical signal of upper limit of quantification.
"""
signal_uloq(cal::MultipleCalibration) = only(predict(cal.model, Table(; x = [uloq(cal)])))

function blank(cal::MultipleCalibration{A, N}) where {A, N}
β = cal.model.model.pp.beta0::Vector{N}
if cal.type
b = length(β) == 1 ? 0 : first(β)
else
length(β) == 2 && return 0
c, b, a = β
a < 0 && return c
max(0, b ^ 2 / 4a - b ^ 2 / 2a + c)
end
end

"""
loq(cal::MultipleCalibration)
Theoretical limit of quantification (LOQ); concentration of signal adding blank signal and 10 times standard deviation of LLOQ signal.
Blank signal is defined as the lowest signal such that the corresponding concentration is larger than 0.
"""
loq(cal::MultipleCalibration) = only(inv_predict(cal, [blank(cal) + 10 * std(cal.table.y[findfirst(cal.table.include)])]))
"""
lod(cal::MultipleCalibration)
Theoretical limit of quantification (LOQ); concentration of signal adding blank signal and 3.3 times standard deviation of LLOQ signal.
Blank signal is defined as the lowest signal such that the corresponding concentration is larger than 0.
"""
lod(cal::MultipleCalibration) = only(inv_predict(cal, [blank(cal) + 3.3 * std(cal.table.y[findfirst(cal.table.include)])]))

"""
weight_repr(cal::MultipleCalibration)
weight_repr(weight::Number)
Expand All @@ -266,6 +295,8 @@ elseif weight == -2
"1/x²"
elseif weight == 0
"none"
elseif weight == 1
"x"
elseif weight > 0
"x^$weight"
else
Expand All @@ -276,12 +307,16 @@ end
Return value of weight from string representation. See "weight_repr".
"""
weight_value(weight) = if weight == "1/√x"
weight_value(weight) = if weight == "none"
0
elseif weight == "1/√x"
-0.5
elseif weight == "1/x"
-1
elseif weight == "1/x²"
-2
elseif weight == "x"
1
else
neg = match(r"1/x\^(\d*)", weight)
isnothing(neg) ? parse(Float64, first(match(r"x\^(\d*)", weight))) : -parse(Float64, first(neg))
Expand Down
26 changes: 21 additions & 5 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,24 @@ end
@test propertynames(rdata.area) == (:Analyte, Symbol.(["S1", "S2", "S3"])...)
end
@testset "Interface.jl" begin
cdata.area[AnalyteTest("G2(drug_a)"), "S1"] = 6
@test cdata.area[AnalyteTest("G2(drug_a)"), "S1"] == 6
cdata.area[AnalyteTest("G2(drug_a)"), "S1"] = 4
@test cdata.area[AnalyteTest("G2(drug_a)"), "S1"] == 4
cdata.area[AnalyteTest("G2(drug_a)"), "S1"] = 6.0
@test cdata.area[AnalyteTest("G2(drug_a)"), "S1"] == 6.0
rdata.area[AnalyteTest("G2(drug_a)"), "S1"] = 4
@test rdata.area[AnalyteTest("G2(drug_a)"), "S1"] == 4
rdata.area[AnalyteTest("G2(drug_a)"), "S1"] = 6
rdata.area[AnalyteTest("G2(drug_a)"), "S1"] = 6.0
@test rdata.area[AnalyteTest("G2(drug_a)"), "S1"] == 6.0
@test rdata.area[1] == (Analyte = analytes[1], S1 = 6.0, S2 = 24.0, S3 = 54.0)
cbatch.calibration[AnalyteTest("G1(drug_a)")] = copy(cbatch.calibration[AnalyteTest("G1(drug_a)")])
@test get!(cdata, :area, cdata.area) == get(cdata, :area, nothing)
@test haskey(cdata, :area)
cdata[:area] = copy(cdata[:area])
@test all([v for (k, v) in zip(keys(rdata), values(rdata))] .== [v for v in rdata])
@test cbatch.isd == cbatch.method.isd
@test cbatch.nonisd == cbatch.method.nonisd
@test cbatch.point == cbatch.method.point
@test cbatch.level == cbatch.method.level
@test collect(cdata.area)[1].var"G1(drug_a)" == collect(rdata.area)[1].var"S1"
@test collect(columns(cdata.area))[2][1] == collect(rows(rdata.area))[1][2]
@test collect(columns(rdata.area))[2][1] == collect(rows(cdata.area))[1][2]
Expand Down Expand Up @@ -181,11 +194,14 @@ end
@test getanalyte(rdata.area, AnalyteG1("G1(drug_b)")) == getanalyte(rdata.area, Symbol("G1(drug_b)"))
@test getsample(cdata.area, "S2") == getsample(cdata.area, Symbol("S2"))
@test getsample(rdata.area, Symbol("S2")) == getproperty(CQA.table(rdata.area), Symbol("S2"))
@test samplename(rdata) == samplename(rdata.area)
@test analytename(cdata) == analytename(cdata.area)
@test all(isapprox.(dynamic_range(cbatch.calibration[1]), (1, 100)))
@test all(isapprox.(signal_range(rbatch.calibration[2]), signal_range(cbatch.calibration[2])))
@test all(isapprox.(signal_range(rbatch.calibration[2]), (signal_lloq(cbatch.calibration[2]), signal_uloq(cbatch.calibration[2]))))
@test endswith(formula_repr_utf8(cbatch.calibration[2]), "x^2")
@test weight_repr_utf8(cbatch.calibration[1]) == "none"
@test weight_value("1/x^3") == -3
@test all(weight_value.(["none", "1/√x", "1/x", "1/x²", "x", "x^2", "1/x^3"]) .== [0, -0.5, -1, -2, 1, 2, -3])
@test all(weight_repr_utf8.(Number[0, -0.5, -1, -2, 1, 2, -3]) .== ["none", "1/x^0.5", "1/x", "1/x^2", "x", "x^2", "1/x^3"])
end
@testset "IO" begin
global initial_mc_c = ChemistryQuantitativeAnalysis.read(joinpath(datapath, "initial_mc_c.batch"), DataFrame)
Expand Down

0 comments on commit 40095d3

Please sign in to comment.