""" 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