In [1]:
######## snakemake preamble start (automatically inserted, do not edit) ########
import sys;sys.path.extend(['/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13/site-packages', '/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline', '/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/bin', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13/lib-dynload', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13/site-packages', '/home/jbloom/.cache/snakemake/snakemake/source-cache/runtime-cache/tmpfro_n8p6/file/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline/notebooks', '/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline/notebooks']);import pickle;from snakemake import script;script.snakemake = pickle.loads(b'\x80\x04\x95\x0cV\x00\x00\x00\x00\x00\x00\x8c\x10snakemake.script\x94\x8c\tSnakemake\x94\x93\x94)\x81\x94}\x94(\x8c\x05input\x94\x8c\x0csnakemake.io\x94\x8c\nInputFiles\x94\x93\x94)\x81\x94(\x8c\'results/plates/plate13/curvefits.pickle\x94\x8c\xbb/home/jbloom/.cache/snakemake/snakemake/source-cache/runtime-cache/tmpfro_n8p6/file/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline/notebook_funcs.py\x94e}\x94(\x8c\x06_names\x94}\x94(\x8c\x07pickles\x94K\x00K\x01\x86\x94\x8c\x0enotebook_funcs\x94K\x01N\x86\x94u\x8c\x12_allowed_overrides\x94]\x94(\x8c\x05index\x94\x8c\x04sort\x94eh\x15h\x06\x8c\x0eAttributeGuard\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94h\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sbh\x0fh\x06\x8c\tNamedlist\x94\x93\x94)\x81\x94h\na}\x94(h\r}\x94h\x13]\x94(h\x15h\x16eh\x15h\x18)\x81\x94}\x94h\x1bh\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sbubh\x11h\x0bub\x8c\x06output\x94h\x06\x8c\x0bOutputFiles\x94\x93\x94)\x81\x94(\x8c4results/sera/EPIHK_EPIHK_16/titers_per_replicate.csv\x94\x8c&results/sera/EPIHK_EPIHK_16/titers.csv\x94\x8c&results/sera/EPIHK_EPIHK_16/curves.pdf\x94\x8c,results/sera/EPIHK_EPIHK_16/curvefits.pickle\x94\x8c(results/sera/EPIHK_EPIHK_16/qc_drops.yml\x94e}\x94(h\r}\x94(\x8c\x0eper_rep_titers\x94K\x00N\x86\x94\x8c\x06titers\x94K\x01N\x86\x94\x8c\ncurves_pdf\x94K\x02N\x86\x94\x8c\x06pickle\x94K\x03N\x86\x94\x8c\x08qc_drops\x94K\x04N\x86\x94uh\x13]\x94(h\x15h\x16eh\x15h\x18)\x81\x94}\x94h\x1bh\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sbh3h,h5h-h7h.h9h/h;h0ub\x8c\r_params_store\x94h\x06\x8c\x06Params\x94\x93\x94)\x81\x94(]\x94(\x8c\x16A/Vermont/10/2025_H1N1\x94\x8c\x13A/Utah/39/2025_H1N1\x94\x8c\x1bA/Hawaii/ISC-1140/2025_H1N1\x94\x8c\x18A/Colorado/218/2024_H1N1\x94\x8c"A/Massachusetts/ISC-1679/2025_H1N1\x94\x8c\x18A/Bangkok/P176/2025_H1N1\x94\x8c\x16A/Vermont/05/2025_H1N1\x94\x8c\x1bA/Santiago/101713/2024_H1N1\x94\x8c\x16A/Oregon/261/2024_H1N1\x94\x8c\x14A/Ohio/259/2024_H1N1\x94\x8c\x19A/Minnesota/131/2024_H1N1\x94\x8c\x18A/Tennessee/04/2025_H1N1\x94\x8c\x17A/Qatar/83328/2024_H1N1\x94\x8c\x15A/Oregon/11/2025_H1N1\x94\x8c\x19A/Maldives/2132/2024_H1N1\x94\x8c\x17A/Ufa/CRIE/47/2024_H1N1\x94\x8c\x1aA/Zacapa/FLU-012/2025_H1N1\x94\x8c"A/Vladimir/RII-MH223382S/2024_H1N1\x94\x8c\x15A/Busan/277/2025_H1N1\x94\x8c\x17A/Maryland/64/2024_H1N1\x94\x8c\x14A/Iowa/123/2024_H1N1\x94\x8c\x17A/Illinois/65/2024_H1N1\x94\x8c\x19A/Tambov/160-1V/2024_H1N1\x94\x8c\x15A/Ulsan/492/2025_H1N1\x94\x8c\x1dA/Singapore/MOH0547/2024_H1N1\x94\x8c\x1bA/KANAGAWA/AC2408/2025_H1N1\x94\x8c\x18A/Wisconsin/30/2025_H1N1\x94\x8c\x18A/Pakistan/306/2024_H1N1\x94\x8c%A/NovaScotia/ET1801CP00018S/2025_H1N1\x94\x8c\x1bA/TOKYO/EIS11-277/2024_H1N1\x94\x8c"A/Wisconsin/NIRC-IS-1111/2025_H1N1\x94\x8c\x1fA/Queensland/IN000684/2024_H1N1\x94\x8c&A/Qinghai-Chengzhong/SWL1410/2024_H1N1\x94\x8c#A/Saint-Petersburg/RII-04/2025_H1N1\x94\x8c\x1fA/Uganda/UVRI_KIS6850_2024_H1N1\x94\x8c%A/Shanghai-Huangpu/SWL12109/2024_H1N1\x94\x8c\x19A/Victoria/3599/2024_H1N1\x94\x8c\x16A/Lisboa/188/2023_H1N1\x94\x8c\x1cA/Madagascar/00003/2025_H1N1\x94\x8c!A/Victoria/4897/2022_IVR-238_H1N1\x94\x8c\x18A/Wisconsin/67/2022_H1N1\x94\x8c\x19A/Wisconsin/588/2019_H1N1\x94\x8c\x15A/Hawaii/70/2019_H1N1\x94\x8c\x17A/Brisbane/02/2018_H1N1\x94\x8c\x17A/Michigan/45/2015_H1N1\x94\x8c\x19A/California/07/2009_H1N1\x94\x8c#A/France/IDF-IPP29542/2023-egg_H3N2\x94\x8c\x1cA/BurkinaFaso/3131/2023_H3N2\x94\x8c\x18A/Norway/12374/2023_H3N2\x94\x8c"A/Massachusetts/ISC-1684/2025_H3N2\x94\x8c!A/DE/DE-DHSS-901/2025_(H3N2)_H3N2\x94\x8c#A/Michigan/UM-10062069629/2025_H3N2\x94\x8c\x17A/Kentucky/57/2024_H3N2\x94\x8c\x1aA/Washington/284/2024_H3N2\x94\x8c\x19A/Colombia/1851/2024_H3N2\x94\x8c,A/France/ARA-RELAB-HCL025017178801/2025_H3N2\x94\x8c\x18A/Canberra/613/2024_H3N2\x94\x8c\x1aA/Texas/ISC-1342/2025_H3N2\x94\x8c\x1cA/Manitoba/RV04865/2024_H3N2\x94\x8c\x18A/Tasmania/790/2024_H3N2\x94\x8c\x18A/Tasmania/788/2024_H3N2\x94\x8c\x19A/Minnesota/126/2024_H3N2\x94\x8c\x18A/Minnesota/97/2024_H3N2\x94\x8c\x18A/Tasmania/836/2024_H3N2\x94\x8c\x1fA/Queensland/IN000692/2024_H3N2\x94\x8c\x1dA/Netherlands/10563/2023_H3N2\x94\x8c\x1fA/Queensland/IN000803/2024_H3N2\x94\x8c\x19A/Maldives/2186/2024_H3N2\x94\x8c\x19A/Victoria/3480/2024_H3N2\x94\x8c\x16A/Lisboa/216/2023_H3N2\x94\x8c\x19A/Minnesota/133/2024_H3N2\x94\x8c\x1dA/CoteD\'Ivoire/4448/2024_H3N2\x94\x8c\x1dA/Switzerland/47775/2024_H3N2\x94\x8c$A/Switzerland/860423897313/2023_H3N2\x94\x8c,A/France/PAC-RELAB-HCL024172122101/2024_H3N2\x94\x8c"A/New_York/GKISBBBE61555/2025_H3N2\x94\x8c\x1cA/Badajoz/18680568/2025_H3N2\x94\x8c\x1cA/Massachusetts/93/2024_H3N2\x94\x8c\x1fA/Washington/UW-25728/2024_H3N2\x94\x8c\x1eA/Punta_Arenas/83659/2024_H3N2\x94\x8c\x1aA/Texas/ISC-1322/2025_H3N2\x94\x8c\x1fA/France/BRE-IPP01880/2025_H3N2\x94\x8c\x18A/Ecuador/1385/2024_H3N2\x94\x8c\x16A/Vermont/13/2025_H3N2\x94\x8c"A/Wisconsin/NIRC-IS-1028/2024_H3N2\x94\x8c\x18A/Tennessee/99/2024_H3N2\x94\x8c#A/Sao_Paulo/358026766-IAL/2024_H3N2\x94\x8c"A/Mato_Grosso_do_Sul/518/2025_H3N2\x94\x8c\x1aA/Texas/ISC-1148/2025_H3N2\x94\x8c\x1cA/Washington/15245/2025_H3N2\x94\x8c\x1bA/Rhode_Island/66/2024_H3N2\x94\x8c\x1cA/Amapa/021563-IEC/2024_H3N2\x94\x8c\x17A/Texas/15550/2024_H3N2\x94\x8c\x18A/New_York/191/2024_H3N2\x94\x8c A/Saskatchewan/RV04835/2024_H3N2\x94\x8c\x1eA/Santa_Catarina/333/2025_H3N2\x94\x8c\x1cA/Ghana/FS-25-0256/2025_H3N2\x94\x8c\x17A/Victoria/96/2025_H3N2\x94\x8c\x16A/Oregon/265/2024_H3N2\x94\x8c\x18A/Michigan/120/2024_H3N2\x94\x8c\x1eA/Rhode_Island/15446/2025_H3N2\x94\x8c\x13A/Utah/87/2024_H3N2\x94\x8c!A/India/Pune-NIV24_3439/2024_H3N2\x94\x8c\x16A/Nevada/216/2024_H3N2\x94\x8c\x19A/Colombia/7681/2024_H3N2\x94\x8c\x1aA/Texas/ISC-1274/2025_H3N2\x94\x8c\x15A/Busan/461/2025_H3N2\x94\x8c\x17A/Slovenia/49/2024_H3N2\x94\x8c\x15A/Sydney/43/2025_H3N2\x94\x8c\x1dA/Netherlands/01502/2025_H3N2\x94\x8c\x13A/Utah/94/2024_H3N2\x94\x8c\x1dA/Colorado/ISC-1416/2024_H3N2\x94\x8c\x17A/Victoria/46/2024_H3N2\x94\x8c\x1cA/Florida/ISC-1241/2025_H3N2\x94\x8c\x16A/Indiana/46/2024_H3N2\x94\x8c\x19A/Wisconsin/172/2024_H3N2\x94\x8c\x17A/New_York/39/2025_H3N2\x94\x8c#A/Michigan/UM-10062100736/2025_H3N2\x94\x8c&A/Massachusetts/BI_MGH-23147/2025_H3N2\x94\x8c\x19A/Victoria/3482/2024_H3N2\x94\x8c\x19A/Maldives/2147/2024_H3N2\x94\x8c\x1cA/Pennsylvania/288/2024_H3N2\x94\x8c\x1fA/Croatia/10136RV/2023-egg_H3N2\x94\x8c!A/DistrictOfColumbia/27/2023_H3N2\x94\x8c\x16A/Thailand/8/2022_H3N2\x94\x8c\x1cA/Massachusetts/18/2022_H3N2\x94\x8c\x14A/Darwin/9/2021_H3N2\x94\x8c\x14A/Darwin/6/2021_H3N2\x94\x8c A/Cambodia/e0826360/2020egg_H3N2\x94\x8c\x1dA/Cambodia/e0826360/2020_H3N2\x94\x8c\x19A/HongKong/2671/2019_H3N2\x94\x8c\x17A/HongKong/45/2019_H3N2\x94\x8c\x15A/Kansas/14/2017_H3N2\x94\x8c*A/Singapore/INFIMH-16-0019/2016X-307A_H3N2\x94\x8c$A/Singapore/INFIMH-16-0019/2016_H3N2\x94\x8c\x1cA/HongKong/4801/2014egg_H3N2\x94\x8c\x19A/HongKong/4801/2014_H3N2\x94\x8c%A/Switzerland/9715293/2013NIB-88_H3N2\x94\x8c\x1fA/Switzerland/9715293/2013_H3N2\x94\x8c\x1aA/Texas/50/2012X-223A_H3N2\x94\x8c\x14A/Texas/50/2012_H3N2\x94e\x8c\x08midpoint\x94}\x94(\x8c\x0emin_replicates\x94K\x01\x8c\x1bmax_fold_change_from_median\x94K\x06\x8c\x11viruses_ignore_qc\x94]\x94u\x8c\x04png8\x94e}\x94(h\r}\x94(\x8c\x17viral_strain_plot_order\x94K\x00N\x86\x94\x8c\x0eserum_titer_as\x94K\x01N\x86\x94\x8c\rqc_thresholds\x94K\x02N\x86\x94\x8c\x14curve_display_method\x94K\x03N\x86\x94uh\x13]\x94(h\x15h\x16eh\x15h\x18)\x81\x94}\x94h\x1bh\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sbh\xddhFh\xdfh\xd4h\xe1h\xd5h\xe3h\xdaub\x8c\r_params_types\x94}\x94\x8c\twildcards\x94h\x06\x8c\tWildcards\x94\x93\x94)\x81\x94(\x8c\x05EPIHK\x94\x8c\x08EPIHK_16\x94e}\x94(h\r}\x94(\x8c\x05group\x94K\x00N\x86\x94\x8c\x05serum\x94K\x01N\x86\x94uh\x13]\x94(h\x15h\x16eh\x15h\x18)\x81\x94}\x94h\x1bh\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sb\x8c\x05group\x94h\xf0\x8c\x05serum\x94h\xf1ub\x8c\x07threads\x94K\x01\x8c\tresources\x94h\x06\x8c\tResources\x94\x93\x94)\x81\x94(K\x01K\x01\x8c\x15/loc/scratch/30805658\x94e}\x94(h\r}\x94(\x8c\x06_cores\x94K\x00N\x86\x94\x8c\x06_nodes\x94K\x01N\x86\x94\x8c\x06tmpdir\x94K\x02N\x86\x94uh\x13]\x94(h\x15h\x16eh\x15h\x18)\x81\x94}\x94h\x1bh\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sbj\x07\x01\x00\x00K\x01j\t\x01\x00\x00K\x01j\x0b\x01\x00\x00j\x04\x01\x00\x00ub\x8c\x03log\x94h\x06\x8c\x03Log\x94\x93\x94)\x81\x94\x8c7results/sera/EPIHK_EPIHK_16/EPIHK_EPIHK_16_titers.ipynb\x94a}\x94(h\r}\x94\x8c\x08notebook\x94K\x00N\x86\x94sh\x13]\x94(h\x15h\x16eh\x15h\x18)\x81\x94}\x94h\x1bh\x15sbh\x16h\x18)\x81\x94}\x94h\x1bh\x16sbj\x19\x01\x00\x00j\x16\x01\x00\x00ub\x8c\x06config\x94}\x94(\x8c\x16recent_vaccine_strains\x94}\x94(\x8c\x1fA/Croatia/10136RV/2023-egg_H3N2\x94\x8c\x1b2025-2026 egg-based vaccine\x94\x8c!A/DistrictOfColumbia/27/2023_H3N2\x94\x8c\x1c2025-2026 cell-based vaccine\x94\x8c!A/Victoria/4897/2022_IVR-238_H1N1\x94\x8c\x1b2025-2026 egg-based vaccine\x94\x8c\x18A/Wisconsin/67/2022_H1N1\x94\x8c\x1c2025-2026 cell-based vaccine\x94\x8c\x16A/Thailand/8/2022_H3N2\x94\x8c\x1b2024-2025 egg-based vaccine\x94\x8c\x1cA/Massachusetts/18/2022_H3N2\x94\x8c\x1c2024-2025 cell-based vaccine\x94u\x8c\x1chuman_sera_groups_to_exclude\x94]\x94\x8c\x03FCI\x94a\x8c\x15human_sera_to_exclude\x94]\x94(\x8c\x06SCH_19\x94\x8c\x06SCH_22\x94\x8c\x06SCH_26\x94e\x8c\x17human_sera_plots_params\x94}\x94(\x8c\x0ctiter_cutoff\x94K\x8c\x8c\x11titer_lower_limit\x94K(\x8c\x10min_frac_strains\x94G?\xec\xcc\xcc\xcc\xcc\xcc\xcd\x8c\rmin_frac_sera\x94G?\xe8\x00\x00\x00\x00\x00\x00\x8c\x0fmin_frac_action\x94\x8c\x05raise\x94u\x8c\x10seqneut-pipeline\x94\x8c\x10seqneut-pipeline\x94\x8c\x04docs\x94\x8c\x04docs\x94\x8c\x0bdescription\x94X\x1b\x01\x00\x00# Sequencing-based neutralization assays using human serum samples collected in late 2024-2025 and combined pdmH1N1 and H3N2 influenza library\n\nThe numerical data and computer code are at [https://github.com/jbloomlab/flu-seqneut-2025](https://github.com/jbloomlab/flu-seqneut-2025)\n\x94\x8c\x0fviral_libraries\x94}\x94(\x8c!flu-seqneut-2025_library_designed\x94\x8cDdata/viral_libraries/flu-seqneut-2025-barcode-to-strain_designed.csv\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8cBdata/viral_libraries/flu-seqneut-2025-barcode-to-strain_actual.csv\x94u\x8c\x17viral_strain_plot_order\x94\x8c4data/viral_libraries/flu-seqneut-2025_plot_order.csv\x94\x8c\x12neut_standard_sets\x94}\x94\x8c\x08loes2023\x94\x8c3data/neut_standard_sets/loes2023_neut_standards.csv\x94s\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\x08upstream\x94\x8c\x1cCCTACAATGTCGGATTTGTATTTAATAG\x94\x8c\ndownstream\x94\x8c\x00\x94\x8c\x04minq\x94K\x14\x8c\x11upstream_mismatch\x94K\x04\x8c\x0ebc_orientation\x94\x8c\x02R2\x94u\x8c#default_process_plate_qc_thresholds\x94}\x94(\x8c\x1bavg_barcode_counts_per_well\x94M\xf4\x01\x8c\x1fmin_neut_standard_frac_per_well\x94G?tz\xe1G\xae\x14{\x8c"no_serum_per_viral_barcode_filters\x94}\x94(\x8c\x08min_frac\x94G?\x1a6\xe2\xeb\x1cC-\x8c\x0fmax_fold_change\x94K\x04\x8c\tmax_wells\x94K\x02u\x8c!per_neut_standard_barcode_filters\x94}\x94(\x8c\x08min_frac\x94G?tz\xe1G\xae\x14{\x8c\x0fmax_fold_change\x94K\x04\x8c\tmax_wells\x94K\x02u\x8c min_neut_standard_count_per_well\x94M\xe8\x03\x8c)min_no_serum_count_per_viral_barcode_well\x94Kd\x8c+max_frac_infectivity_per_viral_barcode_well\x94K\x03\x8c)min_dilutions_per_barcode_serum_replicate\x94K\x06u\x8c%default_process_plate_curvefit_params\x94}\x94(\x8c\x18frac_infectivity_ceiling\x94K\x01\x8c\x06fixtop\x94]\x94(G?\xe3333333K\x01e\x8c\tfixbottom\x94K\x00\x8c\x08fixslope\x94]\x94(G?\xe9\x99\x99\x99\x99\x99\x9aK\neu\x8c!default_process_plate_curvefit_qc\x94}\x94(\x8c\x1dmax_frac_infectivity_at_least\x94G\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x0fgoodness_of_fit\x94}\x94(\x8c\x06min_R2\x94G?\xe0\x00\x00\x00\x00\x00\x00\x8c\x08max_RMSD\x94G?\xc3333333u\x8c#serum_replicates_ignore_curvefit_qc\x94]\x94\x8c+barcode_serum_replicates_ignore_curvefit_qc\x94]\x94u\x8c\x16default_serum_titer_as\x94h\xd4\x8c\x1bdefault_serum_qc_thresholds\x94h\xd5\x8c\x16sera_override_defaults\x94}\x94\x8c\x06plates\x94}\x94(\x8c\x08plate1-2\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94\x8c\x08datetime\x94\x8c\x04date\x94\x93\x94C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c#data/plates/2025-08-11_plate1-2.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\x05wells\x94]\x94(\x8c\x02H1\x94\x8c\x02H2\x94\x8c\x02H3\x94\x8c\x02H4\x94\x8c\x02H5\x94\x8c\x02H6\x94\x8c\x02H7\x94\x8c\x02H8\x94\x8c\x02H9\x94\x8c\x03H10\x94\x8c\x03H11\x94\x8c\x03H12\x94es\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ATCGAT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x08plate2-2\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c#data/plates/2025-08-11_plate2-2.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate3\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-11_plate3.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CAGTTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate4\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-11_plate4.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GTCTAA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate6\x94}\x94(\x8c\x05group\x94\x8c\x03FCI\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-12_plate6.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ATCGAT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\nplate7_FCI\x94}\x94(\x8c\x05group\x94\x8c\x03FCI\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c%data/plates/2025-08-12_plate7_FCI.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\nplate7_SCH\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c%data/plates/2025-08-12_plate7_SCH.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate8\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-12_plate8.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CAGTTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate9\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-13_plate9.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GTCTAA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate10\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-13_plate10.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ACGCTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate11\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-13_plate11.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TATAGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x0bplate12_SCH\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c&data/plates/2025-08-13_plate12_SCH.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CGAGCT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\rplate12_EPIHK\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c(data/plates/2025-08-13_plate12_EPIHK.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CGAGCT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate13\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate13.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate14\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate14.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ATCGAT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate15\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate15.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate16\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate16.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CAGTTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate17\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate17.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GTCTAA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate18\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate18.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ACGCTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate19\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate19.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TATAGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate20\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x1fflu-seqneut-2025_library_actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate20.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(j^\x01\x00\x00M\xf4\x01j_\x01\x00\x00G?tz\xe1G\xae\x14{j`\x01\x00\x00}\x94(jb\x01\x00\x00G?\x1a6\xe2\xeb\x1cC-jc\x01\x00\x00K\x04jd\x01\x00\x00K\x02uje\x01\x00\x00}\x94(jg\x01\x00\x00G?tz\xe1G\xae\x14{jh\x01\x00\x00K\x04ji\x01\x00\x00K\x02ujj\x01\x00\x00M\xe8\x03jk\x01\x00\x00Kdjl\x01\x00\x00K\x03jm\x01\x00\x00K\x06u\x8c\x0fcurvefit_params\x94}\x94(jp\x01\x00\x00K\x01jq\x01\x00\x00jr\x01\x00\x00js\x01\x00\x00K\x00jt\x01\x00\x00ju\x01\x00\x00u\x8c\x0bcurvefit_qc\x94}\x94(jx\x01\x00\x00G\x00\x00\x00\x00\x00\x00\x00\x00jy\x01\x00\x00}\x94(j{\x01\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00j|\x01\x00\x00G?\xc3333333uj}\x01\x00\x00j~\x01\x00\x00j\x7f\x01\x00\x00j\x80\x01\x00\x00u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CGAGCT\x94\x8c\x12upstream2_mismatch\x94K\x01uuu\x8c\x14miscellaneous_plates\x94}\x94(\x8c\x1520250716_initial_pool\x94}\x94(\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x07\x10\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c!flu-seqneut-2025_library_designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c5data/miscellaneous_plates/2025-07-16_initial_pool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x1620250723_balanced_pool\x94}\x94(\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x07\x17\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c!flu-seqneut-2025_library_designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c8data/miscellaneous_plates/2025-07-23_balanced_repool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c(20250723_H3_and_partial_H1_balanced_pool\x94}\x94(\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x07\x17\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c!flu-seqneut-2025_library_designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8cJdata/miscellaneous_plates/2025-07-23_H3_and_partial_H1_balanced_repool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x1620250807_balanced_pool\x94}\x94(\x8c\x04date\x94j\x8e\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c!flu-seqneut-2025_library_designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c8data/miscellaneous_plates/2025-08-07_balanced_repool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uuuu\x8c\x04rule\x94\x8c\x12group_serum_titers\x94\x8c\x0fbench_iteration\x94N\x8c\tscriptdir\x94\x8c`/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline/notebooks\x94ub.');del script;from snakemake.logging import logger;from snakemake.script import snakemake;import os; os.chdir(r'/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025');
######## snakemake preamble end #########

Titers for a serum in a group¶

Analyze titers for a serum assigned to a group, aggregating replicates which may be across multiple plates.

In [2]:
import pickle
import sys

import altair as alt

import matplotlib
import matplotlib.pyplot as plt

import neutcurve

import numpy

import pandas as pd

import ruamel.yaml as yaml

_ = alt.data_transformers.disable_max_rows()

# faster plotting of neut curves
matplotlib.style.use("fast")

Get variables from snakemake:

In [3]:
pickle_fits = snakemake.input.pickles
notebook_funcs = snakemake.input.notebook_funcs
per_rep_titers_csv = snakemake.output.per_rep_titers
titers_csv = snakemake.output.titers
curves_pdf = snakemake.output.curves_pdf
output_pickle = snakemake.output.pickle
qc_drops_file = snakemake.output.qc_drops
viral_strain_plot_order = snakemake.params.viral_strain_plot_order
serum_titer_as = snakemake.params.serum_titer_as
qc_thresholds = snakemake.params.qc_thresholds
serum = snakemake.wildcards.serum
group = snakemake.wildcards.group
curve_display_method = snakemake.params.curve_display_method

print(f"Processing {group=}, {serum=}")
Processing group='EPIHK', serum='EPIHK_16'

Load the notebook funcs:

In [4]:
print(f"Loading {notebook_funcs=}")
%run {notebook_funcs}
Loading notebook_funcs='/home/jbloom/.cache/snakemake/snakemake/source-cache/runtime-cache/tmpfro_n8p6/file/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline/notebook_funcs.py'

Get all titers for this plate¶

Combine all the pickled neutcurve.CurveFits from plates for this serum into a single neutcurve.CurveFits:

In [5]:
print(f"Combining the curve fits for {group=}, {serum=} from {pickle_fits=}")

fits_to_combine = []
for fname in pickle_fits:
    with open(fname, "rb") as f:
        fits_to_combine.append(pickle.load(f))
fits_noqc = neutcurve.CurveFits.combineCurveFits(fits_to_combine, sera=[serum])
Combining the curve fits for group='EPIHK', serum='EPIHK_16' from pickle_fits=['results/plates/plate13/curvefits.pickle']

Indicate how we are calculating the titer:

In [6]:
print(f"Calculating with {serum_titer_as=}")
assert serum_titer_as in {"nt50", "midpoint"}
Calculating with serum_titer_as='midpoint'

Get all the per-replicate fit params with the titers. We also convert the IC50 to NT50, and take inverse of midpoint to get it on same scale as NT50s:

In [7]:
per_rep_titers = fits_noqc.fitParams(average_only=False, no_average=True).assign(
    group=group,
    nt50=lambda x: 1 / x["ic50"],
    midpoint=lambda x: 1 / x["midpoint_bound"],
    titer=lambda x: x["midpoint"] if serum_titer_as == "midpoint" else x["nt50"],
    titer_bound=lambda x: (
        x["midpoint_bound_type"] if serum_titer_as == "midpoint" else x["ic50_bound"]
    ).map({"lower": "upper", "upper": "lower", "interpolated": "interpolated"}),
    titer_as=serum_titer_as,
)[
    [
        "group",
        "serum",
        "virus",
        "replicate",
        "titer",
        "titer_bound",
        "titer_as",
        "nt50",
        "midpoint",
        "top",
        "bottom",
        "slope",
    ]
]
assert per_rep_titers.notnull().all().all()

if len(invalid_titer_as := per_rep_titers.query("(titer_as == 'nt50') and top <= 0.5")):
    raise ValueError(
        f"There are titers computed as nt50 when curve top <= 0.5:\n{invalid_titer_as}"
    )
assert len(per_rep_titers) == per_rep_titers["replicate"].nunique()

# get viruses in the order to plot them
viruses = sorted(per_rep_titers["virus"].unique())
if viral_strain_plot_order is not None:
    if not set(viruses).issubset(viral_strain_plot_order):
        raise ValueError(
            "`viral_strain_plot_order` lacks some viruses with titers:\n"
            + str(set(viruses) - set(viral_strain_plot_order))
        )
    viruses = [v for v in viral_strain_plot_order if v in viruses]
print(f"{serum=} has titers for a total of {len(viruses)} viruses")
serum='EPIHK_16' has titers for a total of 140 viruses

Correlate NT50s with midpoints of curves¶

Plot the correlation of the NT50s with the midpoint (this is an interactive plot, mouse over points for details). This plot can help you determine if you made the correct choice of serum_titer_as when choosing to use the midpoint or NT50 for the titer. For titers where they are well correlated it should not matter which you chose. But if there are titers far from the correlation line, you should look at those measurements and curves to make sure you made the correct choice of calculating the titer as the NT50 versus midpoint:

In [8]:
virus_selection = alt.selection_point(fields=["virus"], on="mouseover", empty=False)

midpoint_vs_nt50_chart = (
    alt.Chart(per_rep_titers)
    .add_params(virus_selection)
    .encode(
        alt.X("nt50", scale=alt.Scale(type="log", nice=False, padding=8)),
        alt.Y("midpoint", scale=alt.Scale(type="log", nice=False, padding=8)),
        alt.Color("titer_bound"),
        strokeWidth=alt.condition(virus_selection, alt.value(3), alt.value(0)),
        size=alt.condition(virus_selection, alt.value(100), alt.value(60)),
        tooltip=[
            alt.Tooltip(c, format=".2g") if per_rep_titers[c].dtype == float else c
            for c in per_rep_titers.columns
            if c not in {"group", "serum", "titer_as"}
        ],
    )
    .mark_circle(stroke="black", fillOpacity=0.45, color="black")
    .properties(
        width=350,
        height=350,
        title=f"NT50 versus midpoint for {group} {serum}",
    )
    .configure_axis(grid=False)
)

midpoint_vs_nt50_chart
Out[8]:

Write the individual per-replicate titers to a file, this is before any QC has been applied:

In [9]:
print(f"Writing per-replicate titers (without QC filtering) to {per_rep_titers_csv=}")
per_rep_titers.to_csv(per_rep_titers_csv, index=False, float_format="%.4g")
Writing per-replicate titers (without QC filtering) to per_rep_titers_csv='results/sera/EPIHK_EPIHK_16/titers_per_replicate.csv'

Plot median titers and determine if they pass QC¶

Get the median titers for each virus across replicates, then add these median titers to the per-replicate titers and calculate the fold-change in titer between each replicate and its median. Finally, for each virus indicates whether it passes the QC:

In [10]:
print(f"Using the following {qc_thresholds=}")


def get_median_bound(s):
    """Get the bound on titer when taking median."""
    s = list(s)
    if len(s) % 2:
        return s[len(s) // 2]
    else:
        bounds = s[len(s) // 2 - 1 : len(s) // 2 + 1]
        assert len(bounds) == 2
        if len(set(bounds)) == 1:
            return bounds[0]
        elif "interpolated" in bounds:
            return [b for b in bounds if b != "interpolated"][0]
        else:
            return "inconsistent"


median_titers_noqc = (
    per_rep_titers.sort_values("titer")  # for getting median bound
    .groupby(["group", "serum", "virus", "titer_as"], as_index=False)
    .aggregate(
        titer=pd.NamedAgg("titer", "median"),
        n_replicates=pd.NamedAgg("replicate", "count"),
        titer_sem=pd.NamedAgg("titer", "sem"),
        titer_bound=pd.NamedAgg("titer_bound", get_median_bound),
    )
)

per_rep_titers_w_fc = (
    per_rep_titers.merge(
        median_titers_noqc[["group", "serum", "virus", "titer"]].rename(
            columns={"titer": "median_titer"}
        ),
        validate="many_to_one",
        on=["group", "serum", "virus"],
    )
    .assign(
        fc_from_median=lambda x: numpy.where(
            x["titer"] > x["median_titer"],
            x["titer"] / x["median_titer"],
            x["median_titer"] / x["titer"],
        ),
    )
    .drop(columns=["group", "serum", "titer_as", "median_titer"])
)

median_titers_noqc = median_titers_noqc.merge(
    per_rep_titers_w_fc.groupby("virus", as_index=False).aggregate(
        max_fc_from_median=pd.NamedAgg("fc_from_median", "max")
    ),
    on="virus",
    validate="one_to_one",
).assign(
    fails_min_reps=lambda x: x["n_replicates"] < qc_thresholds["min_replicates"],
    fails_max_fc=lambda x: (
        x["max_fc_from_median"] >= qc_thresholds["max_fold_change_from_median"]
    ),
    fails_qc=lambda x: x["fails_min_reps"] | x["fails_max_fc"],
    fails_qc_reason=lambda x: (
        x.apply(
            lambda r: ", ".join(
                (["min_replicates"] if r["fails_min_reps"] else [])
                + (["max_fold_change_from_median"] if r["fails_max_fc"] else [])
            ),
            axis=1,
        )
    ),
)

# get viruses failing QC in order to plot
viruses_failing_qc = (
    median_titers_noqc.query("fails_qc").set_index("virus")["fails_qc_reason"].to_dict()
)
viruses_failing_qc = {
    v: viruses_failing_qc[v] for v in viruses if v in viruses_failing_qc
}

median_titers_noqc = median_titers_noqc.drop(
    columns=["fails_min_reps", "fails_max_fc", "fails_qc_reason"]
)

per_rep_titers_w_fc = per_rep_titers_w_fc.merge(
    median_titers_noqc[["virus", "fails_qc"]],
    on="virus",
    validate="many_to_one",
)
Using the following qc_thresholds={'min_replicates': 1, 'max_fold_change_from_median': 6, 'viruses_ignore_qc': []}

Now plot the per-replicate and median titers, indicating any viruses that failed QC. Note that potentially some of these titers may still be retained if the viruses in question are specified in viruses_ignore_qc of qc_thresholds.

In [11]:
virus_selection = alt.selection_point(fields=["virus"], on="mouseover", empty=False)

per_rep_chart = (
    alt.Chart(per_rep_titers_w_fc)
    .encode(
        alt.X("titer", scale=alt.Scale(nice=False, padding=5, type="log")),
        alt.Y("virus", sort=viruses),
        alt.Fill(
            "fails_qc",
            title=f"fails {qc_thresholds['min_replicates']=}, {qc_thresholds['max_fold_change_from_median']=}",
            legend=alt.Legend(titleLimit=500),
        ),
        alt.Shape("titer_bound"),
        strokeWidth=alt.condition(virus_selection, alt.value(2), alt.value(0)),
        tooltip=[
            alt.Tooltip(c, format=".3g") if per_rep_titers_w_fc[c].dtype == float else c
            for c in per_rep_titers_w_fc
        ],
    )
    .mark_point(
        size=35,
        filled=True,
        fillOpacity=0.5,
        strokeOpacity=1,
        stroke="black",
    )
)

median_chart = (
    alt.Chart(median_titers_noqc)
    .encode(
        alt.X("titer", scale=alt.Scale(nice=False, padding=5, type="log")),
        alt.Y("virus", sort=viruses),
        alt.Fill("fails_qc"),
        alt.Shape("titer_bound"),
        strokeWidth=alt.condition(virus_selection, alt.value(2), alt.value(0.5)),
        tooltip=[
            alt.Tooltip(c, format=".3g") if median_titers_noqc[c].dtype == float else c
            for c in median_titers_noqc
        ],
    )
    .mark_point(
        size=75,
        filled=True,
        fillOpacity=0.9,
        strokeOpacity=1,
        stroke="black",
    )
)

titer_chart = (
    (per_rep_chart + median_chart)
    .add_params(virus_selection)
    .properties(
        height=alt.Step(11),
        width=250,
        title=f"{group} {serum} median (large points) and per-replicate (small points) titers",
    )
    .configure_axis(grid=False)
)

titer_chart
Out[11]:

Plot individual curves for any viruses failing QC¶

Plot individual curves for viruses failing QC. Note that potentially some of these titers may still be retained if the viruses in question are specified in viruses_ignore_qc of qc_thresholds.

In [12]:
print(f"Neutralization curves for the {len(viruses_failing_qc)} viruses failing QC:")

if len(viruses_failing_qc):
    fig, _ = fits_noqc.plotReplicates(
        viruses=viruses_failing_qc,
        attempt_shared_legend=False,
        legendfontsize=8,
        titlesize=12,
        ncol=4,
        heightscale=1.2,
        widthscale=1.2,
        subplot_titles="{virus}",
        draw_in_bounds=True,
    )
    _ = fig.suptitle(
        f"neutralization curves for viruses failing QC for {group} {serum}",
        y=1,
        fontsize=18,
        fontweight="bold",
    )
    fig.tight_layout()

    display_curve_fig(fig, curve_display_method)

    plt.close(fig)
Neutralization curves for the 0 viruses failing QC:

Get the viruses to drop for QC failures¶

Drop any viruses that fail QC and are not specified in viruses_ignore_qc of qc_thresholds:

In [13]:
viruses_to_drop = {
    v: reason
    for (v, reason) in viruses_failing_qc.items()
    if v not in qc_thresholds["viruses_ignore_qc"]
}

print(f"Dropping {len(viruses_to_drop)} viruses for failing QC:")
yaml.YAML(typ="rt").dump(viruses_to_drop, sys.stdout)
if nkept := (len(viruses_failing_qc) - len(viruses_to_drop)):
    print(
        f"\nRetaining {nkept} viruses that fail QC because they are in `viruses_ignore_qc`:"
    )
    print(
        {
            v: reason
            for (v, reason) in viruses_failing_qc.items()
            if v in qc_thresholds["viruses_ignore_qc"]
        }
    )

print(f"\nWriting QC drops to {qc_drops_file}")
with open(qc_drops_file, "w") as f:
    yaml.YAML(typ="rt").dump(viruses_to_drop, f)
Dropping 0 viruses for failing QC:
{}
Writing QC drops to results/sera/EPIHK_EPIHK_16/qc_drops.yml

Get and plot the neutralization curves for all retained viruses¶

First, get the CurveFits for just those retained viruses (dropping ones that fail QC), and plot:

In [14]:
fits_qc = neutcurve.CurveFits.combineCurveFits(
    [fits_noqc],
    viruses=[v for v in viruses if v not in viruses_to_drop],
)
assert len(viruses) == len(fits_qc.viruses[serum]) + len(viruses_to_drop)

fig, _ = fits_qc.plotReplicates(
    attempt_shared_legend=False,
    legendfontsize=8,
    titlesize=12,
    ncol=4,
    heightscale=1.2,
    widthscale=1.2,
    subplot_titles="{virus}",
    viruses=[v for v in viruses if v not in viruses_to_drop],
    draw_in_bounds=True,
)
_ = fig.suptitle(
    f"neutralization curves for retained viruses for {group} {serum}",
    y=1,
    fontsize=18,
    fontweight="bold",
)
fig.tight_layout()

display_curve_fig(fig, curve_display_method)

print(f"Saving to plot of curves to {curves_pdf}")
fig.savefig(curves_pdf)
plt.close(fig)
figure
Saving to plot of curves to results/sera/EPIHK_EPIHK_16/curves.pdf

Save the CurveFits to a pickle file:

In [15]:
with open(output_pickle, "wb") as f:
    pickle.dump(fits_qc, f)

Write the titers (excluding QC dropped viruses) to a CSV:

In [16]:
print(f"Writing titers to {titers_csv}")

(
    median_titers_noqc.query("virus not in @viruses_to_drop")[
        [
            "group",
            "serum",
            "virus",
            "titer",
            "titer_bound",
            "titer_sem",
            "n_replicates",
            "titer_as",
        ]
    ].to_csv(titers_csv, index=False, float_format="%.4g")
)
Writing titers to results/sera/EPIHK_EPIHK_16/titers.csv
In [ ]: