PSBoardDataBase/src/DispatchChecker.jl

289 lines
9.5 KiB
Julia

"""
Module to check PSBoard is dispatchable.
Use [`interactive_dispatch_checker`](@ref) for interactive use in QAQC.
"""
module DispatchChecker
using SQLite
using DBInterface
using DataFrames
using Printf
using REPL.TerminalMenus
export DbConnection
export is_dispatchable
"""
Stores connection to database.
DbConnection(db::SQLite.DB)
Constructor.
"""
mutable struct DbConnection
db::SQLite.DB
df_single_result::DataFrame
df_extra_results::DataFrame
df_runs::DataFrame
function DbConnection(db::SQLite.DB)
df_single_results =
DBInterface.execute(db, sql"select * from qaqc_single_run_results") |> DataFrame
df_extra_results =
DBInterface.execute(db, sql"select * from qaqc_extra_run_results") |> DataFrame
df_runs = DBInterface.execute(db, sql"select * from qaqc_runs") |> DataFrame
new(db, df_single_results, df_extra_results, df_runs)
end
end
const THRESHOLD_INSUFFICIENT_RESET_WITH_10_CAMPAIGN_1to5 = 0.1
const THRESHOLD_INSUFFICIENT_RESET_WITH_10_CAMPAIGN_6 = 0.05
const THRESHOLD_RESET_FAILED_THOUGH_RECONFIG_DONE = 0.1
const THRESHOLD_ALWAYS_HIT_FLAG_TRUE = 0.1
const THRESHOLD_BCID_FAIL = 0.05
"""
is_dispatchable(conn::DbConnection, psbid::Int64)
Test whether the PS Board with `psbid` is dispatchable from QAQC results in `conn`.
`conn` is type of [`DbConnection`](@ref).
Since the current implemented logic is somewhat simple, it returns `missing` if it cannot be decided.
"""
function is_dispatchable(conn::DbConnection, psbid::Int64)
single_results = filter(:psboard_id => ==(psbid), conn.df_single_result)
extra_results = filter(:psboard_id => ==(psbid), conn.df_extra_results)
is_single_passed::Bool =
nrow(single_results) == 1 && let
single_result = Tables.rowtable(single_results) |> first
# Clock test update was wrong
# manually assign to psboards whose clock test failed
if 221 <= single_result.runid < 234
if single_result.psboard_id == 915
false
elseif single_result.psboard_id in [860, 889, 876, 892]
# though 860 has 2+ rows
true
end
false
else
single_result.resistance_test_passed == 1 &&
single_result.qspip == 1 &&
single_result.recov == 1 &&
single_result.power == 1 &&
single_result.clock == 1
end
end
@debug "" is_single_passed single_results single_results.note
is_extra_passed::Bool =
nrow(extra_results) == 1 && let
extra_result = Tables.rowtable(extra_results) |> first
f1 =
!ismissing(extra_result.insufficient_reset_with_10) && begin
campaign_id =
filter(:id => ==(extra_result.runid), conn.df_runs).campaign_id
@assert length(campaign_id) == 1
campaign_id = first(campaign_id)
if campaign_id 5
extra_result.insufficient_reset_with_10 >=
extra_result.num_tests *
THRESHOLD_INSUFFICIENT_RESET_WITH_10_CAMPAIGN_1to5
else
extra_result.insufficient_reset_with_10 >=
extra_result.num_tests *
THRESHOLD_INSUFFICIENT_RESET_WITH_10_CAMPAIGN_6
end
end
f2 =
!ismissing(extra_result.reset_failed_though_reconfig_done) &&
extra_result.reset_failed_though_reconfig_done >=
extra_result.num_tests * THRESHOLD_RESET_FAILED_THOUGH_RECONFIG_DONE
f3 =
!ismissing(extra_result.always_hit_flag_true) &&
extra_result.always_hit_flag_true >=
extra_result.num_tests * THRESHOLD_ALWAYS_HIT_FLAG_TRUE
f4 =
!ismissing(extra_result.bcid_fail) &&
extra_result.bcid_fail >= extra_result.num_tests * THRESHOLD_BCID_FAIL
@debug "" extra_result extra_result.note f1 f2 f3 f4
!(f1 || f2 || f3 || f4)
end
@debug "" is_extra_passed extra_results
if is_single_passed & is_extra_passed
return true
end
# TODO: not yet implemented
@info "results" sort(single_results, :runid) sort(
select(
extra_results,
Not(
:id,
:num_tests,
:dac_is_0,
:bcid_fail_111,
:bcid_fail_000,
:low_efficiency,
),
),
:runid,
)
@debug "results(full)" extra_results
return missing
end
"""
Interactive session for QAQC to check PSBoard is ready for dispatch.
"""
function interactive_dispatch_checker end
"""
interactive_dispatch_checker(conn::DbConnection)
"""
function interactive_dispatch_checker(conn::DbConnection)
dispatch_list = Int64[]
println("Type \"quit\" to exit")
for _ in 1:1000
printstyled("PSBoard ID: ", bold = true)
psbid = let
rawin = readline()
if lowercase(rawin) == "quit"
printstyled("Quit\n", italic = true)
println()
break
end
m = match(r"^PS(\d+)", rawin)
if isnothing(m)
printstyled("Invalid input\n", color = :red)
continue
end
parse(Int64, m[1])
end
isdispatchable = is_dispatchable(conn, psbid)
if ismissing(isdispatchable)
printstyled("Please determine [y/n]: ", underline = true, color = :cyan)
isdispatchable = let
rawin = readline()
@info "" rawin
if rawin == "y" || rawin == "Y"
true
elseif rawin == "n" || rawin == "N"
false
else
@warn "Invalid input falling back to \"no\""
false
end
end
end
if isdispatchable
printstyled("Ok\n", bold = true, color = :green)
if psbid in dispatch_list
println("PSBoard ID $(psbid) is already in dispatch list")
else
push!(dispatch_list, psbid)
println("Added to dispatch list")
end
else
printstyled("No\n", bold = true, color = :red)
end
end
printstyled("Finished\n")
map(dispatch_list) do psbid
@sprintf "PS%06d" psbid
end |> (v -> join(v, "\n")) |> print
println()
printstyled("Paste the result to google sheets\n", underline = true)
@info "Tips: You can use `join(ans, \"\\n\") |> clipboard` in REPL to copy the result to the clipboard"
return dispatch_list
end
"""
interactive_dispatch_checker(database_file::AbstractString)
Interactive session for QAQC to check provided PSBoard is ready to dispatch.
"""
function interactive_dispatch_checker(database_file::AbstractString)
conn = DbConnection(SQLite.DB(database_file))
interactive_dispatch_checker(conn)
end
"""
scan_dispatchcheck(conn::DbConnection; reasons = NamedTuple[])
Interactively scan PS Boards to check they passed the QAQC.
If the board have suspicious results but actually passed the test, you can select one reason for that board or
add new reason to explain.
# Arguments
- `reasons::Vector{NamedTuple}`: reasons why given board passed the test. This is *mutated* during the session.
## reason structure
One element of the `reason` should be like this:
```julia
(
name = "name of this category",
passing_pairs = NamedTuple[
(; psboard_id, note = "supplemental note"),
others...
],
)
```
"""
function scan_dispatchcheck(conn::DbConnection; reasons = NamedTuple[])
df_ps_boards = DBInterface.execute(conn.db, sql"select * from ps_boards") |> DataFrame
pushfirst!(reasons, (name = "add new reason", passing_pairs = NamedTuple[]))
for row_ps_boards in eachrow(df_ps_boards)
old_result = is_dispatchable(conn, row_ps_boards.id)
if !ismissing(old_result) && old_result
continue
end
if any(reasons) do reason
any(reason.passing_pairs) do passing_pair
matched = row_ps_boards.id == passing_pair.psboard_id
if matched
@info "psbid $(row_ps_boards.id) passed for $(reason.name)"
end
matched
end
end
continue
end
@info "missing: $(row_ps_boards.id)"
selected = request(
"select passing reason(press q to skip):",
RadioMenu([nt.name for nt in reasons]),
)
if selected == -1
println("none selected. keep missing")
elseif selected == 1
print("new reason name: ")
new_name = readline()
print("note for this case: ")
note = readline()
new_reason = (
name = new_name,
passing_pairs = NamedTuple[(psboard_id = row_ps_boards.id, note)],
)
push!(reasons, new_reason)
else
println("selected reason: $(reasons[selected].name)")
print("note for this case: ")
note = readline()
push!(reasons[selected].passing_pairs, (psboard_id = row_ps_boards.id, note))
end
end
@assert popfirst!(reasons).name == "add new reason"
end
end # module DispatchChecker