import%20marimo%0A%0A__generated_with%20%3D%20%220.17.6%22%0Aapp%20%3D%20marimo.App(width%3D%22full%22)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Titers%20for%20a%20serum%20in%20a%20group%0A%20%20%20%20Analyze%20titers%20for%20a%20serum%20assigned%20to%20a%20group%2C%20aggregating%20replicates%20which%20may%20be%20across%20multiple%20plates.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20io%0A%20%20%20%20import%20pickle%0A%20%20%20%20import%20sys%0A%0A%20%20%20%20import%20altair%20as%20alt%0A%0A%20%20%20%20import%20matplotlib%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%0A%20%20%20%20import%20neutcurve%0A%20%20%20%20from%20neutcurve.marimo_utils%20import%20display_fig_marimo%0A%0A%20%20%20%20import%20numpy%0A%0A%20%20%20%20import%20pandas%20as%20pd%0A%0A%20%20%20%20import%20ruamel.yaml%20as%20yaml%0A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20_%20%3D%20alt.data_transformers.disable_max_rows()%0A%0A%20%20%20%20%23%20faster%20plotting%20of%20neut%20curves%0A%20%20%20%20matplotlib.style.use(%22fast%22)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20alt%2C%0A%20%20%20%20%20%20%20%20display_fig_marimo%2C%0A%20%20%20%20%20%20%20%20io%2C%0A%20%20%20%20%20%20%20%20mo%2C%0A%20%20%20%20%20%20%20%20neutcurve%2C%0A%20%20%20%20%20%20%20%20numpy%2C%0A%20%20%20%20%20%20%20%20pd%2C%0A%20%20%20%20%20%20%20%20pickle%2C%0A%20%20%20%20%20%20%20%20plt%2C%0A%20%20%20%20%20%20%20%20sys%2C%0A%20%20%20%20%20%20%20%20yaml%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell%0Adef%20_(pickle%2C%20sys)%3A%0A%20%20%20%20%23%20Load%20context%20from%20pickled%20file.%0A%20%20%20%20%23%0A%20%20%20%20%23%20This%20cell%20supports%20multiple%20ways%20to%20provide%20context%3A%0A%20%20%20%20%23%201.%20Via%20command-line%3A%20marimo%20export%20html%20notebook.py%20--%20--context-pickle%20path%2Fto%2Fcontext.pickle%0A%20%20%20%20%23%202.%20Via%20saved%20pickle%3A%20Manually%20save%20a%20context%20pickle%20to%20test_example%2Fresults%2Fcontext_dev.pickle%0A%20%20%20%20%23%203.%20Stub%20context%3A%20If%20no%20pickle%20available%2C%20creates%20minimal%20empty%20context%20for%20exploration%0A%20%20%20%20%23%0A%20%20%20%20%23%20For%20interactive%20development%20with%20%60marimo%20edit%60%2C%20you%20can%3A%0A%20%20%20%20%23%20-%20Run%20the%20pipeline%20once%20to%20generate%20a%20real%20context%20pickle%2C%20then%20copy%20it%20to%20context_dev.pickle%0A%20%20%20%20%23%20-%20Or%20work%20with%20the%20stub%20context%20(downstream%20cells%20will%20show%20warnings%2Fempty%20data)%0A%0A%20%20%20%20import%20argparse%0A%20%20%20%20import%20os%0A%20%20%20%20import%20pathlib%0A%0A%20%20%20%20%23%20Check%20if%20context-pickle%20argument%20is%20provided%20(run%20by%20driver%20script)%0A%20%20%20%20from_cmdline%20%3D%20%22--context-pickle%22%20in%20sys.argv%0A%0A%20%20%20%20if%20from_cmdline%3A%0A%20%20%20%20%20%20%20%20%23%20Running%20via%20driver%20script%20-%20parse%20args%0A%20%20%20%20%20%20%20%20print(%22Loading%20context%20from%20command-line%20argument%22)%0A%20%20%20%20%20%20%20%20p%20%3D%20argparse.ArgumentParser()%0A%20%20%20%20%20%20%20%20p.add_argument(%22--context-pickle%22%2C%20required%3DTrue)%0A%20%20%20%20%20%20%20%20args%20%3D%20p.parse_args()%0A%20%20%20%20%20%20%20%20context_pickle_path%20%3D%20pathlib.Path(args.context_pickle)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20Running%20in%20marimo%20edit%20-%20try%20to%20use%20development%20pickle%0A%20%20%20%20%20%20%20%20print(%22Running%20in%20marimo%20edit%20mode%22)%0A%20%20%20%20%20%20%20%20%23%20set%20%60context_pickle_path%60%20to%20valid%20option%20if%20using%20edit%20more%0A%20%20%20%20%20%20%20%20context_pickle_path%20%3D%20None%0A%20%20%20%20%20%20%20%20%23%20context_pickle_path%20%3D%20pathlib.Path(%22test_example%2Fresults%2Fsera%2Fserum_M099d0%2Fserum_M099d0_titers_context.pickle%22)%0A%0A%20%20%20%20%23%20Load%20context%20if%20pickle%20path%20exists%20and%20is%20valid%0A%20%20%20%20if%20context_pickle_path%20and%20context_pickle_path.exists()%3A%0A%20%20%20%20%20%20%20%20print(f%22Reading%20context%20from%20%7Bcontext_pickle_path%7D%22)%0A%20%20%20%20%20%20%20%20with%20open(context_pickle_path%2C%20%22rb%22)%20as%20f_context%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20context%20%3D%20pickle.load(f_context)%0A%0A%20%20%20%20%20%20%20%20%23%20Handle%20working%20directory%0A%20%20%20%20%20%20%20%20context_workdir%20%3D%20context%5B%22workdir%22%5D%0A%20%20%20%20%20%20%20%20current_workdir%20%3D%20os.getcwd()%0A%0A%20%20%20%20%20%20%20%20if%20from_cmdline%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Running%20via%20snakemake%20-%20verify%20workdir%20matches%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20context_workdir%20!%3D%20current_workdir%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20raise%20RuntimeError(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22Context%20workdir%20mismatch!%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22%20%20Context%20was%20created%20in%3A%20%7Bcontext_workdir%7D%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22%20%20Currently%20running%20in%3A%20%20%20%7Bcurrent_workdir%7D%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22This%20should%20not%20happen%20when%20running%20via%20Snakemake.%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Verified%20working%20directory%3A%20%7Bcurrent_workdir%7D%22)%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Running%20in%20marimo%20edit%20-%20change%20to%20context%20workdir%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20context_workdir%20and%20context_workdir%20!%3D%20current_workdir%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Changing%20directory%20from%20%7Bcurrent_workdir%7D%20to%20%7Bcontext_workdir%7D%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20os.chdir(context_workdir)%0A%20%20%20%20%20%20%20%20%20%20%20%20elif%20context_workdir%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Already%20in%20correct%20working%20directory%3A%20%7Bcontext_workdir%7D%22)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20Create%20a%20minimal%20stub%20context%20for%20interactive%20development%0A%20%20%20%20%20%20%20%20print(%22Creating%20minimal%20stub%20context%20that%20you%20need%20to%20complete%22)%0A%20%20%20%20%20%20%20%20context%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22input%22%3A%20%7B%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22output%22%3A%20%7B%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22params%22%3A%20%7B%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22wildcards%22%3A%20%7B%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22threads%22%3A%201%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22resources%22%3A%20%7B%7D%2C%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20return%20(context%2C)%0A%0A%0A%40app.cell%0Adef%20_(context%2C%20mo)%3A%0A%20%20%20%20%23%20Extract%20variables%20from%20context%20-%20raises%20KeyError%20if%20required%20keys%20missing%0A%20%20%20%20pickle_fits%20%3D%20context%5B%22input%22%5D%5B%22pickles%22%5D%0A%20%20%20%20per_rep_titers_csv%20%3D%20context%5B%22output%22%5D%5B%22per_rep_titers%22%5D%0A%20%20%20%20titers_csv%20%3D%20context%5B%22output%22%5D%5B%22titers%22%5D%0A%20%20%20%20curves_pdf%20%3D%20context%5B%22output%22%5D%5B%22curves_pdf%22%5D%0A%20%20%20%20output_pickle%20%3D%20context%5B%22output%22%5D%5B%22pickle%22%5D%0A%20%20%20%20qc_drops_file%20%3D%20context%5B%22output%22%5D%5B%22qc_drops%22%5D%0A%20%20%20%20viral_strain_plot_order%20%3D%20context%5B%22params%22%5D%5B%22viral_strain_plot_order%22%5D%0A%20%20%20%20serum_titer_as%20%3D%20context%5B%22params%22%5D%5B%22serum_titer_as%22%5D%0A%20%20%20%20qc_thresholds%20%3D%20context%5B%22params%22%5D%5B%22qc_thresholds%22%5D%0A%20%20%20%20curve_display_method%20%3D%20context%5B%22params%22%5D%5B%22curve_display_method%22%5D%0A%20%20%20%20serum%20%3D%20context%5B%22wildcards%22%5D%5B%22serum%22%5D%0A%20%20%20%20group%20%3D%20context%5B%22wildcards%22%5D%5B%22group%22%5D%0A%0A%20%20%20%20%23%20Show%20informative%20message%20about%20context%20mode%0A%20%20%20%20if%20not%20context%5B%22input%22%5D%3A%0A%20%20%20%20%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.callout(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22**%E2%9A%A0%EF%B8%8F%20Running%20in%20interactive%20mode%20with%20stub%20context**%5Cn%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22To%20run%20with%20real%20data%3A%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%221.%20Run%20the%20pipeline%20to%20generate%20a%20context%20pickle%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%222.%20Copy%20it%20to%20%60test_example%2Fresults%2Fcontext_dev.pickle%60%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%223.%20Or%20run%3A%20%60marimo%20export%20html%20notebook.py%20--%20--context-pickle%20path%2Fto%2Fcontext.pickle%60%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind%3D%22warn%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.output.append(mo.md(f%22Processing%20%60%7Bgroup%3D%7D%60%2C%20%60%7Bserum%3D%7D%60%22))%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20curve_display_method%2C%0A%20%20%20%20%20%20%20%20curves_pdf%2C%0A%20%20%20%20%20%20%20%20group%2C%0A%20%20%20%20%20%20%20%20output_pickle%2C%0A%20%20%20%20%20%20%20%20per_rep_titers_csv%2C%0A%20%20%20%20%20%20%20%20pickle_fits%2C%0A%20%20%20%20%20%20%20%20qc_drops_file%2C%0A%20%20%20%20%20%20%20%20qc_thresholds%2C%0A%20%20%20%20%20%20%20%20serum%2C%0A%20%20%20%20%20%20%20%20serum_titer_as%2C%0A%20%20%20%20%20%20%20%20titers_csv%2C%0A%20%20%20%20%20%20%20%20viral_strain_plot_order%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Get%20all%20titers%20for%20this%20plate%0A%20%20%20%20Combine%20all%20the%20pickled%20%60neutcurve.CurveFits%60%20from%20plates%20for%20this%20serum%20into%20a%20single%20%60neutcurve.CurveFits%60%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(group%2C%20mo%2C%20neutcurve%2C%20pickle%2C%20pickle_fits%2C%20serum)%3A%0A%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Combining%20the%20curve%20fits%20for%20%60group%3D%7Bgroup!r%7D%60%2C%20%60serum%3D%7Bserum!r%7D%60%20from%20%60pickle_fits%3D%7Bpickle_fits!r%7D%60%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fits_to_combine%20%3D%20%5B%5D%0A%20%20%20%20for%20fname%20in%20pickle_fits%3A%0A%20%20%20%20%20%20%20%20with%20open(fname%2C%20%22rb%22)%20as%20f_combine%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20fits_to_combine.append(pickle.load(f_combine))%0A%20%20%20%20fits_noqc%20%3D%20neutcurve.CurveFits.combineCurveFits(fits_to_combine%2C%20sera%3D%5Bserum%5D)%0A%20%20%20%20return%20(fits_noqc%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Indicate%20how%20we%20are%20calculating%20the%20titer%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20serum_titer_as)%3A%0A%20%20%20%20mo.output.append(mo.md(f%22Calculating%20with%20%60serum_titer_as%3D%7Bserum_titer_as!r%7D%60%22))%0A%20%20%20%20assert%20serum_titer_as%20in%20%7B%22nt50%22%2C%20%22midpoint%22%7D%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Get%20all%20the%20per-replicate%20fit%20params%20with%20the%20titers.%0A%20%20%20%20We%20also%20convert%20the%20IC50%20to%20NT50%2C%20and%20take%20inverse%20of%20midpoint%20to%20get%20it%20on%20same%20scale%20as%20NT50s%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fits_noqc%2C%20group%2C%20mo%2C%20serum%2C%20serum_titer_as%2C%20viral_strain_plot_order)%3A%0A%20%20%20%20per_rep_titers%20%3D%20fits_noqc.fitParams(average_only%3DFalse%2C%20no_average%3DTrue).assign(%0A%20%20%20%20%20%20%20%20group%3Dgroup%2C%0A%20%20%20%20%20%20%20%20nt50%3Dlambda%20x%3A%201%20%2F%20x%5B%22ic50%22%5D%2C%0A%20%20%20%20%20%20%20%20midpoint%3Dlambda%20x%3A%201%20%2F%20x%5B%22midpoint_bound%22%5D%2C%0A%20%20%20%20%20%20%20%20titer%3Dlambda%20x%3A%20x%5B%22midpoint%22%5D%20if%20serum_titer_as%20%3D%3D%20%22midpoint%22%20else%20x%5B%22nt50%22%5D%2C%0A%20%20%20%20%20%20%20%20titer_bound%3Dlambda%20x%3A%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%5B%22midpoint_bound_type%22%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20serum_titer_as%20%3D%3D%20%22midpoint%22%0A%20%20%20%20%20%20%20%20%20%20%20%20else%20x%5B%22ic50_bound%22%5D%0A%20%20%20%20%20%20%20%20).map(%7B%22lower%22%3A%20%22upper%22%2C%20%22upper%22%3A%20%22lower%22%2C%20%22interpolated%22%3A%20%22interpolated%22%7D)%2C%0A%20%20%20%20%20%20%20%20titer_as%3Dserum_titer_as%2C%0A%20%20%20%20)%5B%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22group%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22serum%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22virus%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22replicate%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22titer%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22titer_bound%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22titer_as%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22nt50%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22midpoint%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22top%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22bottom%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22slope%22%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%5D%0A%20%20%20%20assert%20per_rep_titers.notnull().all().all()%0A%0A%20%20%20%20if%20len(%0A%20%20%20%20%20%20%20%20invalid_titer_as%20%3A%3D%20per_rep_titers.query(%22(titer_as%20%3D%3D%20'nt50')%20and%20top%20%3C%3D%200.5%22)%0A%20%20%20%20)%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22There%20are%20titers%20computed%20as%20nt50%20when%20curve%20top%20%3C%3D%200.5%3A%5Cn%7Binvalid_titer_as%7D%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20assert%20len(per_rep_titers)%20%3D%3D%20per_rep_titers%5B%22replicate%22%5D.nunique()%0A%0A%20%20%20%20%23%20get%20viruses%20in%20the%20order%20to%20plot%20them%0A%20%20%20%20viruses%20%3D%20sorted(per_rep_titers%5B%22virus%22%5D.unique())%0A%20%20%20%20if%20viral_strain_plot_order%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20if%20not%20set(viruses).issubset(viral_strain_plot_order)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%60viral_strain_plot_order%60%20lacks%20some%20viruses%20with%20titers%3A%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2B%20str(set(viruses)%20-%20set(viral_strain_plot_order))%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20viruses%20%3D%20%5Bv%20for%20v%20in%20viral_strain_plot_order%20if%20v%20in%20viruses%5D%0A%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20mo.md(f%22%60%7Bserum%7D%60%20has%20titers%20for%20a%20total%20of%20%7Blen(viruses)%7D%20viruses%22)%0A%20%20%20%20)%0A%20%20%20%20return%20per_rep_titers%2C%20viruses%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Correlate%20NT50s%20with%20midpoints%20of%20curves%0A%20%20%20%20Plot%20the%20correlation%20of%20the%20NT50s%20with%20the%20midpoint%20(this%20is%20an%20interactive%20plot%2C%20mouse%20over%20points%20for%20details).%0A%20%20%20%20This%20plot%20can%20help%20you%20determine%20if%20you%20made%20the%20correct%20choice%20of%20%60serum_titer_as%60%20when%20choosing%20to%20use%20the%20midpoint%20or%20NT50%20for%20the%20titer.%0A%20%20%20%20For%20titers%20where%20they%20are%20well%20correlated%20it%20should%20not%20matter%20which%20you%20chose.%0A%20%20%20%20But%20if%20there%20are%20titers%20far%20from%20the%20correlation%20line%2C%20you%20should%20look%20at%20those%20measurements%20and%20curves%20to%20make%20sure%20you%20made%20the%20correct%20choice%20of%20calculating%20the%20titer%20as%20the%20NT50%20versus%20midpoint%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(alt%2C%20group%2C%20mo%2C%20per_rep_titers%2C%20serum)%3A%0A%20%20%20%20_virus_selection_1%20%3D%20alt.selection_point(%0A%20%20%20%20%20%20%20%20fields%3D%5B%22virus%22%5D%2C%20on%3D%22mouseover%22%2C%20empty%3DFalse%0A%20%20%20%20)%0A%20%20%20%20midpoint_vs_nt50_chart%20%3D%20(%0A%20%20%20%20%20%20%20%20alt.Chart(per_rep_titers)%0A%20%20%20%20%20%20%20%20.add_params(_virus_selection_1)%0A%20%20%20%20%20%20%20%20.encode(%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.X(%22nt50%22%2C%20scale%3Dalt.Scale(type%3D%22log%22%2C%20nice%3DFalse%2C%20padding%3D8))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Y(%22midpoint%22%2C%20scale%3Dalt.Scale(type%3D%22log%22%2C%20nice%3DFalse%2C%20padding%3D8))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Color(%22titer_bound%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20strokeWidth%3Dalt.condition(_virus_selection_1%2C%20alt.value(3)%2C%20alt.value(0))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20size%3Dalt.condition(_virus_selection_1%2C%20alt.value(100)%2C%20alt.value(60))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20tooltip%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(c%2C%20format%3D%22.2g%22)%20if%20per_rep_titers%5Bc%5D.dtype%20%3D%3D%20float%20else%20c%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20c%20in%20per_rep_titers.columns%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20c%20not%20in%20%7B%22group%22%2C%20%22serum%22%2C%20%22titer_as%22%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.mark_circle(stroke%3D%22black%22%2C%20fillOpacity%3D0.45%2C%20color%3D%22black%22)%0A%20%20%20%20%20%20%20%20.properties(%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3D350%2C%20height%3D350%2C%20title%3Df%22NT50%20versus%20midpoint%20for%20%7Bgroup%7D%20%7Bserum%7D%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.configure_axis(grid%3DFalse)%0A%20%20%20%20)%0A%20%20%20%20mo.output.append(midpoint_vs_nt50_chart)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Write%20the%20individual%20per-replicate%20titers%20to%20a%20file%2C%20this%20is%20before%20any%20QC%20has%20been%20applied%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20per_rep_titers%2C%20per_rep_titers_csv)%3A%0A%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Writing%20per-replicate%20titers%20(without%20QC%20filtering)%20to%20%60%7Bper_rep_titers_csv%7D%60%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20per_rep_titers.to_csv(per_rep_titers_csv%2C%20index%3DFalse%2C%20float_format%3D%22%25.4g%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Plot%20median%20titers%20and%20determine%20if%20they%20pass%20QC%0A%20%20%20%20Get%20the%20median%20titers%20for%20each%20virus%20across%20replicates%2C%20then%20add%20these%20median%20titers%20to%20the%20per-replicate%20titers%20and%20calculate%20the%20fold-change%20in%20titer%20between%20each%20replicate%20and%20its%20median.%0A%20%20%20%20Finally%2C%20for%20each%20virus%20indicate%20whether%20it%20passes%20the%20QC%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20numpy%2C%20pd%2C%20per_rep_titers%2C%20qc_thresholds%2C%20viruses)%3A%0A%20%20%20%20mo.output.append(mo.md(f%22Using%20the%20following%20%60qc_thresholds%3D%7Bqc_thresholds!r%7D%60%22))%0A%0A%20%20%20%20def%20get_median_bound(s)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Get%20the%20bound%20on%20titer%20when%20taking%20median.%22%22%22%0A%20%20%20%20%20%20%20%20s%20%3D%20list(s)%0A%20%20%20%20%20%20%20%20if%20len(s)%20%25%202%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20s%5Blen(s)%20%2F%2F%202%5D%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20bounds%20%3D%20s%5Blen(s)%20%2F%2F%202%20-%201%20%3A%20len(s)%20%2F%2F%202%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20assert%20len(bounds)%20%3D%3D%202%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20len(set(bounds))%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20bounds%5B0%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20elif%20%22interpolated%22%20in%20bounds%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bb%20for%20b%20in%20bounds%20if%20b%20!%3D%20%22interpolated%22%5D%5B0%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%22inconsistent%22%0A%0A%20%20%20%20median_titers_noqc%20%3D%20(%0A%20%20%20%20%20%20%20%20per_rep_titers.sort_values(%22titer%22)%20%20%23%20for%20getting%20median%20bound%0A%20%20%20%20%20%20%20%20.groupby(%5B%22group%22%2C%20%22serum%22%2C%20%22virus%22%2C%20%22titer_as%22%5D%2C%20as_index%3DFalse)%0A%20%20%20%20%20%20%20%20.aggregate(%0A%20%20%20%20%20%20%20%20%20%20%20%20titer%3Dpd.NamedAgg(%22titer%22%2C%20%22median%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20n_replicates%3Dpd.NamedAgg(%22replicate%22%2C%20%22count%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20titer_sem%3Dpd.NamedAgg(%22titer%22%2C%20%22sem%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20titer_bound%3Dpd.NamedAgg(%22titer_bound%22%2C%20get_median_bound)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20per_rep_titers_w_fc%20%3D%20(%0A%20%20%20%20%20%20%20%20per_rep_titers.merge(%0A%20%20%20%20%20%20%20%20%20%20%20%20median_titers_noqc%5B%5B%22group%22%2C%20%22serum%22%2C%20%22virus%22%2C%20%22titer%22%5D%5D.rename(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20columns%3D%7B%22titer%22%3A%20%22median_titer%22%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20validate%3D%22many_to_one%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20on%3D%5B%22group%22%2C%20%22serum%22%2C%20%22virus%22%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.assign(%0A%20%20%20%20%20%20%20%20%20%20%20%20fc_from_median%3Dlambda%20x%3A%20numpy.where(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%5B%22titer%22%5D%20%3E%20x%5B%22median_titer%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%5B%22titer%22%5D%20%2F%20x%5B%22median_titer%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%5B%22median_titer%22%5D%20%2F%20x%5B%22titer%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.drop(columns%3D%5B%22group%22%2C%20%22serum%22%2C%20%22titer_as%22%2C%20%22median_titer%22%5D)%0A%20%20%20%20)%0A%0A%20%20%20%20median_titers_noqc%20%3D%20median_titers_noqc.merge(%0A%20%20%20%20%20%20%20%20per_rep_titers_w_fc.groupby(%22virus%22%2C%20as_index%3DFalse).aggregate(%0A%20%20%20%20%20%20%20%20%20%20%20%20max_fc_from_median%3Dpd.NamedAgg(%22fc_from_median%22%2C%20%22max%22)%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20on%3D%22virus%22%2C%0A%20%20%20%20%20%20%20%20validate%3D%22one_to_one%22%2C%0A%20%20%20%20).assign(%0A%20%20%20%20%20%20%20%20fails_min_reps%3Dlambda%20x%3A%20x%5B%22n_replicates%22%5D%20%3C%20qc_thresholds%5B%22min_replicates%22%5D%2C%0A%20%20%20%20%20%20%20%20fails_max_fc%3Dlambda%20x%3A%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%5B%22max_fc_from_median%22%5D%20%3E%3D%20qc_thresholds%5B%22max_fold_change_from_median%22%5D%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20fails_qc%3Dlambda%20x%3A%20x%5B%22fails_min_reps%22%5D%20%7C%20x%5B%22fails_max_fc%22%5D%2C%0A%20%20%20%20%20%20%20%20fails_qc_reason%3Dlambda%20x%3A%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20x.apply(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20lambda%20r%3A%20%22%2C%20%22.join(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(%5B%22min_replicates%22%5D%20if%20r%5B%22fails_min_reps%22%5D%20else%20%5B%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2B%20(%5B%22max_fold_change_from_median%22%5D%20if%20r%5B%22fails_max_fc%22%5D%20else%20%5B%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20axis%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20get%20viruses%20failing%20QC%20in%20order%20to%20plot%0A%20%20%20%20viruses_failing_qc%20%3D%20(%0A%20%20%20%20%20%20%20%20median_titers_noqc.query(%22fails_qc%22)%0A%20%20%20%20%20%20%20%20.set_index(%22virus%22)%5B%22fails_qc_reason%22%5D%0A%20%20%20%20%20%20%20%20.to_dict()%0A%20%20%20%20)%0A%20%20%20%20viruses_failing_qc%20%3D%20%7B%0A%20%20%20%20%20%20%20%20v%3A%20viruses_failing_qc%5Bv%5D%20for%20v%20in%20viruses%20if%20v%20in%20viruses_failing_qc%0A%20%20%20%20%7D%0A%0A%20%20%20%20median_titers_noqc%20%3D%20median_titers_noqc.drop(%0A%20%20%20%20%20%20%20%20columns%3D%5B%22fails_min_reps%22%2C%20%22fails_max_fc%22%2C%20%22fails_qc_reason%22%5D%0A%20%20%20%20)%0A%0A%20%20%20%20per_rep_titers_w_fc%20%3D%20per_rep_titers_w_fc.merge(%0A%20%20%20%20%20%20%20%20median_titers_noqc%5B%5B%22virus%22%2C%20%22fails_qc%22%5D%5D%2C%0A%20%20%20%20%20%20%20%20on%3D%22virus%22%2C%0A%20%20%20%20%20%20%20%20validate%3D%22many_to_one%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%20median_titers_noqc%2C%20per_rep_titers_w_fc%2C%20viruses_failing_qc%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Now%20plot%20the%20per-replicate%20and%20median%20titers%2C%20indicating%20any%20viruses%20that%20failed%20QC.%0A%20%20%20%20Note%20that%20potentially%20some%20of%20these%20titers%20may%20still%20be%20retained%20if%20the%20viruses%20in%20question%20are%20specified%20in%20%60viruses_ignore_qc%60%20of%20%60qc_thresholds%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20alt%2C%0A%20%20%20%20group%2C%0A%20%20%20%20median_titers_noqc%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20per_rep_titers_w_fc%2C%0A%20%20%20%20qc_thresholds%2C%0A%20%20%20%20serum%2C%0A%20%20%20%20viruses%2C%0A)%3A%0A%20%20%20%20_virus_selection_2%20%3D%20alt.selection_point(%0A%20%20%20%20%20%20%20%20fields%3D%5B%22virus%22%5D%2C%20on%3D%22mouseover%22%2C%20empty%3DFalse%0A%20%20%20%20)%0A%20%20%20%20per_rep_chart%20%3D%20(%0A%20%20%20%20%20%20%20%20alt.Chart(per_rep_titers_w_fc)%0A%20%20%20%20%20%20%20%20.encode(%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.X(%22titer%22%2C%20scale%3Dalt.Scale(nice%3DFalse%2C%20padding%3D5%2C%20type%3D%22log%22))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Y(%22virus%22%2C%20sort%3Dviruses)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Fill(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22fails_qc%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3Df%22fails%20qc_thresholds%5B'min_replicates'%5D%3D%7Bqc_thresholds%5B'min_replicates'%5D!r%7D%2C%20qc_thresholds%5B'max_fold_change_from_median'%5D%3D%7Bqc_thresholds%5B'max_fold_change_from_median'%5D!r%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20legend%3Dalt.Legend(titleLimit%3D500)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Shape(%22titer_bound%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20strokeWidth%3Dalt.condition(_virus_selection_2%2C%20alt.value(2)%2C%20alt.value(0))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20tooltip%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(c%2C%20format%3D%22.3g%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20per_rep_titers_w_fc%5Bc%5D.dtype%20%3D%3D%20float%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%20c%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20c%20in%20per_rep_titers_w_fc%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.mark_point(%0A%20%20%20%20%20%20%20%20%20%20%20%20size%3D35%2C%20filled%3DTrue%2C%20fillOpacity%3D0.5%2C%20strokeOpacity%3D1%2C%20stroke%3D%22black%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20median_chart%20%3D%20(%0A%20%20%20%20%20%20%20%20alt.Chart(median_titers_noqc)%0A%20%20%20%20%20%20%20%20.encode(%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.X(%22titer%22%2C%20scale%3Dalt.Scale(nice%3DFalse%2C%20padding%3D5%2C%20type%3D%22log%22))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Y(%22virus%22%2C%20sort%3Dviruses)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Fill(%22fails_qc%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20alt.Shape(%22titer_bound%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20strokeWidth%3Dalt.condition(_virus_selection_2%2C%20alt.value(2)%2C%20alt.value(0.5))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20tooltip%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(c%2C%20format%3D%22.3g%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20median_titers_noqc%5Bc%5D.dtype%20%3D%3D%20float%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%20c%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20c%20in%20median_titers_noqc%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.mark_point(%0A%20%20%20%20%20%20%20%20%20%20%20%20size%3D75%2C%20filled%3DTrue%2C%20fillOpacity%3D0.9%2C%20strokeOpacity%3D1%2C%20stroke%3D%22black%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20titer_chart%20%3D%20(%0A%20%20%20%20%20%20%20%20(per_rep_chart%20%2B%20median_chart)%0A%20%20%20%20%20%20%20%20.add_params(_virus_selection_2)%0A%20%20%20%20%20%20%20%20.properties(%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3Dalt.Step(11)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3D250%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20title%3Df%22%7Bgroup%7D%20%7Bserum%7D%20median%20(large%20points)%20and%20per-replicate%20(small%20points)%20titers%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.configure_axis(grid%3DFalse)%0A%20%20%20%20)%0A%20%20%20%20mo.output.append(titer_chart)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Plot%20individual%20curves%20for%20any%20viruses%20failing%20QC%0A%20%20%20%20Plot%20individual%20curves%20for%20viruses%20failing%20QC.%0A%20%20%20%20Note%20that%20potentially%20some%20of%20these%20titers%20may%20still%20be%20retained%20if%20the%20viruses%20in%20question%20are%20specified%20in%20%60viruses_ignore_qc%60%20of%20%60qc_thresholds%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20curve_display_method%2C%0A%20%20%20%20display_fig_marimo%2C%0A%20%20%20%20fits_noqc%2C%0A%20%20%20%20group%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20serum%2C%0A%20%20%20%20viruses_failing_qc%2C%0A)%3A%0A%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Neutralization%20curves%20for%20the%20%7Blen(viruses_failing_qc)%7D%20viruses%20failing%20QC%3A%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20if%20len(viruses_failing_qc)%3A%0A%20%20%20%20%20%20%20%20_fig_failing%2C%20_%20%3D%20fits_noqc.plotReplicates(%0A%20%20%20%20%20%20%20%20%20%20%20%20viruses%3Dviruses_failing_qc%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20attempt_shared_legend%3DFalse%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20legendfontsize%3D8%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20titlesize%3D12%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20ncol%3D4%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20heightscale%3D1.2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20widthscale%3D1.2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20subplot_titles%3D%22%7Bvirus%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20draw_in_bounds%3DTrue%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_%20%3D%20_fig_failing.suptitle(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22neutralization%20curves%20for%20viruses%20failing%20QC%20for%20%7Bgroup%7D%20%7Bserum%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D18%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fontweight%3D%22bold%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_fig_failing.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20%20%20%20%20display_fig_marimo(_fig_failing%2C%20display_method%3Dcurve_display_method)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20plt.close(_fig_failing)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20mo.output.append(mo.md(%22No%20curves%20fail%20QC%22))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Get%20the%20viruses%20to%20drop%20for%20QC%20failures%0A%20%20%20%20Drop%20any%20viruses%20that%20fail%20QC%20and%20are%20not%20specified%20in%20%60viruses_ignore_qc%60%20of%20%60qc_thresholds%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(io%2C%20mo%2C%20qc_drops_file%2C%20qc_thresholds%2C%20viruses_failing_qc%2C%20yaml)%3A%0A%20%20%20%20viruses_to_drop%20%3D%20%7B%0A%20%20%20%20%20%20%20%20v%3A%20reason%0A%20%20%20%20%20%20%20%20for%20v%2C%20reason%20in%20viruses_failing_qc.items()%0A%20%20%20%20%20%20%20%20if%20v%20not%20in%20qc_thresholds%5B%22viruses_ignore_qc%22%5D%0A%20%20%20%20%7D%0A%20%20%20%20mo.output.append(mo.md(f%22Dropping%20%7Blen(viruses_to_drop)%7D%20viruses%20for%20failing%20QC%3A%22))%0A%20%20%20%20yaml_buffer_viruses_drop%20%3D%20io.StringIO()%0A%20%20%20%20yaml.YAML(typ%3D%22rt%22).dump(viruses_to_drop%2C%20stream%3Dyaml_buffer_viruses_drop)%0A%20%20%20%20mo.output.append(mo.md(f%22%60%60%60yaml%5Cn%7Byaml_buffer_viruses_drop.getvalue()%7D%60%60%60%22))%0A%20%20%20%20if%20nkept%20%3A%3D%20(len(viruses_failing_qc)%20-%20len(viruses_to_drop))%3A%0A%20%20%20%20%20%20%20%20kept_viruses%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20v%3A%20reason%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20v%2C%20reason%20in%20viruses_failing_qc.items()%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20v%20in%20qc_thresholds%5B%22viruses_ignore_qc%22%5D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22Retaining%20%7Bnkept%7D%20viruses%20that%20fail%20QC%20because%20they%20are%20in%20%60viruses_ignore_qc%60%3A%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22%7Bkept_viruses%7D%22%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20mo.output.append(mo.md(f%22Writing%20QC%20drops%20to%20%60%7Bqc_drops_file%7D%60%22))%0A%20%20%20%20with%20open(qc_drops_file%2C%20%22w%22)%20as%20f_qc_drops%3A%0A%20%20%20%20%20%20%20%20yaml.YAML(typ%3D%22rt%22).dump(viruses_to_drop%2C%20f_qc_drops)%0A%20%20%20%20return%20(viruses_to_drop%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Get%20and%20plot%20the%20neutralization%20curves%20for%20all%20retained%20viruses%0A%20%20%20%20First%2C%20get%20the%20%60CurveFits%60%20for%20just%20those%20retained%20viruses%20(dropping%20ones%20that%20fail%20QC)%2C%20and%20plot%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20curve_display_method%2C%0A%20%20%20%20curves_pdf%2C%0A%20%20%20%20display_fig_marimo%2C%0A%20%20%20%20fits_noqc%2C%0A%20%20%20%20group%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20neutcurve%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20serum%2C%0A%20%20%20%20viruses%2C%0A%20%20%20%20viruses_to_drop%2C%0A)%3A%0A%20%20%20%20fits_qc%20%3D%20neutcurve.CurveFits.combineCurveFits(%0A%20%20%20%20%20%20%20%20%5Bfits_noqc%5D%2C%0A%20%20%20%20%20%20%20%20viruses%3D%5Bv%20for%20v%20in%20viruses%20if%20v%20not%20in%20viruses_to_drop%5D%2C%0A%20%20%20%20)%0A%20%20%20%20assert%20len(viruses)%20%3D%3D%20len(fits_qc.viruses%5Bserum%5D)%20%2B%20len(viruses_to_drop)%0A%20%20%20%20_fig_retained%2C%20_%20%3D%20fits_qc.plotReplicates(%0A%20%20%20%20%20%20%20%20attempt_shared_legend%3DFalse%2C%0A%20%20%20%20%20%20%20%20legendfontsize%3D8%2C%0A%20%20%20%20%20%20%20%20titlesize%3D12%2C%0A%20%20%20%20%20%20%20%20ncol%3D4%2C%0A%20%20%20%20%20%20%20%20heightscale%3D1.2%2C%0A%20%20%20%20%20%20%20%20widthscale%3D1.2%2C%0A%20%20%20%20%20%20%20%20subplot_titles%3D%22%7Bvirus%7D%22%2C%0A%20%20%20%20%20%20%20%20viruses%3D%5Bv%20for%20v%20in%20viruses%20if%20v%20not%20in%20viruses_to_drop%5D%2C%0A%20%20%20%20%20%20%20%20draw_in_bounds%3DTrue%2C%0A%20%20%20%20)%0A%20%20%20%20_%20%3D%20_fig_retained.suptitle(%0A%20%20%20%20%20%20%20%20f%22neutralization%20curves%20for%20retained%20viruses%20for%20%7Bgroup%7D%20%7Bserum%7D%22%2C%0A%20%20%20%20%20%20%20%20y%3D1%2C%0A%20%20%20%20%20%20%20%20fontsize%3D18%2C%0A%20%20%20%20%20%20%20%20fontweight%3D%22bold%22%2C%0A%20%20%20%20)%0A%20%20%20%20_fig_retained.tight_layout()%0A%20%20%20%20mo.output.append(%0A%20%20%20%20%20%20%20%20display_fig_marimo(_fig_retained%2C%20display_method%3Dcurve_display_method)%0A%20%20%20%20)%0A%20%20%20%20mo.output.append(mo.md(f%22Saving%20to%20plot%20of%20curves%20to%20%60%7Bcurves_pdf%7D%60%22))%0A%20%20%20%20_fig_retained.savefig(curves_pdf)%0A%20%20%20%20plt.close(_fig_retained)%0A%20%20%20%20return%20(fits_qc%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Save%20the%20%60CurveFits%60%20to%20a%20pickle%20file%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fits_qc%2C%20mo%2C%20output_pickle%2C%20pickle)%3A%0A%20%20%20%20with%20open(output_pickle%2C%20%22wb%22)%20as%20f_out_pickle%3A%0A%20%20%20%20%20%20%20%20pickle.dump(fits_qc%2C%20f_out_pickle)%0A%0A%20%20%20%20mo.output.append(mo.md(f%22Writing%20curve%20fits%20to%20%7Boutput_pickle%7D%22))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Write%20the%20titers%20(excluding%20QC%20dropped%20viruses)%20to%20a%20CSV%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(median_titers_noqc%2C%20mo%2C%20titers_csv)%3A%0A%20%20%20%20mo.output.append(mo.md(f%22Writing%20titers%20to%20%60%7Btiters_csv%7D%60%22))%0A%0A%20%20%20%20(%0A%20%20%20%20%20%20%20%20median_titers_noqc.query(%22virus%20not%20in%20%40viruses_to_drop%22)%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22group%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22serum%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22virus%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22titer%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22titer_bound%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22titer_sem%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22n_replicates%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22titer_as%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%5D.to_csv(titers_csv%2C%20index%3DFalse%2C%20float_format%3D%22%25.4g%22)%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
3371c18c11e19f4ca47ef7a14a67221c07fa0e6aad21319957535fc97cdb724e