From 3cb11ea501272281f44dd9f14d051a252e6f5103 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 2 Jun 2026 17:17:29 -0700 Subject: [PATCH 01/24] ux cleanup in simulation code --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 869 +++++++++--------- reports/junit/junit.xml | 2 +- src/tfscreen/genetics/library_manager.py | 19 +- src/tfscreen/simulate/library_prediction.py | 8 +- src/tfscreen/simulate/run_simulation.py | 6 +- .../simulate/scripts/run_simulation_cli.py | 24 +- src/tfscreen/simulate/thermo_to_growth.py | 125 ++- .../tfmodel/inference/run_inference.py | 17 +- src/tfscreen/tfmodel/scripts/fit_model_cli.py | 80 +- .../tfmodel/scripts/sample_posterior_cli.py | 47 +- tests/smoke-tests/test_configure_run_smoke.py | 44 +- .../test_run_growth_analysis_smoke.py | 12 +- .../tfscreen/genetics/test_library_manager.py | 44 +- .../simulate/test_library_prediction.py | 8 +- .../tfscreen/simulate/test_run_simulation.py | 41 +- .../simulate/test_run_simulation_cli.py | 154 ++++ .../simulate/test_thermo_to_growth.py | 228 ++++- .../scripts/test_sample_posterior_cli.py | 30 +- .../tfmodel/test_fit_model_helpers.py | 47 +- .../tfmodel/test_get_posteriors_mapping.py | 47 + 22 files changed, 1192 insertions(+), 664 deletions(-) create mode 100644 tests/tfscreen/simulate/test_run_simulation_cli.py diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index b213db1a..954fc44f 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.95%coverage95.95% \ No newline at end of file +coverage: 95.88%coverage95.88% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 14389206..128d53c8 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3062tests3062 \ No newline at end of file +tests: 3082tests3082 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 55c3da9b..3a215a12 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -114,13 +114,13 @@ ./src/tfscreen/genetics/combine_mutation_effects.py:34:71: W291 trailing whitespace ./src/tfscreen/genetics/combine_mutation_effects.py:35:21: W291 trailing whitespace ./src/tfscreen/genetics/combine_mutation_effects.py:65:1: W293 blank line contains whitespace -./src/tfscreen/genetics/combine_mutation_effects.py:72:27: E261 at least two spaces before inline comment -./src/tfscreen/genetics/combine_mutation_effects.py:73:10: E111 indentation is not a multiple of 4 -./src/tfscreen/genetics/combine_mutation_effects.py:73:10: E117 over-indented -./src/tfscreen/genetics/combine_mutation_effects.py:81:1: W293 blank line contains whitespace -./src/tfscreen/genetics/combine_mutation_effects.py:88:1: W293 blank line contains whitespace -./src/tfscreen/genetics/combine_mutation_effects.py:102:1: W293 blank line contains whitespace -./src/tfscreen/genetics/combine_mutation_effects.py:103:25: W292 no newline at end of file +./src/tfscreen/genetics/combine_mutation_effects.py:73:27: E261 at least two spaces before inline comment +./src/tfscreen/genetics/combine_mutation_effects.py:74:10: E111 indentation is not a multiple of 4 +./src/tfscreen/genetics/combine_mutation_effects.py:74:10: E117 over-indented +./src/tfscreen/genetics/combine_mutation_effects.py:82:1: W293 blank line contains whitespace +./src/tfscreen/genetics/combine_mutation_effects.py:89:1: W293 blank line contains whitespace +./src/tfscreen/genetics/combine_mutation_effects.py:103:1: W293 blank line contains whitespace +./src/tfscreen/genetics/combine_mutation_effects.py:104:25: W292 no newline at end of file ./src/tfscreen/genetics/count_mutation_backgrounds.py:8:1: C901 'count_mutation_backgrounds' is too complex (12) ./src/tfscreen/genetics/data.py:25:8: E231 missing whitespace after ':' ./src/tfscreen/genetics/data.py:26:8: E231 missing whitespace after ':' @@ -228,10 +228,8 @@ ./src/tfscreen/genetics/library_manager.py:251:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:254:27: E231 missing whitespace after ',' ./src/tfscreen/genetics/library_manager.py:254:36: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:255:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:258:32: E231 missing whitespace after ',' ./src/tfscreen/genetics/library_manager.py:258:46: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:259:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:264:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:268:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:269:82: W291 trailing whitespace @@ -268,78 +266,78 @@ ./src/tfscreen/genetics/library_manager.py:386:5: E303 too many blank lines (2) ./src/tfscreen/genetics/library_manager.py:415:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:421:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:425:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:464:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:465:78: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:469:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:474:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:476:25: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:476:38: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:477:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:480:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:486:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:491:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:524:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:525:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:526:5: E303 too many blank lines (2) -./src/tfscreen/genetics/library_manager.py:553:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:558:73: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:558:82: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:559:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:560:41: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:560:55: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:560:63: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:561:39: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:561:43: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:562:34: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:562:38: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:564:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:427:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:466:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:467:78: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:471:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:476:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:478:25: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:478:38: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:479:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:482:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:488:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:493:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:526:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:527:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:528:5: E303 too many blank lines (2) +./src/tfscreen/genetics/library_manager.py:555:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:560:73: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:560:82: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:561:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:562:41: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:562:55: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:562:63: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:563:39: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:563:43: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:564:34: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:564:38: E231 missing whitespace after ',' ./src/tfscreen/genetics/library_manager.py:566:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:603:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:609:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:568:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:605:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:611:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:614:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:615:5: E303 too many blank lines (2) -./src/tfscreen/genetics/library_manager.py:646:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:613:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:616:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:617:5: E303 too many blank lines (2) ./src/tfscreen/genetics/library_manager.py:648:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:654:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:655:31: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:659:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:650:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:656:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:657:31: E231 missing whitespace after ',' ./src/tfscreen/genetics/library_manager.py:661:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:663:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:693:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:696:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:701:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:706:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:665:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:695:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:698:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:703:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:708:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:710:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:711:30: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:719:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:737:28: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:737:41: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:743:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:771:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:712:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:713:30: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:721:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:740:28: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:740:41: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:748:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:776:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:782:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:785:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:786:39: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:789:72: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:781:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:787:1: W293 blank line contains whitespace ./src/tfscreen/genetics/library_manager.py:790:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:793:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:801:9: E303 too many blank lines (2) -./src/tfscreen/genetics/library_manager.py:802:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:805:76: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:806:15: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:820:1: W293 blank line contains whitespace -./src/tfscreen/genetics/library_manager.py:833:79: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:834:75: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:835:34: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:836:58: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:840:64: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:848:37: W291 trailing whitespace -./src/tfscreen/genetics/library_manager.py:849:27: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:851:40: E231 missing whitespace after ',' -./src/tfscreen/genetics/library_manager.py:854:1: W391 blank line at end of file +./src/tfscreen/genetics/library_manager.py:791:39: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:794:72: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:795:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:798:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:806:9: E303 too many blank lines (2) +./src/tfscreen/genetics/library_manager.py:807:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:810:76: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:811:15: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:825:1: W293 blank line contains whitespace +./src/tfscreen/genetics/library_manager.py:838:79: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:839:75: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:840:34: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:841:58: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:845:64: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:853:37: W291 trailing whitespace +./src/tfscreen/genetics/library_manager.py:854:27: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:856:40: E231 missing whitespace after ',' +./src/tfscreen/genetics/library_manager.py:859:1: W391 blank line at end of file ./src/tfscreen/mle/curve_models/__init__.py:4:77: W291 trailing whitespace ./src/tfscreen/mle/curve_models/__init__.py:5:42: W291 trailing whitespace ./src/tfscreen/mle/curve_models/__init__.py:11:77: W291 trailing whitespace @@ -1282,9 +1280,9 @@ ./src/tfscreen/simulate/run_simulation.py:18:36: E231 missing whitespace after ',' ./src/tfscreen/simulate/run_simulation.py:21:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:22:23: E231 missing whitespace after ',' -./src/tfscreen/simulate/run_simulation.py:22:35: E231 missing whitespace after ',' -./src/tfscreen/simulate/run_simulation.py:22:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/run_simulation.py:22:61: E231 missing whitespace after ',' +./src/tfscreen/simulate/run_simulation.py:22:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/run_simulation.py:22:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/run_simulation.py:22:62: E231 missing whitespace after ',' ./src/tfscreen/simulate/run_simulation.py:24:47: E231 missing whitespace after ',' ./src/tfscreen/simulate/run_simulation.py:25:32: E231 missing whitespace after ',' ./src/tfscreen/simulate/run_simulation.py:34:1: W293 blank line contains whitespace @@ -1306,7 +1304,7 @@ ./src/tfscreen/simulate/run_simulation.py:88:51: E231 missing whitespace after ',' ./src/tfscreen/simulate/run_simulation.py:88:62: E231 missing whitespace after ',' ./src/tfscreen/simulate/run_simulation.py:91:26: E231 missing whitespace after ':' -./src/tfscreen/simulate/run_simulation.py:92:28: E231 missing whitespace after ':' +./src/tfscreen/simulate/run_simulation.py:92:29: E231 missing whitespace after ':' ./src/tfscreen/simulate/run_simulation.py:93:33: E231 missing whitespace after ':' ./src/tfscreen/simulate/run_simulation.py:94:25: E231 missing whitespace after ':' ./src/tfscreen/simulate/run_simulation.py:95:25: E231 missing whitespace after ':' @@ -1370,100 +1368,100 @@ ./src/tfscreen/simulate/selection_experiment.py:661:1: W293 blank line contains whitespace ./src/tfscreen/simulate/selection_experiment.py:662:67: W291 trailing whitespace ./src/tfscreen/simulate/selection_experiment.py:712:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:728:79: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:734:29: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:776:43: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:782:74: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:783:18: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:789:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:791:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:796:80: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:801:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:807:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:818:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:861:73: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:866:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:883:25: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:984:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:992:26: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:992:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:992:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:992:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:992:60: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1000:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1001:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1009:42: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1018:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1019:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1021:63: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1027:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1029:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1036:28: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1038:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1039:24: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1046:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1051:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1052:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1054:36: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1056:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1057:61: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1059:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1065:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1066:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1070:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1071:72: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1075:34: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1082:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1084:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1084:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1085:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1085:56: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1087:32: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1088:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1100:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1108:41: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1115:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1117:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1122:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1123:37: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1123:45: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1123:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1123:61: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1128:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:1163:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1184:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1185:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1188:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1188:39: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1193:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1199:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1202:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1210:33: E261 at least two spaces before inline comment -./src/tfscreen/simulate/selection_experiment.py:1211:49: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1212:37: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:1214:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1215:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1217:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1219:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1227:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1228:43: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1232:41: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/selection_experiment.py:1235:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1259:71: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1267:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1268:76: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1273:11: F541 f-string is missing placeholders -./src/tfscreen/simulate/selection_experiment.py:1278:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1295:6: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/simulate/selection_experiment.py:1302:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1303:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1304:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1305:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1308:1: W391 blank line at end of file +./src/tfscreen/simulate/selection_experiment.py:729:79: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:735:29: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:777:43: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:783:74: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:784:18: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:790:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:792:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:797:80: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:802:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:808:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:819:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:862:73: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:867:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:884:25: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:985:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:993:26: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:993:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:993:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:993:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:993:60: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1001:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1002:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1010:42: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1019:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1020:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1022:63: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1028:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1030:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1037:28: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1039:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1040:24: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1047:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1052:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1053:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1055:36: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1057:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1061:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1067:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1068:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1072:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1073:72: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1077:34: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1084:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1086:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1086:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1087:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1087:56: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1089:32: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1090:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1102:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1110:41: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1117:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1119:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1124:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1125:37: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1125:45: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1125:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1125:61: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1130:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:1165:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1186:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1187:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1190:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1190:39: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1195:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1201:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1204:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1212:33: E261 at least two spaces before inline comment +./src/tfscreen/simulate/selection_experiment.py:1213:49: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1214:37: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:1216:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1217:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1219:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1221:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1229:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1230:43: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1234:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/selection_experiment.py:1237:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1261:71: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1269:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1270:76: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1275:11: F541 f-string is missing placeholders +./src/tfscreen/simulate/selection_experiment.py:1280:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1297:6: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/simulate/selection_experiment.py:1304:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1305:48: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1306:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1307:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1310:1: W391 blank line at end of file ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/thermo_to_growth.py:395:5: F841 local variable 'n_geno' is assigned to but never used +./src/tfscreen/simulate/thermo_to_growth.py:476:5: F841 local variable 'n_geno' is assigned to but never used +./src/tfscreen/simulate/thermo_to_growth.py:566:41: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/__init__.py:2:73: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:3:20: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:18:65: W291 trailing whitespace @@ -3289,7 +3287,7 @@ ./src/tfscreen/tfmodel/inference/run_inference.py:123:58: W291 trailing whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:124:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:129:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:132:5: C901 'RunInference.run_optimization' is too complex (17) +./src/tfscreen/tfmodel/inference/run_inference.py:132:5: C901 'RunInference.run_optimization' is too complex (18) ./src/tfscreen/tfmodel/inference/run_inference.py:223:77: W291 trailing whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:224:73: W291 trailing whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:238:1: W293 blank line contains whitespace @@ -3310,61 +3308,60 @@ ./src/tfscreen/tfmodel/inference/run_inference.py:346:36: W291 trailing whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:347:50: W291 trailing whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:348:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:362:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:365:5: C901 'RunInference._get_genotype_dim_map' is too complex (13) -./src/tfscreen/tfmodel/inference/run_inference.py:391:26: E261 at least two spaces before inline comment -./src/tfscreen/tfmodel/inference/run_inference.py:430:5: C901 'RunInference.get_posteriors' is too complex (18) -./src/tfscreen/tfmodel/inference/run_inference.py:473:59: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:489:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:370:5: C901 'RunInference._get_genotype_dim_map' is too complex (14) +./src/tfscreen/tfmodel/inference/run_inference.py:396:26: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/inference/run_inference.py:445:5: C901 'RunInference.get_posteriors' is too complex (18) +./src/tfscreen/tfmodel/inference/run_inference.py:488:59: W291 trailing whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:504:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:518:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:520:71: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:564:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:582:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:598:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:600:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/inference/run_inference.py:600:37: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:600:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:633:31: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:645:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:646:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:653:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/inference/run_inference.py:653:31: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:653:41: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:667:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/inference/run_inference.py:668:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/inference/run_inference.py:669:35: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/inference/run_inference.py:670:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/inference/run_inference.py:671:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/inference/run_inference.py:678:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:679:31: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:721:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:735:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:738:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:739:49: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:748:36: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:755:27: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:755:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:757:61: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:806:5: E303 too many blank lines (4) -./src/tfscreen/tfmodel/inference/run_inference.py:847:26: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:847:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:850:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:862:41: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:864:32: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:864:42: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:915:5: C901 'RunInference.get_map_posteriors' is too complex (13) -./src/tfscreen/tfmodel/inference/run_inference.py:1153:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/inference/run_inference.py:1194:29: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:1197:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1202:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1210:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:1214:30: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:519:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:533:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:535:71: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:579:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:597:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:613:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:615:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/inference/run_inference.py:615:37: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:615:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:648:31: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:660:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:661:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:668:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/inference/run_inference.py:668:31: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:668:41: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:682:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/inference/run_inference.py:683:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/inference/run_inference.py:684:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/inference/run_inference.py:685:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/inference/run_inference.py:686:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/inference/run_inference.py:693:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:694:31: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:736:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:750:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:753:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:754:49: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:763:36: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:770:27: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:770:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:772:61: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:821:5: E303 too many blank lines (4) +./src/tfscreen/tfmodel/inference/run_inference.py:862:26: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:862:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:865:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:877:41: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:879:32: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:879:42: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:930:5: C901 'RunInference.get_map_posteriors' is too complex (13) +./src/tfscreen/tfmodel/inference/run_inference.py:1168:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/inference/run_inference.py:1209:29: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:1212:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:1217:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1221:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1227:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1230:5: C901 'RunInference.get_laplace_posteriors' is too complex (16) -./src/tfscreen/tfmodel/inference/run_inference.py:1440:5: C901 'RunInference.get_nuts_posteriors' is too complex (12) +./src/tfscreen/tfmodel/inference/run_inference.py:1225:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:1229:30: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1232:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1236:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1242:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1245:5: C901 'RunInference.get_laplace_posteriors' is too complex (16) +./src/tfscreen/tfmodel/inference/run_inference.py:1455:5: C901 'RunInference.get_nuts_posteriors' is too complex (12) ./src/tfscreen/tfmodel/model_orchestrator.py:36:1: E303 too many blank lines (3) ./src/tfscreen/tfmodel/model_orchestrator.py:70:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:73:39: E231 missing whitespace after ',' @@ -3590,27 +3587,27 @@ ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:226:63: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:227:55: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:229:1: E305 expected 2 blank lines after class or function definition, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:96:6: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:96:6: E116 unexpected indentation (comment) -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:119:27: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:132:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:134:47: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:138:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:265:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:267:47: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:271:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:547:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:549:60: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:550:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:551:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:552:66: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:553:66: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:554:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:555:65: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:556:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:557:72: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:558:74: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:560:1: E305 expected 2 blank lines after class or function definition, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:97:6: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:97:6: E116 unexpected indentation (comment) +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:120:27: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:124:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:126:47: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:130:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:243:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:245:47: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:249:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:509:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:511:60: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:512:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:513:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:514:66: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:515:66: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:516:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:517:65: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:518:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:519:72: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:520:74: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:255:24: E128 continuation line under-indented for visual indent @@ -3634,6 +3631,21 @@ ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:927:21: E221 multiple spaces before operator ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:950:20: E221 multiple spaces before operator ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:963:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:102:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:103:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:104:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:105:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:106:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:107:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:108:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:109:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:110:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:111:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:112:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:113:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:114:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:115:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:116:50: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used @@ -3909,24 +3921,12 @@ ./tests/smoke-tests/conftest.py:10:1: E302 expected 2 blank lines, found 1 ./tests/smoke-tests/conftest.py:15:1: E302 expected 2 blank lines, found 1 ./tests/smoke-tests/conftest.py:20:1: E302 expected 2 blank lines, found 1 -./tests/smoke-tests/test_configure_run_smoke.py:9:1: E302 expected 2 blank lines, found 1 -./tests/smoke-tests/test_configure_run_smoke.py:32:31: E127 continuation line over-indented for visual indent +./tests/smoke-tests/test_configure_run_smoke.py:10:1: E302 expected 2 blank lines, found 1 ./tests/smoke-tests/test_configure_run_smoke.py:33:31: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:51:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:52:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:53:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:54:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:55:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:56:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:97:31: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:98:31: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:122:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:123:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:124:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:125:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:126:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:127:25: E127 continuation line over-indented for visual indent -./tests/smoke-tests/test_configure_run_smoke.py:170:16: E221 multiple spaces before operator +./tests/smoke-tests/test_configure_run_smoke.py:34:31: E127 continuation line over-indented for visual indent +./tests/smoke-tests/test_configure_run_smoke.py:101:31: E127 continuation line over-indented for visual indent +./tests/smoke-tests/test_configure_run_smoke.py:102:31: E127 continuation line over-indented for visual indent +./tests/smoke-tests/test_configure_run_smoke.py:175:16: E221 multiple spaces before operator ./tests/smoke-tests/test_model_smoke.py:3:1: F401 'shutil' imported but unused ./tests/smoke-tests/test_model_smoke.py:4:1: F401 'jax.numpy as jnp' imported but unused ./tests/smoke-tests/test_model_smoke.py:11:71: W291 trailing whitespace @@ -4072,8 +4072,6 @@ ./tests/smoke-tests/test_run_growth_analysis_smoke.py:37:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:45:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:48:1: W293 blank line contains whitespace -./tests/smoke-tests/test_run_growth_analysis_smoke.py:62:1: W293 blank line contains whitespace -./tests/smoke-tests/test_run_growth_analysis_smoke.py:66:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:4:1: F401 'pandas as pd' imported but unused ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:10:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:17:1: E302 expected 2 blank lines, found 1 @@ -4356,104 +4354,107 @@ ./tests/tfscreen/genetics/test_library_manager.py:169:24: E231 missing whitespace after ':' ./tests/tfscreen/genetics/test_library_manager.py:173:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/genetics/test_library_manager.py:186:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:194:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:199:39: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:203:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:211:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:219:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:224:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:229:50: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:233:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:191:27: E221 multiple spaces before operator +./tests/tfscreen/genetics/test_library_manager.py:192:32: E221 multiple spaces before operator +./tests/tfscreen/genetics/test_library_manager.py:198:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:206:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:211:39: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:215:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:223:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:231:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:236:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:241:50: E261 at least two spaces before inline comment ./tests/tfscreen/genetics/test_library_manager.py:245:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:281:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:300:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:307:14: E201 whitespace after '(' -./tests/tfscreen/genetics/test_library_manager.py:307:39: E202 whitespace before ')' -./tests/tfscreen/genetics/test_library_manager.py:308:14: E201 whitespace after '(' -./tests/tfscreen/genetics/test_library_manager.py:308:55: E202 whitespace before ')' -./tests/tfscreen/genetics/test_library_manager.py:323:22: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:324:23: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:344:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:348:26: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:349:21: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:355:36: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:359:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:365:26: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:366:21: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:372:35: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:373:36: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:375:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:378:36: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:379:26: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:380:21: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:388:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:391:38: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:392:36: E114 indentation is not a multiple of 4 (comment) -./tests/tfscreen/genetics/test_library_manager.py:392:36: E116 unexpected indentation (comment) -./tests/tfscreen/genetics/test_library_manager.py:393:36: E114 indentation is not a multiple of 4 (comment) -./tests/tfscreen/genetics/test_library_manager.py:393:36: E116 unexpected indentation (comment) -./tests/tfscreen/genetics/test_library_manager.py:394:36: E114 indentation is not a multiple of 4 (comment) -./tests/tfscreen/genetics/test_library_manager.py:394:36: E116 unexpected indentation (comment) -./tests/tfscreen/genetics/test_library_manager.py:395:39: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:396:26: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:397:21: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:405:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:409:26: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:409:37: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:410:21: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:463:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:479:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:481:32: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:514:19: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:515:58: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:516:18: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:522:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:527:51: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:528:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:539:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:544:19: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:545:67: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:546:18: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:550:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:554:36: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:567:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:574:19: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:576:35: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:585:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:587:24: E201 whitespace after '{' -./tests/tfscreen/genetics/test_library_manager.py:587:75: E202 whitespace before '}' -./tests/tfscreen/genetics/test_library_manager.py:589:24: E201 whitespace after '{' -./tests/tfscreen/genetics/test_library_manager.py:589:75: E202 whitespace before '}' -./tests/tfscreen/genetics/test_library_manager.py:591:24: E201 whitespace after '{' -./tests/tfscreen/genetics/test_library_manager.py:591:75: E202 whitespace before '}' -./tests/tfscreen/genetics/test_library_manager.py:598:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:613:19: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:615:18: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:636:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:642:19: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:644:18: E222 multiple spaces after operator -./tests/tfscreen/genetics/test_library_manager.py:644:44: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:653:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:668:1: E303 too many blank lines (3) -./tests/tfscreen/genetics/test_library_manager.py:672:1: E302 expected 2 blank lines, found 3 -./tests/tfscreen/genetics/test_library_manager.py:681:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:689:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:691:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:694:48: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:695:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:707:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:711:42: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:712:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:715:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:719:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:724:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:728:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/genetics/test_library_manager.py:734:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:739:1: W293 blank line contains whitespace -./tests/tfscreen/genetics/test_library_manager.py:771:22: E261 at least two spaces before inline comment -./tests/tfscreen/genetics/test_library_manager.py:775:35: E231 missing whitespace after ':' -./tests/tfscreen/genetics/test_library_manager.py:775:49: W291 trailing whitespace -./tests/tfscreen/genetics/test_library_manager.py:800:34: W292 no newline at end of file +./tests/tfscreen/genetics/test_library_manager.py:257:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:293:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:312:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:319:14: E201 whitespace after '(' +./tests/tfscreen/genetics/test_library_manager.py:319:39: E202 whitespace before ')' +./tests/tfscreen/genetics/test_library_manager.py:320:14: E201 whitespace after '(' +./tests/tfscreen/genetics/test_library_manager.py:320:55: E202 whitespace before ')' +./tests/tfscreen/genetics/test_library_manager.py:335:22: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:336:23: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:356:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:360:26: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:361:21: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:367:36: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:371:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:377:26: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:378:21: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:384:35: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:385:36: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:387:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:390:36: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:391:26: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:392:21: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:400:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:403:38: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:404:36: E114 indentation is not a multiple of 4 (comment) +./tests/tfscreen/genetics/test_library_manager.py:404:36: E116 unexpected indentation (comment) +./tests/tfscreen/genetics/test_library_manager.py:405:36: E114 indentation is not a multiple of 4 (comment) +./tests/tfscreen/genetics/test_library_manager.py:405:36: E116 unexpected indentation (comment) +./tests/tfscreen/genetics/test_library_manager.py:406:36: E114 indentation is not a multiple of 4 (comment) +./tests/tfscreen/genetics/test_library_manager.py:406:36: E116 unexpected indentation (comment) +./tests/tfscreen/genetics/test_library_manager.py:407:39: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:408:26: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:409:21: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:417:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:421:26: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:421:37: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:422:21: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:475:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:491:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:493:32: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:526:19: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:527:58: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:528:18: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:534:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:539:51: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:540:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:551:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:556:19: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:557:67: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:558:18: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:562:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:566:36: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:579:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:586:19: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:588:35: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:597:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:599:24: E201 whitespace after '{' +./tests/tfscreen/genetics/test_library_manager.py:599:75: E202 whitespace before '}' +./tests/tfscreen/genetics/test_library_manager.py:601:24: E201 whitespace after '{' +./tests/tfscreen/genetics/test_library_manager.py:601:75: E202 whitespace before '}' +./tests/tfscreen/genetics/test_library_manager.py:603:24: E201 whitespace after '{' +./tests/tfscreen/genetics/test_library_manager.py:603:75: E202 whitespace before '}' +./tests/tfscreen/genetics/test_library_manager.py:610:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:625:19: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:627:18: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:648:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:654:19: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:656:18: E222 multiple spaces after operator +./tests/tfscreen/genetics/test_library_manager.py:656:44: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:665:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:680:1: E303 too many blank lines (3) +./tests/tfscreen/genetics/test_library_manager.py:684:1: E302 expected 2 blank lines, found 3 +./tests/tfscreen/genetics/test_library_manager.py:694:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:702:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:704:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:707:48: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:708:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:720:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:734:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:747:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:751:42: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:759:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:764:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:768:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/genetics/test_library_manager.py:774:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:779:1: W293 blank line contains whitespace +./tests/tfscreen/genetics/test_library_manager.py:811:22: E261 at least two spaces before inline comment +./tests/tfscreen/genetics/test_library_manager.py:815:35: E231 missing whitespace after ':' +./tests/tfscreen/genetics/test_library_manager.py:815:49: W291 trailing whitespace +./tests/tfscreen/genetics/test_library_manager.py:840:34: W292 no newline at end of file ./tests/tfscreen/mle/curve_models/test_guesses.py:7:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/curve_models/test_guesses.py:18:1: W293 blank line contains whitespace ./tests/tfscreen/mle/curve_models/test_guesses.py:21:44: E261 at least two spaces before inline comment @@ -5134,34 +5135,27 @@ ./tests/tfscreen/simulate/test_build_sample_dataframes.py:104:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:115:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:128:55: W292 no newline at end of file +./tests/tfscreen/simulate/test_library_prediction.py:47:41: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_run_simulation.py:3:1: F401 'os' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:5:1: F401 'unittest.mock.MagicMock' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:7:1: F401 'tfscreen.util' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:9:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_run_simulation.py:13:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:29:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:33:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:40:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:46:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:50:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:64:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:69:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:71:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:74:128: E501 line too long (128 > 127 characters) -./tests/tfscreen/simulate/test_run_simulation.py:76:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:78:128: E501 line too long (148 > 127 characters) -./tests/tfscreen/simulate/test_run_simulation.py:80:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:89:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:92:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:94:47: W291 trailing whitespace -./tests/tfscreen/simulate/test_run_simulation.py:97:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_run_simulation.py:99:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:103:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:106:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation.py:109:5: F841 local variable 'results' is assigned to but never used +./tests/tfscreen/simulate/test_run_simulation.py:27:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:30:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:34:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:41:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:47:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:51:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_run_simulation.py:55:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:65:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:70:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_run_simulation.py:82:128: E501 line too long (150 > 127 characters) +./tests/tfscreen/simulate/test_run_simulation.py:101:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_run_simulation.py:110:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_run_simulation.py:113:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_run_simulation.py:116:5: F841 local variable 'results' is assigned to but never used +./tests/tfscreen/simulate/test_run_simulation.py:117:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:4:1: F401 'numpy.ma' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:6:1: F401 'scipy.stats.gmean' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5313,16 +5307,18 @@ ./tests/tfscreen/simulate/test_selection_experiment.py:1269:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:1272:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:1274:73: W292 no newline at end of file -./tests/tfscreen/simulate/test_thermo_to_growth.py:27:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_thermo_to_growth.py:31:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_thermo_to_growth.py:40:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_thermo_to_growth.py:95:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:97:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:103:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:105:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:172:14: E131 continuation line unaligned for hanging indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:199:44: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:206:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:28:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_thermo_to_growth.py:32:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_thermo_to_growth.py:41:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_thermo_to_growth.py:96:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:98:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:104:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:106:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:173:14: E131 continuation line unaligned for hanging indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:200:44: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:207:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:1058:1: E402 module level import not at top of file +./tests/tfscreen/simulate/test_thermo_to_growth.py:1146:9: F841 local variable 'G_out' is assigned to but never used ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 @@ -6297,7 +6293,7 @@ ./tests/tfscreen/tfmodel/components/transformation/test__congression.py:37:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/transformation/test__congression.py:39:32: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/components/transformation/test__congression.py:40:36: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:41:31: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/components/transformation/test__congression.py:41:47: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/components/transformation/test__congression.py:53:26: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/components/transformation/test__congression.py:54:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/transformation/test__congression.py:56:1: W293 blank line contains whitespace @@ -6770,11 +6766,10 @@ ./tests/tfscreen/tfmodel/test_extraction_robustness.py:390:9: F841 local variable 'ds' is assigned to but never used ./tests/tfscreen/tfmodel/test_fit_model_helpers.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_fit_model_helpers.py:42:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_fit_model_helpers.py:87:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_fit_model_helpers.py:103:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_fit_model_helpers.py:113:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_fit_model_helpers.py:125:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_fit_model_helpers.py:162:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_fit_model_helpers.py:78:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_fit_model_helpers.py:88:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_fit_model_helpers.py:100:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_fit_model_helpers.py:135:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:5:1: F401 'numpyro.handlers' imported but unused ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:7:1: F401 'numpyro.infer.SVI' imported but unused ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:7:1: F401 'numpyro.infer.Trace_ELBO' imported but unused @@ -6811,38 +6806,38 @@ ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:110:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:118:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:126:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:129:5: F841 local variable 'num_titrants' is assigned to but never used -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:132:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:137:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:140:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:143:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:145:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:146:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:147:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:147:48: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:148:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:148:46: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:149:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:150:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:152:54: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:154:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:158:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:163:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:170:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:176:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:178:5: F841 local variable 'svi' is assigned to but never used -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:179:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:183:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:188:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:176:5: F841 local variable 'num_titrants' is assigned to but never used +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:179:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:184:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:187:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:190:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:192:59: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:193:28: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:193:52: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:194:28: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:194:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:195:28: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:196:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:200:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:192:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:193:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:194:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:194:48: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:195:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:195:46: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:196:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:197:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:199:54: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:201:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:205:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:210:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:217:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:223:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:225:5: F841 local variable 'svi' is assigned to but never used +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:226:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:230:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:235:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:237:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:239:59: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:240:28: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:240:52: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:241:28: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:241:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:242:28: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:243:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_get_posteriors_mapping.py:247:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_h5_indexing.py:4:1: F401 'os' imported but unused ./tests/tfscreen/tfmodel/test_h5_indexing.py:9:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_h5_indexing.py:11:1: W293 blank line contains whitespace @@ -7518,7 +7513,7 @@ ./tests/tfscreen/util/io/test_read_dataframe.py:192:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/io/test_read_dataframe.py:200:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/io/test_read_dataframe.py:207:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/util/io/test_read_dataframe.py:215:40: W292 no newline at end of file +./tests/tfscreen/util/io/test_read_dataframe.py:215:67: W292 no newline at end of file ./tests/tfscreen/util/numerical/test_array_search.py:78:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_array_search.py:143:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_array_search.py:167:1: E302 expected 2 blank lines, found 1 @@ -7603,14 +7598,14 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -153 E127 continuation line over-indented for visual indent +158 E127 continuation line over-indented for visual indent 66 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1711 E221 multiple spaces before operator +1713 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1112 E231 missing whitespace after ',' @@ -7622,13 +7617,13 @@ 2 E266 too many leading '#' for block comment 150 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -801 E302 expected 2 blank lines, found 1 +803 E302 expected 2 blank lines, found 1 51 E303 too many blank lines (2) 17 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line -32 E402 module level import not at top of file -71 E501 line too long (145 > 127 characters) +33 E402 module level import not at top of file +70 E501 line too long (145 > 127 characters) 46 E701 multiple statements on one line (colon) 1 E702 multiple statements on one line (semicolon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' @@ -7639,9 +7634,9 @@ 214 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 -40 F841 local variable 'e' is assigned to but never used -693 W291 trailing whitespace +41 F841 local variable 'e' is assigned to but never used +691 W291 trailing whitespace 86 W292 no newline at end of file -1721 W293 blank line contains whitespace +1708 W293 blank line contains whitespace 29 W391 blank line at end of file -7597 +7592 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index fd03d45a..9069f37f 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/genetics/library_manager.py b/src/tfscreen/genetics/library_manager.py index 98b2fb17..e1a9c67c 100644 --- a/src/tfscreen/genetics/library_manager.py +++ b/src/tfscreen/genetics/library_manager.py @@ -250,15 +250,15 @@ def _parse_and_validate(self, # -- Validate the sequence/library specification -- # Load wildtype seq - wt_seq = str(run_config["wt_seq"]).strip() + wt_seq = "".join(str(run_config["wt_seq"]).split()) _check_char(wt_seq,"wt_seq",self.standard_bases) - + # Load degenerate sites - degen_sites = str(run_config["degen_sites"]).strip() + degen_sites = "".join(str(run_config["degen_sites"]).split()) _check_char(degen_sites,"degen_sites",self.degen_plus_dot) - + # Load sub-libraries - sub_libraries = str(run_config["sub_libraries"]).strip() + sub_libraries = "".join(str(run_config["sub_libraries"]).split()) _check_contiguous_lib_blocks(sub_libraries) libraries_seen = set(list(sub_libraries)) - {"."} @@ -420,7 +420,9 @@ def _prepare_indiv_lib_blocks(self, lib_to_get: str) -> Tuple[List[List[str]], L end_idx = indexes[-1] + 1 # Get the degenerate sites and wildtype sequences for this sub-library - lib_seq = "".join(self.degen_sites[start_idx:end_idx]) + lib_seq = "".join(d if d != '.' else w + for d, w in zip(self.degen_sites[start_idx:end_idx], + self.wt_seq[start_idx:end_idx])) wt_seq = "".join(self.wt_seq[start_idx:end_idx]) # Extract the region of the library that encodes the degenerate library, @@ -734,11 +736,14 @@ def _get_spiked_seqs(self,list_of_seqs): spiked_seqs = [] for seq in list_of_seqs: - _check_char(seq,"spiked seq",self.standard_bases) + seq = "".join(str(seq).split()) + _check_char(seq,"spiked seq",self.standard_plus_dot) if len(seq) != len(self.wt_seq): raise ValueError( f"spiked seq {seq} is not the same length as the library" ) + seq = "".join(w if s == "." else s + for s, w in zip(seq, self.wt_seq)) spiked_seqs.append(seq) spiked_aa = self._convert_to_aa(spiked_seqs) diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index d6651580..821e77a7 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -41,6 +41,10 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], genotype_theta_df : pandas.DataFrame wide-form dataframe with one row per genotype and one column per unique effector concentration giving the ground-truth theta value + parameters_df : pandas.DataFrame + one row per unique genotype; columns ``dk_geno``, ``activity``, and + any scalar per-genotype fields from the theta component (e.g. + ``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n``) """ # ------------------------------------------------------------------------- @@ -73,7 +77,7 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], theta_rng_key = jax.random.PRNGKey(theta_rng_seed) # Calculate phenotype for each genotype across all conditions in sample_df - phenotype_df, genotype_theta_df = thermo_to_growth( + phenotype_df, genotype_theta_df, parameters_df = thermo_to_growth( genotypes=library_df["genotype"], sim_data=sim_data, sample_df=sample_df, @@ -91,4 +95,4 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], theta_rescale=cf.get('theta_rescale', 'passthrough'), ) - return library_df, phenotype_df, genotype_theta_df + return library_df, phenotype_df, genotype_theta_df, parameters_df diff --git a/src/tfscreen/simulate/run_simulation.py b/src/tfscreen/simulate/run_simulation.py index 90e0b24b..8d44a9fb 100644 --- a/src/tfscreen/simulate/run_simulation.py +++ b/src/tfscreen/simulate/run_simulation.py @@ -19,7 +19,7 @@ def _setup_file_output(output_dir, err = "if output_dir is specified, output_prefix must be a string\n" raise ValueError(err) - roots = ["library","phenotype","genotype_theta","sample","counts"] + roots = ["library","parameters","genotype_theta","sample","counts"] files = [f"{output_prefix}{r}.csv" for r in roots] files_with_path = [os.path.join(output_dir,f) for f in files] file_dict = dict([(roots[i],files_with_path[i]) for i in range(len(roots))]) @@ -82,14 +82,14 @@ def run_simulation(cf: Union[Dict[str, Any], str, Path], file_dict = _setup_file_output(output_dir,output_prefix) # Build library and predict its phenotypes - library_df, phenotype_df, genotype_theta_df = library_prediction(cf) + library_df, phenotype_df, genotype_theta_df, parameters_df = library_prediction(cf) # Perform selection experiment sample_df, counts_df = selection_experiment(cf,library_df,phenotype_df) # Prepare outputs out_dict = {"library":library_df, - "phenotype":phenotype_df, + "parameters":parameters_df, "genotype_theta":genotype_theta_df, "sample":sample_df, "counts":counts_df} diff --git a/src/tfscreen/simulate/scripts/run_simulation_cli.py b/src/tfscreen/simulate/scripts/run_simulation_cli.py index 4fff97a7..f8e4f6ac 100644 --- a/src/tfscreen/simulate/scripts/run_simulation_cli.py +++ b/src/tfscreen/simulate/scripts/run_simulation_cli.py @@ -101,8 +101,9 @@ def _generate_binding_data(cf, library_df, binding_cfg, rng): def run_simulation_from_config( config_file, output_dir, - output_prefix="tfscreen_", - num_replicates=2, + output_prefix="tfs_sim_", + num_replicates=1, + seed=None, ): """ Simulate a TF selection experiment from a YAML configuration file. @@ -120,18 +121,22 @@ def run_simulation_from_config( output_dir : str Directory to write output CSV files into (created if absent). output_prefix : str - Prefix for all output filenames. Default 'tfscreen_'. + Prefix for all output filenames. Default 'tfs_sim_'. num_replicates : int - Number of independent experimental replicates to simulate. Default 2. + Number of independent experimental replicates to simulate. Default 1. + seed : int, optional + Random seed. Overrides random_seed in the config file when provided. """ cf = tfscreen.util.read_yaml(config_file) + if seed is not None: + cf["random_seed"] = seed os.makedirs(output_dir, exist_ok=True) def out_path(name): return os.path.join(output_dir, f"{output_prefix}{name}.csv") - output_names = ["library", "phenotype", "genotype_theta", "growth"] + output_names = ["library", "parameters", "genotype_theta", "growth"] if "binding_data" in cf: output_names.append("binding") @@ -146,7 +151,7 @@ def out_path(name): # ------------------------------------------------------------------------- # Ground-truth library and phenotypes (deterministic given the config) - library_df, phenotype_df, genotype_theta_df = library_prediction(cf) + library_df, phenotype_df, genotype_theta_df, parameters_df = library_prediction(cf) # ------------------------------------------------------------------------- # Simulate independent replicates @@ -193,10 +198,10 @@ def out_path(name): # Write outputs library_df.to_csv(out_path("library"), index=False) - phenotype_df.to_csv(out_path("phenotype"), index=False) + parameters_df.to_csv(out_path("parameters"), index=False) genotype_theta_df.to_csv(out_path("genotype_theta"), index=False) growth_df.to_csv(out_path("growth"), index=False) - print(f"\nWrote: {', '.join(out_path(n) for n in ['library', 'phenotype', 'genotype_theta', 'growth'])}") + print(f"\nWrote: {', '.join(out_path(n) for n in ['library', 'parameters', 'genotype_theta', 'growth'])}") if "binding_data" in cf: binding_df = _generate_binding_data(cf, library_df, cf["binding_data"], rng) @@ -205,4 +210,5 @@ def out_path(name): def main(): - return generalized_main(run_simulation_from_config) + return generalized_main(run_simulation_from_config, + manual_arg_types={"seed": int}) diff --git a/src/tfscreen/simulate/thermo_to_growth.py b/src/tfscreen/simulate/thermo_to_growth.py index d13a35a6..d699bad2 100644 --- a/src/tfscreen/simulate/thermo_to_growth.py +++ b/src/tfscreen/simulate/thermo_to_growth.py @@ -153,6 +153,68 @@ def _sample_hierarchical_activity(unique_genotypes, _ACTIVITY_COMPONENTS = {"fixed", "horseshoe_geno", "hierarchical_geno"} +def _theta_param_to_df(theta_param, unique_genotypes, sim_indices): + """ + Extract per-genotype scalar fields from a ThetaParam pytree. + + Iterates over the dataclass fields of ``theta_param``. Fields whose + array has exactly two dimensions (T × G_sim) are treated as + per-genotype parameters: columns are selected via ``sim_indices`` and + a leading titrant dimension of size 1 is squeezed away; when T > 1 each + titrant gets its own column suffixed ``_T{t}``. Fields with any other + dimensionality (e.g. population moments ``mu``/``sigma`` with shape + T × C × 1) are silently skipped. + + If ``theta_param`` is not a standard or Flax dataclass (e.g. a mock + during testing) the function returns a DataFrame containing only the + ``genotype`` column, to which ``dk_geno`` and ``activity`` are then + added by the caller. + + Parameters + ---------- + theta_param : pytree + Returned by ``sample_theta_prior``. Per-genotype fields have shape + ``(num_titrant_name, num_genotype_sim)``. + unique_genotypes : array-like of str + Unique genotype strings in the desired output row order. + sim_indices : array-like of int + For each entry in ``unique_genotypes``, its column index in the + sim_data / ``library_df`` order stored in ``theta_param``. + + Returns + ------- + pandas.DataFrame + One row per genotype. Columns are the extracted ``theta_param`` + field names (with a ``_T{t}`` suffix when ``num_titrant_name > 1``). + The ``genotype`` column is always present as the first column. + """ + # Use the class-level __dataclass_fields__ so that MagicMock instances + # (which intercept instance-level attribute access) safely return an + # empty dict rather than triggering mock attribute creation. + field_names = list( + getattr(type(theta_param), "__dataclass_fields__", {}).keys() + ) + + col_data = {} + for fname in field_names: + try: + val = np.array(getattr(theta_param, fname)) + except Exception: + continue + if val.ndim != 2: + continue # skip mu, sigma (3-D) and any scalars + selected = val[:, np.asarray(sim_indices)] # (T, n_unique) + if selected.shape[0] == 1: + col_data[fname] = selected[0] + else: + for t in range(selected.shape[0]): + col_data[f"{fname}_T{t}"] = selected[t] + + df = pd.DataFrame(col_data) + df.insert(0, "genotype", list(unique_genotypes)) + return df + + def _assign_dk_geno(unique_genotypes, hyper_loc=-3.5, hyper_scale=1.0, @@ -310,11 +372,19 @@ def thermo_to_growth( phenotype_df : pd.DataFrame Long-form dataframe with one row per (genotype, sample condition). Columns include ``theta``, ``dk_geno``, ``activity``, ``k_pre``, - ``k_sel``. + ``k_sel``. ``dk_geno`` and ``activity`` are retained here so that + ``selection_experiment`` can use them downstream; they are also + collected in ``parameters_df`` for file output. genotype_theta_df : pd.DataFrame Wide-form dataframe with one row per genotype and one column per unique effector concentration, giving the ground-truth theta value. Use this to compare simulation inputs against fitted posteriors. + parameters_df : pd.DataFrame + One row per unique genotype. Always contains ``genotype``, + ``dk_geno``, and ``activity``. Also contains any scalar + per-genotype fields extracted from the ``theta_param`` pytree + (e.g. ``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n`` + for ``hill_geno``). """ print("Sampling theta from prior... ", end="", flush=True) @@ -361,15 +431,25 @@ def thermo_to_growth( epsilon = rng.normal(0.0, theta_noise_sigma_logit, size=theta_gc.shape) theta_gc = 1.0 / (1.0 + np.exp(-(logit_theta + epsilon))) - # ── Build genotype_theta_df (ground-truth parameters for comparison) ───── - # Wide form: rows = genotypes (in sim_data order), cols = concentrations. + # ── Index lookups (shared by genotype_theta_df, phenotype_df, parameters_df) ─ + # Computed once here so that genotype_theta_df can be built from unique + # genotypes rather than the full sim_data order (which may contain duplicates + # when the same sequence appears in more than one sub-library). + + all_genotypes = list(genotypes) # sim_data order (may have duplicates) + geno_to_sim_idx = {g: i for i, g in enumerate(all_genotypes)} + unique_sim_indices = np.array([geno_to_sim_idx[g] for g in unique_genotypes]) + + # ── Build genotype_theta_df (ground-truth theta per unique genotype) ────── + # Wide form: one row per UNIQUE genotype (in sorted unique_genotypes order), + # one column per concentration. unique_sim_indices maps each unique genotype + # to its representative row in theta_gc, eliminating duplicates. - all_genotypes = list(genotypes) # sim_data order conc_vals = np.array(sim_data.titrant_conc) conc_col_names = [f"theta_at_{c:.6g}mM" for c in conc_vals] genotype_theta_df = pd.DataFrame( - theta_gc, - index=all_genotypes, + theta_gc[unique_sim_indices], + index=list(unique_genotypes), columns=conc_col_names, ) genotype_theta_df.index.name = "genotype" @@ -379,11 +459,12 @@ def thermo_to_growth( # ── Map sample_df concentrations to unique-concentration indices ────────── # sample_df may have duplicate concentrations across rows; each row maps to # one column of theta_gc. - conc_to_col = {float(c): i for i, c in enumerate(conc_vals)} - sample_conc_idx = sample_df["titrant_conc"].map(conc_to_col).values - - # ── Build genotype-index lookup into sim_data order ─────────────────────── - geno_to_sim_idx = {g: i for i, g in enumerate(all_genotypes)} + # Use float64 values from sample_df directly (same source as build_sim_data) + # rather than conc_vals, which is float32 from the JAX array and would cause + # dict-lookup misses due to float32→float64 precision loss. + sorted_concs_f64 = np.sort(sample_df["titrant_conc"].unique()).astype(float) + conc_to_col = {c: i for i, c in enumerate(sorted_concs_f64)} + sample_conc_idx = sample_df["titrant_conc"].map(conc_to_col).values.astype(int) print("Calculating growth rates and building phenotype dataframe... ", end="", flush=True) @@ -396,7 +477,7 @@ def thermo_to_growth( n_samples = len(sample_df) # Theta matrix aligned to unique_genotypes order, shape (n_geno, n_samples) - unique_sim_indices = np.array([geno_to_sim_idx[g] for g in unique_genotypes]) + # (unique_sim_indices already computed above) theta_ordered = theta_gc[unique_sim_indices] # (n_geno, C) theta_for_samples = theta_ordered[:, sample_conc_idx] # (n_geno, n_samples) @@ -476,6 +557,24 @@ def thermo_to_growth( final_columns.append("theta") phenotype_df = phenotype_df.loc[:, final_columns] + # ── Build parameters_df (one row per unique genotype) ──────────────────── + # Extracts per-genotype theta_param fields (e.g. theta_low, log_hill_K) + # and pairs them with the already-computed dk_geno and activity values. + # phenotype_df keeps dk_geno/activity so selection_experiment still works. + + parameters_df = _theta_param_to_df(theta_param, unique_genotypes, + unique_sim_indices) + parameters_df["dk_geno"] = genotype_dk_geno_series.reindex( + list(unique_genotypes) + ).values + parameters_df["activity"] = genotype_activity_series.reindex( + list(unique_genotypes) + ).values + _front = ["genotype", "dk_geno", "activity"] + _other = [c for c in parameters_df.columns if c not in _front] + parameters_df = parameters_df[_front + _other] + parameters_df = set_categorical_genotype(parameters_df) + print("Done.", flush=True) - return phenotype_df, genotype_theta_df + return phenotype_df, genotype_theta_df, parameters_df diff --git a/src/tfscreen/tfmodel/inference/run_inference.py b/src/tfscreen/tfmodel/inference/run_inference.py index 85de3478..84f6b3f3 100644 --- a/src/tfscreen/tfmodel/inference/run_inference.py +++ b/src/tfscreen/tfmodel/inference/run_inference.py @@ -357,9 +357,14 @@ def scan_fn(data_on_gpu, carry, indices): self._write_checkpoint(svi_state, out_prefix) break + # Write a final checkpoint when the loop exits by reaching max_num_epochs + # (convergence already writes its own checkpoint via the break path above). + if not converged and total_steps > 0: + self._write_checkpoint(svi_state, out_prefix) + # Get final parameters params = svi.get_params(svi_state) - + return svi_state, params, converged def _get_genotype_dim_map(self): @@ -415,6 +420,16 @@ def _get_genotype_dim_map(self): if in_plate: continue + # Skip sites that belong to a non-genotype plate at the genotype + # dim — they are indexed by a different axis (e.g. condition_pre) + # and must NOT be treated as genotype-indexed. + in_other_plate_at_geno_dim = any( + frame.dim == genotype_dim + for frame in site.get("cond_indep_stack", []) + ) + if in_other_plate_at_geno_dim: + continue + # Fallback for deterministics computed outside the plate # but matching the genotype size at the expected dimension. val = site["value"] diff --git a/src/tfscreen/tfmodel/scripts/fit_model_cli.py b/src/tfscreen/tfmodel/scripts/fit_model_cli.py index 0b78028f..72be763f 100644 --- a/src/tfscreen/tfmodel/scripts/fit_model_cli.py +++ b/src/tfscreen/tfmodel/scripts/fit_model_cli.py @@ -24,10 +24,6 @@ def _run_map(ri, convergence_check_interval=2, checkpoint_interval=10, max_num_epochs=100000, - num_posterior_samples=10000, - sampling_batch_size=100, - forward_batch_size=512, - always_get_posterior=False, init_param_jitter=0.0, epoch_checkpoint_interval=1000): """ @@ -76,6 +72,11 @@ def _run_map(ri, ``checkpoints/`` subdirectory (default 1000). Set to 0 or None to disable. + Notes + ----- + Posterior sampling is not performed here. Call ``tfs-sample-posterior`` + after fitting to draw posterior samples. + Returns ------- svi_state : Any @@ -118,15 +119,6 @@ def _run_map(ri, # Write the current parameter values ri.write_params(params,out_prefix=out_prefix) - if always_get_posterior: - - ri.get_posteriors(svi=map_obj, - svi_state=svi_state, - out_prefix=out_prefix, - num_posterior_samples=num_posterior_samples, - sampling_batch_size=sampling_batch_size, - forward_batch_size=forward_batch_size) - # Write convergence information to stdout if converged: print("MAP run converged.",flush=True) @@ -149,10 +141,6 @@ def _run_svi(ri, convergence_check_interval=2, checkpoint_interval=10, max_num_epochs=100000, - num_posterior_samples=10000, - sampling_batch_size=100, - forward_batch_size=512, - always_get_posterior=False, init_param_jitter=0.1, epoch_checkpoint_interval=1000): """ @@ -194,16 +182,6 @@ def _run_svi(ri, Frequency (in epochs) between checkpoints (default 10). max_num_epochs : int, optional Maximum number of SVI optimization epochs (default 100000). - num_posterior_samples : int, optional - Number of posterior samples to draw after convergence (default 10000). - sampling_batch_size : int, optional - When getting posteriors, sample parameter posteriors in batches of this - size (default 100). - forward_batch_size : int, optional - when getting posteriors, calculate forward predictions in batches of - this size (default 512) - always_get_posterior : bool, optional - If True, always sample posteriors even if not converged (default False). init_param_jitter : float, optional Amount of jitter to add to init_params (default 0.1). epoch_checkpoint_interval : int or None, optional @@ -211,8 +189,17 @@ def _run_svi(ri, ``checkpoints/`` subdirectory (default 1000). Set to 0 or None to disable. + Notes + ----- + Posterior sampling is not performed here. Call ``tfs-sample-posterior`` + after fitting to draw posterior samples. + Returns ------- + svi_obj : Any + The SVI optimizer object created by ``ri.setup_svi``. Returned so + that callers (e.g. ``tfs-sample-posterior``) can pass it directly to + ``ri.get_posteriors`` without needing to re-create the guide. svi_state : Any Final optimizer state object from SVI. svi_params : dict @@ -251,22 +238,13 @@ def _run_svi(ri, epoch_checkpoint_interval=epoch_checkpoint_interval ) - if converged or always_get_posterior: - - ri.get_posteriors(svi=svi_obj, - svi_state=svi_state, - out_prefix=out_prefix, - num_posterior_samples=num_posterior_samples, - sampling_batch_size=sampling_batch_size, - forward_batch_size=forward_batch_size) - # Write convergence information to stdout if converged: print("SVI run converged.",flush=True) else: print("SVI run has not yet converged.",flush=True) - return svi_state, params, converged + return svi_obj, svi_state, params, converged def _run_nuts(ri, out_prefix="tfs", @@ -340,10 +318,7 @@ def fit_model(config_file, convergence_check_interval=2, checkpoint_interval=10, max_num_epochs=100000, - num_posterior_samples=10000, - sampling_batch_size=100, forward_batch_size=512, - always_get_posterior=False, pre_map_num_epoch=1000, init_param_jitter=0.1, nuts_num_warmup=500, @@ -368,7 +343,8 @@ def fit_model(config_file, Path to a checkpoint file to resume SVI from, or None to start fresh. analysis_method : str, optional Method for inference. Allowed values are 'svi' (default), 'map', or 'nuts'. - To draw posterior samples from an existing checkpoint, use tfs-sample-posterior. + Case-insensitive. Posterior sampling is not performed; call + ``tfs-sample-posterior`` after fitting. out_prefix : str, optional Prefix for all output files: checkpoints, parameter files, and the posterior HDF5 (default 'tfs_fit_model'). Files are named @@ -394,17 +370,9 @@ def fit_model(config_file, Frequency (in epochs) between checkpoints (default 10). max_num_epochs : int, optional Maximum number of SVI optimization epochs (default 100000). - num_posterior_samples : int, optional - Number of posterior samples to draw after convergence (default 10000). - sampling_batch_size : int, optional - When getting posteriors, sample parameter posteriors in batches of this - size (default 100). forward_batch_size : int, optional - When getting posteriors, calculate forward predictions in batches of - this size (default 512). - always_get_posterior : bool, optional - If True, always generate and save posterior samples, even if the - optimization did not formally converge (default False). + When getting NUTS posteriors, calculate forward predictions in batches + of this size (default 512). pre_map_num_epoch : int, optional Number of MAP iterations to run prior to SVI (default 1000). Only used if analysis_method is 'svi'. @@ -442,6 +410,8 @@ def fit_model(config_file, if seed is None and checkpoint_file is None: raise ValueError("seed must be provided unless loading from a checkpoint.") + analysis_method = analysis_method.lower() + # Check for existing results to avoid overwriting unless resuming if checkpoint_file is None: checkpoint_path = f"{out_prefix}_checkpoint.pkl" @@ -498,10 +468,6 @@ def fit_model(config_file, convergence_check_interval=convergence_check_interval, checkpoint_interval=checkpoint_interval, max_num_epochs=max_num_epochs, - num_posterior_samples=num_posterior_samples, - sampling_batch_size=sampling_batch_size, - forward_batch_size=forward_batch_size, - always_get_posterior=always_get_posterior, init_param_jitter=init_param_jitter, epoch_checkpoint_interval=epoch_checkpoint_interval) @@ -520,10 +486,6 @@ def fit_model(config_file, convergence_check_interval=convergence_check_interval, checkpoint_interval=checkpoint_interval, max_num_epochs=max_num_epochs, - num_posterior_samples=num_posterior_samples, - sampling_batch_size=sampling_batch_size, - forward_batch_size=forward_batch_size, - always_get_posterior=always_get_posterior, init_param_jitter=init_param_jitter, epoch_checkpoint_interval=epoch_checkpoint_interval) diff --git a/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py b/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py index 1c9e7555..aeea8a44 100644 --- a/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py +++ b/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py @@ -95,30 +95,31 @@ def sample_posterior(config_file, hessian_chunk_size=hessian_chunk_size, ) else: - # SVI checkpoint: resume with 0 epochs, draw samples directly. + # SVI checkpoint: resume with 0 epochs to restore guide state, + # then draw samples directly from the variational posterior. print("Detected SVI checkpoint. Drawing variational posterior samples...", flush=True) - _run_svi(ri, - init_params=init_params, - checkpoint_file=checkpoint_file, - out_prefix=ri_prefix, - max_num_epochs=0, - num_posterior_samples=num_posterior_samples, - sampling_batch_size=sampling_batch_size, - forward_batch_size=forward_batch_size, - always_get_posterior=True, - # Convergence / optimizer kwargs are unused at 0 epochs but - # required by _run_svi's signature; use neutral defaults. - adam_step_size=1e-3, - adam_final_step_size=1e-6, - adam_clip_norm=1.0, - elbo_num_particles=2, - convergence_tolerance=1e-5, - convergence_window=10, - patience=10, - convergence_check_interval=2, - checkpoint_interval=10, - init_param_jitter=0.0, - epoch_checkpoint_interval=None) + svi_obj, svi_state, _, _ = _run_svi(ri, + init_params=init_params, + checkpoint_file=checkpoint_file, + out_prefix=ri_prefix, + max_num_epochs=0, + adam_step_size=1e-3, + adam_final_step_size=1e-6, + adam_clip_norm=1.0, + elbo_num_particles=2, + convergence_tolerance=1e-5, + convergence_window=10, + patience=10, + convergence_check_interval=2, + checkpoint_interval=10, + init_param_jitter=0.0, + epoch_checkpoint_interval=None) + ri.get_posteriors(svi=svi_obj, + svi_state=svi_state, + out_prefix=ri_prefix, + num_posterior_samples=num_posterior_samples, + sampling_batch_size=sampling_batch_size, + forward_batch_size=forward_batch_size) src = f"{ri_prefix}_posterior.h5" dst = f"{out_prefix}.h5" diff --git a/tests/smoke-tests/test_configure_run_smoke.py b/tests/smoke-tests/test_configure_run_smoke.py index dc5598eb..f107bd4e 100644 --- a/tests/smoke-tests/test_configure_run_smoke.py +++ b/tests/smoke-tests/test_configure_run_smoke.py @@ -4,6 +4,7 @@ import yaml from tfscreen.tfmodel.scripts.configure_model_cli import configure_model from tfscreen.tfmodel.scripts.fit_model_cli import fit_model +from tfscreen.tfmodel.scripts.sample_posterior_cli import sample_posterior from tfscreen.tfmodel.scripts.extract_params_cli import extract_params as summarize_posteriors @pytest.mark.slow @@ -48,12 +49,15 @@ def test_configure_run_binding_only_smoke(tmpdir): out_prefix = os.path.join(tmpdir, "test_tfs_binding_only_out") fit_model(config_file=config_file, - seed=42, - max_num_epochs=1, - num_posterior_samples=10, - sampling_batch_size=10, - always_get_posterior=True, - out_prefix=out_prefix) + seed=42, + max_num_epochs=1, + out_prefix=out_prefix) + + sample_posterior(config_file=config_file, + checkpoint_file=f"{out_prefix}_checkpoint.pkl", + out_prefix=f"{out_prefix}_posterior", + num_posterior_samples=10, + sampling_batch_size=10) assert os.path.exists(f"{out_prefix}_posterior.h5") @@ -114,17 +118,18 @@ def test_configure_run_pipeline_smoke(tmpdir): assert "priors" not in config assert "init_params" not in config - # Run analysis (smoke test) - # We use max_num_epochs=1 to make it fast - # We use always_get_posterior=True to ensure the posterior file is written + # Run analysis (smoke test); use max_num_epochs=1 to make it fast out_prefix = os.path.join(tmpdir, "test_tfs_out") fit_model(config_file=config_file, - seed=42, - max_num_epochs=1, - num_posterior_samples=10, - sampling_batch_size=10, - always_get_posterior=True, - out_prefix=out_prefix) + seed=42, + max_num_epochs=1, + out_prefix=out_prefix) + + sample_posterior(config_file=config_file, + checkpoint_file=f"{out_prefix}_checkpoint.pkl", + out_prefix=f"{out_prefix}_posterior", + num_posterior_samples=10, + sampling_batch_size=10) assert os.path.exists(os.path.join(tmpdir, "test_tfs_out_posterior.h5")) @@ -205,9 +210,12 @@ def test_configure_run_binding_weight_smoke(tmpdir): fit_model(config_file=f"{out_auto}_config.yaml", seed=42, max_num_epochs=1, - num_posterior_samples=10, - sampling_batch_size=10, - always_get_posterior=True, out_prefix=out_fit) + sample_posterior(config_file=f"{out_auto}_config.yaml", + checkpoint_file=f"{out_fit}_checkpoint.pkl", + out_prefix=f"{out_fit}_posterior", + num_posterior_samples=10, + sampling_batch_size=10) + assert os.path.exists(f"{out_fit}_posterior.h5") diff --git a/tests/smoke-tests/test_run_growth_analysis_smoke.py b/tests/smoke-tests/test_run_growth_analysis_smoke.py index 525d0125..1a465807 100644 --- a/tests/smoke-tests/test_run_growth_analysis_smoke.py +++ b/tests/smoke-tests/test_run_growth_analysis_smoke.py @@ -54,16 +54,22 @@ def test_run_growth_analysis_smoke(growth_smoke_csv, binding_smoke_csv, tmpdir): analysis_method="svi", out_prefix=out_prefix, max_num_epochs=1, + forward_batch_size=10, + ) + + sample_posterior( + config_file=config_file, + checkpoint_file=f"{out_prefix}_checkpoint.pkl", + out_prefix=f"{out_prefix}_posterior", num_posterior_samples=5, sampling_batch_size=5, forward_batch_size=10, - always_get_posterior=True ) - + # 3. Verify outputs posterior_file = f"{out_prefix}_posterior.h5" assert os.path.exists(posterior_file) - + # Load and check the saved file with h5py.File(posterior_file, 'r') as data: assert "growth_pred" in data diff --git a/tests/tfscreen/genetics/test_library_manager.py b/tests/tfscreen/genetics/test_library_manager.py index 957ff412..333c3098 100644 --- a/tests/tfscreen/genetics/test_library_manager.py +++ b/tests/tfscreen/genetics/test_library_manager.py @@ -183,6 +183,18 @@ def test_parse_success(valid_config): assert lm.libraries_seen == {"2"} assert lm.degen_seq == "gattacnnncgattaca" +def test_parse_whitespace_stripped(valid_config): + """ + Whitespace (spaces, newlines) in wt_seq, degen_sites, and sub_libraries + is removed before validation, allowing YAML literal-block multiline values. + """ + valid_config["wt_seq"] = "gattac\nagtcgattaca" + valid_config["degen_sites"] = "......\nnnn........" + valid_config["sub_libraries"] = "......\n222........" + lm = LibraryManager(valid_config) + assert lm.expected_length == 17 + assert lm.wt_seq == "gattacagtcgattaca" + def test_parse_missing_key(valid_config): """ Tests that a ValueError is raised if a required key is missing. @@ -676,6 +688,7 @@ def lm_for_spiked_seqs() -> LibraryManager: # Set attributes required by the method under test lm.wt_seq = "gattaca" lm.standard_bases = set('acgt') + lm.standard_plus_dot = set('acgt.') return lm def test_get_spiked_seqs_success(lm_for_spiked_seqs): @@ -704,15 +717,42 @@ def test_get_spiked_seqs_success(lm_for_spiked_seqs): assert returned_seqs == valid_seqs assert returned_aa == ["", "Y3K"] +def test_get_spiked_seqs_dot_expansion(lm_for_spiked_seqs): + """ + Dots in spiked sequences are replaced by the corresponding wt_seq base. + """ + # wt_seq = "gattaca"; "gat.acg" -> dot at pos 3 fills in wt 't'; 'g' at + # pos 6 is an explicit mutation (wt is 'a') -> "gattacg" + dot_seq = ["gat.acg"] + + with patch.object(lm_for_spiked_seqs, '_convert_to_aa') as mock_convert: + mock_convert.return_value = ["A6G"] + returned_seqs, _ = lm_for_spiked_seqs._get_spiked_seqs(dot_seq) + + assert returned_seqs == ["gattacg"] + +def test_get_spiked_seqs_whitespace_stripped(lm_for_spiked_seqs): + """ + Whitespace (spaces, newlines) in spiked sequences is stripped before + validation, allowing YAML literal-block multiline entries. + """ + # wt_seq = "gattaca"; "gat\ntaca" has an embedded newline that should be + # removed, yielding "gattaca" (7 chars, matches wt length) + with patch.object(lm_for_spiked_seqs, '_convert_to_aa') as mock_convert: + mock_convert.return_value = [""] + returned_seqs, _ = lm_for_spiked_seqs._get_spiked_seqs(["gat\ntaca"]) + + assert returned_seqs == ["gattaca"] + def test_get_spiked_seqs_invalid_character(lm_for_spiked_seqs): """ Tests that a ValueError is raised for a sequence with non-standard bases. """ invalid_seqs = ["gattaca", "gattacx"] # Contains 'x' - + with pytest.raises(ValueError) as excinfo: lm_for_spiked_seqs._get_spiked_seqs(invalid_seqs) - + assert "Characters not recognized" in str(excinfo.value) assert "spiked seq" in str(excinfo.value) diff --git a/tests/tfscreen/simulate/test_library_prediction.py b/tests/tfscreen/simulate/test_library_prediction.py index 7407b5b7..c6bc5d74 100644 --- a/tests/tfscreen/simulate/test_library_prediction.py +++ b/tests/tfscreen/simulate/test_library_prediction.py @@ -43,15 +43,18 @@ def test_library_prediction_success(mocker, mock_config): mock_phenotype_df = pd.DataFrame({"theta": [0.5, 0.3]}) mock_genotype_theta_df = pd.DataFrame({"genotype": ["wt", "M1"]}) + mock_parameters_df = pd.DataFrame({"genotype": ["wt", "M1"], + "dk_geno": [0.0, -0.01], + "activity": [1.0, 1.0]}) mock_thermo = mocker.patch( "tfscreen.simulate.library_prediction.thermo_to_growth", - return_value=(mock_phenotype_df, mock_genotype_theta_df), + return_value=(mock_phenotype_df, mock_genotype_theta_df, mock_parameters_df), ) mock_jax_key = mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="mock_key") - lib_df, pheno_df, theta_df = library_prediction(cf="config.yaml") + lib_df, pheno_df, theta_df, params_df = library_prediction(cf="config.yaml") tfscreen.util.read_yaml.assert_called_once_with("config.yaml", override_keys=None) mock_lm_cls.assert_called_once_with(mock_config) @@ -77,6 +80,7 @@ def test_library_prediction_success(mocker, mock_config): assert lib_df.equals(mock_library_df) assert pheno_df.equals(mock_phenotype_df) assert theta_df.equals(mock_genotype_theta_df) + assert params_df.equals(mock_parameters_df) def test_library_prediction_config_error(): diff --git a/tests/tfscreen/simulate/test_run_simulation.py b/tests/tfscreen/simulate/test_run_simulation.py index d96d8ed4..2a538cb8 100644 --- a/tests/tfscreen/simulate/test_run_simulation.py +++ b/tests/tfscreen/simulate/test_run_simulation.py @@ -14,10 +14,11 @@ def mock_config(): def mock_result_dfs(): lib_df = pd.DataFrame({"lib": [1]}) pheno_df = pd.DataFrame({"pheno": [1]}) - ddG_df = pd.DataFrame({"ddG": [1]}) + theta_df = pd.DataFrame({"theta": [1]}) + params_df = pd.DataFrame({"genotype": ["wt"], "dk_geno": [0.0], "activity": [1.0]}) sample_df = pd.DataFrame({"sample": [1]}) counts_df = pd.DataFrame({"counts": [1]}) - return lib_df, pheno_df, ddG_df, sample_df, counts_df + return lib_df, pheno_df, theta_df, params_df, sample_df, counts_df # ----------------------------------------------------------------------------- # Tests for _setup_file_output @@ -67,17 +68,20 @@ def test_run_simulation_config_error(): run_simulation("nonexistent_config.yaml", None) def test_run_simulation_success(mocker, mock_config, mock_result_dfs): - lib_df, pheno_df, ddG_df, sample_df, counts_df = mock_result_dfs - + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df = mock_result_dfs + # Mock dependencies mocker.patch("tfscreen.util.read_yaml", return_value=mock_config) - mock_lib_pred = mocker.patch("tfscreen.simulate.run_simulation.library_prediction", return_value=(lib_df, pheno_df, ddG_df)) + mock_lib_pred = mocker.patch( + "tfscreen.simulate.run_simulation.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df), + ) mock_sel_exp = mocker.patch("tfscreen.simulate.run_simulation.selection_experiment", return_value=(sample_df, counts_df)) - - # Mock file output setup - mock_file_dict = {"library": "lib.csv", "phenotype": "pheno.csv", "genotype_theta": "theta.csv", "sample": "sample.csv", "counts": "counts.csv"} + + # Mock file output setup — phenotype removed, parameters added + mock_file_dict = {"library": "lib.csv", "parameters": "params.csv", "genotype_theta": "theta.csv", "sample": "sample.csv", "counts": "counts.csv"} mocker.patch("tfscreen.simulate.run_simulation._setup_file_output", return_value=mock_file_dict) - + # Mock to_csv mocker.patch.object(pd.DataFrame, "to_csv") @@ -86,19 +90,22 @@ def test_run_simulation_success(mocker, mock_config, mock_result_dfs): # Verify calls mock_lib_pred.assert_called_once_with(mock_config) mock_sel_exp.assert_called_once_with(mock_config, lib_df, pheno_df) - + assert results["library"].equals(lib_df) assert results["counts"].equals(counts_df) - - # Verify file writing - assert pd.DataFrame.to_csv.call_count == 5 - # Logic for checking arguments could be added but call_count gives good confidence here given the simple loop logic + assert results["parameters"].equals(params_df) + + # Verify file writing (5 files: library, parameters, genotype_theta, sample, counts) + assert pd.DataFrame.to_csv.call_count == 5 def test_run_simulation_no_output(mocker, mock_config, mock_result_dfs): - lib_df, pheno_df, ddG_df, sample_df, counts_df = mock_result_dfs - + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df = mock_result_dfs + mocker.patch("tfscreen.util.read_yaml", return_value=mock_config) - mocker.patch("tfscreen.simulate.run_simulation.library_prediction", return_value=(lib_df, pheno_df, ddG_df)) + mocker.patch( + "tfscreen.simulate.run_simulation.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df), + ) mocker.patch("tfscreen.simulate.run_simulation.selection_experiment", return_value=(sample_df, counts_df)) # Explicitly check _setup_file_output is called with None diff --git a/tests/tfscreen/simulate/test_run_simulation_cli.py b/tests/tfscreen/simulate/test_run_simulation_cli.py new file mode 100644 index 00000000..2abe93ff --- /dev/null +++ b/tests/tfscreen/simulate/test_run_simulation_cli.py @@ -0,0 +1,154 @@ +"""Tests for tfs-simulate CLI (run_simulation_from_config).""" + +import pytest +from unittest.mock import patch, MagicMock +import pandas as pd + +from tfscreen.simulate.scripts.run_simulation_cli import run_simulation_from_config +from tfscreen.util.cli.generalized_main import generalized_main + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_mock_dfs(): + lib_df = pd.DataFrame({"genotype": ["wt"], "sub_library": ["spiked"]}) + pheno_df = pd.DataFrame({"genotype": ["wt"]}) + theta_df = pd.DataFrame({"genotype": ["wt"]}) + params_df = pd.DataFrame({"genotype": ["wt"], "dk_geno": [0.0], "activity": [1.0]}) + sample_df = pd.DataFrame({"sample": [0]}, index=[0]) + counts_df = pd.DataFrame({"sample": [0], "genotype": ["wt"], "counts": [1]}) + growth_df = pd.DataFrame({"genotype": ["wt"]}) + return lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df + + +# --------------------------------------------------------------------------- +# --seed CLI argument +# --------------------------------------------------------------------------- + +def test_seed_cli_parsed_as_int(): + """--seed is registered with type=int so integer strings are accepted.""" + captured = {} + + def fake_run(config_file, output_dir, output_prefix="tfs_sim_", + num_replicates=1, seed=None): + captured["seed"] = seed + + generalized_main( + fake_run, + argv=["config.yaml", "out_dir", "--seed", "42"], + manual_arg_types={"seed": int}, + ) + assert captured["seed"] == 42 + assert isinstance(captured["seed"], int) + + +def test_seed_cli_defaults_to_none(): + """Omitting --seed leaves seed as None.""" + captured = {} + + def fake_run(config_file, output_dir, output_prefix="tfs_sim_", + num_replicates=1, seed=None): + captured["seed"] = seed + + generalized_main( + fake_run, + argv=["config.yaml", "out_dir"], + manual_arg_types={"seed": int}, + ) + assert captured["seed"] is None + + +# --------------------------------------------------------------------------- +# seed override behaviour in run_simulation_from_config +# --------------------------------------------------------------------------- + +@pytest.fixture() +def patched_simulation(tmp_path): + """Patch all I/O so run_simulation_from_config can run without real data.""" + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df = _make_mock_dfs() + + with patch("tfscreen.util.read_yaml", return_value={"random_seed": 99}) as mock_yaml, \ + patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + return_value=(sample_df, counts_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + return_value=growth_df), \ + patch.object(pd.DataFrame, "to_csv"): + yield mock_yaml, tmp_path + + +def test_seed_overrides_config(patched_simulation): + """When seed is given it replaces random_seed before seeding the RNG.""" + mock_yaml, tmp_path = patched_simulation + + with patch("tfscreen.simulate.scripts.run_simulation_cli.np.random.default_rng", + wraps=lambda s: MagicMock()) as mock_rng: + run_simulation_from_config("config.yaml", str(tmp_path), seed=7) + + mock_rng.assert_called_once_with(7) + + +def test_seed_none_preserves_config(patched_simulation): + """When seed=None the config's random_seed is left unchanged.""" + mock_yaml, tmp_path = patched_simulation + + with patch("tfscreen.simulate.scripts.run_simulation_cli.np.random.default_rng", + wraps=lambda s: MagicMock()) as mock_rng: + run_simulation_from_config("config.yaml", str(tmp_path), seed=None) + + mock_rng.assert_called_once_with(99) + + +# --------------------------------------------------------------------------- +# Output file names +# --------------------------------------------------------------------------- + +def test_writes_parameters_not_phenotype(tmp_path): + """run_simulation_from_config must write parameters.csv, not phenotype.csv.""" + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df = _make_mock_dfs() + + written_paths = [] + + def capture_csv(self_df, path, **kwargs): + written_paths.append(str(path)) + + with patch("tfscreen.util.read_yaml", return_value={"random_seed": 0}), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + return_value=(sample_df, counts_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + return_value=growth_df), \ + patch.object(pd.DataFrame, "to_csv", capture_csv): + run_simulation_from_config("config.yaml", str(tmp_path)) + + written = "\n".join(written_paths) + assert "parameters" in written, "parameters.csv must be written" + assert "phenotype" not in written, "phenotype.csv must NOT be written" + + +def test_output_file_names_include_expected_stems(tmp_path): + """library, parameters, genotype_theta, and growth CSVs are all written.""" + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df = _make_mock_dfs() + + written_paths = [] + + def capture_csv(self_df, path, **kwargs): + written_paths.append(str(path)) + + with patch("tfscreen.util.read_yaml", return_value={"random_seed": 0}), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + return_value=(sample_df, counts_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + return_value=growth_df), \ + patch.object(pd.DataFrame, "to_csv", capture_csv): + run_simulation_from_config("config.yaml", str(tmp_path)) + + written = "\n".join(written_paths) + for stem in ("library", "parameters", "genotype_theta", "growth"): + assert stem in written, f"Expected '{stem}' CSV to be written" diff --git a/tests/tfscreen/simulate/test_thermo_to_growth.py b/tests/tfscreen/simulate/test_thermo_to_growth.py index b4d1b970..72037778 100644 --- a/tests/tfscreen/simulate/test_thermo_to_growth.py +++ b/tests/tfscreen/simulate/test_thermo_to_growth.py @@ -10,6 +10,7 @@ _apply_growth_params, _sample_horseshoe_activity, _sample_hierarchical_activity, + _theta_param_to_df, _ACTIVITY_COMPONENTS, thermo_to_growth, _THETA_RESCALE, @@ -306,7 +307,7 @@ def test_thermo_to_growth_integration( sim_data = _make_sim_data_mock(test_genotypes, concs) _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) - phenotype_df, genotype_theta_df = thermo_to_growth( + phenotype_df, genotype_theta_df, parameters_df = thermo_to_growth( genotypes=test_genotypes, sim_data=sim_data, sample_df=test_sample_df, @@ -319,6 +320,7 @@ def test_thermo_to_growth_integration( assert isinstance(phenotype_df, pd.DataFrame) assert phenotype_df.shape[0] == 6 assert "theta" in phenotype_df.columns + # phenotype_df retains dk_geno and activity for selection_experiment assert "dk_geno" in phenotype_df.columns assert "activity" in phenotype_df.columns assert "k_pre" in phenotype_df.columns @@ -348,6 +350,52 @@ def test_thermo_to_growth_integration( assert "genotype" in genotype_theta_df.columns theta_cols = [c for c in genotype_theta_df.columns if c.startswith("theta_at_")] assert len(theta_cols) == 2 # two unique concentrations + # No duplicate genotypes + assert genotype_theta_df["genotype"].nunique() == len(genotype_theta_df) + + # parameters_df: one row per unique genotype, dk_geno + activity always present + assert isinstance(parameters_df, pd.DataFrame) + assert parameters_df.shape[0] == 3 # 3 unique genotypes + assert list(parameters_df.columns[:3]) == ["genotype", "dk_geno", "activity"] + # wt always gets dk_geno == 0 + wt_row = parameters_df[parameters_df["genotype"] == "wt"] + assert np.isclose(float(wt_row["dk_geno"].iloc[0]), 0.0) + # With activity_mut_scale=0, all activities == 1.0 + np.testing.assert_allclose(parameters_df["activity"].values, 1.0) + + +def test_genotype_theta_df_no_duplicates_with_repeated_genotypes( + mocker, test_sample_df, simple_growth_params +): + """genotype_theta_df must have one row per unique genotype even when the + same genotype appears multiple times in the library (e.g. two sub-libraries).""" + # Supply 5 rows where "wt" and "A1B" each appear twice + genotypes_with_dups = ["wt", "A1B", "wt", "A1B", "A1B/C2D"] + concs = np.array([10.0, 100.0]) + # theta_gc has 5 rows (one per library entry); duplicate genotypes get + # distinct values, but only one row per unique genotype should be kept. + theta_gc = np.array([ + [0.1, 0.9], # wt (first occurrence) + [0.3, 0.7], # A1B (first occurrence) + [0.2, 0.8], # wt (second occurrence — should be dropped) + [0.4, 0.6], # A1B (second occurrence — should be dropped) + [0.5, 0.5], # A1B/C2D + ]) + sim_data = _make_sim_data_mock(genotypes_with_dups, concs) + _patch_thermo_deps(mocker, genotypes_with_dups, test_sample_df, theta_gc) + + _, genotype_theta_df, _ = thermo_to_growth( + genotypes=genotypes_with_dups, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + ) + + assert genotype_theta_df["genotype"].nunique() == len(genotype_theta_df), \ + "genotype_theta_df must not contain duplicate genotype rows" + assert len(genotype_theta_df) == 3 # wt, A1B, A1B/C2D def test_thermo_to_growth_propagates_rng( @@ -362,7 +410,7 @@ def test_thermo_to_growth_propagates_rng( mock_assign_dk = mocker.patch( "tfscreen.simulate.thermo_to_growth._assign_dk_geno", - return_value=pd.Series(0.0, index=["wt", "A1B", "A1B/C2D"]), + return_value=pd.Series({"wt": 0.0, "A1B": 0.0, "A1B/C2D": 0.0}), ) thermo_to_growth( @@ -713,13 +761,17 @@ def test_horseshoe_activities_appear_in_phenotype_df( "tfscreen.simulate.thermo_to_growth._sample_horseshoe_activity", return_value=pd.Series(activity_map), ) - phenotype_df, _ = thermo_to_growth( + phenotype_df, _, parameters_df = thermo_to_growth( genotypes=test_genotypes, sim_data=sim_data, activity_component="horseshoe_geno", **base_call_kwargs, ) for geno, expected in activity_map.items(): rows = phenotype_df[phenotype_df["genotype"] == geno] np.testing.assert_allclose(rows["activity"].values, expected) + # Same activity values must appear in parameters_df (one row per genotype) + for geno, expected in activity_map.items(): + row = parameters_df[parameters_df["genotype"] == geno] + np.testing.assert_allclose(float(row["activity"].iloc[0]), expected) # ============================================================================ @@ -751,7 +803,7 @@ def test_zero_sigma_leaves_theta_unchanged( """theta_noise_sigma_logit=0 must not alter theta values.""" concs = np.array([10.0, 100.0]) theta_gc = np.array([[0.2, 0.8], [0.3, 0.7], [0.5, 0.5]]) - phenotype_df, _ = self._run( + phenotype_df, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=0.0, rng=np.random.default_rng(0), @@ -767,7 +819,7 @@ def test_nonzero_sigma_perturbs_theta( """theta_noise_sigma_logit > 0 must produce theta values different from input.""" concs = np.array([10.0, 100.0]) theta_gc = np.full((3, 2), 0.5) - phenotype_df, _ = self._run( + phenotype_df, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=1.0, rng=np.random.default_rng(42), @@ -781,7 +833,7 @@ def test_noisy_theta_stays_in_unit_interval( """Even with large sigma_logit, theta must remain in (0, 1).""" concs = np.array([10.0, 100.0]) theta_gc = np.array([[0.01, 0.99], [0.5, 0.5], [0.3, 0.7]]) - phenotype_df, _ = self._run( + phenotype_df, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=5.0, rng=np.random.default_rng(0), @@ -798,13 +850,13 @@ def test_reproducible_with_same_rng( concs = np.array([10.0, 100.0]) theta_gc = np.full((3, 2), 0.5) - df1, _ = self._run( + df1, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=0.5, rng=np.random.default_rng(7), **base_kwargs, ) - df2, _ = self._run( + df2, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=0.5, rng=np.random.default_rng(7), @@ -818,13 +870,13 @@ def test_different_seeds_give_different_theta( concs = np.array([10.0, 100.0]) theta_gc = np.full((3, 2), 0.5) - df1, _ = self._run( + df1, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=0.5, rng=np.random.default_rng(1), **base_kwargs, ) - df2, _ = self._run( + df2, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_noise_sigma_logit=0.5, rng=np.random.default_rng(2), @@ -902,12 +954,12 @@ def test_passthrough_matches_default( concs = np.array([10.0, 100.0]) theta_gc = np.array([[0.2, 0.8], [0.3, 0.7], [0.5, 0.5]]) - df_default, _ = self._run( + df_default, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, rng=np.random.default_rng(42), **base_kwargs, ) - df_passthrough, _ = self._run( + df_passthrough, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_rescale="passthrough", rng=np.random.default_rng(42), @@ -929,12 +981,12 @@ def test_logit_changes_k_relative_to_passthrough( concs = np.array([10.0, 100.0]) theta_gc = np.array([[0.2, 0.8], [0.3, 0.7], [0.5, 0.5]]) - df_pass, _ = self._run( + df_pass, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_rescale="passthrough", **base_kwargs, ) - df_logit, _ = self._run( + df_logit, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_rescale="logit", **base_kwargs, @@ -948,7 +1000,7 @@ def test_logit_theta_column_stays_in_unit_interval( concs = np.array([10.0, 100.0]) theta_gc = np.array([[0.2, 0.8], [0.3, 0.7], [0.5, 0.5]]) - df, _ = self._run( + df, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_rescale="logit", **base_kwargs, @@ -967,7 +1019,7 @@ def test_logit_k_matches_hand_calculation( # activity_mut_scale so only the logit transform matters. theta_gc = np.full((3, 2), 0.5) - df, _ = self._run( + df, _, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_rescale="logit", activity_wt=1.0, @@ -988,7 +1040,7 @@ def test_genotype_theta_df_stays_in_unit_interval_under_logit( concs = np.array([10.0, 100.0]) theta_gc = np.array([[0.2, 0.8], [0.3, 0.7], [0.5, 0.5]]) - _, geno_theta_df = self._run( + _, geno_theta_df, _ = self._run( mocker, test_genotypes, concs, theta_gc, theta_rescale="logit", **base_kwargs, @@ -997,3 +1049,145 @@ def test_genotype_theta_df_stays_in_unit_interval_under_logit( vals = geno_theta_df[theta_cols].values assert np.all(vals > 0.0) assert np.all(vals < 1.0) + + +# ============================================================================ +# test _theta_param_to_df +# ============================================================================ + +from dataclasses import dataclass as _stdlib_dataclass + + +@_stdlib_dataclass +class _MockThetaParam2D: + """Minimal stand-in for a real ThetaParam with only 2-D per-genotype fields.""" + theta_low: object + theta_high: object + log_hill_K: object + hill_n: object + + +@_stdlib_dataclass +class _MockThetaParamMixed: + """ThetaParam with 2-D per-genotype fields AND a 3-D population field.""" + theta_low: object + mu: object # 3-D — should be ignored + + +class TestThetaParamToDf: + """Unit tests for _theta_param_to_df.""" + + def _make_param_single_titrant(self, G): + """Return a _MockThetaParam2D with shape (1, G) fields.""" + rng = np.random.default_rng(0) + return _MockThetaParam2D( + theta_low=rng.random((1, G)), + theta_high=rng.random((1, G)), + log_hill_K=rng.standard_normal((1, G)), + hill_n=np.abs(rng.standard_normal((1, G))), + ) + + def test_returns_dataframe(self): + genotypes = ["wt", "A1V", "B2G"] + sim_idx = np.array([0, 1, 2]) + param = self._make_param_single_titrant(3) + df = _theta_param_to_df(param, genotypes, sim_idx) + assert isinstance(df, pd.DataFrame) + + def test_genotype_column_present_and_correct(self): + genotypes = ["wt", "A1V", "B2G"] + sim_idx = np.array([0, 1, 2]) + param = self._make_param_single_titrant(3) + df = _theta_param_to_df(param, genotypes, sim_idx) + assert "genotype" in df.columns + assert list(df["genotype"]) == genotypes + + def test_genotype_is_first_column(self): + genotypes = ["wt", "A1V"] + sim_idx = np.array([0, 1]) + param = self._make_param_single_titrant(2) + df = _theta_param_to_df(param, genotypes, sim_idx) + assert df.columns[0] == "genotype" + + def test_single_titrant_fields_extracted(self): + """All four 2-D fields must appear as columns (T=1 is squeezed).""" + genotypes = ["wt", "A1V", "B2G"] + sim_idx = np.array([0, 1, 2]) + param = self._make_param_single_titrant(3) + df = _theta_param_to_df(param, genotypes, sim_idx) + for fname in ["theta_low", "theta_high", "log_hill_K", "hill_n"]: + assert fname in df.columns, f"Missing column: {fname}" + + def test_3d_field_is_skipped(self): + """A field with ndim=3 (e.g. population moments) must not appear.""" + genotypes = ["wt", "A1V"] + sim_idx = np.array([0, 1]) + param = _MockThetaParamMixed( + theta_low=np.array([[0.1, 0.2]]), # (1, 2) — kept + mu=np.array([[[0.5], [0.6]]]), # (1, 2, 1) — skipped + ) + df = _theta_param_to_df(param, genotypes, sim_idx) + assert "theta_low" in df.columns + assert "mu" not in df.columns + + def test_values_match_theta_param(self): + """Extracted values must match the raw theta_param arrays.""" + genotypes = ["wt", "A1V", "B2G"] + sim_idx = np.array([0, 1, 2]) + param = self._make_param_single_titrant(3) + df = _theta_param_to_df(param, genotypes, sim_idx) + np.testing.assert_allclose(df["theta_low"].values, param.theta_low[0]) + np.testing.assert_allclose(df["theta_high"].values, param.theta_high[0]) + + def test_sim_indices_select_correct_rows(self): + """sim_indices must select the right columns from theta_param.""" + # sim_data has 5 genotypes; we want 3 of them in reverse order + G_sim = 5 + G_out = 3 + sim_idx = np.array([4, 2, 0]) # select in reverse from sim_data + rng = np.random.default_rng(7) + theta_low_full = rng.random((1, G_sim)) + param = _MockThetaParam2D( + theta_low=theta_low_full, + theta_high=rng.random((1, G_sim)), + log_hill_K=rng.standard_normal((1, G_sim)), + hill_n=np.abs(rng.standard_normal((1, G_sim))), + ) + genotypes = ["g4", "g2", "g0"] + df = _theta_param_to_df(param, genotypes, sim_idx) + expected = theta_low_full[0, sim_idx] + np.testing.assert_allclose(df["theta_low"].values, expected) + + def test_multi_titrant_creates_suffixed_columns(self): + """When T > 1, columns are named field_T0, field_T1, …""" + T, G = 2, 3 + rng = np.random.default_rng(1) + param = _MockThetaParam2D( + theta_low=rng.random((T, G)), + theta_high=rng.random((T, G)), + log_hill_K=rng.standard_normal((T, G)), + hill_n=np.abs(rng.standard_normal((T, G))), + ) + genotypes = ["wt", "A1V", "B2G"] + sim_idx = np.array([0, 1, 2]) + df = _theta_param_to_df(param, genotypes, sim_idx) + assert "theta_low_T0" in df.columns + assert "theta_low_T1" in df.columns + assert "theta_low" not in df.columns # unsuffixed form absent + + def test_non_dataclass_returns_genotype_only(self): + """A non-dataclass theta_param (e.g. MagicMock) yields only 'genotype' column.""" + from unittest.mock import MagicMock + mock_param = MagicMock() + genotypes = ["wt", "A1V"] + sim_idx = np.array([0, 1]) + df = _theta_param_to_df(mock_param, genotypes, sim_idx) + assert set(df.columns) == {"genotype"} + assert list(df["genotype"]) == genotypes + + def test_one_row_per_genotype(self): + genotypes = ["wt", "A1V", "B2G", "C3H"] + sim_idx = np.array([0, 1, 2, 3]) + param = self._make_param_single_titrant(4) + df = _theta_param_to_df(param, genotypes, sim_idx) + assert len(df) == 4 diff --git a/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py b/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py index 1c3af7be..c47b3b1d 100644 --- a/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py @@ -180,15 +180,19 @@ def test_map_detection_requires_auto_loc_key(self, tmp_path): ckpt_path = str(tmp_path / "svi.pkl") open(ckpt_path, "w").close() + fake_svi_obj = MagicMock() + fake_svi_state = MagicMock() + with patch("tfscreen.tfmodel.scripts.sample_posterior_cli.read_configuration", return_value=(MagicMock(), {})), \ patch("tfscreen.tfmodel.scripts.sample_posterior_cli.RunInference", return_value=ri), \ patch("tfscreen.tfmodel.scripts.sample_posterior_cli.dill") as mock_dill, \ - patch("tfscreen.tfmodel.scripts.sample_posterior_cli._run_svi") as mock_svi: + patch("tfscreen.tfmodel.scripts.sample_posterior_cli._run_svi", + return_value=(fake_svi_obj, fake_svi_state, {}, False)) as mock_svi: mock_dill.load.return_value = {"svi_state": MagicMock()} - mock_svi.side_effect = lambda *a, **kw: open(h5_src, "w").close() + ri.get_posteriors.side_effect = lambda **kw: open(h5_src, "w").close() from tfscreen.tfmodel.scripts.sample_posterior_cli import sample_posterior sample_posterior("cfg.yaml", ckpt_path, @@ -206,11 +210,9 @@ class TestSamplePosteriorSvi: def _run_svi_test(self, tmp_path): ri = MagicMock() - fake_optim = MagicMock() - fake_optim.get_params.return_value = {"some_param_mean": np.array(1.0)} - fake_svi = MagicMock() - fake_svi.optim = fake_optim - ri.setup_svi.return_value = fake_svi + fake_svi_state = MagicMock() + fake_svi_obj = MagicMock() + ri.setup_svi.return_value = fake_svi_obj h5_src = str(tmp_path / "out_tmp_posterior_posterior.h5") ckpt_path = str(tmp_path / "svi.pkl") @@ -219,11 +221,17 @@ def _run_svi_test(self, tmp_path): captured = {} def fake_run_svi(ri_obj, init_params, checkpoint_file, out_prefix, - max_num_epochs, always_get_posterior=False, **kwargs): + max_num_epochs, **kwargs): captured["max_num_epochs"] = max_num_epochs - captured["always_get_posterior"] = always_get_posterior + return fake_svi_obj, fake_svi_state, {}, False + + def fake_get_posteriors(**kwargs): + captured["get_posteriors_called"] = True + captured["get_posteriors_kwargs"] = kwargs open(h5_src, "w").close() + ri.get_posteriors.side_effect = fake_get_posteriors + with patch("tfscreen.tfmodel.scripts.sample_posterior_cli.read_configuration", return_value=(MagicMock(), {})), \ patch("tfscreen.tfmodel.scripts.sample_posterior_cli.RunInference", @@ -244,9 +252,9 @@ def test_svi_checkpoint_calls_run_svi_with_zero_epochs(self, tmp_path): captured = self._run_svi_test(tmp_path) assert captured["max_num_epochs"] == 0 - def test_svi_always_get_posterior_is_true(self, tmp_path): + def test_svi_calls_get_posteriors_directly(self, tmp_path): captured = self._run_svi_test(tmp_path) - assert captured["always_get_posterior"] is True + assert captured.get("get_posteriors_called") is True def test_svi_output_renamed(self, tmp_path): self._run_svi_test(tmp_path) diff --git a/tests/tfscreen/tfmodel/test_fit_model_helpers.py b/tests/tfscreen/tfmodel/test_fit_model_helpers.py index 65c5e905..cb39838b 100644 --- a/tests/tfscreen/tfmodel/test_fit_model_helpers.py +++ b/tests/tfscreen/tfmodel/test_fit_model_helpers.py @@ -43,19 +43,15 @@ def test_run_svi_flow_converged(mock_run_inference): """Test standard SVI flow where convergence is reached.""" _, ri = mock_run_inference - # Execute - state, params, converged = _run_svi( + svi_obj, state, params, converged = _run_svi( ri, init_params=None, out_prefix="test_root", max_num_epochs=500, - num_posterior_samples=100 ) - # 1. Setup SVI ri.setup_svi.assert_called_once() - # 2. Run Optimization ri.run_optimization.assert_called_once_with( "mock_svi_obj", init_params=None, @@ -71,45 +67,24 @@ def test_run_svi_flow_converged(mock_run_inference): epoch_checkpoint_interval=ANY ) - # 3. Get Posteriors (because converged=True) - ri.get_posteriors.assert_called_once_with( - svi="mock_svi_obj", - svi_state="final_state", - out_prefix="test_root", - num_posterior_samples=100, - sampling_batch_size=ANY, - forward_batch_size=ANY - ) + # Posteriors are never sampled by _run_svi + ri.get_posteriors.assert_not_called() + # svi_obj is the object returned by ri.setup_svi + assert svi_obj == "mock_svi_obj" assert state == "final_state" assert converged is True -def test_run_svi_flow_not_converged_no_posterior(mock_run_inference): - """Test SVI flow where it fails to converge and always_get_posterior is False.""" +def test_run_svi_flow_not_converged(mock_run_inference): + """Test SVI flow where it fails to converge.""" _, ri = mock_run_inference - # Force non-convergence ri.run_optimization.return_value = ("state", {}, False) - state, params, converged = _run_svi( - ri, - init_params=None, - always_get_posterior=False - ) + _, state, params, converged = _run_svi(ri, init_params=None) - # Should NOT get posteriors ri.get_posteriors.assert_not_called() assert converged is False -def test_run_svi_flow_always_posterior(mock_run_inference): - """Test SVI flow where it fails to converge but posteriors are forced.""" - _, ri = mock_run_inference - ri.run_optimization.return_value = ("state", {}, False) - - _run_svi(ri, init_params=None, always_get_posterior=True) - - # Should get posteriors despite no convergence - ri.get_posteriors.assert_called_once() - def test_run_svi_not_converged_stdout(mock_run_inference, capsys): """Test SVI not converged message.""" _, ri = mock_run_inference @@ -131,7 +106,6 @@ def test_run_map_flow(mock_run_inference): init_params=init_params, out_prefix="test_map", max_num_epochs=1000, - num_posterior_samples=100 ) # 1. Setup MAP @@ -153,17 +127,16 @@ def test_run_map_flow(mock_run_inference): epoch_checkpoint_interval=ANY ) - # 3. Write Params ri.write_params.assert_called_once_with({"p": 1}, out_prefix="test_map") - # always_get_posterior=False is default + # Posteriors are never sampled by _run_map ri.get_posteriors.assert_not_called() def test_run_map_not_converged(mock_run_inference, capsys): """Test MAP not converged message.""" _, ri = mock_run_inference ri.run_optimization.return_value = ("state", {"p": 1}, False) - _run_map(ri, init_params={"p": 1}, always_get_posterior=False) + _run_map(ri, init_params={"p": 1}) captured = capsys.readouterr() assert "MAP run converged" not in captured.out assert "MAP run has not yet converged" in captured.out diff --git a/tests/tfscreen/tfmodel/test_get_posteriors_mapping.py b/tests/tfscreen/tfmodel/test_get_posteriors_mapping.py index 4ee486d8..37a4aa5d 100644 --- a/tests/tfscreen/tfmodel/test_get_posteriors_mapping.py +++ b/tests/tfscreen/tfmodel/test_get_posteriors_mapping.py @@ -123,6 +123,53 @@ def test_get_posteriors_batching_mapping(tmpdir): np.testing.assert_allclose(det_p_in, post_batched["det_p_in"][:]) np.testing.assert_allclose(det_p_out, post_batched["det_p_out"][:]) +def test_dim_map_excludes_non_genotype_plate_at_same_dim(): + """A plate at dim=-1 that is NOT the genotype plate must not appear in dim_map. + + Reproduces a bug where hierarchical_factored's tube_offset site (in the + condition_pre plate at dim=-1) was falsely identified as genotype-indexed + when num_condition_pre == probe_size == 2. The shape mismatch caused: + (1, 1, G, 1, 1, 1, G) vs (1, T, CP, 1, 1, TC, G) during posterior sampling. + """ + + class MockModelWithCondPre: + def __init__(self, num_genotype=10, num_cond_pre=2): + self.data = MockData(num_genotype=num_genotype, + batch_size=num_genotype, + batch_idx=jnp.arange(num_genotype)) + self._num_cond_pre = num_cond_pre + self.priors = {} + + def jax_model(self, data, priors): + with numpyro.plate("shared_genotype_plate", data.num_genotype, dim=-1): + numpyro.sample("geno_p", dist.Normal(0, 1)) + # Separate plate at the same dim as genotype — must NOT appear in dim_map + with numpyro.plate("replicate_plate", 1, dim=-2): + with numpyro.plate("cond_pre_plate", self._num_cond_pre, dim=-1): + numpyro.sample("tube_offset", dist.Normal(0, 1)) + + def jax_model_guide(self, data, priors): + return AutoDelta(self.jax_model)(data, priors) + + def get_random_idx(self, key=None, num_batches=1): + if num_batches == 1: + return np.array([0]) + return np.zeros((num_batches, 1), dtype=int) + + def get_batch(self, data, indices): + return MockData(num_genotype=len(indices), + batch_size=len(indices), + batch_idx=indices) + + # Use num_genotype=10, num_cond_pre=2 — probe_size becomes min(2,10)=2 == num_cond_pre + model = MockModelWithCondPre(num_genotype=10, num_cond_pre=2) + ri = RunInference(model, seed=0) + dim_map = ri._get_genotype_dim_map() + + assert "geno_p" in dim_map, "genotype-plate site must appear in dim_map" + assert "tube_offset" not in dim_map, "non-genotype plate site must not appear in dim_map" + + def test_get_posteriors_shape_ambiguity(tmpdir): # Test with num_genotypes == num_titrants to ensure it doesn't get "mixed up" num_genotypes = 3 From 1ee4457d1b820657d75669bce9f83db27d93d0fa Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sat, 6 Jun 2026 08:34:00 -0700 Subject: [PATCH 02/24] ux cleanup in simulation --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 1738 +++++++++-------- reports/junit/junit.xml | 2 +- .../simulate/scripts/run_simulation_cli.py | 153 +- src/tfscreen/simulate/selection_experiment.py | 26 + src/tfscreen/tfmodel/configuration_io.py | 22 +- src/tfscreen/tfmodel/data_class.py | 33 +- .../generative/components/growth/linear.py | 2 + .../generative/components/growth/power.py | 2 + .../components/growth/saturation.py | 2 + .../components/growth_transition/baranyi.py | 2 + .../components/growth_transition/baranyi_k.py | 2 + .../growth_transition/baranyi_tau.py | 2 + .../components/growth_transition/memory.py | 2 + .../components/growth_transition/two_pop.py | 2 + src/tfscreen/tfmodel/generative/model.py | 26 + src/tfscreen/tfmodel/generative/registry.py | 2 + .../tfmodel/inference/run_inference.py | 40 + src/tfscreen/tfmodel/model_orchestrator.py | 162 +- .../tfmodel/scripts/configure_model_cli.py | 7 +- src/tfscreen/tfmodel/scripts/fit_model_cli.py | 12 +- .../scripts/run_prefit_calibration_cli.py | 14 +- .../tfmodel/scripts/sample_posterior_cli.py | 28 +- src/tfscreen/util/__init__.py | 3 +- src/tfscreen/util/validation/__init__.py | 1 + src/tfscreen/util/validation/check.py | 30 + .../simulate/test_generate_presplit_data.py | 247 +++ .../simulate/test_run_simulation_cli.py | 4 +- .../simulate/test_selection_experiment.py | 42 +- .../tfmodel/scripts/test_fit_model_cli.py | 41 + .../scripts/test_run_prefit_calibration.py | 41 +- .../scripts/test_sample_posterior_cli.py | 34 +- .../tfmodel/test_configure_and_run.py | 42 +- .../tfmodel/test_model_orchestrator.py | 49 +- tests/tfscreen/tfmodel/test_presplit.py | 247 +++ tests/tfscreen/util/validation/test_check.py | 37 +- 37 files changed, 2159 insertions(+), 944 deletions(-) create mode 100644 tests/tfscreen/simulate/test_generate_presplit_data.py create mode 100644 tests/tfscreen/tfmodel/test_presplit.py diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 954fc44f..91c6cb22 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.88%coverage95.88% \ No newline at end of file +coverage: 95.84%coverage95.84% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 128d53c8..3333b017 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3082tests3082 \ No newline at end of file +tests: 3123tests3123 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 3a215a12..d90d0691 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1313,150 +1313,170 @@ ./src/tfscreen/simulate/run_simulation.py:101:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:106:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:107:1: W391 blank line at end of file +./src/tfscreen/simulate/scripts/run_simulation_cli.py:141:15: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:143:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:144:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:145:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:149:20: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:150:22: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:151:21: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:170:43: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/scripts/run_simulation_cli.py:171:14: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:172:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:177:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:190:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:194:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:195:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:196:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:199:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:201:25: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:203:20: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:204:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:218:19: E221 multiple spaces before operator ./src/tfscreen/simulate/scripts/setup_sim_grid_cli.py:101:1: C901 'setup_sim_grid' is too complex (17) -./src/tfscreen/simulate/selection_experiment.py:41:38: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:42:37: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:43:36: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:42:38: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:43:37: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:44:36: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:45:36: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:46:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:47:1: C901 '_check_dict_number' is too complex (13) -./src/tfscreen/simulate/selection_experiment.py:47:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:90:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:133:1: C901 '_check_cf' is too complex (26) -./src/tfscreen/simulate/selection_experiment.py:155:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:175:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:177:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:177:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:177:65: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:177:85: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:200:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:201:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:267:1: C901 '_check_lib_spec' is too complex (12) -./src/tfscreen/simulate/selection_experiment.py:267:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:319:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:324:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:346:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:399:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:415:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:423:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:423:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:433:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:443:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:486:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:490:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:491:75: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:501:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:504:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:550:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:561:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:562:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:565:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:569:55: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:575:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:580:30: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:581:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:585:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:627:51: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:633:35: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:638:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:655:50: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:655:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:657:75: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:659:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:660:46: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:661:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:662:67: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:712:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:729:79: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:735:29: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:777:43: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:783:74: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:784:18: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:790:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:792:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:797:80: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:802:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:808:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:819:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:862:73: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:867:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:884:25: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:985:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:993:26: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:993:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:993:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:993:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:993:60: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1001:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1002:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1010:42: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1019:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1020:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1022:63: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1028:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1030:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1037:28: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1039:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1040:24: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1047:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1052:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1053:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1055:36: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1057:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1061:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1067:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1068:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1072:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1073:72: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1077:34: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1084:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1086:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1086:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1087:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1087:56: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1089:32: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1090:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1102:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1110:41: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1117:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1119:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1124:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1125:37: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1125:45: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1125:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1125:61: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1130:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:1165:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1186:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1187:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1190:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1190:39: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1195:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1201:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1204:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1212:33: E261 at least two spaces before inline comment -./src/tfscreen/simulate/selection_experiment.py:1213:49: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1214:37: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:1216:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1217:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1219:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:46:36: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:70:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:71:1: C901 '_check_dict_number' is too complex (13) +./src/tfscreen/simulate/selection_experiment.py:71:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:114:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:157:1: C901 '_check_cf' is too complex (26) +./src/tfscreen/simulate/selection_experiment.py:179:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:201:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:203:48: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:203:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:203:65: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:203:85: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:226:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:227:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:293:1: C901 '_check_lib_spec' is too complex (12) +./src/tfscreen/simulate/selection_experiment.py:293:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:345:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:350:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:372:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:425:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:441:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:449:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:449:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:459:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:469:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:512:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:516:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:517:75: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:527:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:530:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:576:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:587:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:588:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:591:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:595:55: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:601:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:606:30: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:607:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:611:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:653:51: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:659:35: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:664:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:681:50: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:681:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:683:75: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:685:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:686:46: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:687:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:688:67: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:738:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:755:79: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:761:29: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:803:43: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:809:74: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:810:18: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:816:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:818:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:823:80: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:828:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:834:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:845:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:888:73: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:893:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:910:25: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1011:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1019:26: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1019:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1019:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1019:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1019:60: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1027:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1028:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1036:42: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1045:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1046:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1048:63: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1054:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1056:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1063:28: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1065:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1066:24: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1073:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1078:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1079:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1081:36: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1083:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1087:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1093:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1094:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1098:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1099:72: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1103:34: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1110:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1112:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1112:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1113:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1113:56: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1115:32: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1116:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1128:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1136:41: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1143:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1145:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1150:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1151:37: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1151:45: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1151:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1151:61: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1156:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:1191:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1212:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1213:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1216:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1216:39: E231 missing whitespace after ',' ./src/tfscreen/simulate/selection_experiment.py:1221:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1229:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1230:43: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1234:41: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/selection_experiment.py:1237:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1261:71: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1269:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1270:76: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1275:11: F541 f-string is missing placeholders -./src/tfscreen/simulate/selection_experiment.py:1280:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1297:6: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/simulate/selection_experiment.py:1304:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1305:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1306:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1307:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1310:1: W391 blank line at end of file +./src/tfscreen/simulate/selection_experiment.py:1227:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1230:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1238:33: E261 at least two spaces before inline comment +./src/tfscreen/simulate/selection_experiment.py:1239:49: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1240:37: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:1242:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1243:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1245:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1247:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1255:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1256:43: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1260:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/selection_experiment.py:1263:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1287:71: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1295:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1296:76: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1301:11: F541 f-string is missing placeholders +./src/tfscreen/simulate/selection_experiment.py:1306:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1323:6: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/simulate/selection_experiment.py:1330:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1331:48: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1332:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1333:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1336:1: W391 blank line at end of file ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent @@ -1543,31 +1563,32 @@ ./src/tfscreen/tfmodel/analysis/prediction.py:316:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prior_predictive.py:104:33: E203 whitespace before ':' ./src/tfscreen/tfmodel/analysis/sbc.py:130:1: C901 'summarize_sbc' is too complex (17) -./src/tfscreen/tfmodel/configuration_io.py:9:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/configuration_io.py:27:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/configuration_io.py:106:1: C901 'write_configuration' is too complex (30) -./src/tfscreen/tfmodel/configuration_io.py:106:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/configuration_io.py:177:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:180:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:182:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:183:40: E713 test for membership should be 'not in' -./src/tfscreen/tfmodel/configuration_io.py:189:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:190:31: E713 test for membership should be 'not in' -./src/tfscreen/tfmodel/configuration_io.py:196:18: W291 trailing whitespace -./src/tfscreen/tfmodel/configuration_io.py:212:33: E261 at least two spaces before inline comment -./src/tfscreen/tfmodel/configuration_io.py:224:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:238:1: E305 expected 2 blank lines after class or function definition, found 1 -./src/tfscreen/tfmodel/configuration_io.py:238:1: E402 module level import not at top of file -./src/tfscreen/tfmodel/configuration_io.py:240:1: C901 'read_configuration' is too complex (19) -./src/tfscreen/tfmodel/configuration_io.py:240:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/configuration_io.py:273:128: E501 line too long (139 > 127 characters) -./src/tfscreen/tfmodel/configuration_io.py:278:22: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/configuration_io.py:279:22: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/configuration_io.py:280:22: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/configuration_io.py:286:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:298:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/configuration_io.py:313:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:19:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/configuration_io.py:37:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/configuration_io.py:116:1: C901 'write_configuration' is too complex (31) +./src/tfscreen/tfmodel/configuration_io.py:116:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/configuration_io.py:190:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:193:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:195:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:196:40: E713 test for membership should be 'not in' +./src/tfscreen/tfmodel/configuration_io.py:202:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:203:31: E713 test for membership should be 'not in' +./src/tfscreen/tfmodel/configuration_io.py:209:18: W291 trailing whitespace +./src/tfscreen/tfmodel/configuration_io.py:225:33: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/configuration_io.py:237:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:251:1: E305 expected 2 blank lines after class or function definition, found 1 +./src/tfscreen/tfmodel/configuration_io.py:251:1: E402 module level import not at top of file +./src/tfscreen/tfmodel/configuration_io.py:253:1: C901 'read_configuration' is too complex (19) +./src/tfscreen/tfmodel/configuration_io.py:253:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/configuration_io.py:289:128: E501 line too long (139 > 127 characters) +./src/tfscreen/tfmodel/configuration_io.py:297:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/configuration_io.py:298:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/configuration_io.py:299:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/configuration_io.py:300:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/configuration_io.py:306:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/configuration_io.py:318:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:333:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/configuration_io.py:338:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/data_class.py:12:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/data_class.py:27:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/data_class.py:28:5: E303 too many blank lines (2) @@ -1575,11 +1596,9 @@ ./src/tfscreen/tfmodel/data_class.py:104:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/data_class.py:121:49: W291 trailing whitespace ./src/tfscreen/tfmodel/data_class.py:123:76: W291 trailing whitespace -./src/tfscreen/tfmodel/data_class.py:160:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/data_class.py:166:48: W291 trailing whitespace -./src/tfscreen/tfmodel/data_class.py:185:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/data_class.py:192:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/data_class.py:193:5: E266 too many leading '#' for block comment +./src/tfscreen/tfmodel/data_class.py:214:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/data_class.py:221:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/data_class.py:222:5: E266 too many leading '#' for block comment ./src/tfscreen/tfmodel/generative/components/__init__.py:3:4: W292 no newline at end of file ./src/tfscreen/tfmodel/generative/components/activity/fixed.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/generative/components/activity/fixed.py:16:1: E302 expected 2 blank lines, found 1 @@ -3171,18 +3190,20 @@ ./src/tfscreen/tfmodel/generative/components/transformation/single.py:40:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/generative/components/transformation/single.py:46:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/generative/components/transformation/single.py:52:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/generative/model.py:11:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/generative/model.py:43:54: W291 trailing whitespace -./src/tfscreen/tfmodel/generative/model.py:45:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/generative/model.py:103:37: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/generative/model.py:104:24: F541 f-string is missing placeholders -./src/tfscreen/tfmodel/generative/model.py:104:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/generative/model.py:133:36: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/generative/model.py:134:24: F541 f-string is missing placeholders -./src/tfscreen/tfmodel/generative/model.py:134:44: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/generative/model.py:213:28: F541 f-string is missing placeholders -./src/tfscreen/tfmodel/generative/model.py:214:28: F541 f-string is missing placeholders -./src/tfscreen/tfmodel/generative/model.py:220:1: W391 blank line at end of file +./src/tfscreen/tfmodel/generative/model.py:12:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/generative/model.py:44:54: W291 trailing whitespace +./src/tfscreen/tfmodel/generative/model.py:46:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/generative/model.py:104:37: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/generative/model.py:105:24: F541 f-string is missing placeholders +./src/tfscreen/tfmodel/generative/model.py:105:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/generative/model.py:134:36: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/generative/model.py:135:24: F541 f-string is missing placeholders +./src/tfscreen/tfmodel/generative/model.py:135:44: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/generative/model.py:195:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/model.py:196:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/model.py:239:28: F541 f-string is missing placeholders +./src/tfscreen/tfmodel/generative/model.py:240:28: F541 f-string is missing placeholders +./src/tfscreen/tfmodel/generative/model.py:246:1: W391 blank line at end of file ./src/tfscreen/tfmodel/generative/observe/binding.py:9:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/generative/observe/binding.py:42:79: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/generative/observe/binding.py:43:83: E231 missing whitespace after ',' @@ -3201,64 +3222,65 @@ ./src/tfscreen/tfmodel/generative/observe/growth.py:87:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/generative/observe/growth.py:95:5: F841 local variable 'nu' is assigned to but never used ./src/tfscreen/tfmodel/generative/observe/growth.py:97:11: W292 no newline at end of file -./src/tfscreen/tfmodel/generative/registry.py:56:29: W291 trailing whitespace -./src/tfscreen/tfmodel/generative/registry.py:57:28: W291 trailing whitespace -./src/tfscreen/tfmodel/generative/registry.py:60:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:61:17: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:62:16: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:63:21: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:65:14: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:66:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:67:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:69:14: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:70:16: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:71:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:73:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:74:16: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:75:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:76:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:77:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:78:24: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:80:21: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:85:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:89:12: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:90:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:91:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:92:19: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:93:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:94:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:95:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:96:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:57:29: W291 trailing whitespace +./src/tfscreen/tfmodel/generative/registry.py:58:28: W291 trailing whitespace +./src/tfscreen/tfmodel/generative/registry.py:61:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:62:17: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:63:16: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:64:21: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:66:14: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:67:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:68:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:70:14: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:71:16: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:72:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:74:15: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:75:16: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:76:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:77:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:78:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:79:24: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:81:21: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:86:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:90:12: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:91:18: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:92:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:93:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:94:19: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:95:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:96:36: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/generative/registry.py:97:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:98:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:99:35: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:100:37: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:101:37: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:102:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:98:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:99:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:100:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:101:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:102:37: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/generative/registry.py:103:37: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:104:37: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:106:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:107:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:108:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:109:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:111:26: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:112:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:113:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:115:19: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:116:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:117:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:104:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:105:37: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:106:37: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:108:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:109:15: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:110:15: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:111:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:113:26: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:114:15: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:115:15: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:117:19: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:118:15: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/generative/registry.py:119:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:120:15: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:121:17: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:123:24: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:124:18: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:125:17: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:121:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:122:15: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:123:17: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:125:24: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/generative/registry.py:126:18: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:127:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:128:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:129:18: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:131:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/generative/registry.py:132:21: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:127:17: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:128:18: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:129:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:130:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:131:18: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:133:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/generative/registry.py:134:21: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/inference/posteriors.py:4:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/inference/posteriors.py:42:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/inference/posteriors.py:50:1: E302 expected 2 blank lines, found 1 @@ -3335,231 +3357,231 @@ ./src/tfscreen/tfmodel/inference/run_inference.py:686:32: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/inference/run_inference.py:693:38: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/inference/run_inference.py:694:31: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:736:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:750:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:753:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:754:49: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:763:36: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:770:27: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:770:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:772:61: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:821:5: E303 too many blank lines (4) -./src/tfscreen/tfmodel/inference/run_inference.py:862:26: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:862:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:865:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:877:41: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:879:32: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:879:42: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:930:5: C901 'RunInference.get_map_posteriors' is too complex (13) -./src/tfscreen/tfmodel/inference/run_inference.py:1168:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/inference/run_inference.py:1209:29: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:1212:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1217:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1225:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/inference/run_inference.py:1229:30: W291 trailing whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1232:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1236:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1242:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/run_inference.py:1245:5: C901 'RunInference.get_laplace_posteriors' is too complex (16) -./src/tfscreen/tfmodel/inference/run_inference.py:1455:5: C901 'RunInference.get_nuts_posteriors' is too complex (12) -./src/tfscreen/tfmodel/model_orchestrator.py:36:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:70:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:73:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:77:25: E222 multiple spaces after operator -./src/tfscreen/tfmodel/model_orchestrator.py:77:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:78:42: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:82:65: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:82:88: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:87:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:91:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:91:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:91:55: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:91:63: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:92:52: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:102:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:106:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:110:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:111:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:113:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/model_orchestrator.py:148:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:151:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:151:73: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:152:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:776:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:790:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:793:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:794:49: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:803:36: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:810:27: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:810:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:812:61: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:861:5: E303 too many blank lines (4) +./src/tfscreen/tfmodel/inference/run_inference.py:902:26: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:902:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:905:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:917:41: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:919:32: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:919:42: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:970:5: C901 'RunInference.get_map_posteriors' is too complex (13) +./src/tfscreen/tfmodel/inference/run_inference.py:1208:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/inference/run_inference.py:1249:29: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:1252:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1257:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1265:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/inference/run_inference.py:1269:30: W291 trailing whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1272:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1276:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1282:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/run_inference.py:1285:5: C901 'RunInference.get_laplace_posteriors' is too complex (16) +./src/tfscreen/tfmodel/inference/run_inference.py:1495:5: C901 'RunInference.get_nuts_posteriors' is too complex (12) +./src/tfscreen/tfmodel/model_orchestrator.py:38:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:72:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:75:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:79:25: E222 multiple spaces after operator +./src/tfscreen/tfmodel/model_orchestrator.py:79:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:80:42: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:84:65: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:84:88: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:89:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:93:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:93:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:93:55: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:93:63: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:94:52: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:104:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:108:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:112:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:113:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:115:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/model_orchestrator.py:150:58: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:153:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:154:61: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:155:61: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:156:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:157:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:159:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:160:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:161:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:162:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:165:63: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:166:73: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:167:73: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:169:59: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:170:70: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:171:72: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:175:63: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:178:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:179:15: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:180:42: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:180:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:181:29: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:182:40: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:183:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:183:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:183:72: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:224:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:233:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:233:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:234:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:237:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:243:27: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:270:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/model_orchestrator.py:302:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:303:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:304:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:306:23: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:307:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:308:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:312:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:347:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:350:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:355:52: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:356:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:361:80: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:363:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:367:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:383:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:384:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:404:82: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:409:87: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:412:84: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:417:79: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:497:5: C901 'ModelOrchestrator._initialize_data' is too complex (16) -./src/tfscreen/tfmodel/model_orchestrator.py:497:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:522:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:523:28: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:525:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:526:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:526:48: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:526:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:527:46: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:527:66: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:528:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:532:39: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:533:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:538:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:545:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:547:10: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:547:10: E116 unexpected indentation (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:549:76: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:552:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:554:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:556:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:568:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:576:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:598:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:599:41: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:599:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:600:44: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:600:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:601:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:601:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:602:46: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:602:71: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:604:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:611:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:616:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:616:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:616:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:623:40: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:669:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:670:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:671:26: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:672:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:712:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:715:78: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:716:32: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:717:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:721:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:722:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:723:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:724:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:726:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:737:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:737:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:740:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:741:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:746:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:747:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:748:58: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:750:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:755:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:759:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:763:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:766:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:772:86: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:774:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:781:77: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:808:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:809:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:810:39: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:814:64: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:817:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:818:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:819:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:895:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:896:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:898:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:971:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) -./src/tfscreen/tfmodel/model_orchestrator.py:991:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:993:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:994:71: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:996:68: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:997:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:998:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:999:40: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1016:42: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1017:43: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1018:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1018:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1018:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1018:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1018:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1066:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1069:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1073:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1075:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1085:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1087:9: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1140:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1145:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1159:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1169:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1179:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1193:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1196:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1225:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1237:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1249:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1255:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1256:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1257:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1258:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1259:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1260:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1261:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1262:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1263:29: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1264:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1265:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1266:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1267:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1268:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1269:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:191:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:153:73: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:154:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:155:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:156:61: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:157:61: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:158:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:159:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:161:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:162:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:163:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:164:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:167:63: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:168:73: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:169:73: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:171:59: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:172:70: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:173:72: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:177:63: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:180:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:181:15: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:182:42: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:182:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:183:29: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:184:40: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:185:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:185:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:185:72: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:226:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:235:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:235:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:236:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:239:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:245:27: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:276:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/model_orchestrator.py:308:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:309:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:310:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:312:23: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:313:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:314:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:318:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:398:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:458:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:461:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:466:52: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:467:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:472:80: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:474:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:478:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:494:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:495:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:515:82: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:520:87: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:523:84: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:528:79: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:610:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) +./src/tfscreen/tfmodel/model_orchestrator.py:610:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:635:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:636:28: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:638:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:639:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:639:48: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:639:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:640:46: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:640:66: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:641:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:645:39: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:646:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:651:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:658:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E116 unexpected indentation (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:662:76: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:665:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:667:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:669:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:681:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:689:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:711:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:712:41: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:712:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:713:44: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:713:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:714:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:714:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:715:46: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:715:71: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:717:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:724:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:729:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:729:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:729:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:736:40: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:782:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:783:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:784:26: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:785:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:825:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:828:78: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:829:32: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:830:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:834:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:835:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:836:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:837:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:839:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:850:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:850:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:853:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:854:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:859:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:860:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:861:58: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:863:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:868:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:872:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:876:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:879:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:885:86: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:887:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:894:77: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:950:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:951:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:952:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:953:39: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:960:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:961:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:962:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1038:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:1039:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1041:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1114:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) +./src/tfscreen/tfmodel/model_orchestrator.py:1134:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1136:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1137:71: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1139:68: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1140:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1141:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1142:40: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1159:42: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1160:43: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1209:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1212:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1216:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1218:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1228:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1230:9: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1283:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1288:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1302:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1312:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1322:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1336:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1339:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1368:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1380:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1392:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1398:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1399:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1400:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1401:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1402:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1403:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1404:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1405:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1406:29: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1407:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1408:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1409:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1410:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1411:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1412:31: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:192:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:193:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:194:22: E128 continuation line under-indented for visual indent @@ -3578,74 +3600,62 @@ ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:207:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:208:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:209:22: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:219:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:221:59: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:222:58: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:223:55: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:224:60: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:210:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:211:22: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:223:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:225:59: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:226:63: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:227:55: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/configure_model_cli.py:229:1: E305 expected 2 blank lines after class or function definition, found 1 +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:226:58: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:227:60: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:228:55: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:229:60: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:230:59: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:231:63: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:232:55: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/configure_model_cli.py:234:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:97:6: E114 indentation is not a multiple of 4 (comment) ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:97:6: E116 unexpected indentation (comment) ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:120:27: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:124:35: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:126:47: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:130:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:243:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:245:47: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:249:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:509:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:511:60: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:512:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:513:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:514:66: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:515:66: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:516:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:517:65: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:245:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:247:51: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:251:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:511:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:513:60: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:514:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:515:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:516:66: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:517:66: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:518:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:519:72: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:520:74: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:1: E305 expected 2 blank lines after class or function definition, found 1 +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:519:65: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:520:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:521:72: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:74: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:255:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:256:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:259:1: C901 '_inject_calibration_priors' is too complex (19) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:441:1: C901 '_build_csv_updates' is too complex (13) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:543:1: C901 '_apply_guesses_updates' is too complex (12) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:698:1: C901 '_make_calibration_plots' is too complex (23) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:784:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:787:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:788:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:839:7: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:840:7: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:843:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:844:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:845:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:846:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:889:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:890:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:912:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:927:21: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:950:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:963:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:102:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:103:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:104:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:105:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:106:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:107:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:108:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:109:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:110:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:111:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:112:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:113:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:114:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:115:50: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/scripts/sample_posterior_cli.py:116:50: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:251:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:252:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:253:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:256:1: C901 '_inject_calibration_priors' is too complex (19) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:438:1: C901 '_build_csv_updates' is too complex (13) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:540:1: C901 '_apply_guesses_updates' is too complex (12) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:695:1: C901 '_make_calibration_plots' is too complex (23) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:781:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:784:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:785:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:836:7: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:837:7: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:840:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:841:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:842:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:843:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:886:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:887:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:909:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:924:21: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:947:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:960:25: E221 multiple spaces before operator ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used @@ -3735,7 +3745,7 @@ ./src/tfscreen/tfmodel/tensors/tensor_manager.py:512:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/tensors/tensor_manager.py:517:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/tensors/tensor_manager.py:528:1: W391 blank line at end of file -./src/tfscreen/util/__init__.py:70:2: W292 no newline at end of file +./src/tfscreen/util/__init__.py:71:2: W292 no newline at end of file ./src/tfscreen/util/cli/__init__.py:16:1: W391 blank line at end of file ./src/tfscreen/util/cli/generalized_main.py:1:11: W291 trailing whitespace ./src/tfscreen/util/cli/generalized_main.py:5:1: C901 'generalized_main' is too complex (15) @@ -3864,10 +3874,10 @@ ./src/tfscreen/util/numerical/zero_truncated_poisson.py:71:34: W291 trailing whitespace ./src/tfscreen/util/numerical/zero_truncated_poisson.py:73:1: W293 blank line contains whitespace ./src/tfscreen/util/numerical/zero_truncated_poisson.py:74:23: W292 no newline at end of file -./src/tfscreen/util/validation/check.py:7:1: C901 'check_number' is too complex (12) -./src/tfscreen/util/validation/check.py:7:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/util/validation/check.py:62:1: W293 blank line contains whitespace -./src/tfscreen/util/validation/check.py:88:18: W292 no newline at end of file +./src/tfscreen/util/validation/check.py:37:1: C901 'check_number' is too complex (12) +./src/tfscreen/util/validation/check.py:37:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/util/validation/check.py:92:1: W293 blank line contains whitespace +./src/tfscreen/util/validation/check.py:118:18: W292 no newline at end of file ./tests/conftest.py:9:1: E302 expected 2 blank lines, found 1 ./tests/conftest.py:40:63: E231 missing whitespace after ',' ./tests/conftest.py:54:49: E231 missing whitespace after ',' @@ -5135,6 +5145,30 @@ ./tests/tfscreen/simulate/test_build_sample_dataframes.py:104:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:115:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:128:55: W292 no newline at end of file +./tests/tfscreen/simulate/test_generate_presplit_data.py:11:1: F401 'pytest' imported but unused +./tests/tfscreen/simulate/test_generate_presplit_data.py:14:1: F401 'unittest.mock.MagicMock' imported but unused +./tests/tfscreen/simulate/test_generate_presplit_data.py:75:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:87:42: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:88:42: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:90:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:98:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:106:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:124:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:125:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:141:20: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:142:51: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:143:22: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:144:51: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:146:22: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:159:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:161:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:163:36: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:174:11: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:178:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:222:11: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:227:33: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:229:31: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:231:33: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_library_prediction.py:47:41: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_run_simulation.py:3:1: F401 'os' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:5:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5159,154 +5193,154 @@ ./tests/tfscreen/simulate/test_selection_experiment.py:4:1: F401 'numpy.ma' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:6:1: F401 'scipy.stats.gmean' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused -./tests/tfscreen/simulate/test_selection_experiment.py:33:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:38:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:68:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:77:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:85:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:116:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:141:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:175:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:178:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:185:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:194:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:203:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:221:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:227:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:236:51: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:237:63: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:249:16: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:251:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:251:57: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:252:69: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:272:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:276:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:280:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:283:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:286:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:289:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:293:52: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:294:64: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:303:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:306:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:314:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:320:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:322:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:326:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:332:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:335:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:337:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:338:61: E231 missing whitespace after ',' -./tests/tfscreen/simulate/test_selection_experiment.py:343:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:345:31: E231 missing whitespace after ',' -./tests/tfscreen/simulate/test_selection_experiment.py:345:32: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:368:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:373:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:380:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:383:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:385:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:390:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:400:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:406:1: E303 too many blank lines (3) -./tests/tfscreen/simulate/test_selection_experiment.py:410:1: E302 expected 2 blank lines, found 3 -./tests/tfscreen/simulate/test_selection_experiment.py:416:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:418:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:421:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:428:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:436:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:444:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:448:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:452:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:456:31: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:460:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:464:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:470:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:485:22: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:486:24: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:488:70: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:497:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:500:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:514:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:517:25: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:528:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:533:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:550:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:553:20: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:34:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:39:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:69:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:78:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:86:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:117:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:142:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:176:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:179:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:186:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:195:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:204:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:222:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:228:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:237:51: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:238:63: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:250:16: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:252:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:252:57: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:253:69: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:273:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:277:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:281:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:284:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:287:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:290:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:294:52: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:295:64: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:304:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:307:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:315:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:321:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:323:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:327:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:333:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:336:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:338:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:339:61: E231 missing whitespace after ',' +./tests/tfscreen/simulate/test_selection_experiment.py:344:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:346:31: E231 missing whitespace after ',' +./tests/tfscreen/simulate/test_selection_experiment.py:346:32: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:369:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:374:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:381:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:384:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:386:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:391:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:401:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:407:1: E303 too many blank lines (3) +./tests/tfscreen/simulate/test_selection_experiment.py:411:1: E302 expected 2 blank lines, found 3 +./tests/tfscreen/simulate/test_selection_experiment.py:417:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:419:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:422:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:429:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:437:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:445:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:449:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:453:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:457:31: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:461:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:465:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:471:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:486:22: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:487:24: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:489:70: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:498:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:501:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:515:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:518:25: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:529:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:534:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:551:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:554:20: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:555:20: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:569:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:576:47: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:582:20: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:556:20: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:570:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:577:47: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:583:20: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:584:20: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:592:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:595:9: F811 redefinition of unused 'gmean' from line 6 -./tests/tfscreen/simulate/test_selection_experiment.py:603:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:607:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:614:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:621:59: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:623:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:640:41: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:654:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:662:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:678:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:686:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:693:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:701:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:712:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:713:48: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:585:20: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:593:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:596:9: F811 redefinition of unused 'gmean' from line 6 +./tests/tfscreen/simulate/test_selection_experiment.py:604:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:608:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:615:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:622:59: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:624:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:641:41: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:655:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:663:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:679:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:687:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:694:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:702:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:713:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:714:48: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:720:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:723:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:731:43: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:746:31: E201 whitespace after '[' -./tests/tfscreen/simulate/test_selection_experiment.py:747:54: E202 whitespace before ']' -./tests/tfscreen/simulate/test_selection_experiment.py:748:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:752:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:754:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:755:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:764:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:766:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:770:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:778:63: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:779:75: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:780:77: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:789:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:794:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:797:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:803:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:808:25: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:814:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:818:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:820:45: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:821:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:822:48: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:825:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:833:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:838:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:842:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:848:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:852:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:855:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:858:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:862:37: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:883:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:888:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:891:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:896:41: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:916:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:1147:12: F841 local variable 'k_sel' is assigned to but never used -./tests/tfscreen/simulate/test_selection_experiment.py:1162:29: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_selection_experiment.py:1164:29: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_selection_experiment.py:1186:20: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_selection_experiment.py:1201:14: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_selection_experiment.py:1204:14: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_selection_experiment.py:1242:69: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1254:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1265:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1269:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1272:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1274:73: W292 no newline at end of file +./tests/tfscreen/simulate/test_selection_experiment.py:715:48: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:721:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:724:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:732:43: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:747:31: E201 whitespace after '[' +./tests/tfscreen/simulate/test_selection_experiment.py:748:54: E202 whitespace before ']' +./tests/tfscreen/simulate/test_selection_experiment.py:749:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:753:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:755:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:756:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:765:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:767:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:771:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:779:63: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:780:75: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:781:77: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:790:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:795:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:798:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:804:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:809:25: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:815:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:819:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:821:45: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:822:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:823:48: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:826:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:834:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:839:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:843:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:849:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:853:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:856:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:859:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:863:37: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:884:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:889:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:892:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:897:41: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:917:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:1148:12: F841 local variable 'k_sel' is assigned to but never used +./tests/tfscreen/simulate/test_selection_experiment.py:1163:29: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_selection_experiment.py:1165:29: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_selection_experiment.py:1187:20: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_selection_experiment.py:1202:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_selection_experiment.py:1205:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_selection_experiment.py:1243:69: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1255:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1266:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1270:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1273:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1314:27: W292 no newline at end of file ./tests/tfscreen/simulate/test_thermo_to_growth.py:28:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:32:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:41:1: E302 expected 2 blank lines, found 1 @@ -6436,33 +6470,33 @@ ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.run_prefit_calibration_cli._identify_field_mapping' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:295:1: E303 too many blank lines (4) ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:299:1: E302 expected 2 blank lines, found 4 -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1003:67: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1039:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1058:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1073:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1093:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1099:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1100:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1103:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1104:13: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1105:13: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1106:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1110:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1114:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1118:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1119:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1120:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1121:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1148:24: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1150:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1167:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1168:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1175:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1176:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1640:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1666:63: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1705:38: E702 multiple statements on one line (semicolon) -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1724:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1015:67: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1051:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1070:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1085:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1105:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1111:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1112:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1115:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1116:13: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1117:13: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1118:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1122:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1126:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1130:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1131:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1132:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1133:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1160:24: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1162:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1179:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1180:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1187:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1188:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1671:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1697:63: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1736:38: E702 multiple statements on one line (semicolon) +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1755:20: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:84:31: E741 ambiguous variable name 'l' ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:598:28: E127 continuation line over-indented for visual indent @@ -6553,83 +6587,82 @@ ./tests/tfscreen/tfmodel/test_checkpoint_protection.py:42:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_checkpoint_protection.py:62:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_checkpoint_protection.py:78:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:20:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:24:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:27:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:32:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:42:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:52:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:55:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:58:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:63:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:69:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:78:39: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:79:40: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:80:42: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:82:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:84:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:88:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:91:45: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:92:48: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:93:44: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:94:41: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:96:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:100:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:109:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:117:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:121:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:123:71: E203 whitespace before ',' -./tests/tfscreen/tfmodel/test_configure_and_run.py:124:1: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/test_configure_and_run.py:125:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:127:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:132:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:134:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:137:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:142:30: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:142:40: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:142:55: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:142:74: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:145:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:148:30: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:148:40: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:148:55: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:148:74: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:154:34: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:154:49: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:156:14: E111 indentation is not a multiple of 4 -./tests/tfscreen/tfmodel/test_configure_and_run.py:156:14: E117 over-indented -./tests/tfscreen/tfmodel/test_configure_and_run.py:156:31: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:156:41: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:156:56: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:156:75: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:162:34: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:162:54: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:14: E111 indentation is not a multiple of 4 -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:14: E117 over-indented -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:31: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:41: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:56: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:75: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:164:128: E501 line too long (142 > 127 characters) -./tests/tfscreen/tfmodel/test_configure_and_run.py:165:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:168:66: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:14: E111 indentation is not a multiple of 4 -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:14: E117 over-indented -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:31: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:41: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:56: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:75: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:174:128: E501 line too long (150 > 127 characters) -./tests/tfscreen/tfmodel/test_configure_and_run.py:178:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:183:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:185:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/test_configure_and_run.py:188:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/test_configure_and_run.py:316:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:331:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:339:114: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:342:9: F841 local variable 'mock_ri_class' is assigned to but never used -./tests/tfscreen/tfmodel/test_configure_and_run.py:345:44: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:361:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:21:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:25:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:28:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:33:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:43:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:53:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:56:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:59:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:64:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:70:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:79:39: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:80:40: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:81:42: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:83:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:85:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:89:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:92:45: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:93:48: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:94:44: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:95:41: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:97:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:101:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:110:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:118:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:122:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:124:71: E203 whitespace before ',' +./tests/tfscreen/tfmodel/test_configure_and_run.py:125:1: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/test_configure_and_run.py:126:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:128:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:133:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:135:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:138:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:143:30: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:143:40: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:143:55: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:143:74: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:146:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:149:30: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:149:40: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:149:55: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:149:74: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:155:34: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:155:49: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:157:14: E111 indentation is not a multiple of 4 +./tests/tfscreen/tfmodel/test_configure_and_run.py:157:14: E117 over-indented +./tests/tfscreen/tfmodel/test_configure_and_run.py:157:31: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:157:41: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:157:56: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:157:75: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:163:34: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:163:54: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:14: E111 indentation is not a multiple of 4 +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:14: E117 over-indented +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:31: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:41: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:56: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:75: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:165:128: E501 line too long (142 > 127 characters) +./tests/tfscreen/tfmodel/test_configure_and_run.py:166:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_configure_and_run.py:169:66: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:14: E111 indentation is not a multiple of 4 +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:14: E117 over-indented +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:31: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:41: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:56: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:75: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:175:128: E501 line too long (150 > 127 characters) +./tests/tfscreen/tfmodel/test_configure_and_run.py:223:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:225:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/test_configure_and_run.py:228:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/test_configure_and_run.py:356:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:371:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:379:114: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:382:9: F841 local variable 'mock_ri_class' is assigned to but never used +./tests/tfscreen/tfmodel/test_configure_and_run.py:385:44: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:401:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_congression_mask.py:1:1: F401 'pytest' imported but unused ./tests/tfscreen/tfmodel/test_congression_mask.py:5:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_congression_mask.py:7:1: W293 blank line contains whitespace @@ -6961,76 +6994,73 @@ ./tests/tfscreen/tfmodel/test_model_orchestrator.py:120:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:129:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_model_orchestrator.py:146:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:149:51: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:155:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:159:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:241:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:264:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:270:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:272:76: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:295:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:303:128: E501 line too long (184 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:304:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:314:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:319:48: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:319:50: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:319:52: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:319:54: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:319:56: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:319:58: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:320:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:328:112: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:328:128: E501 line too long (191 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:333:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:336:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:340:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:347:128: E501 line too long (184 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:366:128: E501 line too long (198 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:371:128: E501 line too long (167 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:419:79: E712 comparison to False should be 'if cond is False:' or 'if not cond:' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:420:64: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:423:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:431:51: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:437:77: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:437:98: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:437:125: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:437:128: E501 line too long (131 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:438:128: E501 line too long (138 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:440:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:442:86: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:454:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:459:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:467:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:475:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:486:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:489:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:496:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:503:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:511:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:516:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:521:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:527:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:274:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:297:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:303:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:305:76: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:328:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:336:128: E501 line too long (184 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:337:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:347:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:48: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:50: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:52: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:54: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:56: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:58: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:353:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:361:112: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:361:128: E501 line too long (191 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:366:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:369:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:380:128: E501 line too long (184 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:399:128: E501 line too long (198 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:404:128: E501 line too long (167 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:452:79: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:453:64: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:456:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:464:51: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:77: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:98: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:125: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:128: E501 line too long (131 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:471:128: E501 line too long (138 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:473:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:475:86: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:487:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:492:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:500:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:508:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:519:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:522:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:529:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:536:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:540:26: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:550:30: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:551:25: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:553:34: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:554:33: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:561:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:566:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:43: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:45: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:47: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:49: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:51: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:53: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:568:55: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:572:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:581:26: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_model_orchestrator.py:582:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:585:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:588:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:765:67: W292 no newline at end of file +./tests/tfscreen/tfmodel/test_model_orchestrator.py:544:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:549:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:554:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:560:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:569:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:573:26: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:583:30: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:584:25: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:586:34: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:587:33: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:594:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:599:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:43: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:45: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:47: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:49: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:51: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:53: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:55: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:605:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:614:26: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_model_orchestrator.py:615:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:618:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:621:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:798:67: W292 no newline at end of file ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:16: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:31: E272 multiple spaces before keyword ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:31:16: E221 multiple spaces before operator @@ -7087,6 +7117,16 @@ ./tests/tfscreen/tfmodel/test_prediction.py:85:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_prediction.py:89:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_prediction.py:91:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_presplit.py:12:1: F401 'numpy as np' imported but unused +./tests/tfscreen/tfmodel/test_presplit.py:15:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/test_presplit.py:24:1: F401 'tfscreen.tfmodel.data_class.DataClass' imported but unused +./tests/tfscreen/tfmodel/test_presplit.py:25:1: F401 'tfscreen.tfmodel.tensors.populate_dataclass.populate_dataclass' imported but unused +./tests/tfscreen/tfmodel/test_presplit.py:93:52: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:96:33: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:123:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:136:48: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:170:47: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:214:47: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/test_reference_math.py:29:11: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_reference_math.py:32:9: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_reference_math.py:75:14: E221 multiple spaces before operator @@ -7589,7 +7629,7 @@ ./tests/tfscreen/util/numerical/test_zero_truncated_poisson.py:81:1: W293 blank line contains whitespace ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/util/validation/test_check.py:42:38: W292 no newline at end of file +./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file 45 C901 'cat_fit' is too complex (19) 16 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) @@ -7598,17 +7638,17 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -158 E127 continuation line over-indented for visual indent -66 E128 continuation line under-indented for visual indent +166 E127 continuation line over-indented for visual indent +68 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1713 E221 multiple spaces before operator +1741 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1112 E231 missing whitespace after ',' +1115 E231 missing whitespace after ',' 79 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 226 E261 at least two spaces before inline comment @@ -7617,7 +7657,7 @@ 2 E266 too many leading '#' for block comment 150 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -803 E302 expected 2 blank lines, found 1 +802 E302 expected 2 blank lines, found 1 51 E303 too many blank lines (2) 17 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 @@ -7631,12 +7671,12 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 12 E741 ambiguous variable name 'l' -214 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +220 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 41 F841 local variable 'e' is assigned to but never used -691 W291 trailing whitespace +688 W291 trailing whitespace 86 W292 no newline at end of file -1708 W293 blank line contains whitespace +1705 W293 blank line contains whitespace 29 W391 blank line at end of file -7592 +7632 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 9069f37f..da5f13e1 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/scripts/run_simulation_cli.py b/src/tfscreen/simulate/scripts/run_simulation_cli.py index f8e4f6ac..594a5931 100644 --- a/src/tfscreen/simulate/scripts/run_simulation_cli.py +++ b/src/tfscreen/simulate/scripts/run_simulation_cli.py @@ -5,6 +5,7 @@ import tfscreen from tfscreen.simulate import library_prediction, selection_experiment +from tfscreen.simulate.selection_experiment import _sim_index_hop from tfscreen.simulate.sim_data_class import build_sim_data from tfscreen.simulate.sample_theta import sample_theta_prior from tfscreen.genetics import standardize_genotypes @@ -98,11 +99,143 @@ def _generate_binding_data(cf, library_df, binding_cfg, rng): return pd.DataFrame(rows) +def _generate_presplit_data(combined_sample_df, combined_counts_df, cf, rng): + """ + Generate simulated pre-split (t = -t_pre) data from selection-experiment + outputs. + + For each unique ``(replicate, condition_pre)`` pair, this function draws a + synthetic sequencing sample from the initial library-frequency distribution + (encoded as ``ln_cfu_0`` in ``combined_counts_df``). The resulting counts + are converted to ``ln_cfu`` and ``ln_cfu_std`` using the same + variance-propagation formula as ``counts_to_lncfu``, yielding a DataFrame + that can be passed directly to ``tfs-configure-model --presplit_df``. + + Parameters + ---------- + combined_sample_df : pandas.DataFrame + Concatenated sample metadata across all replicates (index = sample ID). + Must contain columns ``replicate`` and ``condition_pre``. + combined_counts_df : pandas.DataFrame + Concatenated genotype counts across all replicates. Must contain + columns ``sample``, ``genotype``, and ``ln_cfu_0`` (the ground-truth + initial log-CFU per genotype computed by the simulation). + cf : dict + Full run configuration (already read from YAML). Used keys: + ``cfu0``, ``total_num_reads``, ``final_cfu_pct_err``, + ``prob_index_hop``. The optional ``presplit_data`` sub-dict may + contain a scalar ``noise`` key (default 0) to add extra Gaussian noise + to ``ln_cfu`` on the log scale. + rng : numpy.random.Generator + Seeded random-number generator shared with the rest of the simulation. + + Returns + ------- + pandas.DataFrame + Columns: ``replicate``, ``condition_pre``, ``genotype``, + ``ln_cfu``, ``ln_cfu_std``, ``ln_cfu_0_true``. + ``ln_cfu_0_true`` records the simulation ground truth for validation. + Rows with zero initial frequency (genotypes absent from the + transformation pool) receive ``ln_cfu = NaN``. + """ + total_cfu0 = float(cf["cfu0"]) + final_cfu_pct_err = float(cf.get("final_cfu_pct_err", 0.05)) + presplit_cfg = cf.get("presplit_data", {}) + extra_noise = float(presplit_cfg.get("noise", 0.0)) if presplit_cfg else 0.0 + pseudocount = 1 + + # Reads per presplit sample: use the same budget as the main experiment + # (total reads / total selection samples). + total_num_reads = int(cf["total_num_reads"]) + total_num_samples = len(combined_sample_df) + reads_per_sample = max(1, int(round(total_num_reads / total_num_samples))) + + # Attach condition_pre and replicate to each (genotype, sample) row so we + # can group by (replicate, condition_pre) below. + sample_meta = (combined_sample_df + .reset_index()[["sample", "replicate", "condition_pre"]] + .drop_duplicates("sample")) + counts_meta = pd.merge(combined_counts_df, sample_meta, on="sample", how="left") + + # One row per (replicate, condition_pre, genotype), keeping the first + # occurrence of ln_cfu_0 (same value for all selection samples within a + # library group). + source = (counts_meta + .groupby(["replicate", "condition_pre", "genotype"], observed=True) + .first() + .reset_index()[["replicate", "condition_pre", "genotype", "ln_cfu_0"]]) + + rows = [] + for (rep, cp), grp in source.groupby(["replicate", "condition_pre"], + observed=True): + genos = grp["genotype"].values + ln_cfu0 = grp["ln_cfu_0"].values + + # Convert ground-truth ln_cfu_0 to initial frequencies. + # Genotypes with ln_cfu_0 = -inf (absent from transformation pool) + # get frequency 0. + cfu0_raw = np.where(np.isfinite(ln_cfu0), np.exp(ln_cfu0), 0.0) + total_cfu_group = cfu0_raw.sum() + if total_cfu_group == 0: + continue + freqs = cfu0_raw / total_cfu_group + + # Simulate multinomial sequencing draw from the initial distribution. + counts = rng.multinomial(reads_per_sample, freqs) + + # Optionally apply index hopping (same as the selection samples). + counts = _sim_index_hop(counts, cf.get("prob_index_hop"), rng) + + # ---------- counts → ln_cfu (mirrors counts_to_lncfu logic) ---------- + sample_cfu = total_cfu0 + sample_cfu_std = total_cfu0 * final_cfu_pct_err + + total_adjusted = counts.sum() + len(counts) * pseudocount + adj_counts = counts + pseudocount + freq_est = adj_counts / total_adjusted + cfu_est = freq_est * sample_cfu + + # Variance from binomial frequency uncertainty + sample-CFU uncertainty + var_freq = freq_est * (1.0 - freq_est) / total_adjusted + with np.errstate(divide="ignore", invalid="ignore"): + rel_var_freq = np.where(freq_est > 0, + var_freq / (freq_est ** 2), 0.0) + rel_var_cfu = (sample_cfu_std / sample_cfu) ** 2 + ln_cfu_var = rel_var_freq + rel_var_cfu + + # Optional additional noise on the log scale + if extra_noise > 0.0: + ln_cfu_var = ln_cfu_var + extra_noise ** 2 + ln_cfu_shift = rng.normal(0.0, extra_noise, size=len(genos)) + else: + ln_cfu_shift = np.zeros(len(genos)) + + with np.errstate(divide="ignore"): + ln_cfu_vals = np.log(cfu_est) + ln_cfu_shift + + # Zero-count entries become NaN + ln_cfu_vals = np.where(cfu_est > 0, ln_cfu_vals, np.nan) + ln_cfu_var = np.where(cfu_est > 0, ln_cfu_var, np.nan) + ln_cfu_std_vals = np.sqrt(ln_cfu_var) + + for i, geno in enumerate(genos): + rows.append({ + "replicate": rep, + "condition_pre": cp, + "genotype": geno, + "ln_cfu": float(ln_cfu_vals[i]), + "ln_cfu_std": float(ln_cfu_std_vals[i]), + "ln_cfu_0_true": float(ln_cfu0[i]), + }) + + return pd.DataFrame(rows) + + def run_simulation_from_config( config_file, output_dir, output_prefix="tfs_sim_", - num_replicates=1, + num_replicates=2, seed=None, ): """ @@ -123,7 +256,7 @@ def run_simulation_from_config( output_prefix : str Prefix for all output filenames. Default 'tfs_sim_'. num_replicates : int - Number of independent experimental replicates to simulate. Default 1. + Number of independent experimental replicates to simulate. Default 2. seed : int, optional Random seed. Overrides random_seed in the config file when provided. """ @@ -139,11 +272,17 @@ def out_path(name): output_names = ["library", "parameters", "genotype_theta", "growth"] if "binding_data" in cf: output_names.append("binding") + if "presplit_data" in cf: + output_names.append("presplit") existing = [n for n in output_names if os.path.exists(out_path(n))] if existing: paths = ", ".join(out_path(n) for n in existing) - raise FileExistsError(f"Output files already exist: {paths}") + raise FileExistsError( + f"Output files already exist: {paths}\n" + f"Delete them or choose a different output_dir / output_prefix " + f"before re-running." + ) base_seed = cf.get("random_seed", None) rng = np.random.default_rng(base_seed) @@ -208,6 +347,14 @@ def out_path(name): binding_df.to_csv(out_path("binding"), index=False) print(f"Wrote: {out_path('binding')}") + if "presplit_data" in cf: + print("\nGenerating presplit data...", flush=True) + presplit_df = _generate_presplit_data(combined_sample_df, + combined_counts_df, + cf, rng) + presplit_df.to_csv(out_path("presplit"), index=False) + print(f"Wrote: {out_path('presplit')}") + def main(): return generalized_main(run_simulation_from_config, diff --git a/src/tfscreen/simulate/selection_experiment.py b/src/tfscreen/simulate/selection_experiment.py index 77e7f0d4..7af1fdac 100644 --- a/src/tfscreen/simulate/selection_experiment.py +++ b/src/tfscreen/simulate/selection_experiment.py @@ -5,6 +5,7 @@ read_dataframe, read_yaml, ) +from tfscreen.util.validation import check_unknown_keys from tfscreen.simulate.growth.transition_linkage import get_transition_model from tfscreen.util.numerical import ( vstack_padded, @@ -43,6 +44,29 @@ "min":ma.min, "max":ma.max, "sum":ma.sum} + +# All recognized top-level keys for a simulate config file. +SIMULATE_KNOWN_KEYS = frozenset({ + # Library genetics (passed to LibraryManager) + "reading_frame", "first_amplicon_residue", "wt_seq", "degen_sites", + "sub_libraries", "library_combos", "spiked_seqs", "expected_5p", "expected_3p", + # Phenotype / theta calculation + "theta_component", "theta_rng_seed", "thermo_data", "theta_priors", "theta_rescale", + # Conditions and growth + "condition_blocks", "growth", + "dk_geno_hyper_loc", "dk_geno_hyper_scale", "dk_geno_hyper_shift", + "activity_wt", "activity_mut_scale", "activity_component", "activity_priors", + # Experimental simulation parameters + "transform_sizes", "library_mixture", "lib_assembly_skew_sigma", + "transformation_poisson_lambda", "multi_plasmid_combine_fcn", "cfu0", + "tube_noise_sigma", "growth_transition", + # Data collection + "total_num_reads", "prob_index_hop", "random_seed", "final_cfu_pct_err", + # Column selectors (rarely overridden) + "condition_selector", "library_selector", + # Optional output blocks + "binding_data", "presplit_data", +}) def _check_dict_number( key: str, @@ -163,6 +187,8 @@ def _check_cf( # Load from YAML if a path is provided, otherwise assume it's a dict cf = read_yaml(cf) + check_unknown_keys(cf, SIMULATE_KNOWN_KEYS, label="simulate config") + if "final_cfu_pct_err" not in cf: cf["final_cfu_pct_err"] = 0.05 diff --git a/src/tfscreen/tfmodel/configuration_io.py b/src/tfscreen/tfmodel/configuration_io.py index a364195e..0c1a06c5 100644 --- a/src/tfscreen/tfmodel/configuration_io.py +++ b/src/tfscreen/tfmodel/configuration_io.py @@ -5,6 +5,16 @@ import jax.numpy as jnp import warnings from tfscreen.__version__ import __version__ +from tfscreen.util.validation import check_unknown_keys + +# All recognized top-level keys for a tfmodel config file. +TFMODEL_KNOWN_KEYS = frozenset({ + "tfscreen_version", + "data", + "components", + "priors_file", + "guesses_file", +}) def _extract_scalars(obj, prefix=""): """Recursively extract scalar values from PriorsClass.""" @@ -106,7 +116,8 @@ def _update_dataclass(dc, prefix, flat_dict): def write_configuration(gm, out_prefix, growth_df_path=None, - binding_df_path=None): + binding_df_path=None, + presplit_df_path=None): """ Write model configuration and extracted priors/guesses to files. @@ -144,6 +155,8 @@ def write_configuration(gm, data_paths["growth"] = growth_df_path if binding_df_path is not None: data_paths["binding"] = binding_df_path + if presplit_df_path is not None: + data_paths["presplit"] = presplit_df_path config = { "tfscreen_version": __version__, @@ -260,10 +273,13 @@ def read_configuration(config_file): with open(config_file, "r") as f: config = yaml.safe_load(f) + check_unknown_keys(config, TFMODEL_KNOWN_KEYS, label="tfmodel config") + # Check sanity of format and read in data paths and components if "data" in config and "components" in config: growth_df_path = config["data"].get("growth") binding_df_path = config["data"].get("binding") + presplit_df_path = config["data"].get("presplit") settings = config["components"] else: raise ValueError(f"Configuration file '{config_file}' has an unrecognized format.") @@ -273,10 +289,14 @@ def read_configuration(config_file): warnings.warn(f"Configuration file version {config['tfscreen_version']} does not match current tfscreen version {__version__}") batch_size = settings.pop("batch_size", None) + # presplit_df is a data path, not a component setting; pop it if it + # ended up in settings (older configs that serialised it there). + settings.pop("presplit_df", None) gm = ModelOrchestrator(growth_df_path, binding_df_path, batch_size=batch_size, + presplit_df=presplit_df_path, **settings) # Update Priors from CSV diff --git a/src/tfscreen/tfmodel/data_class.py b/src/tfscreen/tfmodel/data_class.py index 345a67dc..cc0e963f 100644 --- a/src/tfscreen/tfmodel/data_class.py +++ b/src/tfscreen/tfmodel/data_class.py @@ -149,6 +149,33 @@ class BindingData: struct_contact_distances: Any = field(pytree_node=False, default=None) +@dataclass(frozen=True) +class PreSplitData: + """ + Holds the optional pre-split (t = -t_pre) sequencing observations. + + These are taken from a single pooled aliquot just before the culture is + split into titrant-concentration conditions, so they carry no + condition_sel or titrant_conc index. The prediction for each observation + is simply ``ln_cfu0[replicate, condition_pre, genotype]``, making this a + direct side-channel constraint on the initial-population parameter. + + Tensors are shaped ``(num_replicate, num_condition_pre, num_genotype)`` + and use the same categorical orderings as the companion GrowthData so + that slicing with ``data.growth.batch_idx`` aligns the genotype axis. + """ + + # Data tensors + ln_cfu_t0: jnp.ndarray + ln_cfu_t0_std: jnp.ndarray + good_mask: jnp.ndarray + + # Tensor dimensions (aligned with GrowthData) + num_replicate: int = field(pytree_node=False) + num_condition_pre: int = field(pytree_node=False) + num_genotype: int = field(pytree_node=False) + + @dataclass(frozen=True) class DataClass: """ @@ -157,17 +184,19 @@ class DataClass: """ num_genotype: int = field(pytree_node=False) - + batch_idx: jnp.ndarray batch_size: int = field(pytree_node=False) not_binding_idx: jnp.ndarray not_binding_batch_size: int = field(pytree_node=False) - num_binding: int = field(pytree_node=False) + num_binding: int = field(pytree_node=False) # GrowthData when running the joint growth+binding model; None in binding_only mode. growth: Any = field(default=None) binding: BindingData = field(default=None) + # Optional pre-split observations (t = -t_pre aliquot before condition split). + presplit: Any = field(default=None) @dataclass(frozen=True) diff --git a/src/tfscreen/tfmodel/generative/components/growth/linear.py b/src/tfscreen/tfmodel/generative/components/growth/linear.py index 2f687ff5..e63d8115 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/linear.py +++ b/src/tfscreen/tfmodel/generative/components/growth/linear.py @@ -246,6 +246,8 @@ def get_priors(): def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth/power.py b/src/tfscreen/tfmodel/generative/components/growth/power.py index e5b7180d..a6e2f952 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/power.py +++ b/src/tfscreen/tfmodel/generative/components/growth/power.py @@ -189,6 +189,8 @@ def get_priors(): def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth/saturation.py b/src/tfscreen/tfmodel/generative/components/growth/saturation.py index a5532e56..87daca34 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/saturation.py +++ b/src/tfscreen/tfmodel/generative/components/growth/saturation.py @@ -157,6 +157,8 @@ def get_priors(): def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi.py b/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi.py index 84b391de..86099bfa 100644 --- a/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi.py +++ b/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi.py @@ -160,6 +160,8 @@ def get_priors() -> ModelPriors: def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_k.py b/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_k.py index 81227f69..f405751a 100644 --- a/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_k.py +++ b/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_k.py @@ -198,6 +198,8 @@ def get_priors() -> ModelPriors: def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_tau.py b/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_tau.py index 7ad8017a..edc64c9f 100644 --- a/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_tau.py +++ b/src/tfscreen/tfmodel/generative/components/growth_transition/baranyi_tau.py @@ -198,6 +198,8 @@ def get_priors() -> ModelPriors: def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth_transition/memory.py b/src/tfscreen/tfmodel/generative/components/growth_transition/memory.py index 6c42f789..7cba807a 100644 --- a/src/tfscreen/tfmodel/generative/components/growth_transition/memory.py +++ b/src/tfscreen/tfmodel/generative/components/growth_transition/memory.py @@ -194,6 +194,8 @@ def get_priors() -> ModelPriors: def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/components/growth_transition/two_pop.py b/src/tfscreen/tfmodel/generative/components/growth_transition/two_pop.py index e9da6adc..8b0e15aa 100644 --- a/src/tfscreen/tfmodel/generative/components/growth_transition/two_pop.py +++ b/src/tfscreen/tfmodel/generative/components/growth_transition/two_pop.py @@ -235,6 +235,8 @@ def get_priors() -> ModelPriors: def get_extract_specs(ctx): + if "condition_rep" not in ctx.growth_tm.map_groups: + return [] cond_rep_cols = (["condition_rep"] if ctx.growth_shares_replicates else ["replicate", "condition_rep"]) return [dict( diff --git a/src/tfscreen/tfmodel/generative/model.py b/src/tfscreen/tfmodel/generative/model.py index 9d8e2c70..10860140 100644 --- a/src/tfscreen/tfmodel/generative/model.py +++ b/src/tfscreen/tfmodel/generative/model.py @@ -7,6 +7,7 @@ import jax.numpy as jnp import numpyro as pyro +import numpyro.distributions as dist def jax_model(data: DataClass, priors: PriorsClass, @@ -184,6 +185,31 @@ def jax_model(data: DataClass, # real calculation else: + # Pre-split (t = -t_pre) observations — direct constraint on ln_cfu0. + # ln_cfu0 shape: (num_rep, 1, num_cp, 1, 1, 1, batch_size) + # Squeeze broadcast dims to get (num_rep, num_cp, batch_size). + if getattr(data, "presplit", None) is not None: + ln_cfu0_3d = ln_cfu0[:, 0, :, 0, 0, 0, :] + ps = data.presplit + bi = data.growth.batch_idx + obs_t0 = ps.ln_cfu_t0[:, :, bi] + std_t0 = ps.ln_cfu_t0_std[:, :, bi] + mask_t0 = ps.good_mask[:, :, bi] + with pyro.plate("presplit_replicate", + size=ps.num_replicate, dim=-3): + with pyro.plate("presplit_condition_pre", + size=ps.num_condition_pre, dim=-2): + with pyro.plate("shared_genotype_plate", + size=data.growth.batch_size, dim=-1): + with pyro.handlers.scale( + scale=data.growth.scale_vector): + with pyro.handlers.mask(mask=mask_t0): + pyro.sample( + "presplit_obs", + dist.Normal(ln_cfu0_3d, std_t0), + obs=obs_t0, + ) + # calculate observable (all tensors have correct dimensions) g_pre, g_sel = calculate_growth(params=growth_params, dk_geno=dk_geno, diff --git a/src/tfscreen/tfmodel/generative/registry.py b/src/tfscreen/tfmodel/generative/registry.py index df430dbc..ae58e6e2 100644 --- a/src/tfscreen/tfmodel/generative/registry.py +++ b/src/tfscreen/tfmodel/generative/registry.py @@ -21,6 +21,7 @@ from .components.activity import hierarchical_mut as activity_mut_decomp from .components.activity import horseshoe_mut as activity_horseshoe_mut +from .components.theta import _simple as theta_simple from .components.theta import categorical_geno as theta_cat from .components.theta import hill_geno as theta_hill from .components.theta import hill_mut as theta_hill_mut @@ -87,6 +88,7 @@ "logit": theta_rescale_logit, }, "theta":{ + "_simple":theta_simple, "categorical_geno":theta_cat, "hill_geno":theta_hill, "hill_mut":theta_hill_mut, diff --git a/src/tfscreen/tfmodel/inference/run_inference.py b/src/tfscreen/tfmodel/inference/run_inference.py index 84f6b3f3..1a210f38 100644 --- a/src/tfscreen/tfmodel/inference/run_inference.py +++ b/src/tfscreen/tfmodel/inference/run_inference.py @@ -733,6 +733,46 @@ def _write_epoch_checkpoint(self, svi_state, epoch): dill.dump(out_dict, f) os.replace(tmp_file, epoch_file) + def restore_svi_from_checkpoint(self, checkpoint_file, init_params=None): + """ + Rebuild a component SVI object and restore its state from a checkpoint, + without running any optimization steps or convergence checks. + + ``svi.init()`` must be called at least once to wire up ``constrain_fn`` + on the SVI object before ``svi.get_params()`` can be used. The state + produced by ``init()`` is immediately discarded and replaced with the + one loaded from the checkpoint. + + Parameters + ---------- + checkpoint_file : str + Path to the checkpoint .pkl file produced by tfs-fit-model. + init_params : dict or None, optional + Initial parameter values forwarded to ``svi.init()``. The values + are never used in inference (the checkpoint overwrites them), but + they must be structurally valid for the model. + + Returns + ------- + svi : numpyro.infer.SVI + Initialized SVI object with ``constrain_fn`` populated. + svi_state : numpyro.infer.svi.SVIState + Optimizer state restored from the checkpoint. + """ + svi = self.setup_svi(guide_type="component") + + # init() is required to populate svi.constrain_fn; the resulting state + # is thrown away — the checkpoint state is used instead. + data_on_gpu = jax.device_put(self.model.data) + batch_idx = jax.device_put(self.model.get_random_idx()) + batch_data = self.model.get_batch(data_on_gpu, batch_idx) + init_key = self.get_key() + svi.init(init_key, init_params=init_params, + priors=self.model.priors, data=batch_data) + + svi_state = self._restore_checkpoint(checkpoint_file) + return svi, svi_state + def _restore_checkpoint(self,checkpoint_file): """ Load an SVI state and PRNG key from a checkpoint file. diff --git a/src/tfscreen/tfmodel/model_orchestrator.py b/src/tfscreen/tfmodel/model_orchestrator.py index feed17cc..e5fb77ea 100644 --- a/src/tfscreen/tfmodel/model_orchestrator.py +++ b/src/tfscreen/tfmodel/model_orchestrator.py @@ -12,6 +12,7 @@ DataClass, BindingData, GrowthData, + PreSplitData, PriorsClass, GrowthPriors, BindingPriors @@ -24,6 +25,7 @@ from functools import partial import inspect +import warnings # Declare float datatype FLOAT_DTYPE = jnp.float64 if jax.config.read("jax_enable_x64") else jnp.float32 @@ -243,14 +245,18 @@ def _read_binding_df(binding_df, cols = ["genotype","titrant_name"] binding_seen = binding_df[cols].drop_duplicates().set_index(cols) growth_seen = growth_df[cols].drop_duplicates().set_index(cols) - is_subset = binding_seen.index.isin(growth_seen.index).all() - if not is_subset: - raise ValueError( - "binding_df contains genotype/titrant_name pairs that were not seen " - "in the growth_df." + extra = binding_seen.index[~binding_seen.index.isin(growth_seen.index)] + if len(extra) > 0: + warnings.warn( + f"binding_df contains {len(extra)} genotype/titrant_name pair(s) " + f"not present in growth_df; those rows will be dropped. " + f"Extra pairs: {list(extra[:5])}{'...' if len(extra) > 5 else ''}", + UserWarning, + stacklevel=2, ) # Inherit map_theta_group indices from growth_df so parameter indexing - # stays consistent across the two datasets. + # stays consistent across the two datasets. Rows in binding_df with no + # match in growth_df are silently dropped by add_group_columns. binding_df = add_group_columns(binding_df, theta_group_cols, "map_theta_group", existing_df=growth_df) else: @@ -313,6 +319,111 @@ def _build_binding_tm(binding_df): return binding_tm +def _read_presplit_df(presplit_df, growth_df): + """ + Read and validate the optional pre-split DataFrame. + + Rows whose genotype does not appear in growth_df are silently dropped. + Genotypes present in growth_df but absent from presplit_df are kept; they + will produce NaN entries in the tensor (masked out by good_mask). + + Parameters + ---------- + presplit_df : pd.DataFrame or str + DataFrame or path to file with pre-split data. Required columns: + ``replicate``, ``condition_pre``, ``genotype``, ``ln_cfu``, + ``ln_cfu_std``. + growth_df : pd.DataFrame + The already-processed growth DataFrame (growth_tm.df). Used to + determine the valid set of genotypes, replicates, and condition_pre + values. + + Returns + ------- + pd.DataFrame + A copy of the processed pre-split DataFrame restricted to genotypes + present in growth_df. + """ + presplit_df = tfscreen.util.io.read_dataframe(presplit_df) + presplit_df = tfscreen.genetics.set_categorical_genotype(presplit_df, + standardize=True) + tfscreen.util.dataframe.check_columns( + presplit_df, + required_columns=["replicate", "condition_pre", "genotype", + "ln_cfu", "ln_cfu_std"], + ) + + growth_genotypes = set(growth_df["genotype"]) + n_before = len(presplit_df) + presplit_df = presplit_df[ + presplit_df["genotype"].isin(growth_genotypes) + ].copy() + n_dropped = n_before - len(presplit_df) + if n_dropped > 0: + print( + f"presplit_df: dropped {n_dropped} rows whose genotype was not " + f"found in growth_df.", + flush=True, + ) + + return presplit_df + + +def _build_presplit_tm(presplit_df, growth_tm): + """ + Build a TensorManager for the pre-split (t = -t_pre) data. + + The output tensor has shape ``(num_replicate, num_condition_pre, + num_genotype)`` using the *same* categorical orderings as growth_tm so + that the genotype axis aligns with ``growth_tm.batch_idx``. Entries + with no presplit observation are filled with 1.0 (masked out by + good_mask). + + Parameters + ---------- + presplit_df : pd.DataFrame + The processed pre-split DataFrame from ``_read_presplit_df``. + growth_tm : TensorManager + The fully-built growth TensorManager (after ``create_tensors()``). + + Returns + ------- + TensorManager + A fully-built TensorManager with tensors ``ln_cfu``, ``ln_cfu_std``, + and ``good_mask``. + """ + # Inherit categorical orderings from growth_tm so indices match exactly. + dim_names = growth_tm.tensor_dim_names + rep_cats = growth_tm.tensor_dim_labels[dim_names.index("replicate")] + cp_cats = growth_tm.tensor_dim_labels[dim_names.index("condition_pre")] + geno_cats = growth_tm.tensor_dim_labels[dim_names.index("genotype")] + + presplit_df = presplit_df.copy() + presplit_df["replicate"] = pd.Categorical( + presplit_df["replicate"], categories=rep_cats + ) + presplit_df["condition_pre"] = pd.Categorical( + presplit_df["condition_pre"], categories=cp_cats + ) + presplit_df["genotype"] = pd.Categorical( + presplit_df["genotype"], categories=geno_cats + ) + + presplit_tm = TensorManager(presplit_df) + presplit_tm.add_pivot_index(tensor_dim_name="replicate", + cat_column="replicate") + presplit_tm.add_pivot_index(tensor_dim_name="condition_pre", + cat_column="condition_pre") + presplit_tm.add_pivot_index(tensor_dim_name="genotype", + cat_column="genotype") + + presplit_tm.add_data_tensor("ln_cfu", dtype=FLOAT_DTYPE) + presplit_tm.add_data_tensor("ln_cfu_std", dtype=FLOAT_DTYPE) + + presplit_tm.create_tensors() + return presplit_tm + + def _setup_batching(growth_genotypes, binding_genotypes, batch_size): @@ -464,10 +575,12 @@ def __init__(self, growth_shares_replicates=False, epistasis=False, thermo_data=None, - binding_weight=None): + binding_weight=None, + presplit_df=None): self._ln_cfu_df = growth_df self._binding_df = binding_df + self._presplit_df = presplit_df self._batch_size = batch_size self._binding_only = binding_only @@ -805,13 +918,43 @@ def _initialize_data(self): binding_dataclass = populate_dataclass(BindingData, sources=binding_data_sources) + # --------------------------------------------------------------------- + # presplit dataclass (optional) + + if self._presplit_df is not None: + self.presplit_df = _read_presplit_df(self._presplit_df, + self.growth_tm.df) + self.presplit_tm = _build_presplit_tm(self.presplit_df, + self.growth_tm) + ps_tensors = { + "ln_cfu_t0": self.presplit_tm.tensors["ln_cfu"], + "ln_cfu_t0_std": self.presplit_tm.tensors["ln_cfu_std"], + "good_mask": self.presplit_tm.tensors["good_mask"], + } + ps_sizes = { + "num_replicate": self.presplit_tm.tensor_shape[0], + "num_condition_pre": self.presplit_tm.tensor_shape[1], + "num_genotype": self.presplit_tm.tensor_shape[2], + } + presplit_dataclass = populate_dataclass(PreSplitData, + sources=[ps_tensors, + ps_sizes]) + else: + self.presplit_df = None + self.presplit_tm = None + presplit_dataclass = None + + # --------------------------------------------------------------------- + # Populate DataClass + source_data = [{"growth":growth_dataclass, "binding":binding_dataclass, + "presplit":presplit_dataclass, "num_genotype":self.growth_tm.tensor_shape[-1]}] source_data.append(full_batch_data) - # Build the aggregated `DataClass` flax dataclass with the growth - # and binding dataclasses. Expose as a model attribute. + # Build the aggregated `DataClass` flax dataclass with the growth, + # binding, and presplit dataclasses. Expose as a model attribute. self._data = populate_dataclass(DataClass, sources=source_data) @@ -1271,4 +1414,5 @@ def settings(self): "epistasis": self._epistasis, "thermo_data": self._thermo_data, "binding_weight": self._binding_weight, + "presplit_df": getattr(self, "_presplit_df", None), } diff --git a/src/tfscreen/tfmodel/scripts/configure_model_cli.py b/src/tfscreen/tfmodel/scripts/configure_model_cli.py index d1df0a33..aeeb9244 100644 --- a/src/tfscreen/tfmodel/scripts/configure_model_cli.py +++ b/src/tfscreen/tfmodel/scripts/configure_model_cli.py @@ -48,6 +48,7 @@ def check_component_compatibility(condition_growth_model, theta_rescale_model): def configure_model(binding_df, growth_df=None, + presplit_df=None, out_prefix="tfs_configure", condition_growth_model="linear", growth_transition_model="instant", @@ -189,6 +190,7 @@ def configure_model(binding_df, # Initialize model to build mappings and get guesses gm = ModelOrchestrator(growth_df, binding_df, + presplit_df=presplit_df, binding_only=binding_only, condition_growth=condition_growth_model, growth_transition=growth_transition_model, @@ -211,15 +213,18 @@ def configure_model(binding_df, # Write the model configuration to a file. This includes the model component # names, the data file paths, and the parameter guesses/priors. growth_path = None if binding_only else (growth_df if isinstance(growth_df, str) else "growth.csv") + presplit_path = presplit_df if isinstance(presplit_df, str) else None write_configuration(gm=gm, out_prefix=out_prefix, growth_df_path=growth_path, - binding_df_path=binding_df if isinstance(binding_df, str) else "binding.csv") + binding_df_path=binding_df if isinstance(binding_df, str) else "binding.csv", + presplit_df_path=presplit_path) def main(): return generalized_main(configure_model, manual_arg_types={"binding_df":str, "growth_df":str, + "presplit_df":str, "spiked":list, "thermo_data":str, "batch_size":int, diff --git a/src/tfscreen/tfmodel/scripts/fit_model_cli.py b/src/tfscreen/tfmodel/scripts/fit_model_cli.py index 72be763f..9dbc558b 100644 --- a/src/tfscreen/tfmodel/scripts/fit_model_cli.py +++ b/src/tfscreen/tfmodel/scripts/fit_model_cli.py @@ -238,11 +238,13 @@ def _run_svi(ri, epoch_checkpoint_interval=epoch_checkpoint_interval ) - # Write convergence information to stdout - if converged: - print("SVI run converged.",flush=True) - else: - print("SVI run has not yet converged.",flush=True) + # Write convergence information to stdout (skip when restoring from + # checkpoint with no additional epochs — convergence is not meaningful). + if max_num_epochs > 0: + if converged: + print("SVI run converged.",flush=True) + else: + print("SVI run has not yet converged.",flush=True) return svi_obj, svi_state, params, converged diff --git a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py index fbc1a090..8ac4fe1a 100644 --- a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py @@ -11,7 +11,9 @@ - ``theta`` → ``simple`` (theta values pinned from binding data) - ``activity`` → ``hierarchical`` with hyperparams *pinned* to their prior locs (degenerate, no learning) -- ``dk_geno`` → ``hierarchical`` with hyperparams *pinned* +- ``dk_geno`` → ``fixed`` (all zeros; eliminates the WT-anchor + bias that arises when mutants have non-zero mean + pleiotropic effects) - ``ln_cfu0`` → ``hierarchical`` with hyperparams *pinned* - ``transformation`` → ``single`` (no learning) - ``theta_*_noise`` → ``zero`` (no learning) @@ -75,7 +77,7 @@ _CALIBRATION_OVERRIDES = { "theta": "_simple", "activity": "hierarchical_geno", - "dk_geno": "hierarchical_geno", + "dk_geno": "fixed", "ln_cfu0": "hierarchical", "transformation": "single", "theta_growth_noise": "zero", @@ -92,11 +94,6 @@ ("hyper_loc", "hyper_loc_loc"), ("hyper_scale", "hyper_scale_loc"), ), - "dk_geno": ( - ("hyper_loc", "hyper_loc_loc"), - ("hyper_scale", "hyper_scale_loc"), - ("hyper_shift", "hyper_shift_loc"), - ), "ln_cfu0": ( ("hyper_loc", "ln_cfu0_hyper_loc_loc"), ("hyper_scale", "ln_cfu0_hyper_scale_loc"), @@ -984,7 +981,8 @@ def _bc_t(arr, *, dtype=None): axes[extra_i // n_cols][extra_i % n_cols].set_visible(False) fig.tight_layout(rect=[0, 0, 1, 0.95]) - pdf_path = f"{out_prefix}_calib_{geno_name}.pdf" + safe_geno_name = geno_name.replace("/", "-") + pdf_path = f"{out_prefix}_calib_{safe_geno_name}.pdf" fig.savefig(pdf_path, format="pdf", bbox_inches="tight") plt.close(fig) print(f" Saved {pdf_path}", flush=True) diff --git a/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py b/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py index aeea8a44..b0cbf089 100644 --- a/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py +++ b/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py @@ -2,7 +2,6 @@ import dill from tfscreen.tfmodel.configuration_io import read_configuration from tfscreen.tfmodel.inference.run_inference import RunInference -from tfscreen.tfmodel.scripts.fit_model_cli import _run_svi from tfscreen.util.cli.generalized_main import generalized_main @@ -23,8 +22,8 @@ def sample_posterior(config_file, writes posterior predictives. 2. MAP checkpoint (AutoDelta guide): forms a Laplace (Gaussian) approximation via the Hessian of the log-joint at the MAP point, then draws samples. - 3. SVI checkpoint (component guide): resumes the fitted guide with 0 additional - epochs and draws posterior samples directly. + 3. SVI checkpoint (component guide): restores the fitted variational state + and draws posterior samples directly from the guide. The .h5 file written by this command can be passed directly to tfs-predict-growth, tfs-predict-theta, and tfs-extract-params to obtain @@ -95,25 +94,12 @@ def sample_posterior(config_file, hessian_chunk_size=hessian_chunk_size, ) else: - # SVI checkpoint: resume with 0 epochs to restore guide state, - # then draw samples directly from the variational posterior. + # SVI checkpoint: rebuild the guide object then restore the saved + # variational state directly — no optimization loop needed. print("Detected SVI checkpoint. Drawing variational posterior samples...", flush=True) - svi_obj, svi_state, _, _ = _run_svi(ri, - init_params=init_params, - checkpoint_file=checkpoint_file, - out_prefix=ri_prefix, - max_num_epochs=0, - adam_step_size=1e-3, - adam_final_step_size=1e-6, - adam_clip_norm=1.0, - elbo_num_particles=2, - convergence_tolerance=1e-5, - convergence_window=10, - patience=10, - convergence_check_interval=2, - checkpoint_interval=10, - init_param_jitter=0.0, - epoch_checkpoint_interval=None) + svi_obj, svi_state = ri.restore_svi_from_checkpoint( + checkpoint_file, init_params=init_params + ) ri.get_posteriors(svi=svi_obj, svi_state=svi_state, out_prefix=ri_prefix, diff --git a/src/tfscreen/util/__init__.py b/src/tfscreen/util/__init__.py index f920d19e..e33eba31 100644 --- a/src/tfscreen/util/__init__.py +++ b/src/tfscreen/util/__init__.py @@ -1,6 +1,7 @@ from .validation import ( # noqa: F401 - check_number + check_number, + check_unknown_keys, ) from .io import ( # noqa: F401 diff --git a/src/tfscreen/util/validation/__init__.py b/src/tfscreen/util/validation/__init__.py index fc84a13b..033e15ca 100644 --- a/src/tfscreen/util/validation/__init__.py +++ b/src/tfscreen/util/validation/__init__.py @@ -1,2 +1,3 @@ from .check import check_number # noqa: F401 +from .check import check_unknown_keys # noqa: F401 diff --git a/src/tfscreen/util/validation/check.py b/src/tfscreen/util/validation/check.py index 92e776f5..fc597640 100644 --- a/src/tfscreen/util/validation/check.py +++ b/src/tfscreen/util/validation/check.py @@ -4,6 +4,36 @@ # Define a generic Numeric type for hinting _Numeric = TypeVar("_Numeric", int, float) + +def check_unknown_keys( + config: dict, + allowed_keys: frozenset, + label: str = "config", +) -> None: + """ + Raise ValueError if config contains any keys not in allowed_keys. + + Parameters + ---------- + config : dict + The configuration dictionary to validate. + allowed_keys : frozenset + The complete set of recognized top-level keys. + label : str + Human-readable name for the config type, used in the error message. + + Raises + ------ + ValueError + If any key in config is not in allowed_keys, listing all unknown keys. + """ + unknown = sorted(set(config) - allowed_keys) + if unknown: + raise ValueError( + f"Unrecognized key(s) in {label}: {unknown}.\n" + f"Valid keys are: {sorted(allowed_keys)}." + ) + def check_number( value: Any, param_name: Optional[str] = None, diff --git a/tests/tfscreen/simulate/test_generate_presplit_data.py b/tests/tfscreen/simulate/test_generate_presplit_data.py new file mode 100644 index 00000000..8d3693f3 --- /dev/null +++ b/tests/tfscreen/simulate/test_generate_presplit_data.py @@ -0,0 +1,247 @@ +""" +Tests for _generate_presplit_data in run_simulation_cli. + +Coverage: + - Output columns and shape for a simple two-genotype, two-condition case + - Genotypes absent from the transformation pool (ln_cfu_0 = -inf) → NaN ln_cfu + - extra_noise parameter inflates ln_cfu_std + - Output is deterministic given the same rng seed + - run_simulation_from_config writes a presplit CSV when presplit_data is in cf +""" +import pytest +import numpy as np +import pandas as pd +from unittest.mock import patch, MagicMock + +from tfscreen.simulate.scripts.run_simulation_cli import _generate_presplit_data + + +# --------------------------------------------------------------------------- +# Minimal fixtures +# --------------------------------------------------------------------------- + +def _make_inputs( + replicates=(1, 2), + condition_pres=("kanR", "pheS"), + genotypes=("wt", "A1V", "A2V"), + ln_cfu_0_val=10.0, +): + """Build minimal combined_sample_df and combined_counts_df.""" + sample_rows = [] + counts_rows = [] + sample_id = 0 + + for rep in replicates: + for cp in condition_pres: + # Two selection samples per (rep, cp) (different t_sel) + for t_sel in (60.0, 90.0): + sample_rows.append( + {"sample": sample_id, "replicate": rep, + "condition_pre": cp, "t_sel": t_sel, + "sample_cfu": 1e8, "sample_cfu_std": 5e6} + ) + for geno in genotypes: + counts_rows.append( + {"sample": sample_id, "genotype": geno, + "ln_cfu_0": ln_cfu_0_val, "counts": 100} + ) + sample_id += 1 + + sample_df = pd.DataFrame(sample_rows).set_index("sample") + counts_df = pd.DataFrame(counts_rows) + return sample_df, counts_df + + +def _minimal_cf(noise=0.0, extra_keys=None): + cf = { + "cfu0": 1e8, + "total_num_reads": 30_000_000, + "final_cfu_pct_err": 0.05, + "prob_index_hop": None, + "presplit_data": {"noise": noise}, + } + if extra_keys: + cf.update(extra_keys) + return cf + + +# --------------------------------------------------------------------------- +# Basic output structure +# --------------------------------------------------------------------------- + +def test_output_columns(): + sample_df, counts_df = _make_inputs() + result = _generate_presplit_data(sample_df, counts_df, _minimal_cf(), + np.random.default_rng(0)) + for col in ["replicate", "condition_pre", "genotype", "ln_cfu", "ln_cfu_std", + "ln_cfu_0_true"]: + assert col in result.columns, f"missing column: {col}" + + +def test_output_row_count(): + """One row per (replicate, condition_pre, genotype).""" + replicates = (1, 2) + cps = ("kanR", "pheS") + genos = ("wt", "A1V", "A2V") + sample_df, counts_df = _make_inputs(replicates=replicates, + condition_pres=cps, + genotypes=genos) + result = _generate_presplit_data(sample_df, counts_df, _minimal_cf(), + np.random.default_rng(0)) + expected = len(replicates) * len(cps) * len(genos) + assert len(result) == expected + + +def test_ln_cfu_is_finite_for_present_genotypes(): + sample_df, counts_df = _make_inputs() + result = _generate_presplit_data(sample_df, counts_df, _minimal_cf(), + np.random.default_rng(0)) + # All genotypes have finite ln_cfu_0 so ln_cfu should be finite + assert result["ln_cfu"].notna().all() + + +def test_ln_cfu_std_is_positive(): + sample_df, counts_df = _make_inputs() + result = _generate_presplit_data(sample_df, counts_df, _minimal_cf(), + np.random.default_rng(0)) + assert (result["ln_cfu_std"] > 0).all() + + +# --------------------------------------------------------------------------- +# Absent genotypes (ln_cfu_0 = -inf) +# --------------------------------------------------------------------------- + +def test_absent_genotype_lower_lncfu(): + """A genotype with ln_cfu_0 = -inf (never transformed) gets zero counts + from the multinomial draw. After applying the pseudocount it still gets a + finite (but very low) ln_cfu estimate — the same behaviour as + counts_to_lncfu — and its ln_cfu is substantially lower than a present + genotype.""" + sample_df, counts_df = _make_inputs() + # Set one genotype's ln_cfu_0 to -inf across all samples + counts_df.loc[counts_df["genotype"] == "A2V", "ln_cfu_0"] = -np.inf + result = _generate_presplit_data(sample_df, counts_df, _minimal_cf(), + np.random.default_rng(0)) + absent_ln = result.loc[result["genotype"] == "A2V", "ln_cfu"].mean() + present_ln = result.loc[result["genotype"] == "wt", "ln_cfu"].mean() + # Absent genotype should be much lower than a normally-present genotype + assert absent_ln < present_ln - 5.0 + + +# --------------------------------------------------------------------------- +# extra_noise parameter +# --------------------------------------------------------------------------- + +def test_extra_noise_inflates_ln_cfu_std(): + """Adding extra noise should increase the mean ln_cfu_std.""" + sample_df, counts_df = _make_inputs() + rng0 = np.random.default_rng(42) + rng1 = np.random.default_rng(42) + + result_no_noise = _generate_presplit_data(sample_df, counts_df, + _minimal_cf(noise=0.0), rng0) + result_with_noise = _generate_presplit_data(sample_df, counts_df, + _minimal_cf(noise=0.5), rng1) + + mean_std_no_noise = result_no_noise["ln_cfu_std"].mean() + mean_std_with_noise = result_with_noise["ln_cfu_std"].mean() + assert mean_std_with_noise > mean_std_no_noise + + +# --------------------------------------------------------------------------- +# Reproducibility +# --------------------------------------------------------------------------- + +def test_reproducibility(): + """Same rng seed → identical output.""" + sample_df, counts_df = _make_inputs() + r1 = _generate_presplit_data(sample_df.copy(), counts_df.copy(), _minimal_cf(), + np.random.default_rng(7)) + r2 = _generate_presplit_data(sample_df.copy(), counts_df.copy(), _minimal_cf(), + np.random.default_rng(7)) + pd.testing.assert_frame_equal(r1.reset_index(drop=True), + r2.reset_index(drop=True)) + + +# --------------------------------------------------------------------------- +# Integration: run_simulation_from_config writes presplit CSV +# --------------------------------------------------------------------------- + +def test_run_simulation_writes_presplit_csv(tmp_path): + """presplit CSV is written when presplit_data block is in the config.""" + from tfscreen.simulate.scripts.run_simulation_cli import run_simulation_from_config + + lib_df = pd.DataFrame({"genotype": ["wt", "A1V"]}) + pheno_df = pd.DataFrame({"genotype": ["wt", "A1V"]}) + theta_df = pd.DataFrame({"genotype": ["wt", "A1V"]}) + params_df = pd.DataFrame({"genotype": ["wt", "A1V"], + "dk_geno": [0.0, 0.0], "activity": [1.0, 1.0]}) + + # Sample/counts for one (replicate, condition_pre, t_sel) combo + # Real _simulate_library_group returns sample_df with "sample" as a + # regular column and an unnamed integer index. + sample_df = pd.DataFrame([{ + "sample": 0, "replicate": 1, "condition_pre": "kanR", + "t_sel": 60.0, "sample_cfu": 1e8, "sample_cfu_std": 5e6, + }], index=[0]) + counts_df = pd.DataFrame([ + {"sample": 0, "genotype": "wt", "counts": 500, "ln_cfu_0": 10.0}, + {"sample": 0, "genotype": "A1V", "counts": 500, "ln_cfu_0": 10.0}, + ]) + growth_df = pd.DataFrame({"genotype": ["wt", "A1V"], "ln_cfu": [10.0, 9.9]}) + + cf = { + "random_seed": 1, + "cfu0": 1e8, + "total_num_reads": 10_000_000, + "final_cfu_pct_err": 0.05, + "prob_index_hop": None, + "presplit_data": {"noise": 0.0}, + } + + with patch("tfscreen.util.read_yaml", return_value=cf), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + return_value=(sample_df, counts_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + return_value=growth_df): + run_simulation_from_config("fake_config.yaml", str(tmp_path)) + + presplit_path = tmp_path / "tfs_sim_presplit.csv" + assert presplit_path.exists(), "presplit CSV was not written" + presplit_df = pd.read_csv(presplit_path) + for col in ["replicate", "condition_pre", "genotype", "ln_cfu", "ln_cfu_std"]: + assert col in presplit_df.columns + + +def test_run_simulation_no_presplit_without_config(tmp_path): + """presplit CSV is NOT written when presplit_data is absent from config.""" + from tfscreen.simulate.scripts.run_simulation_cli import run_simulation_from_config + + lib_df = pd.DataFrame({"genotype": ["wt"]}) + pheno_df = pd.DataFrame({"genotype": ["wt"]}) + theta_df = pd.DataFrame({"genotype": ["wt"]}) + params_df = pd.DataFrame({"genotype": ["wt"], "dk_geno": [0.0], "activity": [1.0]}) + sample_df = pd.DataFrame([{"sample": 0, "replicate": 1, + "condition_pre": "kanR", "t_sel": 60.0, + "sample_cfu": 1e8, "sample_cfu_std": 5e6}], + index=[0]) + counts_df = pd.DataFrame([{"sample": 0, "genotype": "wt", + "counts": 1000, "ln_cfu_0": 10.0}]) + growth_df = pd.DataFrame({"genotype": ["wt"], "ln_cfu": [10.0]}) + + cf = {"random_seed": 1, "cfu0": 1e8, "total_num_reads": 1_000_000, + "final_cfu_pct_err": 0.05, "prob_index_hop": None} + + with patch("tfscreen.util.read_yaml", return_value=cf), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + return_value=(sample_df, counts_df)), \ + patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + return_value=growth_df): + run_simulation_from_config("fake_config.yaml", str(tmp_path)) + + presplit_path = tmp_path / "tfs_sim_presplit.csv" + assert not presplit_path.exists(), "presplit CSV should not be written without config block" diff --git a/tests/tfscreen/simulate/test_run_simulation_cli.py b/tests/tfscreen/simulate/test_run_simulation_cli.py index 2abe93ff..17977a54 100644 --- a/tests/tfscreen/simulate/test_run_simulation_cli.py +++ b/tests/tfscreen/simulate/test_run_simulation_cli.py @@ -32,7 +32,7 @@ def test_seed_cli_parsed_as_int(): captured = {} def fake_run(config_file, output_dir, output_prefix="tfs_sim_", - num_replicates=1, seed=None): + num_replicates=2, seed=None): captured["seed"] = seed generalized_main( @@ -49,7 +49,7 @@ def test_seed_cli_defaults_to_none(): captured = {} def fake_run(config_file, output_dir, output_prefix="tfs_sim_", - num_replicates=1, seed=None): + num_replicates=2, seed=None): captured["seed"] = seed generalized_main( diff --git a/tests/tfscreen/simulate/test_selection_experiment.py b/tests/tfscreen/simulate/test_selection_experiment.py index db1468d2..d50b07ad 100644 --- a/tests/tfscreen/simulate/test_selection_experiment.py +++ b/tests/tfscreen/simulate/test_selection_experiment.py @@ -19,6 +19,7 @@ _sim_transform_and_mix, _sim_growth, MULTI_PLASMID_COMBINE_FCNS, + SIMULATE_KNOWN_KEYS, _sim_sequencing, _calc_genotype_cfu0, _compute_kt, @@ -1271,4 +1272,43 @@ def test_selection_experiment_end_to_end(mocker, base_config: dict, assert counts_df.shape[0] == base_phenotype_df.shape[0] total_reads = base_config["total_num_reads"] - assert np.isclose(counts_df["counts"].sum(), total_reads, rtol=0.01) \ No newline at end of file + assert np.isclose(counts_df["counts"].sum(), total_reads, rtol=0.01) + + +# ---------------------------------------------------------------------------- +# test SIMULATE_KNOWN_KEYS / unknown-key validation in _check_cf +# ---------------------------------------------------------------------------- + +def test_simulate_known_keys_is_frozenset(): + assert isinstance(SIMULATE_KNOWN_KEYS, frozenset) + assert len(SIMULATE_KNOWN_KEYS) > 0 + + +def test_check_cf_unknown_key_raises(base_config: dict): + bad = dict(base_config) + bad["not_a_real_key"] = 42 + with pytest.raises(ValueError, match="not_a_real_key"): + _check_cf(bad) + + +def test_check_cf_unknown_key_error_mentions_label(base_config: dict): + bad = dict(base_config) + bad["typo_key"] = "oops" + with pytest.raises(ValueError, match="simulate config"): + _check_cf(bad) + + +def test_check_cf_multiple_unknown_keys(base_config: dict): + bad = dict(base_config) + bad["key_one"] = 1 + bad["key_two"] = 2 + with pytest.raises(ValueError) as exc_info: + _check_cf(bad) + msg = str(exc_info.value) + assert "key_one" in msg + assert "key_two" in msg + + +def test_check_cf_all_known_keys_accepted(base_config: dict): + # Every key in the fixture must already be a known key; no error should be raised. + _check_cf(base_config) \ No newline at end of file diff --git a/tests/tfscreen/tfmodel/scripts/test_fit_model_cli.py b/tests/tfscreen/tfmodel/scripts/test_fit_model_cli.py index a64c810c..fcb70140 100644 --- a/tests/tfscreen/tfmodel/scripts/test_fit_model_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_fit_model_cli.py @@ -8,6 +8,7 @@ from tfscreen.tfmodel.scripts.fit_model_cli import ( fit_model, + _run_svi, ) @@ -235,3 +236,43 @@ def test_svi_epoch_checkpoint_interval_default_is_1000(self, tmp_path, mocker): kwargs = run_svi_mock.call_args.kwargs assert kwargs["epoch_checkpoint_interval"] == 1000 + + +# --------------------------------------------------------------------------- +# _run_svi — convergence message suppression when max_num_epochs=0 +# --------------------------------------------------------------------------- + +class TestRunSviConvergenceMessage: + """_run_svi must not print a converged/not-converged message when + max_num_epochs=0 (checkpoint-restore mode used by tfs-sample-posterior).""" + + def _make_ri(self): + ri = MagicMock() + ri._iterations_per_epoch = 1 + ri.setup_svi.return_value = MagicMock() + # run_optimization returns (state, params, converged=False) + ri.run_optimization.return_value = (MagicMock(), {}, False) + return ri + + def test_no_message_when_zero_epochs(self, capsys): + ri = self._make_ri() + _run_svi(ri, init_params={}, checkpoint_file=None, + out_prefix="tfs", max_num_epochs=0) + out = capsys.readouterr().out + assert "converged" not in out.lower() + + def test_converged_message_when_epochs_run_and_converged(self, capsys): + ri = self._make_ri() + ri.run_optimization.return_value = (MagicMock(), {}, True) + _run_svi(ri, init_params={}, checkpoint_file=None, + out_prefix="tfs", max_num_epochs=10) + out = capsys.readouterr().out + assert "SVI run converged." in out + + def test_not_converged_message_when_epochs_run_and_not_converged(self, capsys): + ri = self._make_ri() + ri.run_optimization.return_value = (MagicMock(), {}, False) + _run_svi(ri, init_params={}, checkpoint_file=None, + out_prefix="tfs", max_num_epochs=10) + out = capsys.readouterr().out + assert "SVI run has not yet converged." in out diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py index 09871fde..21474eea 100644 --- a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py +++ b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py @@ -760,13 +760,25 @@ def test_pins_activity_hyperparams_to_prior_locs(self): for suffix, _ in _PINNED_COMPONENTS["activity"]: assert suffix in pinned - def test_pins_dk_geno_and_ln_cfu0(self): + def test_pins_ln_cfu0_hyperparams(self): gm_cal, gm_prod = self._make_models() _inject_calibration_priors(gm_cal, gm_prod, np.array([[0.5]])) - for comp in ("dk_geno", "ln_cfu0"): - pinned = getattr(gm_cal._priors.growth, comp).pinned - for suffix, _ in _PINNED_COMPONENTS[comp]: - assert suffix in pinned + pinned = gm_cal._priors.growth.ln_cfu0.pinned + for suffix, _ in _PINNED_COMPONENTS["ln_cfu0"]: + assert suffix in pinned + + def test_dk_geno_not_in_pinned_components(self): + # dk_geno uses the fixed component during calibration (all zeros). + # Pinning hyperparameters is meaningless for a fixed component, so + # dk_geno must not appear in _PINNED_COMPONENTS. + assert "dk_geno" not in _PINNED_COMPONENTS + + def test_dk_geno_not_pinned_after_inject(self): + # _inject_calibration_priors must leave dk_geno.pinned untouched + # (empty) because the fixed component has no hyperparameters. + gm_cal, gm_prod = self._make_models() + _inject_calibration_priors(gm_cal, gm_prod, np.array([[0.5]])) + assert gm_cal._priors.growth.dk_geno.pinned == {} def test_theta_values_are_set(self): gm_cal, gm_prod = self._make_models() @@ -1239,6 +1251,25 @@ def test_pdf_names_use_out_prefix_and_genotype(self, tmp_path, mocker): assert "myrun_calib_geno0.pdf" in pdf_names assert "myrun_calib_geno1.pdf" in pdf_names + def test_slash_in_genotype_name_replaced_with_dash(self, tmp_path, mocker): + """Genotype names containing '/' (multi-mutation notation) must have '/' + replaced with '-' so the output path is a valid filename.""" + gm_cal, map_samples = _build_fake_gm_cal(n_geno=2) + # Override genotype labels so one contains a slash. + gm_cal.growth_tm.tensor_dim_labels[ + gm_cal.growth_tm.tensor_dim_names.index("genotype") + ] = ["A1T/G2P", "wt"] + self._patch_predictive(mocker, map_samples) + + _make_calibration_plots(gm_cal, params={}, + out_prefix=str(tmp_path / "run")) + + pdf_names = {p.name for p in tmp_path.glob("*.pdf")} + assert "run_calib_A1T-G2P.pdf" in pdf_names + assert "run_calib_wt.pdf" in pdf_names + # The original slash form must not appear as a file. + assert not any("A1T/G2P" in name for name in pdf_names) + def test_skips_genotype_with_no_valid_observations(self, tmp_path, mocker): gm_cal, map_samples = _build_fake_gm_cal(n_geno=2) diff --git a/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py b/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py index c47b3b1d..fe653793 100644 --- a/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py @@ -180,16 +180,13 @@ def test_map_detection_requires_auto_loc_key(self, tmp_path): ckpt_path = str(tmp_path / "svi.pkl") open(ckpt_path, "w").close() - fake_svi_obj = MagicMock() - fake_svi_state = MagicMock() + ri.restore_svi_from_checkpoint.return_value = (MagicMock(), MagicMock()) with patch("tfscreen.tfmodel.scripts.sample_posterior_cli.read_configuration", return_value=(MagicMock(), {})), \ patch("tfscreen.tfmodel.scripts.sample_posterior_cli.RunInference", return_value=ri), \ - patch("tfscreen.tfmodel.scripts.sample_posterior_cli.dill") as mock_dill, \ - patch("tfscreen.tfmodel.scripts.sample_posterior_cli._run_svi", - return_value=(fake_svi_obj, fake_svi_state, {}, False)) as mock_svi: + patch("tfscreen.tfmodel.scripts.sample_posterior_cli.dill") as mock_dill: mock_dill.load.return_value = {"svi_state": MagicMock()} ri.get_posteriors.side_effect = lambda **kw: open(h5_src, "w").close() @@ -199,7 +196,7 @@ def test_map_detection_requires_auto_loc_key(self, tmp_path): out_prefix=str(tmp_path / "out")) ri.get_laplace_posteriors.assert_not_called() - mock_svi.assert_called_once() + ri.get_posteriors.assert_called_once() # --------------------------------------------------------------------------- @@ -210,9 +207,9 @@ class TestSamplePosteriorSvi: def _run_svi_test(self, tmp_path): ri = MagicMock() - fake_svi_state = MagicMock() fake_svi_obj = MagicMock() - ri.setup_svi.return_value = fake_svi_obj + fake_svi_state = MagicMock() + ri.restore_svi_from_checkpoint.return_value = (fake_svi_obj, fake_svi_state) h5_src = str(tmp_path / "out_tmp_posterior_posterior.h5") ckpt_path = str(tmp_path / "svi.pkl") @@ -220,11 +217,6 @@ def _run_svi_test(self, tmp_path): captured = {} - def fake_run_svi(ri_obj, init_params, checkpoint_file, out_prefix, - max_num_epochs, **kwargs): - captured["max_num_epochs"] = max_num_epochs - return fake_svi_obj, fake_svi_state, {}, False - def fake_get_posteriors(**kwargs): captured["get_posteriors_called"] = True captured["get_posteriors_kwargs"] = kwargs @@ -236,9 +228,7 @@ def fake_get_posteriors(**kwargs): return_value=(MagicMock(), {})), \ patch("tfscreen.tfmodel.scripts.sample_posterior_cli.RunInference", return_value=ri), \ - patch("tfscreen.tfmodel.scripts.sample_posterior_cli.dill") as mock_dill, \ - patch("tfscreen.tfmodel.scripts.sample_posterior_cli._run_svi", - side_effect=fake_run_svi): + patch("tfscreen.tfmodel.scripts.sample_posterior_cli.dill") as mock_dill: mock_dill.load.return_value = {"svi_state": MagicMock()} @@ -246,14 +236,14 @@ def fake_get_posteriors(**kwargs): sample_posterior("cfg.yaml", ckpt_path, out_prefix=str(tmp_path / "out")) - return captured + return ri, captured - def test_svi_checkpoint_calls_run_svi_with_zero_epochs(self, tmp_path): - captured = self._run_svi_test(tmp_path) - assert captured["max_num_epochs"] == 0 + def test_svi_calls_restore_svi_from_checkpoint(self, tmp_path): + ri, _ = self._run_svi_test(tmp_path) + ri.restore_svi_from_checkpoint.assert_called_once() - def test_svi_calls_get_posteriors_directly(self, tmp_path): - captured = self._run_svi_test(tmp_path) + def test_svi_calls_get_posteriors(self, tmp_path): + _, captured = self._run_svi_test(tmp_path) assert captured.get("get_posteriors_called") is True def test_svi_output_renamed(self, tmp_path): diff --git a/tests/tfscreen/tfmodel/test_configure_and_run.py b/tests/tfscreen/tfmodel/test_configure_and_run.py index b48a0604..6b4bc942 100644 --- a/tests/tfscreen/tfmodel/test_configure_and_run.py +++ b/tests/tfscreen/tfmodel/test_configure_and_run.py @@ -14,7 +14,8 @@ from tfscreen.tfmodel.scripts.fit_model_cli import fit_model from tfscreen.tfmodel.configuration_io import ( read_configuration, - _update_dataclass + _update_dataclass, + TFMODEL_KNOWN_KEYS, ) @pytest.fixture @@ -175,6 +176,45 @@ def test_read_configuration_errors(tmpdir): with pytest.raises(FileNotFoundError, match="Guesses file not found"): read_configuration(config_path) + +# ---------------------------------------------------------------------------- +# test TFMODEL_KNOWN_KEYS / unknown-key validation in read_configuration +# ---------------------------------------------------------------------------- + +def test_tfmodel_known_keys_is_frozenset(): + assert isinstance(TFMODEL_KNOWN_KEYS, frozenset) + assert "data" in TFMODEL_KNOWN_KEYS + assert "components" in TFMODEL_KNOWN_KEYS + + +def test_read_configuration_unknown_key_raises(tmpdir): + config_path = os.path.join(tmpdir, "bad_config.yaml") + with open(config_path, "w") as f: + yaml.dump({ + "data": {"growth": "g"}, + "components": {}, + "priors_file": "priors.csv", + "guesses_file": "guesses.csv", + "not_a_real_key": 42, + }, f) + with pytest.raises(ValueError, match="not_a_real_key"): + read_configuration(config_path) + + +def test_read_configuration_unknown_key_error_mentions_label(tmpdir): + config_path = os.path.join(tmpdir, "bad_config.yaml") + with open(config_path, "w") as f: + yaml.dump({ + "data": {"growth": "g"}, + "components": {}, + "priors_file": "priors.csv", + "guesses_file": "guesses.csv", + "typo_key": "oops", + }, f) + with pytest.raises(ValueError, match="tfmodel config"): + read_configuration(config_path) + + def test_configure_model_errors(tmpdir): # Missing binding_df with pytest.raises(ValueError, match="binding_df must be provided"): diff --git a/tests/tfscreen/tfmodel/test_model_orchestrator.py b/tests/tfscreen/tfmodel/test_model_orchestrator.py index 9844bf50..6560f877 100644 --- a/tests/tfscreen/tfmodel/test_model_orchestrator.py +++ b/tests/tfscreen/tfmodel/test_model_orchestrator.py @@ -143,25 +143,58 @@ def test_read_binding_df(mocker): res = _read_binding_df("path.csv", growth_df=growth_df) assert "theta_obs" in res.columns -def test_read_binding_df_errors(mocker): +def test_read_binding_df_missing_col_error(mocker): growth_df = pd.DataFrame({"genotype": ["A"], "titrant_name": ["T1"]}) binding_df = pd.DataFrame({ - "genotype": ["B"], "titrant_name": ["T1"], + "genotype": ["A"], "titrant_name": ["T1"], "theta_obs": [0.5], "theta_std": [0.1], "titrant_conc": [1.0] }) mocker.patch("tfscreen.util.io.read_dataframe", return_value=binding_df) mocker.patch("tfscreen.genetics.set_categorical_genotype", return_value=binding_df) - mocker.patch("tfscreen.util.dataframe.check_columns") - - with patch("pandas.Index.isin", return_value=np.array([False])): - with pytest.raises(ValueError, match="not seen"): - _read_binding_df("path.csv", growth_df=growth_df) - mocker.patch("tfscreen.util.dataframe.check_columns", side_effect=ValueError("missing col")) with pytest.raises(ValueError, match="missing col"): _read_binding_df("path.csv", growth_df=growth_df) +def test_read_binding_df_extra_pairs_dropped_with_warning(): + """binding rows whose (genotype, titrant_name) is absent from growth_df are dropped.""" + from tfscreen.genetics import set_categorical_genotype + import tfscreen.util.dataframe + + growth_df = pd.DataFrame({ + "genotype": ["wt", "wt"], + "titrant_name": ["iptg", "iptg"], + "titrant_conc": [0.0, 1.0], + "ln_cfu": [10.0, 10.0], + "ln_cfu_std": [0.1, 0.1], + "t_pre": [1.0, 1.0], + "t_sel": [1.0, 1.0], + "replicate": [1, 1], + "condition_pre": ["kan", "kan"], + "condition_sel": ["kan", "kan"], + }) + growth_df = set_categorical_genotype(growth_df, standardize=True) + growth_df = tfscreen.util.dataframe.add_group_columns( + growth_df, ["genotype", "titrant_name"], "map_theta_group" + ) + + # binding has "wt" (in growth) and "M42I" (not in growth) + binding_df = pd.DataFrame({ + "genotype": ["wt", "M42I"], + "titrant_name": ["iptg", "iptg"], + "titrant_conc": [1.0, 1.0], + "theta_obs": [0.1, 0.5], + "theta_std": [0.02, 0.02], + }) + + with pytest.warns(UserWarning, match="will be dropped"): + result = _read_binding_df(binding_df, growth_df=growth_df) + + # Only the "wt" row should survive + assert set(result["genotype"]) == {"wt"} + assert len(result) == 1 + + def test_read_binding_df_preserves_canonical_genotype_order(): # Regression test: the pd.merge inside add_group_columns converts the # categorical genotype column to object dtype. _read_binding_df must diff --git a/tests/tfscreen/tfmodel/test_presplit.py b/tests/tfscreen/tfmodel/test_presplit.py new file mode 100644 index 00000000..81f61841 --- /dev/null +++ b/tests/tfscreen/tfmodel/test_presplit.py @@ -0,0 +1,247 @@ +""" +Tests for the optional pre-split (t = -t_pre) data path. + +Coverage: + - _read_presplit_df: column checks, silent genotype dropping + - _build_presplit_tm: tensor shape/alignment with growth_tm + - ModelOrchestrator: presplit=None (no change), presplit passed in + - jax_model: presplit observation site fires when data.presplit is set +""" + +import pytest +import numpy as np +import pandas as pd +import jax.numpy as jnp +import numpyro +from numpyro.handlers import trace, seed + +from tfscreen.tfmodel.model_orchestrator import ( + _read_presplit_df, + _build_presplit_tm, + _read_growth_df, + _build_growth_tm, +) +from tfscreen.tfmodel.data_class import PreSplitData, DataClass +from tfscreen.tfmodel.tensors.populate_dataclass import populate_dataclass + + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def minimal_growth_df(): + """Minimal growth DataFrame with two genotypes, two replicates, two + condition_pre values — enough to exercise the presplit alignment logic.""" + rows = [] + for rep in [1, 2]: + for cp in ["kanR", "pheS"]: + for geno in ["wt", "A1V", "A2V"]: + for t_sel in [60.0, 90.0]: + rows.append({ + "replicate": rep, + "condition_pre": cp, + "condition_sel": cp, + "titrant_name": "iptg", + "titrant_conc": 0.0, + "t_pre": 30.0, + "t_sel": t_sel, + "genotype": geno, + "ln_cfu": 10.0, + "ln_cfu_std": 0.5, + }) + return pd.DataFrame(rows) + + +@pytest.fixture +def growth_tm(minimal_growth_df): + gdf = _read_growth_df(minimal_growth_df.copy()) + return _build_growth_tm(gdf) + + +@pytest.fixture +def minimal_presplit_df(): + """Pre-split data for all (replicate, condition_pre, genotype) combos.""" + rows = [] + for rep in [1, 2]: + for cp in ["kanR", "pheS"]: + for geno in ["wt", "A1V", "A2V"]: + rows.append({ + "replicate": rep, + "condition_pre": cp, + "genotype": geno, + "ln_cfu": 9.5, + "ln_cfu_std": 0.4, + }) + return pd.DataFrame(rows) + + +# --------------------------------------------------------------------------- +# _read_presplit_df +# --------------------------------------------------------------------------- + +def test_read_presplit_df_passes_through_valid(minimal_presplit_df, + minimal_growth_df): + gdf = _read_growth_df(minimal_growth_df.copy()) + result = _read_presplit_df(minimal_presplit_df, gdf) + assert len(result) == len(minimal_presplit_df) + for col in ["replicate", "condition_pre", "genotype", "ln_cfu", "ln_cfu_std"]: + assert col in result.columns + + +def test_read_presplit_df_drops_unknown_genotypes(minimal_presplit_df, + minimal_growth_df): + df_extra = minimal_presplit_df.copy() + extra_row = pd.DataFrame([{"replicate": 1, "condition_pre": "kanR", + "genotype": "A99V", + "ln_cfu": 5.0, "ln_cfu_std": 0.1}]) + df_extra = pd.concat([df_extra, extra_row], ignore_index=True) + + gdf = _read_growth_df(minimal_growth_df.copy()) + result = _read_presplit_df(df_extra, gdf) + + assert "A99V" not in result["genotype"].values + assert len(result) == len(minimal_presplit_df) + + +def test_read_presplit_df_missing_column_raises(minimal_growth_df): + bad_df = pd.DataFrame({ + "replicate": [1], "condition_pre": ["kanR"], + "genotype": ["wt"], "ln_cfu": [9.0] + # missing ln_cfu_std + }) + gdf = _read_growth_df(minimal_growth_df.copy()) + with pytest.raises(Exception): + _read_presplit_df(bad_df, gdf) + + +# --------------------------------------------------------------------------- +# _build_presplit_tm +# --------------------------------------------------------------------------- + +def test_build_presplit_tm_shape(minimal_presplit_df, minimal_growth_df, + growth_tm): + gdf = _read_growth_df(minimal_growth_df.copy()) + psdf = _read_presplit_df(minimal_presplit_df, gdf) + ps_tm = _build_presplit_tm(psdf, growth_tm) + + # shape should be (num_replicate, num_condition_pre, num_genotype) + assert ps_tm.tensor_shape == (2, 2, 3) + assert "ln_cfu" in ps_tm.tensors + assert "ln_cfu_std" in ps_tm.tensors + assert "good_mask" in ps_tm.tensors + + +def test_build_presplit_tm_genotype_alignment(minimal_presplit_df, + minimal_growth_df, growth_tm): + """Genotype ordering in presplit_tm must match growth_tm.""" + gdf = _read_growth_df(minimal_growth_df.copy()) + psdf = _read_presplit_df(minimal_presplit_df, gdf) + ps_tm = _build_presplit_tm(psdf, growth_tm) + + growth_geno_labels = growth_tm.tensor_dim_labels[ + growth_tm.tensor_dim_names.index("genotype") + ] + presplit_geno_labels = ps_tm.tensor_dim_labels[ + ps_tm.tensor_dim_names.index("genotype") + ] + assert list(growth_geno_labels) == list(presplit_geno_labels) + + +def test_build_presplit_tm_partial_coverage(minimal_growth_df, growth_tm): + """Genotypes absent from presplit_df get NaN (masked) in the tensor.""" + partial_df = pd.DataFrame([ + {"replicate": 1, "condition_pre": "kanR", "genotype": "wt", + "ln_cfu": 9.5, "ln_cfu_std": 0.4}, + ]) + gdf = _read_growth_df(minimal_growth_df.copy()) + psdf = _read_presplit_df(partial_df, gdf) + ps_tm = _build_presplit_tm(psdf, growth_tm) + + # Only one entry should be valid; the rest masked + assert ps_tm.tensors["good_mask"].sum() == 1 + + +# --------------------------------------------------------------------------- +# ModelOrchestrator integration (light smoke test, no JAX tracing) +# --------------------------------------------------------------------------- + +def test_model_orchestrator_without_presplit(minimal_growth_df, + minimal_presplit_df): + """Passing presplit_df=None leaves data.presplit as None.""" + from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator + binding_df = pd.DataFrame({ + "genotype": ["wt", "A1V", "A2V"], + "titrant_name": ["iptg"] * 3, + "titrant_conc": [0.0] * 3, + "theta_obs": [0.5] * 3, + "theta_std": [0.1] * 3, + }) + gm = ModelOrchestrator(minimal_growth_df, binding_df, presplit_df=None) + assert gm.data.presplit is None + + +def test_model_orchestrator_with_presplit(minimal_growth_df, + minimal_presplit_df): + """Passing presplit_df populates data.presplit as a PreSplitData.""" + from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator + binding_df = pd.DataFrame({ + "genotype": ["wt", "A1V", "A2V"], + "titrant_name": ["iptg"] * 3, + "titrant_conc": [0.0] * 3, + "theta_obs": [0.5] * 3, + "theta_std": [0.1] * 3, + }) + gm = ModelOrchestrator(minimal_growth_df, binding_df, + presplit_df=minimal_presplit_df) + ps = gm.data.presplit + assert ps is not None + assert isinstance(ps, PreSplitData) + assert ps.num_replicate == 2 + assert ps.num_condition_pre == 2 + assert ps.num_genotype == 3 + assert ps.ln_cfu_t0.shape == (2, 2, 3) + assert ps.good_mask.shape == (2, 2, 3) + # All entries should be valid (full coverage) + assert bool(jnp.all(ps.good_mask)) + + +# --------------------------------------------------------------------------- +# jax_model: presplit_obs site fires +# --------------------------------------------------------------------------- + +def test_jax_model_presplit_obs_site_present(minimal_growth_df, + minimal_presplit_df): + """The presplit_obs sample site should appear in the model trace when + data.presplit is not None.""" + from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator + binding_df = pd.DataFrame({ + "genotype": ["wt", "A1V", "A2V"], + "titrant_name": ["iptg"] * 3, + "titrant_conc": [0.0] * 3, + "theta_obs": [0.5] * 3, + "theta_std": [0.1] * 3, + }) + gm = ModelOrchestrator(minimal_growth_df, binding_df, + presplit_df=minimal_presplit_df) + + model_fn = gm.jax_model + tr = trace(seed(model_fn, 0)).get_trace(gm.data, gm.priors) + assert "presplit_obs" in tr + + +def test_jax_model_no_presplit_obs_site_without_data(minimal_growth_df): + """The presplit_obs site must NOT appear when presplit_df is not given.""" + from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator + binding_df = pd.DataFrame({ + "genotype": ["wt", "A1V", "A2V"], + "titrant_name": ["iptg"] * 3, + "titrant_conc": [0.0] * 3, + "theta_obs": [0.5] * 3, + "theta_std": [0.1] * 3, + }) + gm = ModelOrchestrator(minimal_growth_df, binding_df) + + model_fn = gm.jax_model + tr = trace(seed(model_fn, 0)).get_trace(gm.data, gm.priors) + assert "presplit_obs" not in tr diff --git a/tests/tfscreen/util/validation/test_check.py b/tests/tfscreen/util/validation/test_check.py index 97b12d19..1f73b171 100644 --- a/tests/tfscreen/util/validation/test_check.py +++ b/tests/tfscreen/util/validation/test_check.py @@ -1,5 +1,5 @@ import pytest -from tfscreen.util.validation.check import check_number +from tfscreen.util.validation.check import check_number, check_unknown_keys # ---------------------------------------------------------------------------- # test check_number @@ -39,4 +39,37 @@ def test_check_number_success(value, kwargs, expected): def test_check_number_failures(value, kwargs, error, match): """Tests that check_number correctly raises errors for invalid inputs.""" with pytest.raises(error, match=match): - check_number(value, **kwargs) \ No newline at end of file + check_number(value, **kwargs) + + +# ---------------------------------------------------------------------------- +# test check_unknown_keys +# ---------------------------------------------------------------------------- + +ALLOWED = frozenset({"alpha", "beta", "gamma"}) + + +def test_check_unknown_keys_all_valid(): + check_unknown_keys({"alpha": 1, "beta": 2}, ALLOWED) + + +def test_check_unknown_keys_empty_config(): + check_unknown_keys({}, ALLOWED) + + +def test_check_unknown_keys_single_unknown(): + with pytest.raises(ValueError, match="typo"): + check_unknown_keys({"alpha": 1, "typo": 99}, ALLOWED) + + +def test_check_unknown_keys_multiple_unknown(): + with pytest.raises(ValueError) as exc_info: + check_unknown_keys({"alpha": 1, "bad1": 2, "bad2": 3}, ALLOWED) + msg = str(exc_info.value) + assert "bad1" in msg + assert "bad2" in msg + + +def test_check_unknown_keys_label_in_message(): + with pytest.raises(ValueError, match="my_label"): + check_unknown_keys({"unknown": 1}, ALLOWED, label="my_label") \ No newline at end of file From 7fd6a9736150284a1af311aae63ae6dc25dec60a Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 7 Jun 2026 07:11:56 -0700 Subject: [PATCH 03/24] promoted geno trajectory plot out of calibration --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 225 ++++--- reports/junit/junit.xml | 2 +- src/tfscreen/plot/__init__.py | 4 + src/tfscreen/plot/geno_trajectory.py | 619 ++++++++++++++++++ src/tfscreen/simulate/library_prediction.py | 5 +- .../simulate/scripts/run_simulation_cli.py | 7 +- src/tfscreen/simulate/thermo_to_growth.py | 56 +- src/tfscreen/tfmodel/analysis/prediction.py | 85 ++- .../scripts/run_prefit_calibration_cli.py | 342 +--------- .../tfmodel/scripts/summarize_fit_cli.py | 36 +- tests/tfscreen/plot/test_geno_trajectory.py | 484 ++++++++++++++ .../simulate/test_thermo_to_growth.py | 24 +- .../scripts/test_run_prefit_calibration.py | 372 +---------- .../tfmodel/scripts/test_summarize_fit_cli.py | 52 +- tests/tfscreen/tfmodel/test_prediction.py | 137 +++- 17 files changed, 1619 insertions(+), 835 deletions(-) create mode 100644 src/tfscreen/plot/geno_trajectory.py create mode 100644 tests/tfscreen/plot/test_geno_trajectory.py diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 91c6cb22..a9795298 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.84%coverage95.84% \ No newline at end of file +coverage: 95.78%coverage95.78% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 3333b017..56cf0483 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3123tests3123 \ No newline at end of file +tests: 3143tests3143 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index d90d0691..fe53acc9 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -778,6 +778,36 @@ ./src/tfscreen/plot/est_v_real_summary.py:102:1: W293 blank line contains whitespace ./src/tfscreen/plot/est_v_real_summary.py:104:1: W293 blank line contains whitespace ./src/tfscreen/plot/est_v_real_summary.py:106:1: W391 blank line at end of file +./src/tfscreen/plot/geno_trajectory.py:88:14: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:93:14: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:107:16: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:172:11: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:173:11: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:192:1: C901 'plot_geno_trajectory' is too complex (32) +./src/tfscreen/plot/geno_trajectory.py:313:14: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:316:15: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:328:15: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:329:14: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:330:14: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:331:14: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:359:18: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:361:21: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:457:15: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:458:15: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:480:19: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:495:21: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:518:20: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:555:27: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:570:26: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:571:26: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:572:25: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:573:25: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:582:29: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:583:29: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:584:28: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:585:27: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:586:27: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:613:17: E221 multiple spaces before operator ./src/tfscreen/plot/heatmap/aa_v_res_heatmap.py:6:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/plot/heatmap/aa_v_res_heatmap.py:15:53: E231 missing whitespace after ':' ./src/tfscreen/plot/heatmap/aa_v_res_heatmap.py:16:53: E231 missing whitespace after ':' @@ -1480,8 +1510,7 @@ ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/thermo_to_growth.py:476:5: F841 local variable 'n_geno' is assigned to but never used -./src/tfscreen/simulate/thermo_to_growth.py:566:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/thermo_to_growth.py:582:41: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/__init__.py:2:73: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:3:20: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:18:65: W291 trailing whitespace @@ -1525,42 +1554,39 @@ ./src/tfscreen/tfmodel/analysis/prediction.py:40:73: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:77:71: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:98:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:105:1: C901 'predict' is too complex (25) ./src/tfscreen/tfmodel/analysis/prediction.py:105:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:166:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:177:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/analysis/prediction.py:179:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:192:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:194:18: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:194:18: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:203:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:207:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:216:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:228:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:231:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:236:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:239:67: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:240:76: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:244:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:251:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:260:58: E741 ambiguous variable name 'l' -./src/tfscreen/tfmodel/analysis/prediction.py:263:26: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:263:26: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:266:25: E122 continuation line missing indentation or outdented -./src/tfscreen/tfmodel/analysis/prediction.py:272:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:273:46: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:274:62: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:276:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:278:40: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:279:42: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:280:47: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:282:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:285:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:302:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:303:71: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:307:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:312:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:316:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:153:1: C901 'predict' is too complex (26) +./src/tfscreen/tfmodel/analysis/prediction.py:214:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:225:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/analysis/prediction.py:227:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:240:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:242:18: E111 indentation is not a multiple of 4 +./src/tfscreen/tfmodel/analysis/prediction.py:242:18: E117 over-indented +./src/tfscreen/tfmodel/analysis/prediction.py:295:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:298:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:303:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:306:67: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:307:76: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:311:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:318:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:327:58: E741 ambiguous variable name 'l' +./src/tfscreen/tfmodel/analysis/prediction.py:330:26: E111 indentation is not a multiple of 4 +./src/tfscreen/tfmodel/analysis/prediction.py:330:26: E117 over-indented +./src/tfscreen/tfmodel/analysis/prediction.py:333:25: E122 continuation line missing indentation or outdented +./src/tfscreen/tfmodel/analysis/prediction.py:339:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:340:46: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:341:62: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:343:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:345:40: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:346:42: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:347:47: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:349:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:352:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:369:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:370:71: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:374:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:379:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:383:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prior_predictive.py:104:33: E203 whitespace before ':' ./src/tfscreen/tfmodel/analysis/sbc.py:130:1: C901 'summarize_sbc' is too complex (17) ./src/tfscreen/tfmodel/configuration_io.py:19:1: E302 expected 2 blank lines, found 1 @@ -3634,33 +3660,17 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:251:24: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:252:24: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:253:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:256:1: C901 '_inject_calibration_priors' is too complex (19) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:438:1: C901 '_build_csv_updates' is too complex (13) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:540:1: C901 '_apply_guesses_updates' is too complex (12) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:695:1: C901 '_make_calibration_plots' is too complex (23) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:781:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:784:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:785:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:836:7: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:837:7: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:840:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:841:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:842:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:843:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:886:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:887:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:909:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:924:21: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:947:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:960:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:257:1: C901 '_inject_calibration_priors' is too complex (19) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:439:1: C901 '_build_csv_updates' is too complex (13) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:541:1: C901 '_apply_guesses_updates' is too complex (12) ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used ./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:43:1: C901 '_read_all_losses' is too complex (11) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:125:1: C901 'summarize_fit' is too complex (45) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:125:1: C901 'summarize_fit' is too complex (49) ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:47:30: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:76:1: C901 'summarize_grid' is too complex (14) ./src/tfscreen/tfmodel/tensors/batch.py:5:1: E302 expected 2 blank lines, found 1 @@ -4891,6 +4901,38 @@ ./tests/tfscreen/plot/test_est_v_real_summary.py:22:1: W293 blank line contains whitespace ./tests/tfscreen/plot/test_est_v_real_summary.py:25:1: W293 blank line contains whitespace ./tests/tfscreen/plot/test_est_v_real_summary.py:30:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/plot/test_geno_trajectory.py:21:1: F401 'json' imported but unused +./tests/tfscreen/plot/test_geno_trajectory.py:23:1: F401 'sys' imported but unused +./tests/tfscreen/plot/test_geno_trajectory.py:24:1: F401 'tempfile' imported but unused +./tests/tfscreen/plot/test_geno_trajectory.py:43:11: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:44:10: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:45:9: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:46:9: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:47:9: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:48:8: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:75:17: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:76:13: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:77:13: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:78:14: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:85:21: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:87:21: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:91:16: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:92:18: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:119:24: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:123:25: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:137:12: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:138:17: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:147:16: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:148:16: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:152:14: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:337:21: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:338:23: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:342:24: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:343:25: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:413:55: E127 continuation line over-indented for visual indent +./tests/tfscreen/plot/test_geno_trajectory.py:418:55: E127 continuation line over-indented for visual indent +./tests/tfscreen/plot/test_geno_trajectory.py:428:24: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:429:24: E221 multiple spaces before operator ./tests/tfscreen/plot/test_helper.py:3:1: F401 'pytest' imported but unused ./tests/tfscreen/plot/test_helper.py:6:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/plot/test_helper.py:9:1: W293 blank line contains whitespace @@ -5351,8 +5393,8 @@ ./tests/tfscreen/simulate/test_thermo_to_growth.py:173:14: E131 continuation line unaligned for hanging indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:200:44: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:207:35: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:1058:1: E402 module level import not at top of file -./tests/tfscreen/simulate/test_thermo_to_growth.py:1146:9: F841 local variable 'G_out' is assigned to but never used +./tests/tfscreen/simulate/test_thermo_to_growth.py:1064:1: E402 module level import not at top of file +./tests/tfscreen/simulate/test_thermo_to_growth.py:1152:9: F841 local variable 'G_out' is assigned to but never used ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 @@ -6470,33 +6512,12 @@ ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.run_prefit_calibration_cli._identify_field_mapping' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:295:1: E303 too many blank lines (4) ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:299:1: E302 expected 2 blank lines, found 4 -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1015:67: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1051:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1070:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1085:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1105:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1111:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1112:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1115:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1116:13: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1117:13: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1118:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1122:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1126:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1130:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1131:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1132:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1133:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1160:24: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1162:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1179:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1180:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1187:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1188:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1671:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1697:63: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1736:38: E702 multiple statements on one line (semicolon) -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1755:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1353:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1379:63: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1435:20: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:84:31: E741 ambiguous variable name 'l' ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:598:28: E127 continuation line over-indented for visual indent @@ -7105,18 +7126,15 @@ ./tests/tfscreen/tfmodel/test_posteriors.py:101:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_posteriors.py:108:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_posteriors.py:138:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:3:1: F401 'numpy as np' imported but unused -./tests/tfscreen/tfmodel/test_prediction.py:7:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:33:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:42:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:64:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:68:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_prediction.py:71:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_prediction.py:75:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:85:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_prediction.py:89:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_prediction.py:91:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_prediction.py:11:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:37:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:46:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:58:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:72:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_prediction.py:75:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_prediction.py:79:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:89:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_presplit.py:12:1: F401 'numpy as np' imported but unused ./tests/tfscreen/tfmodel/test_presplit.py:15:1: F401 'numpyro' imported but unused ./tests/tfscreen/tfmodel/test_presplit.py:24:1: F401 'tfscreen.tfmodel.data_class.DataClass' imported but unused @@ -7638,14 +7656,14 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -166 E127 continuation line over-indented for visual indent +167 E127 continuation line over-indented for visual indent 68 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1741 E221 multiple spaces before operator +1767 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1115 E231 missing whitespace after ',' @@ -7655,7 +7673,7 @@ 1 E262 inline comment should start with '# ' 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment -150 E272 multiple spaces before keyword +146 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 802 E302 expected 2 blank lines, found 1 51 E303 too many blank lines (2) @@ -7665,18 +7683,17 @@ 33 E402 module level import not at top of file 70 E501 line too long (145 > 127 characters) 46 E701 multiple statements on one line (colon) -1 E702 multiple statements on one line (semicolon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' 2 E713 test for membership should be 'not in' 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 12 E741 ambiguous variable name 'l' -220 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +222 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 -41 F841 local variable 'e' is assigned to but never used +40 F841 local variable 'e' is assigned to but never used 688 W291 trailing whitespace 86 W292 no newline at end of file -1705 W293 blank line contains whitespace +1700 W293 blank line contains whitespace 29 W391 blank line at end of file -7632 +7650 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index da5f13e1..60da105d 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/plot/__init__.py b/src/tfscreen/plot/__init__.py index 1a657d60..8a63e847 100644 --- a/src/tfscreen/plot/__init__.py +++ b/src/tfscreen/plot/__init__.py @@ -42,3 +42,7 @@ from .heatmap import ( # noqa: F401 heatmap ) + +from .geno_trajectory import ( # noqa: F401 + plot_geno_trajectory, +) diff --git a/src/tfscreen/plot/geno_trajectory.py b/src/tfscreen/plot/geno_trajectory.py new file mode 100644 index 00000000..90d4e29d --- /dev/null +++ b/src/tfscreen/plot/geno_trajectory.py @@ -0,0 +1,619 @@ +""" +Public function for plotting per-genotype growth trajectory fits overlaid +on experimental data. + +The core value of :func:`plot_geno_trajectory` is the interpolative step: +it runs the forward model on a fine time grid to produce smooth trajectory +curves that can be compared visually against the raw ln_cfu observations. +Simpler downstream diagnostics (correlation plots, goodness-of-fit statistics) +are intentionally left to the caller so they can be combined or skipped as +needed. +""" + +import sys + +import numpy as np +import pandas as pd +import jax +import jax.numpy as jnp + +# Number of equally-spaced time points used for the fine-grid selection-phase +# trajectory in the MAP prediction path. +_T_FINE = 50 + + +# --------------------------------------------------------------------------- +# Prediction helpers +# --------------------------------------------------------------------------- + +def _compute_map_predictions(gm, params): + """ + Run the forward model at the MAP point and return prediction tensors. + + Two forward passes are performed: one at the observed timepoints and one + on a :data:`_T_FINE`-point fine grid spanning ``[0, global_max_t_sel]``. + The fine-grid pass is used to draw smooth selection-phase trajectory curves. + + Parameters + ---------- + gm : ModelOrchestrator + Fitted model orchestrator. + params : dict + MAP parameter dict with ``{site}_auto_loc`` keys (unconstrained space). + + Returns + ------- + growth_pred : np.ndarray + Shape ``(R, T, CP, CS, TN, TC, G)`` — predictions at observed timepoints. + ln_cfu0_map : np.ndarray or None + Shape ``(R, CP, G)`` — MAP ``ln_cfu0`` deterministic site, or ``None`` + if absent from the trace. + growth_pred_fine : np.ndarray + Shape ``(R, T_FINE, CP, CS, TN, TC, G)`` — predictions on the fine grid. + t_fine_1d : np.ndarray + Shape ``(T_FINE,)`` — time values for ``growth_pred_fine``. + + Raises + ------ + ValueError + If ``growth_pred`` is not present in the model trace. + """ + from numpyro.infer import Predictive + from numpyro.infer.autoguide import AutoDelta + + guide = AutoDelta(gm.jax_model) + all_indices = jnp.arange(gm.data.num_genotype) + full_data = gm.get_batch(gm.data, all_indices) + pred_fn = Predictive(gm.jax_model, guide=guide, params=params, num_samples=1) + map_samples = pred_fn( + jax.random.PRNGKey(0), data=full_data, priors=gm.priors + ) + + if "growth_pred" not in map_samples: + raise ValueError( + "growth_pred not found in the model trace; " + "cannot generate trajectory plots." + ) + + # Remove leading num_samples dimension (num_samples=1). + growth_pred = np.asarray(map_samples["growth_pred"][0]) # (R,T,CP,CS,TN,TC,G) + ln_cfu0_map = ( + np.asarray(map_samples["ln_cfu0"][0]) + if "ln_cfu0" in map_samples + else None + ) + + # --- Fine-grid forward pass --- + gd = gm.data.growth + good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) + t_sel_tensor = np.asarray(gd.t_sel) + n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno = good_mask.shape + + global_max_t_sel = float(np.nanmax(t_sel_tensor[good_mask])) + t_fine_1d = np.linspace(0.0, global_max_t_sel, _T_FINE) + fine_shape = (n_rep, _T_FINE, n_cp, n_cs, n_tn, n_tc, n_geno) + + # Broadcast a 7-D array from T=1 to T_FINE; all per-condition tensors are + # constant along the T axis so slicing any timepoint is exact. + def _bc_t(arr, *, dtype=None): + a = np.asarray(arr) + r = np.broadcast_to(a[:, 0:1, ...], fine_shape).copy() + return jnp.array(r if dtype is None else r.astype(dtype)) + + t_sel_fine = np.broadcast_to( + t_fine_1d[None, :, None, None, None, None, None], fine_shape + ).copy() + # Active wherever any observed timepoint was valid for that cell. + has_data_bc = good_mask.any(axis=1, keepdims=True) + good_mask_fine = np.broadcast_to(has_data_bc, fine_shape).copy() + + gd_full = full_data.growth + fine_gd = gd_full.replace( + num_time=_T_FINE, + t_sel=jnp.array(t_sel_fine), + t_pre=_bc_t(gd_full.t_pre), + good_mask=jnp.array(good_mask_fine), + map_condition_pre=_bc_t(gd_full.map_condition_pre, dtype=int), + map_condition_sel=_bc_t(gd_full.map_condition_sel, dtype=int), + ln_cfu=jnp.zeros(fine_shape), + ln_cfu_std=jnp.ones(fine_shape), + ) + fine_data = full_data.replace(growth=fine_gd) + fine_samples = pred_fn(jax.random.PRNGKey(1), data=fine_data, priors=gm.priors) + growth_pred_fine = np.asarray(fine_samples["growth_pred"][0]) + + return growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d + + +def _compute_posterior_predictions(gm, posterior_file): + """ + Load ``growth_pred`` from a posterior HDF5 file and compute summary statistics. + + The posterior file must have been produced by + ``RunInference.get_posteriors()``, ``get_laplace_posteriors()``, or + ``get_map_posteriors()``. + + Parameters + ---------- + gm : ModelOrchestrator + Fitted model orchestrator (used only for shape validation; not called). + posterior_file : str or h5py.File + Path to an HDF5 posterior file or an already-open ``h5py.File`` handle. + + Returns + ------- + growth_pred_median : np.ndarray + Shape ``(R, T, CP, CS, TN, TC, G)`` — median across posterior samples. + growth_pred_lo : np.ndarray + Shape ``(R, T, CP, CS, TN, TC, G)`` — 5th-percentile (lower bound of + 90 % credible interval). + growth_pred_hi : np.ndarray + Shape ``(R, T, CP, CS, TN, TC, G)`` — 95th-percentile (upper bound). + ln_cfu0_median : np.ndarray or None + Shape ``(R, CP, G)`` — median ``ln_cfu0`` if present in the file, + otherwise ``None``. + + Raises + ------ + ValueError + If ``growth_pred`` is not a dataset in the HDF5 file. + """ + import h5py + + def _read(fh): + if "growth_pred" not in fh: + raise ValueError( + "growth_pred not found in posterior file; " + "cannot generate trajectory plots." + ) + # Shape: (S, R, T, CP, CS, TN, TC, G) + samples = fh["growth_pred"][:] + median = np.median(samples, axis=0) + lo = np.percentile(samples, 5, axis=0) + hi = np.percentile(samples, 95, axis=0) + ln_cfu0 = ( + np.median(fh["ln_cfu0"][:], axis=0) + if "ln_cfu0" in fh + else None + ) + return median, lo, hi, ln_cfu0 + + if isinstance(posterior_file, str): + with h5py.File(posterior_file, "r") as fh: + return _read(fh) + else: + return _read(posterior_file) + + +# --------------------------------------------------------------------------- +# Public entry point +# --------------------------------------------------------------------------- + +def plot_geno_trajectory( + gm, + out_prefix, + params=None, + posterior_file=None, + genotypes=None, + titrant_names=None, + write_csv=True, + growth_pred_std=None, +): + """ + Generate per-genotype growth trajectory plots from a fitted model. + + For each genotype a PDF is written containing one subplot per + ``(condition_pre, condition_sel, titrant_name, titrant_conc)`` + combination. Each subplot shows: + + * **Observed data** — ``ln_cfu ± ln_cfu_std`` error bars at their + ``t_sel`` x-coordinates (circles, one colour per replicate). + * **Pre-split observations** *(optional)* — ``ln_cfu_t0 ± ln_cfu_t0_std`` + as square markers at ``x = −t_pre`` when ``gm.data.presplit`` is not + ``None``. The same pooled aliquot value appears in every subplot that + shares the same ``(replicate, condition_pre, genotype)`` triple, which + is correct because the measurement predates the condition split. + * **Model prediction**: + + - *MAP path* — a smooth 50-point trajectory from ``−t_pre`` through + ``max(t_sel)``, computed via a fine-grid forward pass. + - *Posterior path* — a median line from ``−t_pre`` through the + observed timepoints, with a 90 % credible-interval shaded band + covering the selection phase (``t ≥ 0``). + + Parameters + ---------- + gm : ModelOrchestrator + Fitted model orchestrator. + out_prefix : str + Prefix for output files. Each genotype's PDF is written to + ``{out_prefix}_calib_{genotype}.pdf``. + params : dict or str, optional + MAP parameter dict with ``{site}_auto_loc`` keys, or a path to a + ``_params.npz`` file produced by ``RunInference.write_params()``. + Exactly one of *params* or *posterior_file* must be supplied. + posterior_file : str or h5py.File, optional + Path to a ``_posterior.h5`` file (or an open ``h5py.File`` handle) + produced by ``RunInference.get_posteriors()``. The plot shows the + posterior median with a 90 % credible-interval shaded band. + genotypes : list of str, optional + Subset of genotype names to plot. If ``None``, all genotypes in + ``gm`` are plotted. Names not found in the model are warned about + and silently skipped. + titrant_names : list of str, optional + Subset of titrant names to include. If ``None``, all titrant names + are included. + write_csv : bool, optional + If ``True`` (default), write + ``{out_prefix}_calib_growth_df.csv`` containing the observed and + predicted ``ln_cfu`` values. + growth_pred_std : np.ndarray, optional + Laplace-based per-cell prediction standard deviations, same shape + as the ``growth_pred`` tensor ``(R, T, CP, CS, TN, TC, G)``. + Only used with the MAP path (ignored for posteriors). When + *write_csv* is ``True`` this is written as the ``ln_cfu_pred_std`` + column. + + Returns + ------- + growth_df_out : pd.DataFrame or None + Merged DataFrame of observed and predicted ``ln_cfu`` values (one + row per valid tensor cell), or ``None`` if matplotlib is not + available. Columns include all columns from the growth + ``TensorManager`` DataFrame plus ``ln_cfu_pred`` and, depending on + the prediction path: + + - MAP + *growth_pred_std*: ``ln_cfu_pred_std`` + - Posterior: ``ln_cfu_pred_lo``, ``ln_cfu_pred_hi`` + + Raises + ------ + ValueError + If neither or both of *params* / *posterior_file* are supplied, or + if required data are missing from the posterior file. + """ + if (params is None) == (posterior_file is None): + raise ValueError( + "Exactly one of params or posterior_file must be provided." + ) + + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ImportError: + print( + " warning: matplotlib not available; skipping calibration plots.", + file=sys.stderr, + ) + return None + + # ----------------------------------------------------------------------- + # Compute predictions + # ----------------------------------------------------------------------- + is_map = params is not None + growth_pred_lo = growth_pred_hi = None + growth_pred_fine = t_fine_1d = None + + if is_map: + if isinstance(params, str): + params = dict(np.load(params)) + growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d = ( + _compute_map_predictions(gm, params) + ) + else: + growth_pred, growth_pred_lo, growth_pred_hi, ln_cfu0_map = ( + _compute_posterior_predictions(gm, posterior_file) + ) + + # ----------------------------------------------------------------------- + # Data tensors + # ----------------------------------------------------------------------- + gd = gm.data.growth + good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) + t_pre_tensor = np.asarray(gd.t_pre) + t_sel_tensor = np.asarray(gd.t_sel) + ln_cfu_obs = np.asarray(gd.ln_cfu) + ln_cfu_std_t = np.asarray(gd.ln_cfu_std) + + n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno = good_mask.shape + + # ----------------------------------------------------------------------- + # Dimension labels and helper maps + # ----------------------------------------------------------------------- + tm = gm.growth_tm + dn = tm.tensor_dim_names + + geno_labels = list(tm.tensor_dim_labels[dn.index("genotype")]) + rep_labels = list(tm.tensor_dim_labels[dn.index("replicate")]) + cp_labels = list(tm.tensor_dim_labels[dn.index("condition_pre")]) + tn_labels = list(tm.tensor_dim_labels[dn.index("titrant_name")]) + tc_labels = list(tm.tensor_dim_labels[dn.index("titrant_conc")]) + + df = tm.df + # (cp_idx, cs_idx) → condition_sel label string + cs_name_map = {} + for _, row in df.drop_duplicates( + ["condition_pre_idx", "condition_sel_idx"] + ).iterrows(): + cs_name_map[ + (int(row["condition_pre_idx"]), int(row["condition_sel_idx"])) + ] = str(row["condition_sel"]) + + # (genotype, titrant_name, titrant_conc) → mean observed theta + theta_map: dict = {} + for _, row in gm.binding_df.iterrows(): + key = ( + str(row["genotype"]), + str(row["titrant_name"]), + float(row["titrant_conc"]), + ) + theta_map.setdefault(key, []).append(float(row["theta_obs"])) + theta_map = {k: float(np.nanmean(v)) for k, v in theta_map.items()} + + # ----------------------------------------------------------------------- + # Pre-split data (optional) + # ----------------------------------------------------------------------- + presplit = gm.data.presplit + if presplit is not None: + ps_ln_cfu = np.asarray(presplit.ln_cfu_t0) # (R, CP, G) + ps_ln_cfu_std = np.asarray(presplit.ln_cfu_t0_std) # (R, CP, G) + ps_good_mask = np.asarray(presplit.good_mask) # (R, CP, G) + else: + ps_ln_cfu = ps_ln_cfu_std = ps_good_mask = None + + # ----------------------------------------------------------------------- + # Genotype filter + # ----------------------------------------------------------------------- + avail_geno = {str(g): i for i, g in enumerate(geno_labels)} + if genotypes is not None: + requested = [str(g) for g in genotypes] + missing = sorted(set(requested) - set(avail_geno)) + if missing: + print( + f" warning: requested genotype(s) not found in model: {missing}", + file=sys.stderr, + ) + geno_iter = [ + (avail_geno[g], g) + for g in requested + if g in avail_geno + ] + else: + geno_iter = [(i, str(g)) for i, g in enumerate(geno_labels)] + + # ----------------------------------------------------------------------- + # Titrant_name filter + # ----------------------------------------------------------------------- + if titrant_names is not None: + requested_tn = set(str(t) for t in titrant_names) + tn_indices = frozenset( + i for i, tn in enumerate(tn_labels) if str(tn) in requested_tn + ) + else: + tn_indices = frozenset(range(n_tn)) + + # ----------------------------------------------------------------------- + # Build predictions DataFrame (used for CSV and returned to caller) + # ----------------------------------------------------------------------- + r_idx, t_idx, cp_idx, cs_idx, tn_idx, tc_idx, g_idx = np.where(good_mask) + flat_idx = (r_idx, t_idx, cp_idx, cs_idx, tn_idx, tc_idx, g_idx) + + pred_dict: dict = { + "replicate_idx": r_idx.astype(int), + "condition_pre_idx": cp_idx.astype(int), + "condition_sel_idx": cs_idx.astype(int), + "titrant_name_idx": tn_idx.astype(int), + "titrant_conc_idx": tc_idx.astype(int), + "genotype_idx": g_idx.astype(int), + "t_sel": t_sel_tensor[flat_idx], + "ln_cfu_pred": growth_pred[flat_idx], + } + if is_map and growth_pred_std is not None: + pred_dict["ln_cfu_pred_std"] = growth_pred_std[flat_idx] + elif not is_map: + pred_dict["ln_cfu_pred_lo"] = growth_pred_lo[flat_idx] + pred_dict["ln_cfu_pred_hi"] = growth_pred_hi[flat_idx] + + merge_cols = [ + "replicate_idx", "condition_pre_idx", "condition_sel_idx", + "titrant_name_idx", "titrant_conc_idx", "genotype_idx", "t_sel", + ] + growth_df_out = df.merge(pd.DataFrame(pred_dict), on=merge_cols, how="left") + + if write_csv: + csv_path = f"{out_prefix}_calib_growth_df.csv" + growth_df_out.to_csv(csv_path, index=False) + print(f" Saved {csv_path}", flush=True) + + # ----------------------------------------------------------------------- + # Per-genotype PDF plots + # ----------------------------------------------------------------------- + print("Generating calibration quality plots ...", flush=True) + + prop_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + + for g_i, geno_name in geno_iter: + + # Collect valid (cp, cs, tn, tc) combos → list of replicates. + # Apply titrant_name filter here. + condition_combos: dict = {} + for r_i in range(n_rep): + for cp_i in range(n_cp): + for cs_i in range(n_cs): + for tn_i in range(n_tn): + if tn_i not in tn_indices: + continue + for tc_i in range(n_tc): + if good_mask[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i].any(): + condition_combos.setdefault( + (cp_i, cs_i, tn_i, tc_i), [] + ).append(r_i) + + if not condition_combos: + continue + + n_combos = len(condition_combos) + n_cols = min(3, n_combos) + n_rows = (n_combos + n_cols - 1) // n_cols + + fig, axes = plt.subplots( + n_rows, n_cols, + figsize=(5 * n_cols, 4 * n_rows), + squeeze=False, + sharey=True, + ) + fig.suptitle( + f"Calibration fit — genotype: {geno_name}", + fontsize=13, + fontweight="bold", + ) + + for combo_i, ((cp_i, cs_i, tn_i, tc_i), rep_list) in enumerate( + condition_combos.items() + ): + ax = axes[combo_i // n_cols][combo_i % n_cols] + + cp_name = str(cp_labels[cp_i]) + cs_name = cs_name_map.get((cp_i, cs_i), f"sel_{cs_i}") + tn_name = str(tn_labels[tn_i]) + tc_val = float(tc_labels[tc_i]) + + theta_obs = theta_map.get((geno_name, tn_name, tc_val)) + theta_str = f", θ = {theta_obs:.3f}" if theta_obs is not None else "" + ax.set_title( + f"{cp_name} → {cs_name}\n{tn_name} = {tc_val:.3g}{theta_str}", + fontsize=9, + ) + ax.set_xlabel("Time") + ax.set_ylabel("ln(CFU)") + ax.axvline(0.0, color="0.6", lw=0.8, ls="--") + + # Build per-replicate observation records. + rep_data = [] + for r_i in rep_list: + mask = good_mask[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i] + valid_t = np.where(mask)[0] + if not len(valid_t): + continue + rep_data.append({ + "r_i": r_i, + "valid_t": valid_t, + "t_sel": t_sel_tensor[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], + "obs": ln_cfu_obs[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], + "std": ln_cfu_std_t[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], + "t_pre": float(np.nanmedian( + t_pre_tensor[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] + )), + }) + + if not rep_data: + ax.set_visible(False) + continue + + max_t_sel = float(max(np.nanmax(rd["t_sel"]) for rd in rep_data)) + max_t_pre = float(max(rd["t_pre"] for rd in rep_data)) + + for rd in rep_data: + r_i = rd["r_i"] + rep_color = prop_colors[r_i % len(prop_colors)] + rep_label = str(rep_labels[r_i]) + + # --- Pre-split observed point (square marker) --- + # Shown at x = −t_pre; same pooled aliquot for all conditions + # sharing this (replicate, condition_pre, genotype) triple. + if ps_good_mask is not None and ps_good_mask[r_i, cp_i, g_i]: + ax.errorbar( + -rd["t_pre"], + float(ps_ln_cfu[r_i, cp_i, g_i]), + yerr=float(ps_ln_cfu_std[r_i, cp_i, g_i]), + fmt="s", + color=rep_color, + ms=5, + lw=1, + capsize=3, + zorder=3, + ) + + # --- Selection-phase observed data (circle marker) --- + ax.errorbar( + rd["t_sel"], + rd["obs"], + yerr=rd["std"], + fmt="o", + color=rep_color, + ms=5, + lw=1, + capsize=3, + label=rep_label, + zorder=3, + ) + + # --- Model prediction --- + if is_map: + # Smooth fine-grid line (pre-selection + selection phases). + y_fine = growth_pred_fine[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i] + calc_at_0 = float(y_fine[0]) + ln_cfu0_r = ( + float(ln_cfu0_map[r_i, cp_i, g_i]) + if (ln_cfu0_map is not None and ln_cfu0_map.ndim == 3) + else calc_at_0 + ) + t_smooth = np.concatenate([[-rd["t_pre"], 0.0], t_fine_1d]) + y_smooth = np.concatenate([[ln_cfu0_r, calc_at_0], y_fine]) + ax.plot(t_smooth, y_smooth, "-", color=rep_color, lw=1.8, zorder=4) + + else: + # Posterior: median line (both phases) + 90 % credible + # interval shaded band (selection phase only). + valid_t = rd["valid_t"] + t_obs = rd["t_sel"] + y_med = growth_pred[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] + y_lo = growth_pred_lo[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] + y_hi = growth_pred_hi[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] + + ln_cfu0_r = ( + float(ln_cfu0_map[r_i, cp_i, g_i]) + if (ln_cfu0_map is not None and ln_cfu0_map.ndim == 3) + else float(y_med[0]) + ) + + # Sort by time for clean connected lines. + sort_idx = np.argsort(t_obs) + t_sorted = t_obs[sort_idx] + y_med_s = y_med[sort_idx] + y_lo_s = y_lo[sort_idx] + y_hi_s = y_hi[sort_idx] + + # Full line: pre-selection anchor → observed medians. + t_line = np.concatenate([[-rd["t_pre"]], t_sorted]) + y_line = np.concatenate([[ln_cfu0_r], y_med_s]) + ax.plot(t_line, y_line, "-", color=rep_color, lw=1.8, zorder=4) + + # Shaded credible interval — selection phase only. + ax.fill_between( + t_sorted, + y_lo_s, + y_hi_s, + color=rep_color, + alpha=0.2, + zorder=2, + ) + + ax.legend(fontsize=8, loc="best") + x_pad = (max_t_sel + max_t_pre) * 0.03 + ax.set_xlim(-max_t_pre - x_pad, max_t_sel + x_pad) + + # Hide unused subplot cells. + for extra_i in range(n_combos, n_rows * n_cols): + axes[extra_i // n_cols][extra_i % n_cols].set_visible(False) + + fig.tight_layout(rect=[0, 0, 1, 0.95]) + safe_name = geno_name.replace("/", "-") + pdf_path = f"{out_prefix}_calib_{safe_name}.pdf" + fig.savefig(pdf_path, format="pdf", bbox_inches="tight") + plt.close(fig) + print(f" Saved {pdf_path}", flush=True) + + print("Calibration plots complete.", flush=True) + return growth_df_out diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index 821e77a7..8425fb98 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -39,8 +39,9 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], dataframe with predicted fractional occupancy and growth rates for each of the genotypes in each of the conditions specified in the configuration genotype_theta_df : pandas.DataFrame - wide-form dataframe with one row per genotype and one column per unique - effector concentration giving the ground-truth theta value + long-form dataframe with one row per (genotype, titrant_name, + titrant_conc) combination; columns: genotype, titrant_name, + titrant_conc, theta parameters_df : pandas.DataFrame one row per unique genotype; columns ``dk_geno``, ``activity``, and any scalar per-genotype fields from the theta component (e.g. diff --git a/src/tfscreen/simulate/scripts/run_simulation_cli.py b/src/tfscreen/simulate/scripts/run_simulation_cli.py index 594a5931..3f9a9954 100644 --- a/src/tfscreen/simulate/scripts/run_simulation_cli.py +++ b/src/tfscreen/simulate/scripts/run_simulation_cli.py @@ -243,9 +243,10 @@ def run_simulation_from_config( Runs library_prediction once to establish ground-truth phenotypes, then simulates num_replicates independent experimental replicates using - selection_experiment. Writes library, phenotype, genotype_theta, and - analysis-ready growth CSV files. If the config contains a 'binding_data' - block, also writes a simulated binding curve CSV. + selection_experiment. Writes library, parameters, genotype_theta (long-form: + genotype/titrant_name/titrant_conc/theta), and analysis-ready growth CSV + files. If the config contains a 'binding_data' block, also writes a + simulated binding curve CSV. Parameters ---------- diff --git a/src/tfscreen/simulate/thermo_to_growth.py b/src/tfscreen/simulate/thermo_to_growth.py index d699bad2..ee064e94 100644 --- a/src/tfscreen/simulate/thermo_to_growth.py +++ b/src/tfscreen/simulate/thermo_to_growth.py @@ -376,8 +376,10 @@ def thermo_to_growth( ``selection_experiment`` can use them downstream; they are also collected in ``parameters_df`` for file output. genotype_theta_df : pd.DataFrame - Wide-form dataframe with one row per genotype and one column per - unique effector concentration, giving the ground-truth theta value. + Long-form dataframe with one row per (genotype, titrant_name, + titrant_conc) combination. Columns: ``genotype``, ``titrant_name``, + ``titrant_conc``, ``theta``. Sorted by (genotype, titrant_name, + titrant_conc); ``genotype`` is an ordered categorical. Use this to compare simulation inputs against fitted posteriors. parameters_df : pd.DataFrame One row per unique genotype. Always contains ``genotype``, @@ -440,32 +442,46 @@ def thermo_to_growth( geno_to_sim_idx = {g: i for i, g in enumerate(all_genotypes)} unique_sim_indices = np.array([geno_to_sim_idx[g] for g in unique_genotypes]) - # ── Build genotype_theta_df (ground-truth theta per unique genotype) ────── - # Wide form: one row per UNIQUE genotype (in sorted unique_genotypes order), - # one column per concentration. unique_sim_indices maps each unique genotype - # to its representative row in theta_gc, eliminating duplicates. - - conc_vals = np.array(sim_data.titrant_conc) - conc_col_names = [f"theta_at_{c:.6g}mM" for c in conc_vals] - genotype_theta_df = pd.DataFrame( - theta_gc[unique_sim_indices], - index=list(unique_genotypes), - columns=conc_col_names, - ) - genotype_theta_df.index.name = "genotype" - genotype_theta_df = genotype_theta_df.reset_index() - genotype_theta_df = set_categorical_genotype(genotype_theta_df) - # ── Map sample_df concentrations to unique-concentration indices ────────── # sample_df may have duplicate concentrations across rows; each row maps to # one column of theta_gc. # Use float64 values from sample_df directly (same source as build_sim_data) - # rather than conc_vals, which is float32 from the JAX array and would cause - # dict-lookup misses due to float32→float64 precision loss. + # rather than sim_data.titrant_conc, which is float32 from the JAX array and + # would cause dict-lookup misses due to float32→float64 precision loss. sorted_concs_f64 = np.sort(sample_df["titrant_conc"].unique()).astype(float) conc_to_col = {c: i for i, c in enumerate(sorted_concs_f64)} sample_conc_idx = sample_df["titrant_conc"].map(conc_to_col).values.astype(int) + # ── Build genotype_theta_df (ground-truth theta per unique genotype) ────── + # Long form: one row per (genotype, titrant_name, titrant_conc). + # unique_sim_indices maps each unique genotype to its representative row in + # theta_gc, eliminating duplicates from sub-libraries. + tn_conc_pairs = ( + sample_df[["titrant_name", "titrant_conc"]] + .drop_duplicates() + .sort_values(["titrant_name", "titrant_conc"]) + .reset_index(drop=True) + ) + pair_col_indices = np.array( + [conc_to_col[float(c)] for c in tn_conc_pairs["titrant_conc"]] + ) + # theta_vals shape: (n_unique_geno, n_pairs) + theta_vals = theta_gc[unique_sim_indices][:, pair_col_indices] + + n_geno = len(unique_genotypes) + n_pairs = len(tn_conc_pairs) + genotype_theta_df = pd.DataFrame({ + "genotype": np.repeat(list(unique_genotypes), n_pairs), + "titrant_name": np.tile(tn_conc_pairs["titrant_name"].values, n_geno), + "titrant_conc": np.tile(tn_conc_pairs["titrant_conc"].values.astype(float), + n_geno), + "theta": theta_vals.ravel(), + }) + genotype_theta_df = set_categorical_genotype(genotype_theta_df) + genotype_theta_df = genotype_theta_df.sort_values( + ["genotype", "titrant_name", "titrant_conc"] + ).reset_index(drop=True) + print("Calculating growth rates and building phenotype dataframe... ", end="", flush=True) diff --git a/src/tfscreen/tfmodel/analysis/prediction.py b/src/tfscreen/tfmodel/analysis/prediction.py index 79294f87..cd663c67 100644 --- a/src/tfscreen/tfmodel/analysis/prediction.py +++ b/src/tfscreen/tfmodel/analysis/prediction.py @@ -102,6 +102,54 @@ def _get_input(value, col_name): **settings ) +def _convert_map_params(map_params, model_trace): + """ + Convert a raw MAP parameter dict to a constrained posterior dict. + + ``AutoDelta`` guide parameters are stored in *unconstrained* space with + ``{site}_auto_loc`` keys. ``Predictive(posterior_samples=...)`` expects + values in *constrained* space keyed by bare site name, with a leading + sample dimension. This function applies the per-site bijection and adds + that dimension so the rest of :func:`predict` can treat the MAP point as + a 1-sample posterior. + + Parameters + ---------- + map_params : dict-like + Raw MAP parameter dict (e.g., from ``np.load("_params.npz")``). + All keys must end with ``_auto_loc``. + model_trace : dict + NumPyro model trace produced by ``numpyro.handlers.trace``. Used to + look up the ``fn.support`` bijection for each latent site. + + Returns + ------- + dict + Constrained values keyed by bare site name, each with shape + ``(1, *site_shape)``. + """ + from numpyro.distributions.transforms import biject_to + + constrained = {} + for k, v in map_params.items(): + k = str(k) + if not k.endswith("_auto_loc"): + continue + site_name = k[: -len("_auto_loc")] + val = jnp.array(np.asarray(v)) + + site = model_trace.get(site_name) + if (site is not None + and site["type"] == "sample" + and not site.get("is_observed", False)): + val = biject_to(site["fn"].support)(val) + + # Add the leading sample dimension expected by predict(). + constrained[site_name] = jnp.expand_dims(val, 0) + + return constrained + + def predict(model_class, param_posteriors, predict_sites=None, @@ -198,13 +246,39 @@ def predict(model_class, # Use existing get_batch to subset the data tensors subset_data = new_mc.get_batch(new_mc.data, jnp.array(genotype_indices)) + # ------------------------------------------------------------------------- + # Model trace — run first so bijections are available for MAP detection. + # + # We use the original model_class because posteriors match its structure. + + seeded_model = seed(model_class.jax_model, rng_seed=0) + traced_model = trace(seeded_model) + model_trace = traced_model.get_trace(data=model_class.data, + priors=model_class.priors) + + # ------------------------------------------------------------------------- + # MAP param conversion (if needed) + # + # Raw MAP params from RunInference.write_params() / np.load("_params.npz") + # have keys like ``{site}_auto_loc`` in *unconstrained* space and no + # leading sample dimension. Convert them to constrained space with a + # size-1 leading dim so the rest of this function treats them as a + # 1-sample posterior. + # + # Genuine posterior files produced by get_posteriors() / get_map_posteriors() + # already store constrained values keyed by bare site name and are left + # unchanged. + + if any(str(k).endswith("_auto_loc") for k in param_posteriors.keys()): + param_posteriors = _convert_map_params(param_posteriors, model_trace) + # ------------------------------------------------------------------------- # Sample selection from posterior - + # Identify how many samples we have first_key = next(iter(param_posteriors.keys())) total_available = param_posteriors[first_key].shape[0] - + # Sample indices for running through the model (quantile computation) n_for_quantiles = total_available if num_marginal_samples is None else min(num_marginal_samples, total_available) rng = np.random.default_rng() @@ -213,13 +287,6 @@ def predict(model_class, # ------------------------------------------------------------------------- # Parameter slicing - - # Run a trace of the original model to identify plate structure - # We use the original model class because posteriors match its structure. - seeded_model = seed(model_class.jax_model, rng_seed=0) - traced_model = trace(seeded_model) - model_trace = traced_model.get_trace(data=model_class.data, - priors=model_class.priors) sliced_samples = {} for site_name, site in model_trace.items(): diff --git a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py index 8ac4fe1a..4c4ff55e 100644 --- a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py @@ -64,6 +64,7 @@ from tfscreen.tfmodel.configuration_io import ( read_configuration, ) +from tfscreen.plot.geno_trajectory import plot_geno_trajectory # --------------------------------------------------------------------------- @@ -692,335 +693,35 @@ def _run_calibration_map(ri, # Diagnostic plots # --------------------------------------------------------------------------- -def _make_calibration_plots(gm_cal, params, out_prefix, growth_pred_std=None): +def _run_calibration_diagnostics(gm_cal, params, out_prefix, growth_pred_std=None): """ - Generate per-genotype calibration diagnostic plots as PDFs. - - For each genotype in the calibration data, writes one PDF containing - one subplot per (condition_pre, condition_sel, titrant_name, titrant_conc) - combination. Each subplot shows: - - * **Observed** — the experimental ``ln_cfu ± ln_cfu_std`` data points - plotted at their ``t_sel`` x-coordinates (selection-phase time). - * **Model prediction** — a smooth 100-point trajectory computed from - the MAP parameter estimates: - - - Pre-selection phase (x from ``-t_pre`` to 0): a straight line from - ``(−t_pre, ln_cfu0)`` to ``(0, ln_cfu0 + g_pre·t_pre)``, where - ``ln_cfu0`` comes from the ``ln_cfu0`` deterministic site and the - pre-selection slope ``g_pre`` is derived from the selection-phase - intercept. - - Selection phase (x from 0 to ``max(t_sel)``): a straight line with - slope ``g_sel`` estimated by fitting the MAP ``growth_pred`` values - at the observed ``t_sel`` times. - - The linear-in-time approximation for the smooth trajectory is exact for - the ``instant`` growth-transition component and a reasonable visual - approximation for non-linear variants. + Generate per-genotype trajectory PDFs and write correlation / stats outputs. + + Delegates the trajectory plots to :func:`tfscreen.plot.plot_geno_trajectory`, + then uses the returned predictions DataFrame to write the goodness-of-fit + stats JSON and the observed-vs-predicted correlation PDF. Parameters ---------- gm_cal : ModelOrchestrator - Calibration ModelOrchestrator (exposes ``growth_tm``, ``data``, - ``priors``, ``jax_model``). + Calibration ModelOrchestrator. params : dict - MAP parameter dict from the SVI optimiser (keys follow the - ``{site}_auto_loc`` convention). + MAP parameter dict (``{site}_auto_loc`` keys). out_prefix : str - File-name prefix; each genotype's PDF is written to - ``{out_prefix}_calib_{genotype}.pdf``. + File-name prefix for all outputs. + growth_pred_std : np.ndarray or None + Optional Laplace-based per-cell prediction standard deviations, + shape ``(R, T, CP, CS, TN, TC, G)``. """ - try: - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - except ImportError: - print( - " warning: matplotlib not available; skipping calibration plots.", - file=sys.stderr, - ) - return - - from numpyro.infer import Predictive - from numpyro.infer.autoguide import AutoDelta - import jax - - print("Generating calibration quality plots ...", flush=True) - - # --- Run Predictive at the MAP point to recover growth_pred and ln_cfu0 --- - guide = AutoDelta(gm_cal.jax_model) - all_indices = jnp.arange(gm_cal.data.num_genotype) - full_data = gm_cal.get_batch(gm_cal.data, all_indices) - pred_fn = Predictive(gm_cal.jax_model, guide=guide, params=params, num_samples=1) - map_samples = pred_fn( - jax.random.PRNGKey(0), data=full_data, priors=gm_cal.priors + growth_df_out = plot_geno_trajectory( + gm_cal, + out_prefix, + params=params, + growth_pred_std=growth_pred_std, ) - - if "growth_pred" not in map_samples: - print( - " warning: growth_pred not in model trace; skipping plots.", - file=sys.stderr, - ) + if growth_df_out is None: return - # Remove leading sample dimension (num_samples=1). - # growth_pred: (R, T, CP, CS, TN, TC, G) at the original observed timepoints. - # Kept separately from growth_pred_fine (fine-grid) so the CSV can record - # predictions that align row-for-row with the observed growth_df entries. - growth_pred = np.asarray(map_samples["growth_pred"][0]) - - # ln_cfu0 deterministic: (R, CP, G) — registered by the hierarchical component - ln_cfu0_map = ( - np.asarray(map_samples["ln_cfu0"][0]) - if "ln_cfu0" in map_samples - else None - ) - - # --- Data tensors from the calibration model --- - gd = gm_cal.data.growth - good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) - t_pre_tensor = np.asarray(gd.t_pre) # (R, T, CP, CS, TN, TC, G) - t_sel_tensor = np.asarray(gd.t_sel) # (R, T, CP, CS, TN, TC, G) - ln_cfu_obs = np.asarray(gd.ln_cfu) # (R, T, CP, CS, TN, TC, G) - ln_cfu_std = np.asarray(gd.ln_cfu_std) # (R, T, CP, CS, TN, TC, G) - - n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno = good_mask.shape - - # --- Fine-grid Predictive for exact smooth selection-phase trajectories --- - # Replace the T dimension with T_FINE evenly-spaced points from 0 to the - # global max t_sel. t_pre is constant over T within each condition cell, - # so we broadcast from the first observed timepoint. The same pred_fn is - # reused with the new data argument; JAX will retrace for the new shape. - T_FINE = 50 - global_max_t_sel = float(np.nanmax(t_sel_tensor[good_mask])) - t_fine_1d = np.linspace(0.0, global_max_t_sel, T_FINE) - fine_shape = (n_rep, T_FINE, n_cp, n_cs, n_tn, n_tc, n_geno) - - # Helper: broadcast a 7-D array from T=1 (time-constant) to T_FINE. - # All per-condition tensors are constant along the T axis; slicing any - # timepoint and broadcasting is therefore exact. - def _bc_t(arr, *, dtype=None): - a = np.asarray(arr) - r = np.broadcast_to(a[:, 0:1, ...], fine_shape).copy() - return jnp.array(r if dtype is None else r.astype(dtype)) - - t_sel_fine = np.broadcast_to( - t_fine_1d[None, :, None, None, None, None, None], fine_shape - ).copy() - - # Active wherever any observed timepoint was valid for that cell. - has_data_bc = good_mask.any(axis=1, keepdims=True) # (R,1,CP,CS,TN,TC,G) - good_mask_fine = np.broadcast_to(has_data_bc, fine_shape).copy() - - # map_condition_pre / map_condition_sel are also shape (R,T,CP,CS,TN,TC,G); - # each element is an index into per-condition-rep arrays, constant over T. - gd_full = full_data.growth - fine_gd = gd_full.replace( - num_time=T_FINE, - t_sel=jnp.array(t_sel_fine), - t_pre=_bc_t(gd_full.t_pre), - good_mask=jnp.array(good_mask_fine), - map_condition_pre=_bc_t(gd_full.map_condition_pre, dtype=int), - map_condition_sel=_bc_t(gd_full.map_condition_sel, dtype=int), - ln_cfu=jnp.zeros(fine_shape), - ln_cfu_std=jnp.ones(fine_shape), - ) - fine_data = full_data.replace(growth=fine_gd) - map_samples_fine = pred_fn( - jax.random.PRNGKey(1), data=fine_data, priors=gm_cal.priors - ) - # shape: (R, T_FINE, CP, CS, TN, TC, G) - growth_pred_fine = np.asarray(map_samples_fine["growth_pred"][0]) - - # --- Dimension labels from the TensorManager --- - tm = gm_cal.growth_tm - dn = tm.tensor_dim_names - - geno_labels = list(tm.tensor_dim_labels[dn.index("genotype")]) - rep_labels = list(tm.tensor_dim_labels[dn.index("replicate")]) - cp_labels = list(tm.tensor_dim_labels[dn.index("condition_pre")]) - tn_labels = list(tm.tensor_dim_labels[dn.index("titrant_name")]) - tc_labels = list(tm.tensor_dim_labels[dn.index("titrant_conc")]) - - # Map (cp_idx, cs_idx) → actual condition_sel name. The tensor uses the - # reduced (integer) condition_sel dimension; the original string name lives - # in the processed DataFrame alongside the integer index column. - df = tm.df - cs_name_map = {} - for _, row in df.drop_duplicates( - ["condition_pre_idx", "condition_sel_idx"] - ).iterrows(): - cs_name_map[ - (int(row["condition_pre_idx"]), int(row["condition_sel_idx"])) - ] = str(row["condition_sel"]) - - # Map (genotype, titrant_name, titrant_conc) → mean observed theta. - # Averages over binding replicates so a single scalar appears in the title. - theta_map: dict = {} - for _, row in gm_cal.binding_df.iterrows(): - key = (str(row["genotype"]), str(row["titrant_name"]), float(row["titrant_conc"])) - theta_map.setdefault(key, []).append(float(row["theta_obs"])) - theta_map = {k: float(np.nanmean(v)) for k, v in theta_map.items()} - - prop_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] - - for g_i, geno_name in enumerate(geno_labels): - - # Collect valid (cp, cs, tn, tc) combinations and the replicates that - # contribute observations. - condition_combos: dict = {} - for r_i in range(n_rep): - for cp_i in range(n_cp): - for cs_i in range(n_cs): - for tn_i in range(n_tn): - for tc_i in range(n_tc): - if good_mask[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i].any(): - condition_combos.setdefault( - (cp_i, cs_i, tn_i, tc_i), [] - ).append(r_i) - - if not condition_combos: - continue - - n_combos = len(condition_combos) - n_cols = min(3, n_combos) - n_rows = (n_combos + n_cols - 1) // n_cols - - fig, axes = plt.subplots( - n_rows, n_cols, - figsize=(5 * n_cols, 4 * n_rows), - squeeze=False, - sharey=True, - ) - fig.suptitle( - f"Calibration fit — genotype: {geno_name}", - fontsize=13, - fontweight="bold", - ) - - for combo_i, ((cp_i, cs_i, tn_i, tc_i), rep_list) in enumerate( - condition_combos.items() - ): - ax = axes[combo_i // n_cols][combo_i % n_cols] - - cp_name = str(cp_labels[cp_i]) - cs_name = cs_name_map.get((cp_i, cs_i), f"sel_{cs_i}") - tn_name = str(tn_labels[tn_i]) - tc_val = float(tc_labels[tc_i]) - - theta_obs = theta_map.get((geno_name, tn_name, tc_val)) - theta_str = f", θ = {theta_obs:.3f}" if theta_obs is not None else "" - ax.set_title( - f"{cp_name} → {cs_name}\n{tn_name} = {tc_val:.3g}{theta_str}", - fontsize=9, - ) - ax.set_xlabel("Time") - ax.set_ylabel("ln(CFU)") - ax.axvline(0.0, color="0.6", lw=0.8, ls="--") - - # Collect valid per-replicate data for this condition combination. - rep_data = [] - for r_i in rep_list: - mask = good_mask[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i] - valid_t = np.where(mask)[0] - if len(valid_t) == 0: - continue - rep_data.append({ - "r_i": r_i, - "t_sel": t_sel_tensor[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], - "obs": ln_cfu_obs[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], - "std": ln_cfu_std[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], - "t_pre": float(np.nanmedian( - t_pre_tensor[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] - )), - }) - - if not rep_data: - ax.set_visible(False) - continue - - max_t_sel = float(max(np.nanmax(rd["t_sel"]) for rd in rep_data)) - max_t_pre = float(max(rd["t_pre"] for rd in rep_data)) - - # Plot each replicate in its own colour: data points + model line. - for rd in rep_data: - r_i = rd["r_i"] - rep_color = prop_colors[r_i % len(prop_colors)] - rep_label = str(rep_labels[r_i]) - - # Observed data with error bars - ax.errorbar( - rd["t_sel"], rd["obs"], yerr=rd["std"], - fmt="o", color=rep_color, ms=5, lw=1, capsize=3, - label=rep_label, zorder=3, - ) - - # Exact model prediction for this replicate. - # growth_pred_fine: (R, T_FINE, CP, CS, TN, TC, G) - y_fine_r = growth_pred_fine[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i] - calc_at_0_r = float(y_fine_r[0]) - - # ln_cfu0 anchor; fall back to calc_at_0 when site is absent. - if ln_cfu0_map is not None and ln_cfu0_map.ndim == 3: - ln_cfu0_r = float(ln_cfu0_map[r_i, cp_i, g_i]) - else: - ln_cfu0_r = calc_at_0_r - - # Pre-selection: two-point line (-t_pre, ln_cfu0) → (0, calc_at_0) - # Selection: exact fine-grid predictions - t_smooth = np.concatenate([np.array([-rd["t_pre"], 0.0]), t_fine_1d]) - y_smooth = np.concatenate([np.array([ln_cfu0_r, calc_at_0_r]), y_fine_r]) - ax.plot(t_smooth, y_smooth, "-", color=rep_color, lw=1.8, zorder=4) - - ax.legend(fontsize=8, loc="best") - x_pad = (max_t_sel + max_t_pre) * 0.03 - ax.set_xlim(-max_t_pre - x_pad, max_t_sel + x_pad) - - # Hide unused subplots - for extra_i in range(n_combos, n_rows * n_cols): - axes[extra_i // n_cols][extra_i % n_cols].set_visible(False) - - fig.tight_layout(rect=[0, 0, 1, 0.95]) - safe_geno_name = geno_name.replace("/", "-") - pdf_path = f"{out_prefix}_calib_{safe_geno_name}.pdf" - fig.savefig(pdf_path, format="pdf", bbox_inches="tight") - plt.close(fig) - print(f" Saved {pdf_path}", flush=True) - - # --- Write predictions CSV --- - # Extract the MAP-predicted ln_cfu at every valid (observed) tensor cell, - # then attach it as a new column alongside the original growth_df rows. - # - # The tensor has 7 dimensions: (R, T, CP, CS, TN, TC, G). - # tm.df carries _idx columns for 6 of them (all except T); the T dimension - # is identified by the t_sel float value, which is the same in both the - # tensor and the DataFrame. - r_idx, t_idx, cp_idx, cs_idx, tn_idx, tc_idx, g_idx = np.where(good_mask) - - idx = (r_idx, t_idx, cp_idx, cs_idx, tn_idx, tc_idx, g_idx) - pred_lookup_dict = { - "replicate_idx": r_idx.astype(int), - "condition_pre_idx": cp_idx.astype(int), - "condition_sel_idx": cs_idx.astype(int), - "titrant_name_idx": tn_idx.astype(int), - "titrant_conc_idx": tc_idx.astype(int), - "genotype_idx": g_idx.astype(int), - "t_sel": t_sel_tensor[idx], - "ln_cfu_pred": growth_pred[idx], - } - if growth_pred_std is not None: - pred_lookup_dict["ln_cfu_pred_std"] = growth_pred_std[idx] - pred_lookup = pd.DataFrame(pred_lookup_dict) - - merge_cols = [ - "replicate_idx", "condition_pre_idx", "condition_sel_idx", - "titrant_name_idx", "titrant_conc_idx", "genotype_idx", "t_sel", - ] - growth_df_out = df.merge(pred_lookup, on=merge_cols, how="left") - csv_path = f"{out_prefix}_calib_growth_df.csv" - growth_df_out.to_csv(csv_path, index=False) - print(f" Saved {csv_path}", flush=True) - n_params = int(sum(np.asarray(v).size for k, v in params.items() if k.endswith("_auto_loc"))) @@ -1029,8 +730,6 @@ def _bc_t(arr, *, dtype=None): n_params=n_params, n_obs=n_obs) _make_correlation_plot(growth_df_out, out_prefix) - print("Calibration plots complete.", flush=True) - # --------------------------------------------------------------------------- # Calibration stats JSON and correlation plot @@ -1396,7 +1095,8 @@ def run_prefit_calibration(config_file, # 6. Write per-genotype diagnostic plots with Laplace-based prediction uncertainty. print("Computing growth_pred uncertainty via Laplace samples ...", flush=True) growth_pred_std = _compute_growth_pred_std(ri, params, gm_cal) - _make_calibration_plots(gm_cal, params, out_prefix, growth_pred_std=growth_pred_std) + _run_calibration_diagnostics(gm_cal, params, out_prefix, + growth_pred_std=growth_pred_std) return svi_state, params, converged diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index f210e942..a17ff81c 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -147,9 +147,12 @@ def summarize_fit(run_dir, run_dir : str Directory containing model fit outputs. ground_truth_file : str, optional - CSV with columns genotype, titrant_name, titrant_conc, theta_obs, - theta_std containing known theta values for evaluating out-of-sample - prediction. When omitted, only training statistics are computed. + CSV with columns genotype, titrant_name, titrant_conc, and a theta + column containing known theta values for evaluating out-of-sample + prediction. The theta column is resolved in order: ``theta_obs`` + (preferred) → ``theta`` (fallback). If neither is present a warning + is issued and test statistics are skipped. When the argument is + omitted entirely, only training statistics are computed. out_prefix : str, optional Prefix for output files. Defaults to {run_dir}/tfs_summarize. """ @@ -261,17 +264,30 @@ def summarize_fit(run_dir, try: gt_df = pd.read_csv(ground_truth_file) join_cols = ["genotype", "titrant_name", "titrant_conc"] + if "theta_obs" in gt_df.columns: + theta_col = "theta_obs" + elif "theta" in gt_df.columns: + theta_col = "theta" + else: + warnings.warn( + f"Ground truth file {ground_truth_file} has neither " + f"'theta_obs' nor 'theta' column; skipping test statistics" + ) + theta_col = None + if theta_col is not None: + gt_df = gt_df.rename(columns={theta_col: "theta_obs"}) test_merged = pred_df.merge( gt_df[join_cols + ["theta_obs"]], on=join_cols, how="inner", - ) - metadata["n_theta_test_points"] = len(test_merged) - if len(test_merged) > 0: - theta_test_stats = _run_stats( - test_merged["median"].values, - test_merged["theta_obs"].values, - ) + ) if theta_col is not None else None + if test_merged is not None: + metadata["n_theta_test_points"] = len(test_merged) + if len(test_merged) > 0: + theta_test_stats = _run_stats( + test_merged["median"].values, + test_merged["theta_obs"].values, + ) except Exception as exc: warnings.warn(f"Could not compute theta test statistics: {exc}") diff --git a/tests/tfscreen/plot/test_geno_trajectory.py b/tests/tfscreen/plot/test_geno_trajectory.py new file mode 100644 index 00000000..3bcb11b2 --- /dev/null +++ b/tests/tfscreen/plot/test_geno_trajectory.py @@ -0,0 +1,484 @@ +""" +Tests for tfscreen.plot.geno_trajectory.plot_geno_trajectory. + +Strategy +-------- +The public function has two expensive internal steps: (a) a JAX forward +pass via _compute_map_predictions / _compute_posterior_predictions, and +(b) matplotlib PDF rendering. Both are mocked out here so tests run fast +and without GPU/display dependencies. + +A helper ``_make_fake_gm`` constructs a minimal mock ModelOrchestrator +whose attributes match what plot_geno_trajectory reads. The mock returns +deterministic numpy arrays for all data tensors and a small but realistic +dimension structure (2 replicates, 3 timepoints, 1 condition_pre, +1 condition_sel, 1 titrant, 1 concentration, 2 genotypes). + +The prediction arrays (growth_pred, growth_pred_fine, ln_cfu0_map) are +returned by mocked helpers so no JAX compilation is triggered. +""" + +import json +import os +import sys +import tempfile +from unittest.mock import MagicMock, patch + +import numpy as np +import pandas as pd +import pytest + +from tfscreen.plot.geno_trajectory import ( + _compute_posterior_predictions, + plot_geno_trajectory, +) + + +# --------------------------------------------------------------------------- +# Shared shape constants +# --------------------------------------------------------------------------- + +N_REP, N_T, N_CP, N_CS, N_TN, N_TC, N_GENO = 2, 3, 1, 1, 1, 1, 2 +SHAPE = (N_REP, N_T, N_CP, N_CS, N_TN, N_TC, N_GENO) +GENO_NAMES = ["WT", "mut1"] +REP_NAMES = ["rep1", "rep2"] +CP_NAMES = ["preinduction"] +CS_NAMES = ["selection"] +TN_NAMES = ["IPTG"] +TC_VALS = [0.0] + + +# --------------------------------------------------------------------------- +# Fake ModelOrchestrator +# --------------------------------------------------------------------------- + +def _make_fake_gm(with_presplit=False): + """ + Return a MagicMock that quacks like a ModelOrchestrator for the + attributes accessed by plot_geno_trajectory. + """ + rng = np.random.default_rng(0) + + # All timepoints are valid except the last one for rep 0 genotype 0. + good_mask = np.ones(SHAPE, dtype=bool) + good_mask[0, -1, 0, 0, 0, 0, 0] = False + + t_sel = np.broadcast_to( + np.linspace(0.0, 10.0, N_T)[None, :, None, None, None, None, None], + SHAPE, + ).copy() + t_pre = np.full(SHAPE, 5.0) + ln_cfu = rng.normal(10.0, 1.0, SHAPE) + ln_cfu_std = np.ones(SHAPE) * 0.5 + + gd = MagicMock() + gd.good_mask = good_mask + gd.t_sel = t_sel + gd.t_pre = t_pre + gd.ln_cfu = ln_cfu + gd.ln_cfu_std = ln_cfu_std + + # --- presplit --- + presplit = None + if with_presplit: + ps = MagicMock() + ps.ln_cfu_t0 = rng.normal(10.0, 0.5, (N_REP, N_CP, N_GENO)) + ps.ln_cfu_t0_std = np.ones((N_REP, N_CP, N_GENO)) * 0.3 + ps.good_mask = np.ones((N_REP, N_CP, N_GENO), dtype=bool) + presplit = ps + + data = MagicMock() + data.growth = gd + data.presplit = presplit + data.num_genotype = N_GENO + + # --- TensorManager --- + # Build a small DataFrame that mirrors what tm.df looks like after + # TensorManager.create_tensors(). Only the columns that plot_geno_trajectory + # accesses are required. + rows = [] + for r_i in range(N_REP): + for t_i in range(N_T): + for g_i in range(N_GENO): + rows.append({ + "replicate_idx": r_i, + "condition_pre_idx": 0, + "condition_sel_idx": 0, + "titrant_name_idx": 0, + "titrant_conc_idx": 0, + "genotype_idx": g_i, + "t_sel": t_sel[r_i, t_i, 0, 0, 0, 0, g_i], + "ln_cfu": ln_cfu[r_i, t_i, 0, 0, 0, 0, g_i], + "genotype": GENO_NAMES[g_i], + "condition_sel": "selection", + "condition_pre": "preinduction", + }) + tm_df = pd.DataFrame(rows) + + tm = MagicMock() + tm.tensor_dim_names = [ + "replicate", "time", "condition_pre", "condition_sel", + "titrant_name", "titrant_conc", "genotype", + ] + tm.tensor_dim_labels = [ + REP_NAMES, [f"t{i}" for i in range(N_T)], + CP_NAMES, CS_NAMES, TN_NAMES, TC_VALS, GENO_NAMES, + ] + tm.df = tm_df + + binding_df = pd.DataFrame({ + "genotype": GENO_NAMES * 2, + "titrant_name": ["IPTG"] * 4, + "titrant_conc": [0.0] * 4, + "theta_obs": [0.5, 0.3, 0.5, 0.3], + }) + + gm = MagicMock() + gm.data = data + gm.growth_tm = tm + gm.binding_df = binding_df + + return gm + + +def _fake_map_predictions(shape): + """Return plausible synthetic MAP prediction tensors.""" + rng = np.random.default_rng(1) + growth_pred = rng.normal(10.0, 1.0, shape) + ln_cfu0_map = rng.normal(10.0, 0.5, (shape[0], shape[2], shape[6])) + growth_pred_fine = rng.normal(10.0, 1.0, + (shape[0], 50, shape[2], shape[3], + shape[4], shape[5], shape[6])) + t_fine_1d = np.linspace(0.0, 10.0, 50) + return growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d + + +# --------------------------------------------------------------------------- +# Argument validation +# --------------------------------------------------------------------------- + +class TestArgumentValidation: + def test_raises_when_neither_provided(self, tmp_path): + gm = _make_fake_gm() + with pytest.raises(ValueError, match="Exactly one"): + plot_geno_trajectory(gm, str(tmp_path / "out")) + + def test_raises_when_both_provided(self, tmp_path): + gm = _make_fake_gm() + with pytest.raises(ValueError, match="Exactly one"): + plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + posterior_file="some_file.h5", + ) + + +# --------------------------------------------------------------------------- +# MAP path — basic smoke test with mocked internals +# --------------------------------------------------------------------------- + +class TestMapPath: + @pytest.fixture + def patched_plot(self, tmp_path): + """Patch _compute_map_predictions and matplotlib.pyplot.savefig.""" + gm = _make_fake_gm() + fake_preds = _fake_map_predictions(SHAPE) + + with patch( + "tfscreen.plot.geno_trajectory._compute_map_predictions", + return_value=fake_preds, + ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): + yield gm, tmp_path + + def test_returns_dataframe(self, patched_plot): + gm, tmp_path = patched_plot + result = plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + ) + assert isinstance(result, pd.DataFrame) + + def test_df_has_ln_cfu_pred(self, patched_plot): + gm, tmp_path = patched_plot + result = plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + ) + assert "ln_cfu_pred" in result.columns + + def test_csv_written_by_default(self, patched_plot): + gm, tmp_path = patched_plot + plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + ) + assert os.path.exists(tmp_path / "out_calib_growth_df.csv") + + def test_csv_not_written_when_disabled(self, patched_plot): + gm, tmp_path = patched_plot + plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + write_csv=False, + ) + assert not os.path.exists(tmp_path / "out_calib_growth_df.csv") + + def test_growth_pred_std_in_csv(self, patched_plot): + gm, tmp_path = patched_plot + gps = np.ones(SHAPE) * 0.1 + plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + growth_pred_std=gps, + ) + df = pd.read_csv(tmp_path / "out_calib_growth_df.csv") + assert "ln_cfu_pred_std" in df.columns + + +# --------------------------------------------------------------------------- +# Genotype filtering +# --------------------------------------------------------------------------- + +class TestGenotypeFilter: + @pytest.fixture + def gm_and_path(self, tmp_path): + gm = _make_fake_gm() + return gm, tmp_path + + def _run(self, gm, tmp_path, **kwargs): + fake_preds = _fake_map_predictions(SHAPE) + with patch( + "tfscreen.plot.geno_trajectory._compute_map_predictions", + return_value=fake_preds, + ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): + return plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + **kwargs, + ) + + def test_no_filter_plots_all_genotypes(self, gm_and_path, capsys): + gm, tmp_path = gm_and_path + self._run(gm, tmp_path) + captured = capsys.readouterr() + for name in GENO_NAMES: + assert name in captured.out + + def test_genotype_subset(self, gm_and_path, capsys): + gm, tmp_path = gm_and_path + self._run(gm, tmp_path, genotypes=["WT"]) + captured = capsys.readouterr() + assert "WT" in captured.out + assert "mut1" not in captured.out + + def test_unknown_genotype_warns(self, gm_and_path, capsys): + gm, tmp_path = gm_and_path + self._run(gm, tmp_path, genotypes=["WT", "nonexistent"]) + captured = capsys.readouterr() + assert "nonexistent" in captured.err + + def test_all_unknown_returns_df(self, gm_and_path): + gm, tmp_path = gm_and_path + result = self._run(gm, tmp_path, genotypes=["nonexistent"]) + # Should still return a DataFrame (from the full data), just no PDFs. + assert isinstance(result, pd.DataFrame) + + +# --------------------------------------------------------------------------- +# Titrant_name filtering +# --------------------------------------------------------------------------- + +class TestTitrantNameFilter: + def _run(self, gm, tmp_path, **kwargs): + fake_preds = _fake_map_predictions(SHAPE) + with patch( + "tfscreen.plot.geno_trajectory._compute_map_predictions", + return_value=fake_preds, + ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): + return plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + **kwargs, + ) + + def test_matching_titrant_produces_output(self, tmp_path, capsys): + gm = _make_fake_gm() + self._run(gm, tmp_path, titrant_names=["IPTG"]) + captured = capsys.readouterr() + # PDFs should still be saved (the filter matches). + assert "Saved" in captured.out + + def test_nonmatching_titrant_no_pdfs(self, tmp_path, capsys): + gm = _make_fake_gm() + self._run(gm, tmp_path, titrant_names=["arabinose"]) + captured = capsys.readouterr() + # No valid condition combos → no PDF files saved (the CSV may still be). + assert ".pdf" not in captured.out + + +# --------------------------------------------------------------------------- +# Presplit overlay +# --------------------------------------------------------------------------- + +class TestPresplit: + def _run(self, gm, tmp_path): + fake_preds = _fake_map_predictions(SHAPE) + ax_mock = MagicMock() + # Capture all errorbar calls to verify presplit is rendered. + ax_mock.errorbar = MagicMock() + ax_mock.plot = MagicMock() + ax_mock.legend = MagicMock() + ax_mock.set_title = MagicMock() + ax_mock.set_xlabel = MagicMock() + ax_mock.set_ylabel = MagicMock() + ax_mock.axvline = MagicMock() + ax_mock.set_xlim = MagicMock() + ax_mock.set_visible = MagicMock() + + axes_array = np.empty((1, 1), dtype=object) + axes_array[0, 0] = ax_mock + + with patch( + "tfscreen.plot.geno_trajectory._compute_map_predictions", + return_value=fake_preds, + ), patch( + "matplotlib.pyplot.subplots", + return_value=(MagicMock(), axes_array), + ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): + plot_geno_trajectory( + gm, + str(tmp_path / "out"), + params={"x_auto_loc": np.array(1.0)}, + ) + return ax_mock + + @staticmethod + def _is_scalar_negative(x): + """True iff x is a 0-d value strictly less than zero. + + The presplit errorbar passes a scalar (−t_pre). Selection-phase + errorbars pass an array of t_sel values. Using ndim prevents + float() from raising on multi-element arrays. + """ + a = np.asarray(x) + return a.ndim == 0 and float(a) < 0 + + def test_presplit_errorbar_called_when_data_present(self, tmp_path): + gm = _make_fake_gm(with_presplit=True) + ax_mock = self._run(gm, tmp_path) + # The presplit errorbar is called with a scalar negative x (−t_pre); + # selection-phase errorbars use array x values. + neg_x_calls = [ + c for c in ax_mock.errorbar.call_args_list + if c.args and self._is_scalar_negative(c.args[0]) + ] + assert len(neg_x_calls) > 0, ( + "Expected at least one scalar-negative-x errorbar call (presplit) " + f"but got: {ax_mock.errorbar.call_args_list}" + ) + + def test_no_presplit_errorbar_at_negative_x(self, tmp_path): + gm = _make_fake_gm(with_presplit=False) + ax_mock = self._run(gm, tmp_path) + neg_x_calls = [ + c for c in ax_mock.errorbar.call_args_list + if c.args and self._is_scalar_negative(c.args[0]) + ] + assert len(neg_x_calls) == 0 + + +# --------------------------------------------------------------------------- +# Posterior path +# --------------------------------------------------------------------------- + +class TestPosteriorPath: + def _make_h5(self, tmp_path): + """Write a minimal synthetic posterior HDF5 file.""" + import h5py + + h5_path = str(tmp_path / "posterior.h5") + n_samples = 5 + with h5py.File(h5_path, "w") as fh: + fh.create_dataset( + "growth_pred", + data=np.random.default_rng(2).normal(10.0, 1.0, + (n_samples,) + SHAPE), + ) + fh.create_dataset( + "ln_cfu0", + data=np.random.default_rng(3).normal(10.0, 0.5, + (n_samples, N_REP, + N_CP, N_GENO)), + ) + return h5_path + + def test_compute_posterior_predictions_shapes(self, tmp_path): + h5_path = self._make_h5(tmp_path) + gm = _make_fake_gm() + med, lo, hi, ln_cfu0 = _compute_posterior_predictions(gm, h5_path) + assert med.shape == SHAPE + assert lo.shape == SHAPE + assert hi.shape == SHAPE + assert ln_cfu0.shape == (N_REP, N_CP, N_GENO) + + def test_compute_posterior_predictions_ordering(self, tmp_path): + """5th percentile ≤ median ≤ 95th percentile everywhere.""" + h5_path = self._make_h5(tmp_path) + gm = _make_fake_gm() + med, lo, hi, _ = _compute_posterior_predictions(gm, h5_path) + assert np.all(lo <= med + 1e-9) + assert np.all(med <= hi + 1e-9) + + def test_posterior_path_returns_df_with_lo_hi(self, tmp_path): + h5_path = self._make_h5(tmp_path) + gm = _make_fake_gm() + with patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): + result = plot_geno_trajectory( + gm, + str(tmp_path / "out"), + posterior_file=h5_path, + ) + assert isinstance(result, pd.DataFrame) + assert "ln_cfu_pred_lo" in result.columns + assert "ln_cfu_pred_hi" in result.columns + + def test_posterior_raises_on_missing_growth_pred(self, tmp_path): + import h5py + + h5_path = str(tmp_path / "empty.h5") + with h5py.File(h5_path, "w") as fh: + fh.create_dataset("something_else", data=np.zeros(3)) + + gm = _make_fake_gm() + with pytest.raises(ValueError, match="growth_pred not found"): + _compute_posterior_predictions(gm, h5_path) + + def test_params_path_loads_npz(self, tmp_path): + """Passing a string path to params loads the .npz file.""" + npz_path = str(tmp_path / "params.npz") + np.savez(npz_path, x_auto_loc=np.array(1.0)) + + fake_preds = _fake_map_predictions(SHAPE) + gm = _make_fake_gm() + + with patch( + "tfscreen.plot.geno_trajectory._compute_map_predictions", + return_value=fake_preds, + ) as mock_fn, patch("matplotlib.pyplot.savefig"), patch( + "matplotlib.pyplot.close" + ): + plot_geno_trajectory(gm, str(tmp_path / "out"), params=npz_path) + + # The helper should have been called with a dict (loaded from npz), + # not the raw string path. + called_params = mock_fn.call_args[0][1] + assert isinstance(called_params, dict) + assert "x_auto_loc" in called_params diff --git a/tests/tfscreen/simulate/test_thermo_to_growth.py b/tests/tfscreen/simulate/test_thermo_to_growth.py index 72037778..31a6808a 100644 --- a/tests/tfscreen/simulate/test_thermo_to_growth.py +++ b/tests/tfscreen/simulate/test_thermo_to_growth.py @@ -345,13 +345,16 @@ def test_thermo_to_growth_integration( phenotype_df["k_sel"].to_numpy(), b_sel + activity * m_sel * theta + dk ) - # genotype_theta_df: one row per unique genotype, columns theta_at_*mM + # genotype_theta_df: long form — one row per (genotype, titrant_name, titrant_conc) assert isinstance(genotype_theta_df, pd.DataFrame) - assert "genotype" in genotype_theta_df.columns - theta_cols = [c for c in genotype_theta_df.columns if c.startswith("theta_at_")] - assert len(theta_cols) == 2 # two unique concentrations - # No duplicate genotypes - assert genotype_theta_df["genotype"].nunique() == len(genotype_theta_df) + assert list(genotype_theta_df.columns) == ["genotype", "titrant_name", + "titrant_conc", "theta"] + # 3 unique genotypes × 2 unique concentrations = 6 rows + assert len(genotype_theta_df) == 6 + # Each (genotype, titrant_name, titrant_conc) triple is unique + assert genotype_theta_df.duplicated( + subset=["genotype", "titrant_name", "titrant_conc"] + ).sum() == 0 # parameters_df: one row per unique genotype, dk_geno + activity always present assert isinstance(parameters_df, pd.DataFrame) @@ -393,9 +396,12 @@ def test_genotype_theta_df_no_duplicates_with_repeated_genotypes( growth_params=simple_growth_params, ) - assert genotype_theta_df["genotype"].nunique() == len(genotype_theta_df), \ - "genotype_theta_df must not contain duplicate genotype rows" - assert len(genotype_theta_df) == 3 # wt, A1B, A1B/C2D + # 3 unique genotypes × 2 concentrations = 6 rows; no duplicated (geno, conc) pairs + assert genotype_theta_df["genotype"].nunique() == 3 + assert len(genotype_theta_df) == 6 + assert genotype_theta_df.duplicated( + subset=["genotype", "titrant_name", "titrant_conc"] + ).sum() == 0 def test_thermo_to_growth_propagates_rng( diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py index 21474eea..3645a178 100644 --- a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py +++ b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py @@ -34,7 +34,7 @@ _identify_field_mapping, _inject_calibration_priors, _intersect_data, - _make_calibration_plots, + _run_calibration_diagnostics, _make_correlation_plot, _resolve_csv_paths, _write_calibration_stats, @@ -862,7 +862,7 @@ def _patch_pipeline(self, mocker, gm_cal=None, params=None, ) mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_calibration_plots", + ".run_prefit_calibration_cli._run_calibration_diagnostics", ) return mock_ri, mock_run_map @@ -1011,10 +1011,10 @@ def test_compute_growth_pred_std_called_with_ri_and_params(self, tmp_path, assert call_args.args[0] is mock_ri # first positional: ri assert call_args.args[1] is mock_params # second positional: params - def test_growth_pred_std_forwarded_to_make_calibration_plots(self, tmp_path, - mocker): + def test_growth_pred_std_forwarded_to_run_calibration_diagnostics( + self, tmp_path, mocker): """The growth_pred_std returned by _compute_growth_pred_std is passed - as the growth_pred_std keyword argument to _make_calibration_plots.""" + as the growth_pred_std keyword argument to _run_calibration_diagnostics.""" cfg, _, _ = self._write_yaml_and_csvs(tmp_path) sentinel = np.ones((1, 2, 1, 1, 1, 1, 2)) self._patch_pipeline(mocker) @@ -1024,14 +1024,14 @@ def test_growth_pred_std_forwarded_to_make_calibration_plots(self, tmp_path, ".run_prefit_calibration_cli._compute_growth_pred_std", return_value=sentinel, ) - mock_plots = mocker.patch( + mock_diag = mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_calibration_plots", + ".run_prefit_calibration_cli._run_calibration_diagnostics", ) run_prefit_calibration(config_file=cfg, seed=1) - plots_kwargs = mock_plots.call_args.kwargs - assert plots_kwargs.get("growth_pred_std") is sentinel + diag_kwargs = mock_diag.call_args.kwargs + assert diag_kwargs.get("growth_pred_std") is sentinel # --------------------------------------------------------------------------- @@ -1087,324 +1087,6 @@ def test_main_forwards_checkpoint_file(self, tmp_path): assert mock_run.call_args.kwargs["checkpoint_file"] == "/tmp/ck.pkl" -# --------------------------------------------------------------------------- -# _make_calibration_plots -# --------------------------------------------------------------------------- - -def _build_fake_gm_cal(n_rep=1, n_t=3, n_cp=1, n_cs=1, n_tn=1, n_tc=1, - n_geno=2, include_ln_cfu0=True): - """ - Build a minimal MagicMock stand-in for ModelOrchestrator that satisfies - every attribute access in _make_calibration_plots. - - Tensor shape convention: (R, T, CP, CS, TN, TC, G). - """ - shape = (n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno) - - # growth data tensors - good_mask = np.ones(shape, dtype=bool) - good_mask[0, 0, 0, 0, 0, 0, 0] = False # one padded cell to exercise masking - t_pre_tensor = np.full(shape, 5.0) - t_sel_tensor = np.zeros(shape) - for t_i in range(n_t): - t_sel_tensor[:, t_i, ...] = float(t_i + 1) - ln_cfu_obs = np.ones(shape) * 10.0 - ln_cfu_std = np.ones(shape) * 0.5 - - gd = MagicMock() - gd.good_mask = good_mask - gd.t_pre = t_pre_tensor - gd.t_sel = t_sel_tensor - gd.ln_cfu = ln_cfu_obs - gd.ln_cfu_std = ln_cfu_std - - data = MagicMock() - data.growth = gd - data.num_genotype = n_geno - - # TensorManager mock - dim_names = ["replicate", "condition_pre", "condition_sel", - "titrant_name", "titrant_conc", "genotype"] - dim_labels = [ - [f"rep{i}" for i in range(n_rep)], - [f"cp{i}" for i in range(n_cp)], - [f"cs{i}" for i in range(n_cs)], - [f"tn{i}" for i in range(n_tn)], - [float(i) for i in range(n_tc)], - [f"geno{i}" for i in range(n_geno)], - ] - - # Build a realistic tm.df with all index columns needed for the merge in - # _write_predictions_csv, and condition_sel for cs_name_map building. - rows = [] - for r_i in range(n_rep): - for t_i in range(n_t): - for cp_i in range(n_cp): - for cs_i in range(n_cs): - for tn_i in range(n_tn): - for tc_i in range(n_tc): - for g_i in range(n_geno): - rows.append({ - "replicate_idx": r_i, - "condition_pre_idx": cp_i, - "condition_sel_idx": cs_i, - "titrant_name_idx": tn_i, - "titrant_conc_idx": tc_i, - "genotype_idx": g_i, - "t_sel": float(t_i + 1), - "condition_sel": f"cs{cs_i}", - }) - df = pd.DataFrame(rows) - - tm = MagicMock() - tm.tensor_dim_names = dim_names - tm.tensor_dim_labels = dim_labels - tm.df = df - - # MAP samples returned by Predictive - growth_pred_val = np.ones((1,) + shape) * 10.0 # (samples, R,T,CP,CS,TN,TC,G) - map_samples = {"growth_pred": growth_pred_val} - if include_ln_cfu0: - ln_cfu0_shape = (1, n_rep, n_cp, n_geno) # (samples, R, CP, G) - map_samples["ln_cfu0"] = np.ones(ln_cfu0_shape) * 9.0 - - # Configure the full_data mock returned by get_batch. The production code - # accesses full_data.growth.{map_condition_pre, map_condition_sel, t_pre} - # to build the fine-grid data; they must be proper numpy arrays so that - # np.asarray() and slicing work correctly inside _bc_t. - map_cond = np.zeros(shape, dtype=int) # constant-zero indices are fine - full_data_gd = MagicMock() - full_data_gd.map_condition_pre = map_cond - full_data_gd.map_condition_sel = map_cond - full_data_gd.t_pre = t_pre_tensor - full_data_gd.good_mask = good_mask - # .replace() is called but its return value is only passed to pred_fn, - # which is fully mocked; so returning a plain MagicMock is fine. - full_data_mock = MagicMock() - full_data_mock.growth = full_data_gd - - gm_cal = MagicMock() - gm_cal.data = data - gm_cal.priors = MagicMock() - gm_cal.growth_tm = tm - gm_cal.jax_model = MagicMock() - gm_cal.get_batch = MagicMock(return_value=full_data_mock) - - return gm_cal, map_samples - - -class TestMakeCalibrationPlots: - """Tests for _make_calibration_plots.""" - - def _patch_predictive(self, mocker, map_samples): - """Patch numpyro.infer.Predictive and AutoDelta inside the function. - - _make_calibration_plots calls pred_fn twice: - 1st call — checked for 'growth_pred' presence and provides ln_cfu0. - 2nd call — fine-grid predictions; growth_pred must have T_FINE=50 - time points so that np.concatenate([..., t_fine_1d], - [..., y_smooth_sel]) does not mis-match. - """ - T_FINE = 50 - if "growth_pred" in map_samples: - orig_gp = map_samples["growth_pred"] # (1, R, n_t, CP, CS, TN, TC, G) - shape_fine = list(orig_gp.shape) - shape_fine[2] = T_FINE - map_samples_fine = {**map_samples, - "growth_pred": np.ones(shape_fine) * 10.0} - else: - map_samples_fine = map_samples - - mock_pred_instance = MagicMock( - side_effect=[map_samples, map_samples_fine] - ) - mock_pred_cls = mocker.patch( - "numpyro.infer.Predictive", - return_value=mock_pred_instance, - ) - mocker.patch("numpyro.infer.autoguide.AutoDelta") - mocker.patch("jax.random.PRNGKey", return_value=0) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.jnp.arange", - return_value=np.arange(2), - ) - return mock_pred_cls - - def test_creates_one_pdf_per_genotype(self, tmp_path, mocker): - gm_cal, map_samples = _build_fake_gm_cal(n_geno=3) - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, out_prefix=str(tmp_path / "run")) - - pdfs = sorted(tmp_path.glob("*.pdf")) - assert len(pdfs) == 3 - - def test_pdf_names_use_out_prefix_and_genotype(self, tmp_path, mocker): - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2) - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "myrun")) - - pdf_names = {p.name for p in tmp_path.glob("*.pdf")} - assert "myrun_calib_geno0.pdf" in pdf_names - assert "myrun_calib_geno1.pdf" in pdf_names - - def test_slash_in_genotype_name_replaced_with_dash(self, tmp_path, mocker): - """Genotype names containing '/' (multi-mutation notation) must have '/' - replaced with '-' so the output path is a valid filename.""" - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2) - # Override genotype labels so one contains a slash. - gm_cal.growth_tm.tensor_dim_labels[ - gm_cal.growth_tm.tensor_dim_names.index("genotype") - ] = ["A1T/G2P", "wt"] - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - pdf_names = {p.name for p in tmp_path.glob("*.pdf")} - assert "run_calib_A1T-G2P.pdf" in pdf_names - assert "run_calib_wt.pdf" in pdf_names - # The original slash form must not appear as a file. - assert not any("A1T/G2P" in name for name in pdf_names) - - def test_skips_genotype_with_no_valid_observations(self, tmp_path, mocker): - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2) - - # Make all observations invalid for genotype 0 only. - good_mask = np.asarray(gm_cal.data.growth.good_mask) - good_mask[..., 0] = False - gm_cal.data.growth.good_mask = good_mask - - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - pdfs = {p.name for p in tmp_path.glob("*.pdf")} - assert "run_calib_geno0.pdf" not in pdfs - assert "run_calib_geno1.pdf" in pdfs - - def test_warns_and_returns_when_growth_pred_missing(self, tmp_path, mocker, - capsys): - gm_cal, _ = _build_fake_gm_cal() - # Exclude growth_pred so the function should bail out early. - map_samples_no_pred = {"ln_cfu0": np.ones((1, 1, 1, 2)) * 9.0} - self._patch_predictive(mocker, map_samples_no_pred) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - assert not list(tmp_path.glob("*.pdf")) - captured = capsys.readouterr() - assert "growth_pred" in captured.err - - def test_handles_missing_ln_cfu0_gracefully(self, tmp_path, mocker): - """When ln_cfu0 is absent from map_samples, falls back to val_at_t0.""" - gm_cal, map_samples = _build_fake_gm_cal(include_ln_cfu0=False) - self._patch_predictive(mocker, map_samples) - - # Should not raise even without ln_cfu0. - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - assert len(list(tmp_path.glob("*.pdf"))) == 2 - - def test_single_time_point_does_not_crash(self, tmp_path, mocker): - """polyfit requires ≥2 pts; single-point case uses fallback branch.""" - gm_cal, map_samples = _build_fake_gm_cal(n_t=1) - # Remove the masked cell so both genotypes have a valid observation. - good_mask = np.asarray(gm_cal.data.growth.good_mask) - good_mask[:] = True - gm_cal.data.growth.good_mask = good_mask - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - # A PDF must still be produced for each genotype. - assert len(list(tmp_path.glob("*.pdf"))) == 2 - - def test_matplotlib_unavailable_warns_and_returns(self, tmp_path, mocker, - capsys): - """If matplotlib cannot be imported, a warning is emitted and no PDF - is written.""" - gm_cal, map_samples = _build_fake_gm_cal() - self._patch_predictive(mocker, map_samples) - - # Simulate ImportError for matplotlib by making the import raise. - real_import = __builtins__["__import__"] if isinstance(__builtins__, dict) \ - else __import__ - - def fake_import(name, *args, **kwargs): - if name == "matplotlib": - raise ImportError("no matplotlib") - return real_import(name, *args, **kwargs) - - mocker.patch("builtins.__import__", side_effect=fake_import) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - assert not list(tmp_path.glob("*.pdf")) - captured = capsys.readouterr() - assert "matplotlib" in captured.err - - def test_writes_predictions_csv(self, tmp_path, mocker): - """A CSV named {out_prefix}_calib_growth_df.csv is written with a - ln_cfu_pred column populated for every valid observation.""" - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2, n_t=3) - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - csv_path = tmp_path / "run_calib_growth_df.csv" - assert csv_path.exists(), "predictions CSV was not created" - - result = pd.read_csv(csv_path) - assert "ln_cfu_pred" in result.columns - - # The default fixture sets growth_pred to 10.0 everywhere; the masked - # cell (r=0, t=0, cp=0, cs=0, tn=0, tc=0, g=0) stays NaN because it - # is excluded from good_mask and therefore from pred_lookup. - assert result["ln_cfu_pred"].notna().any() - valid_preds = result["ln_cfu_pred"].dropna() - assert np.allclose(valid_preds.values, 10.0) - - def test_writes_ln_cfu_pred_std_when_provided(self, tmp_path, mocker): - """ln_cfu_pred_std column appears in the CSV when growth_pred_std is given.""" - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2, n_t=3) - self._patch_predictive(mocker, map_samples) - - good_mask = np.asarray(gm_cal.data.growth.good_mask) - std_array = np.full(good_mask.shape, 0.25) - - _make_calibration_plots( - gm_cal, params={}, - out_prefix=str(tmp_path / "run"), - growth_pred_std=std_array, - ) - - result = pd.read_csv(tmp_path / "run_calib_growth_df.csv") - assert "ln_cfu_pred_std" in result.columns - valid_std = result.dropna(subset=["ln_cfu_pred_std"])["ln_cfu_pred_std"] - assert np.allclose(valid_std.values, 0.25) - - def test_no_ln_cfu_pred_std_column_when_not_provided(self, tmp_path, mocker): - """When growth_pred_std is None the CSV must not contain the std column.""" - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2, n_t=3) - self._patch_predictive(mocker, map_samples) - - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) - - result = pd.read_csv(tmp_path / "run_calib_growth_df.csv") - assert "ln_cfu_pred_std" not in result.columns - - # --------------------------------------------------------------------------- # _compute_growth_pred_std # --------------------------------------------------------------------------- @@ -1717,9 +1399,9 @@ def fake_import(name, *args, **kwargs): _make_correlation_plot(df, str(tmp_path / "run")) assert not (tmp_path / "run_calib_correlation.pdf").exists() - def test_make_calibration_plots_calls_both_helpers(self, tmp_path, mocker): - """_make_calibration_plots must call _write_calibration_stats and - _make_correlation_plot after writing the CSV.""" + def test_run_calibration_diagnostics_calls_both_helpers(self, tmp_path, mocker): + """_run_calibration_diagnostics must call _write_calibration_stats and + _make_correlation_plot after plot_geno_trajectory returns a DataFrame.""" mock_stats = mocker.patch( "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli._write_calibration_stats", @@ -1728,29 +1410,27 @@ def test_make_calibration_plots_calls_both_helpers(self, tmp_path, mocker): "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli._make_correlation_plot", ) - gm_cal, map_samples = _build_fake_gm_cal(n_geno=2, n_t=2) - - # Minimal _patch_predictive inline (avoids importing the fixture helper). - T_FINE = 50 - orig = map_samples["growth_pred"] - shape_fine = list(orig.shape); shape_fine[2] = T_FINE - fine = {**map_samples, "growth_pred": np.ones(shape_fine) * 10.0} - mock_pred = MagicMock(side_effect=[map_samples, fine]) - mocker.patch("numpyro.infer.Predictive", return_value=mock_pred) - mocker.patch("numpyro.infer.autoguide.AutoDelta") - mocker.patch("jax.random.PRNGKey", return_value=0) + + # plot_geno_trajectory returns a fake predictions DataFrame. + fake_df = pd.DataFrame({ + "ln_cfu": [1.0, 2.0, 3.0], + "ln_cfu_pred": [1.1, 2.2, np.nan], # one NaN → n_obs == 2 + }) mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.jnp.arange", - return_value=np.arange(2), + ".run_prefit_calibration_cli.plot_geno_trajectory", + return_value=fake_df, ) - _make_calibration_plots(gm_cal, params={}, - out_prefix=str(tmp_path / "run")) + # params has one _auto_loc key with 2 elements → n_params == 2. + params = {"alpha_auto_loc": np.array([0.5, 0.6])} + _run_calibration_diagnostics( + MagicMock(), params, out_prefix=str(tmp_path / "run") + ) assert mock_stats.call_count == 1 assert mock_corr.call_count == 1 - # Both receive the same merged DataFrame as first argument. + # Both receive the same DataFrame as first argument. df_arg_stats = mock_stats.call_args.args[0] df_arg_corr = mock_corr.call_args.args[0] assert isinstance(df_arg_stats, pd.DataFrame) diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index ad9a4605..be76a636 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -127,20 +127,25 @@ def run_dir(tmp_path): return str(tmp_path) -@pytest.fixture -def ground_truth_file(tmp_path): - """Ground-truth file containing the 4th genotype (not in training data).""" +def _make_ground_truth_csv(path, theta_col="theta_obs"): + """Write a ground-truth CSV for the 4th genotype using the given column name.""" rows = [] for tc in TITRANT_CONCS: rows.append({ "genotype": "E4F", "titrant_name": TITRANT_NAME, "titrant_conc": tc, - "theta_obs": THETA_OBS[3] + tc * 0.0001, + theta_col: THETA_OBS[3] + tc * 0.0001, "theta_std": 0.02, }) - path = str(tmp_path / "ground_truth.csv") pd.DataFrame(rows).to_csv(path, index=False) + + +@pytest.fixture +def ground_truth_file(tmp_path): + """Ground-truth file containing the 4th genotype (not in training data).""" + path = str(tmp_path / "ground_truth.csv") + _make_ground_truth_csv(path, theta_col="theta_obs") return path @@ -456,6 +461,43 @@ def test_theta_test_pearson_r_reasonable(self, run_dir, ground_truth_file): # Predictions are close to truth so r should be high assert r is not None and r > 0.9 + def test_theta_col_fallback_to_theta(self, run_dir, tmp_path): + """Ground-truth file with 'theta' column (no 'theta_obs') still works.""" + pred_path = os.path.join(run_dir, "run_theta_pred.csv") + _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) + + gt_path = str(tmp_path / "gt_theta_col.csv") + _make_ground_truth_csv(gt_path, theta_col="theta") + + summarize_fit(run_dir, ground_truth_file=gt_path) + with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + data = json.load(fh) + assert data["theta"]["test"] is not None + assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) + + def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): + """Ground-truth file with no theta column issues a warning and skips test stats.""" + pred_path = os.path.join(run_dir, "run_theta_pred.csv") + _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) + + gt_path = str(tmp_path / "gt_no_theta.csv") + pd.DataFrame({ + "genotype": ["E4F"], + "titrant_name": [TITRANT_NAME], + "titrant_conc": [0.0], + "some_other_col": [0.5], + }).to_csv(gt_path, index=False) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + summarize_fit(run_dir, ground_truth_file=gt_path) + assert any("neither" in str(x.message).lower() for x in w) + + with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + data = json.load(fh) + assert data["theta"]["test"] is None + assert data["metadata"]["n_theta_test_points"] is None + # --------------------------------------------------------------------------- # summarize_fit — graceful failure cases diff --git a/tests/tfscreen/tfmodel/test_prediction.py b/tests/tfscreen/tfmodel/test_prediction.py index f66e8ff8..daf2657c 100644 --- a/tests/tfscreen/tfmodel/test_prediction.py +++ b/tests/tfscreen/tfmodel/test_prediction.py @@ -1,8 +1,12 @@ import pytest import pandas as pd import numpy as np +import jax.numpy as jnp +import numpyro.distributions as dist +from numpyro.distributions.transforms import biject_to + from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator -from tfscreen.tfmodel.analysis.prediction import copy_model_class +from tfscreen.tfmodel.analysis.prediction import copy_model_class, _convert_map_params @pytest.fixture def dummy_mc(): @@ -86,8 +90,135 @@ def test_copy_model_class_no_subsetting_binding(dummy_mc): """Test that binding_df is NOT subsetted during copy.""" # Original binding_df has wt and M42V assert len(dummy_mc.binding_df) == 2 - + new_mc = copy_model_class(dummy_mc) - + # New binding_df should still have all genotypes assert len(new_mc.binding_df) == 2 + + +# --------------------------------------------------------------------------- +# _convert_map_params +# --------------------------------------------------------------------------- + +@pytest.fixture +def fake_trace(): + """ + Minimal model trace for testing _convert_map_params. + + Includes: + - an unconstrained site (Normal → real support → identity bijection) + - a positive-constrained site (HalfNormal → positive support → softplus bijection) + - an observed site (must be ignored) + - a deterministic site (must be ignored) + """ + return { + "real_site": { + "type": "sample", + "is_observed": False, + "fn": dist.Normal(0.0, 1.0), + }, + "positive_site": { + "type": "sample", + "is_observed": False, + "fn": dist.HalfNormal(1.0), + }, + "observed_site": { + "type": "sample", + "is_observed": True, + "fn": dist.Normal(0.0, 1.0), + }, + "det_site": { + "type": "deterministic", + }, + } + + +class TestConvertMapParams: + """Unit tests for _convert_map_params.""" + + def test_keys_are_renamed(self, fake_trace): + """_auto_loc suffix is stripped from output keys.""" + map_params = {"real_site_auto_loc": np.array(0.5)} + out = _convert_map_params(map_params, fake_trace) + assert "real_site" in out + assert "real_site_auto_loc" not in out + + def test_leading_sample_dim_added(self, fake_trace): + """Each output value has a leading sample dimension of size 1.""" + map_params = { + "real_site_auto_loc": np.array(0.5), + "positive_site_auto_loc": np.array(-1.0), + } + out = _convert_map_params(map_params, fake_trace) + for site_name, val in out.items(): + assert val.shape[0] == 1, ( + f"{site_name}: expected leading dim 1, got shape {val.shape}" + ) + + def test_identity_for_unconstrained_site(self, fake_trace): + """Normal (real support) site value is unchanged by the bijection.""" + raw_val = np.array(-2.3) + map_params = {"real_site_auto_loc": raw_val} + out = _convert_map_params(map_params, fake_trace) + np.testing.assert_allclose( + np.asarray(out["real_site"][0]), + raw_val, + rtol=1e-5, + ) + + def test_bijection_for_positive_site(self, fake_trace): + """HalfNormal (positive support) site value is transformed to be positive.""" + raw_val = np.array(-3.0) # arbitrary unconstrained value + map_params = {"positive_site_auto_loc": raw_val} + out = _convert_map_params(map_params, fake_trace) + constrained = float(out["positive_site"][0]) + # Result must be strictly positive. + assert constrained > 0, f"Expected positive constrained value, got {constrained}" + # Must match what biject_to gives. + expected = float(biject_to(dist.HalfNormal(1.0).support)(jnp.array(raw_val))) + np.testing.assert_allclose(constrained, expected, rtol=1e-5) + + def test_observed_site_ignored(self, fake_trace): + """Keys whose site is marked is_observed are not converted.""" + map_params = { + "real_site_auto_loc": np.array(0.0), + "observed_site_auto_loc": np.array(1.0), + } + out = _convert_map_params(map_params, fake_trace) + # observed_site should still appear (key is not filtered by _convert_map_params + # — it has no _auto_loc guard in the trace loop, so the bijection path is + # simply skipped and the raw value is passed through with leading dim). + # The important thing: real_site IS bijected, observed_site is NOT blocked. + assert "real_site" in out + # observed_site: site is_observed=True so bijection branch is skipped; + # value should be expanded raw value. + assert "observed_site" in out + assert out["observed_site"].shape[0] == 1 + + def test_non_auto_loc_keys_skipped(self, fake_trace): + """Keys without _auto_loc suffix are not included in the output.""" + map_params = { + "real_site_auto_loc": np.array(0.0), + "some_other_key": np.array(99.0), + } + out = _convert_map_params(map_params, fake_trace) + assert "some_other_key" not in out + assert "real_site" in out + + def test_array_site_shape_preserved(self, fake_trace): + """Multi-element sites get shape (1, N) not (N,).""" + map_params = {"real_site_auto_loc": np.array([1.0, 2.0, 3.0])} + out = _convert_map_params(map_params, fake_trace) + assert out["real_site"].shape == (1, 3) + + def test_empty_input_returns_empty(self, fake_trace): + out = _convert_map_params({}, fake_trace) + assert out == {} + + def test_unknown_site_passes_value_through(self, fake_trace): + """A key whose site is absent from the trace is still converted (no bijection).""" + map_params = {"mystery_site_auto_loc": np.array(5.0)} + out = _convert_map_params(map_params, fake_trace) + assert "mystery_site" in out + np.testing.assert_allclose(float(out["mystery_site"][0]), 5.0) From be21b654f3bb175ed60ec32f3a207371a57f3053 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 7 Jun 2026 16:06:54 -0700 Subject: [PATCH 04/24] cleanup ModelOrchestrator naming throughout codebase --- reports/flake.txt | 45 ++--- reports/junit/junit.xml | 2 +- src/tfscreen/plot/geno_trajectory.py | 42 ++--- src/tfscreen/tfmodel/analysis/extraction.py | 76 ++++----- src/tfscreen/tfmodel/analysis/prediction.py | 86 +++++----- .../tfmodel/analysis/prior_predictive.py | 26 +-- src/tfscreen/tfmodel/configuration_io.py | 30 ++-- .../components/transformation/_congression.py | 2 +- .../tfmodel/inference/checkpoint_io.py | 6 +- .../tfmodel/scripts/configure_model_cli.py | 4 +- .../tfmodel/scripts/diagnose_nan_cli.py | 4 +- .../tfmodel/scripts/extract_params_cli.py | 16 +- src/tfscreen/tfmodel/scripts/fit_model_cli.py | 4 +- .../tfmodel/scripts/predict_growth_cli.py | 16 +- .../tfmodel/scripts/predict_theta_cli.py | 24 +-- .../tfmodel/scripts/prior_predictive_cli.py | 6 +- .../scripts/run_prefit_calibration_cli.py | 80 ++++----- .../tfmodel/scripts/sample_posterior_cli.py | 4 +- .../test_run_growth_analysis_smoke.py | 18 +- tests/tfscreen/plot/test_geno_trajectory.py | 124 +++++++------- .../components/ln_cfu0/test_hierarchical.py | 2 +- .../scripts/test_extract_params_cli.py | 6 +- .../scripts/test_predict_growth_cli.py | 64 +++---- .../tfmodel/scripts/test_predict_theta_cli.py | 62 +++---- .../scripts/test_prior_predictive_cli.py | 6 +- .../scripts/test_run_prefit_calibration.py | 156 +++++++++--------- .../tfmodel/test_checkpoint_protection.py | 8 +- .../tfmodel/test_configure_and_run.py | 54 +++--- tests/tfscreen/tfmodel/test_h5_indexing.py | 4 +- .../tfscreen/tfmodel/test_needs_mut_wiring.py | 18 +- tests/tfscreen/tfmodel/test_prediction.py | 62 +++---- tests/tfscreen/tfmodel/test_presplit.py | 20 +-- .../tfscreen/tfmodel/test_prior_predictive.py | 86 +++++----- .../tfscreen/tfmodel/test_spiked_genotypes.py | 16 +- tests/tfscreen/tfmodel/test_struct_wiring.py | 110 ++++++------ 35 files changed, 645 insertions(+), 644 deletions(-) diff --git a/reports/flake.txt b/reports/flake.txt index fe53acc9..0d5ffb2c 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1527,9 +1527,6 @@ ./src/tfscreen/tfmodel/analysis/extraction.py:81:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:85:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:103:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/extraction.py:116:72: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/extraction.py:117:71: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/extraction.py:118:70: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:182:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/analysis/extraction.py:260:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/analysis/extraction.py:261:29: E128 continuation line under-indented for visual indent @@ -1545,12 +1542,6 @@ ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:99:16: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:120:13: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/prediction.py:11:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:20:76: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:21:37: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:28:75: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:29:47: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:31:70: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:34:68: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:40:73: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:77:71: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:98:1: W293 blank line contains whitespace @@ -1574,7 +1565,7 @@ ./src/tfscreen/tfmodel/analysis/prediction.py:330:26: E117 over-indented ./src/tfscreen/tfmodel/analysis/prediction.py:333:25: E122 continuation line missing indentation or outdented ./src/tfscreen/tfmodel/analysis/prediction.py:339:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:340:46: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:340:56: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:341:62: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:343:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:345:40: W291 trailing whitespace @@ -3191,7 +3182,6 @@ ./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:462:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:463:54: W291 trailing whitespace ./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:464:79: W291 trailing whitespace -./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:465:65: W291 trailing whitespace ./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:466:74: W291 trailing whitespace ./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:470:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/generative/components/transformation/_congression.py:473:1: E302 expected 2 blank lines, found 1 @@ -4920,8 +4910,8 @@ ./tests/tfscreen/plot/test_geno_trajectory.py:92:18: E221 multiple spaces before operator ./tests/tfscreen/plot/test_geno_trajectory.py:119:24: E221 multiple spaces before operator ./tests/tfscreen/plot/test_geno_trajectory.py:123:25: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:137:12: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:138:17: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:137:22: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:138:27: E221 multiple spaces before operator ./tests/tfscreen/plot/test_geno_trajectory.py:147:16: E221 multiple spaces before operator ./tests/tfscreen/plot/test_geno_trajectory.py:148:16: E221 multiple spaces before operator ./tests/tfscreen/plot/test_geno_trajectory.py:152:14: E221 multiple spaces before operator @@ -6512,6 +6502,15 @@ ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.run_prefit_calibration_cli._identify_field_mapping' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:295:1: E303 too many blank lines (4) ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:299:1: E302 expected 2 blank lines, found 4 +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:667:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:668:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:669:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:670:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:671:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:672:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:673:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:674:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:675:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1051:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1070:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1085:14: E127 continuation line over-indented for visual indent @@ -6636,7 +6635,6 @@ ./tests/tfscreen/tfmodel/test_configure_and_run.py:124:71: E203 whitespace before ',' ./tests/tfscreen/tfmodel/test_configure_and_run.py:125:1: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/test_configure_and_run.py:126:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:128:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_configure_and_run.py:133:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_configure_and_run.py:135:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_configure_and_run.py:138:1: W293 blank line contains whitespace @@ -6667,7 +6665,7 @@ ./tests/tfscreen/tfmodel/test_configure_and_run.py:165:75: E231 missing whitespace after ':' ./tests/tfscreen/tfmodel/test_configure_and_run.py:165:128: E501 line too long (142 > 127 characters) ./tests/tfscreen/tfmodel/test_configure_and_run.py:166:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_configure_and_run.py:169:66: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_configure_and_run.py:169:76: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/test_configure_and_run.py:175:14: E111 indentation is not a multiple of 4 ./tests/tfscreen/tfmodel/test_configure_and_run.py:175:14: E117 over-indented ./tests/tfscreen/tfmodel/test_configure_and_run.py:175:31: E231 missing whitespace after ':' @@ -6680,7 +6678,8 @@ ./tests/tfscreen/tfmodel/test_configure_and_run.py:228:5: E306 expected 1 blank line before a nested definition, found 0 ./tests/tfscreen/tfmodel/test_configure_and_run.py:356:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_configure_and_run.py:371:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:379:114: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:379:124: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:379:128: E501 line too long (130 > 127 characters) ./tests/tfscreen/tfmodel/test_configure_and_run.py:382:9: F841 local variable 'mock_ri_class' is assigned to but never used ./tests/tfscreen/tfmodel/test_configure_and_run.py:385:44: E231 missing whitespace after ':' ./tests/tfscreen/tfmodel/test_configure_and_run.py:401:1: E302 expected 2 blank lines, found 1 @@ -7144,7 +7143,9 @@ ./tests/tfscreen/tfmodel/test_presplit.py:123:35: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/test_presplit.py:136:48: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/test_presplit.py:170:47: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:196:28: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/test_presplit.py:214:47: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:226:28: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/test_reference_math.py:29:11: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_reference_math.py:32:9: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_reference_math.py:75:14: E221 multiple spaces before operator @@ -7391,7 +7392,7 @@ ./tests/tfscreen/tfmodel/test_struct_wiring.py:195:16: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_struct_wiring.py:197:12: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_struct_wiring.py:226:9: F401 'jax.numpy as jnp' imported but unused -./tests/tfscreen/tfmodel/test_struct_wiring.py:233:61: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/test_struct_wiring.py:233:71: E272 multiple spaces before keyword ./tests/tfscreen/tfmodel/test_struct_wiring.py:247:20: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_struct_wiring.py:249:16: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_struct_wiring.py:283:5: F401 'os' imported but unused @@ -7657,7 +7658,7 @@ 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line 167 E127 continuation line over-indented for visual indent -68 E128 continuation line under-indented for visual indent +79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' @@ -7681,7 +7682,7 @@ 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line 33 E402 module level import not at top of file -70 E501 line too long (145 > 127 characters) +71 E501 line too long (145 > 127 characters) 46 E701 multiple statements on one line (colon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' 2 E713 test for membership should be 'not in' @@ -7692,8 +7693,8 @@ 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -688 W291 trailing whitespace +678 W291 trailing whitespace 86 W292 no newline at end of file -1700 W293 blank line contains whitespace +1699 W293 blank line contains whitespace 29 W391 blank line at end of file -7650 +7651 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 60da105d..5465f3b0 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/plot/geno_trajectory.py b/src/tfscreen/plot/geno_trajectory.py index 90d4e29d..5f4922be 100644 --- a/src/tfscreen/plot/geno_trajectory.py +++ b/src/tfscreen/plot/geno_trajectory.py @@ -26,7 +26,7 @@ # Prediction helpers # --------------------------------------------------------------------------- -def _compute_map_predictions(gm, params): +def _compute_map_predictions(orchestrator, params): """ Run the forward model at the MAP point and return prediction tensors. @@ -36,7 +36,7 @@ def _compute_map_predictions(gm, params): Parameters ---------- - gm : ModelOrchestrator + orchestrator : ModelOrchestrator Fitted model orchestrator. params : dict MAP parameter dict with ``{site}_auto_loc`` keys (unconstrained space). @@ -61,12 +61,12 @@ def _compute_map_predictions(gm, params): from numpyro.infer import Predictive from numpyro.infer.autoguide import AutoDelta - guide = AutoDelta(gm.jax_model) - all_indices = jnp.arange(gm.data.num_genotype) - full_data = gm.get_batch(gm.data, all_indices) - pred_fn = Predictive(gm.jax_model, guide=guide, params=params, num_samples=1) + guide = AutoDelta(orchestrator.jax_model) + all_indices = jnp.arange(orchestrator.data.num_genotype) + full_data = orchestrator.get_batch(orchestrator.data, all_indices) + pred_fn = Predictive(orchestrator.jax_model, guide=guide, params=params, num_samples=1) map_samples = pred_fn( - jax.random.PRNGKey(0), data=full_data, priors=gm.priors + jax.random.PRNGKey(0), data=full_data, priors=orchestrator.priors ) if "growth_pred" not in map_samples: @@ -84,7 +84,7 @@ def _compute_map_predictions(gm, params): ) # --- Fine-grid forward pass --- - gd = gm.data.growth + gd = orchestrator.data.growth good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) t_sel_tensor = np.asarray(gd.t_sel) n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno = good_mask.shape @@ -119,13 +119,13 @@ def _bc_t(arr, *, dtype=None): ln_cfu_std=jnp.ones(fine_shape), ) fine_data = full_data.replace(growth=fine_gd) - fine_samples = pred_fn(jax.random.PRNGKey(1), data=fine_data, priors=gm.priors) + fine_samples = pred_fn(jax.random.PRNGKey(1), data=fine_data, priors=orchestrator.priors) growth_pred_fine = np.asarray(fine_samples["growth_pred"][0]) return growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d -def _compute_posterior_predictions(gm, posterior_file): +def _compute_posterior_predictions(orchestrator, posterior_file): """ Load ``growth_pred`` from a posterior HDF5 file and compute summary statistics. @@ -135,7 +135,7 @@ def _compute_posterior_predictions(gm, posterior_file): Parameters ---------- - gm : ModelOrchestrator + orchestrator : ModelOrchestrator Fitted model orchestrator (used only for shape validation; not called). posterior_file : str or h5py.File Path to an HDF5 posterior file or an already-open ``h5py.File`` handle. @@ -190,7 +190,7 @@ def _read(fh): # --------------------------------------------------------------------------- def plot_geno_trajectory( - gm, + orchestrator, out_prefix, params=None, posterior_file=None, @@ -209,7 +209,7 @@ def plot_geno_trajectory( * **Observed data** — ``ln_cfu ± ln_cfu_std`` error bars at their ``t_sel`` x-coordinates (circles, one colour per replicate). * **Pre-split observations** *(optional)* — ``ln_cfu_t0 ± ln_cfu_t0_std`` - as square markers at ``x = −t_pre`` when ``gm.data.presplit`` is not + as square markers at ``x = −t_pre`` when ``orchestrator.data.presplit`` is not ``None``. The same pooled aliquot value appears in every subplot that shares the same ``(replicate, condition_pre, genotype)`` triple, which is correct because the measurement predates the condition split. @@ -223,7 +223,7 @@ def plot_geno_trajectory( Parameters ---------- - gm : ModelOrchestrator + orchestrator : ModelOrchestrator Fitted model orchestrator. out_prefix : str Prefix for output files. Each genotype's PDF is written to @@ -238,7 +238,7 @@ def plot_geno_trajectory( posterior median with a 90 % credible-interval shaded band. genotypes : list of str, optional Subset of genotype names to plot. If ``None``, all genotypes in - ``gm`` are plotted. Names not found in the model are warned about + ``orchestrator`` are plotted. Names not found in the model are warned about and silently skipped. titrant_names : list of str, optional Subset of titrant names to include. If ``None``, all titrant names @@ -299,17 +299,17 @@ def plot_geno_trajectory( if isinstance(params, str): params = dict(np.load(params)) growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d = ( - _compute_map_predictions(gm, params) + _compute_map_predictions(orchestrator, params) ) else: growth_pred, growth_pred_lo, growth_pred_hi, ln_cfu0_map = ( - _compute_posterior_predictions(gm, posterior_file) + _compute_posterior_predictions(orchestrator, posterior_file) ) # ----------------------------------------------------------------------- # Data tensors # ----------------------------------------------------------------------- - gd = gm.data.growth + gd = orchestrator.data.growth good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) t_pre_tensor = np.asarray(gd.t_pre) t_sel_tensor = np.asarray(gd.t_sel) @@ -321,7 +321,7 @@ def plot_geno_trajectory( # ----------------------------------------------------------------------- # Dimension labels and helper maps # ----------------------------------------------------------------------- - tm = gm.growth_tm + tm = orchestrator.growth_tm dn = tm.tensor_dim_names geno_labels = list(tm.tensor_dim_labels[dn.index("genotype")]) @@ -342,7 +342,7 @@ def plot_geno_trajectory( # (genotype, titrant_name, titrant_conc) → mean observed theta theta_map: dict = {} - for _, row in gm.binding_df.iterrows(): + for _, row in orchestrator.binding_df.iterrows(): key = ( str(row["genotype"]), str(row["titrant_name"]), @@ -354,7 +354,7 @@ def plot_geno_trajectory( # ----------------------------------------------------------------------- # Pre-split data (optional) # ----------------------------------------------------------------------- - presplit = gm.data.presplit + presplit = orchestrator.data.presplit if presplit is not None: ps_ln_cfu = np.asarray(presplit.ln_cfu_t0) # (R, CP, G) ps_ln_cfu_std = np.asarray(presplit.ln_cfu_t0_std) # (R, CP, G) diff --git a/src/tfscreen/tfmodel/analysis/extraction.py b/src/tfscreen/tfmodel/analysis/extraction.py index d0be96a1..220dcf08 100644 --- a/src/tfscreen/tfmodel/analysis/extraction.py +++ b/src/tfscreen/tfmodel/analysis/extraction.py @@ -100,7 +100,7 @@ def _extract_param_est(input_df, return out_dfs -def extract_parameters(model, posteriors, q_to_get=None): +def extract_parameters(orchestrator, posteriors, q_to_get=None): """ Extract parameter quantiles from posterior samples. @@ -110,12 +110,12 @@ def extract_parameters(model, posteriors, q_to_get=None): Parameters ---------- - model : ModelOrchestrator + orchestrator : ModelOrchestrator The model instance to extract parameters from. posteriors : dict or str - Assumes this is a dictionary of posteriors keying parameters to - numpy arrays, a numpy.lib.npyio.NpzFile object, or a path to a - .npz or .h5/.hdf5 file containing posterior samples for model + Assumes this is a dictionary of posteriors keying parameters to + numpy arrays, a numpy.lib.npyio.NpzFile object, or a path to a + .npz or .h5/.hdf5 file containing posterior samples for model parameters. q_to_get : dict, optional Dictionary mapping output column names to quantile values (between 0 and 1) @@ -138,20 +138,20 @@ def extract_parameters(model, posteriors, q_to_get=None): q_to_get, param_posteriors = load_posteriors(posteriors, q_to_get) ctx = ExtractionContext( - growth_tm=model.training_tm, - mut_labels=model.mut_labels, - pair_labels=model.pair_labels, - growth_shares_replicates=model._growth_shares_replicates, + growth_tm=orchestrator.training_tm, + mut_labels=orchestrator.mut_labels, + pair_labels=orchestrator.pair_labels, + growth_shares_replicates=orchestrator._growth_shares_replicates, ) component_selections = [ - ("condition_growth", model._condition_growth), - ("growth_transition", model._growth_transition), + ("condition_growth", orchestrator._condition_growth), + ("growth_transition", orchestrator._growth_transition), ("ln_cfu0", "hierarchical"), - ("dk_geno", model._dk_geno), - ("activity", model._activity), - ("theta", model._theta), - ("transformation", model._transformation), + ("dk_geno", orchestrator._dk_geno), + ("activity", orchestrator._activity), + ("theta", orchestrator._theta), + ("transformation", orchestrator._transformation), ] extract = [] @@ -179,7 +179,7 @@ def extract_parameters(model, posteriors, q_to_get=None): return params -def extract_theta_curves(model, posteriors, q_to_get=None, manual_titrant_df=None, +def extract_theta_curves(orchestrator, posteriors, q_to_get=None, manual_titrant_df=None, num_samples=100): """ Extract theta curves by sampling from the joint posterior distribution. @@ -190,7 +190,7 @@ def extract_theta_curves(model, posteriors, q_to_get=None, manual_titrant_df=Non Parameters ---------- - model : ModelOrchestrator + orchestrator : ModelOrchestrator The model instance to extract parameters from. posteriors : dict or str Assumes this is a dictionary of posteriors keying parameters to @@ -206,7 +206,7 @@ def extract_theta_curves(model, posteriors, q_to_get=None, manual_titrant_df=Non at which to calculate theta. If provided, it overrides the default calculation at the concentrations present in the input data. If 'genotype' is present, it will be used; otherwise, the method - will calculate theta for all genotypes in the model. + will calculate theta for all genotypes in the orchestrator. num_samples : int or None, optional Randomly select this many joint posterior samples and return them as columns ``sample_0``, ``sample_1``, ... alongside the quantile @@ -228,18 +228,18 @@ def extract_theta_curves(model, posteriors, q_to_get=None, manual_titrant_df=Non If `q_to_get` is not a dictionary. If `manual_titrant_df` is missing required columns. """ - module = model_registry.get("theta", {}).get(model._theta) + module = model_registry.get("theta", {}).get(orchestrator._theta) if module is None or not (hasattr(module, "build_calc_df") and hasattr(module, "compute_theta_samples")): raise ValueError( f"extract_theta_curves requires the theta component to implement " f"build_calc_df and compute_theta_samples. " - f"'{model._theta}' does not support this interface." + f"'{orchestrator._theta}' does not support this interface." ) q_to_get, param_posteriors = load_posteriors(posteriors, q_to_get) - calc_df, internal_cols, extra_kwargs = module.build_calc_df(model, manual_titrant_df) + calc_df, internal_cols, extra_kwargs = module.build_calc_df(orchestrator, manual_titrant_df) theta_samples = module.compute_theta_samples(calc_df, param_posteriors, **extra_kwargs) for q_name, q_val in q_to_get.items(): @@ -257,7 +257,7 @@ def extract_theta_curves(model, posteriors, q_to_get=None, manual_titrant_df=Non return calc_df.drop(columns=internal_cols) -def extract_theta_unmeasured(model, posteriors, target_genotypes, +def extract_theta_unmeasured(orchestrator, posteriors, target_genotypes, manual_titrant_df, q_to_get=None, genotype_batch_size=2000): """ @@ -273,7 +273,7 @@ def extract_theta_unmeasured(model, posteriors, target_genotypes, Parameters ---------- - model : ModelOrchestrator + orchestrator : ModelOrchestrator Fitted model instance (must carry ``mut_labels``, ``pair_labels``, ``growth_tm``, and ``priors``). posteriors : dict or str @@ -284,7 +284,7 @@ def extract_theta_unmeasured(model, posteriors, target_genotypes, manual_titrant_df : pd.DataFrame Must have ``'titrant_name'`` and ``'titrant_conc'`` columns specifying the concentration grid. All ``titrant_name`` values must be present in - the model. + the orchestrator. q_to_get : dict, optional Quantiles to extract. Defaults to the standard set used by other extraction functions. @@ -305,20 +305,20 @@ def extract_theta_unmeasured(model, posteriors, target_genotypes, ValueError If the active theta component does not implement ``predict_unmeasured``. """ - module = model_registry.get("theta", {}).get(model._theta) + module = model_registry.get("theta", {}).get(orchestrator._theta) if module is None or not hasattr(module, "predict_unmeasured"): raise ValueError( f"extract_theta_unmeasured requires the theta component to implement " - f"predict_unmeasured. '{model._theta}' does not support this interface." + f"predict_unmeasured. '{orchestrator._theta}' does not support this interface." ) q_to_get, param_posteriors = load_posteriors(posteriors, q_to_get) - titrant_dim = model.training_tm.tensor_dim_names.index("titrant_name") - titrant_names = list(model.training_tm.tensor_dim_labels[titrant_dim]) + titrant_dim = orchestrator.training_tm.tensor_dim_names.index("titrant_name") + titrant_names = list(orchestrator.training_tm.tensor_dim_labels[titrant_dim]) extra_kwargs = {} - theta_priors = model.priors.theta + theta_priors = orchestrator.priors.theta if hasattr(theta_priors, "theta_tf_total_M"): extra_kwargs["tf_total"] = float(theta_priors.theta_tf_total_M) if hasattr(theta_priors, "theta_op_total_M"): @@ -334,8 +334,8 @@ def extract_theta_unmeasured(model, posteriors, target_genotypes, target_genotypes=target_genotypes, titrant_names=titrant_names, manual_titrant_df=manual_titrant_df, - mut_labels=model.mut_labels, - pair_labels=model.pair_labels, + mut_labels=orchestrator.mut_labels, + pair_labels=orchestrator.pair_labels, param_posteriors=param_posteriors, q_to_get=q_to_get, **extra_kwargs, @@ -355,8 +355,8 @@ def extract_theta_unmeasured(model, posteriors, target_genotypes, target_genotypes=batch, titrant_names=titrant_names, manual_titrant_df=manual_titrant_df, - mut_labels=model.mut_labels, - pair_labels=model.pair_labels, + mut_labels=orchestrator.mut_labels, + pair_labels=orchestrator.pair_labels, param_posteriors=param_posteriors, q_to_get=q_to_get, **extra_kwargs, @@ -366,7 +366,7 @@ def extract_theta_unmeasured(model, posteriors, target_genotypes, return pd.concat(result_dfs, ignore_index=True) -def extract_growth_predictions(model, +def extract_growth_predictions(orchestrator, posteriors, q_to_get=None, num_samples=100): @@ -374,11 +374,11 @@ def extract_growth_predictions(model, Extract predicted ln_cfu values matching the input growth data. This method pulls the 'growth_pred' values from the posterior samples - and maps them back to the original rows in `model.growth_df`. + and maps them back to the original rows in `orchestrator.growth_df`. Parameters ---------- - model : ModelOrchestrator + orchestrator : ModelOrchestrator The model instance to extract parameters from. posteriors : dict or str Assumes this is a dictionary of posteriors keying parameters to @@ -401,7 +401,7 @@ def extract_growth_predictions(model, Returns ------- pd.DataFrame - A copy of `model.growth_df` with new columns for the requested + A copy of `orchestrator.growth_df` with new columns for the requested quantiles of 'ln_cfu_pred' and (unless ``num_samples`` is ``None``) ``sample_0`` … ``sample_{num_samples-1}``. @@ -429,7 +429,7 @@ def extract_growth_predictions(model, group_cols = ["replicate_idx", "time_idx", "condition_pre_idx", "condition_sel_idx", "titrant_name_idx", "titrant_conc_idx"] - out_df = model.growth_df.copy() + out_df = orchestrator.growth_df.copy() out_df = out_df.sort_values(by=group_cols + ["genotype_idx"]) keep_columns = ["replicate", "genotype", diff --git a/src/tfscreen/tfmodel/analysis/prediction.py b/src/tfscreen/tfmodel/analysis/prediction.py index cd663c67..98a6d003 100644 --- a/src/tfscreen/tfmodel/analysis/prediction.py +++ b/src/tfscreen/tfmodel/analysis/prediction.py @@ -8,31 +8,31 @@ from numpyro.handlers import seed, trace from numpyro.infer import Predictive -def copy_model_class(model_class, - t_pre=None, - t_sel=None, - titrant_conc=None): +def copy_orchestrator(orchestrator, + t_pre=None, + t_sel=None, + titrant_conc=None): """ Generate a fresh ModelOrchestrator instance with model components from an old ModelOrchestrator and new quantitative data passed in by the user. - The default behavior is to use values from `model_class.growth_df`. - Quantitative inputs (t_pre, t_sel, titrant_conc) can be expanded beyond - those in the original dataframe. + The default behavior is to use values from `orchestrator.growth_df`. + Quantitative inputs (t_pre, t_sel, titrant_conc) can be expanded beyond + those in the original dataframe. Parameters ---------- - model_class : ModelOrchestrator + orchestrator : ModelOrchestrator The original ModelOrchestrator instance to copy. t_pre : list, optional - List of timepoints for pre-growth. Must be >= 0. If None, uses the - value(s) from `model_class.growth_df`. + List of timepoints for pre-growth. Must be >= 0. If None, uses the + value(s) from `orchestrator.growth_df`. t_sel : list, optional - List of timepoints for selection. Must be >= 0. If None, uses - values from `model_class.growth_df`. + List of timepoints for selection. Must be >= 0. If None, uses + values from `orchestrator.growth_df`. titrant_conc : list, optional - List of titrant concentrations. Must be >= 0. If None, uses - values from `model_class.growth_df`. + List of titrant concentrations. Must be >= 0. If None, uses + values from `orchestrator.growth_df`. Returns ------- @@ -41,7 +41,7 @@ def copy_model_class(model_class, combinations of all inputs. """ - df = model_class.growth_df + df = orchestrator.growth_df def _get_input(value, col_name): """ @@ -91,10 +91,10 @@ def _get_input(value, col_name): # We keep the binding_df as is, as it's keyed by genotype/titrant_name # and we aren't subsetting those in this step. - new_binding_df = model_class.binding_df.copy() + new_binding_df = orchestrator.binding_df.copy() # Create new ModelOrchestrator using settings from the old one - settings = model_class.settings.copy() + settings = orchestrator.settings.copy() return ModelOrchestrator( growth_df=new_growth_df, @@ -150,7 +150,7 @@ def _convert_map_params(map_params, model_trace): return constrained -def predict(model_class, +def predict(orchestrator, param_posteriors, predict_sites=None, q_to_get=None, @@ -166,7 +166,7 @@ def predict(model_class, Parameters ---------- - model_class : ModelOrchestrator + orchestrator : ModelOrchestrator The original ModelOrchestrator used for training. param_posteriors : dict or str Posterior samples. Can be a dictionary, or path to .h5 file. @@ -186,16 +186,16 @@ def predict(model_class, quantile predictions. If None, uses all available samples. t_pre : list, optional List of timepoints for pre-growth. Must be >= 0. If None, uses the - value(s) from `model_class.growth_df`. + value(s) from `orchestrator.growth_df`. t_sel : list, optional List of timepoints for selection. Must be >= 0. If None, uses - values from `model_class.growth_df`. + values from `orchestrator.growth_df`. titrant_conc : list, optional List of titrant concentrations. Must be >= 0. If None, uses - values from `model_class.growth_df`. + values from `orchestrator.growth_df`. genotypes : list, optional List of genotypes to include in the prediction. Must be a subset - of those in `model_class.growth_df`. If None, uses all genotypes. + of those in `orchestrator.growth_df`. If None, uses all genotypes. Returns ------- @@ -216,16 +216,16 @@ def predict(model_class, predict_sites = [predict_sites] # Create the expanded prediction model - new_mc = copy_model_class(model_class, - t_pre=t_pre, - t_sel=t_sel, - titrant_conc=titrant_conc) + new_orchestrator = copy_orchestrator(orchestrator, + t_pre=t_pre, + t_sel=t_sel, + titrant_conc=titrant_conc) # ------------------------------------------------------------------------- # Genotype subsetting - all_genotypes = new_mc.growth_tm.tensor_dim_labels[-1] + all_genotypes = new_orchestrator.growth_tm.tensor_dim_labels[-1] if genotypes is None: genotypes = all_genotypes.tolist() genotype_indices = np.arange(len(all_genotypes)) @@ -244,17 +244,17 @@ def predict(model_class, genotype_indices = np.array(genotype_indices) # Use existing get_batch to subset the data tensors - subset_data = new_mc.get_batch(new_mc.data, jnp.array(genotype_indices)) + subset_data = new_orchestrator.get_batch(new_orchestrator.data, jnp.array(genotype_indices)) # ------------------------------------------------------------------------- # Model trace — run first so bijections are available for MAP detection. # - # We use the original model_class because posteriors match its structure. + # We use the original orchestrator because posteriors match its structure. - seeded_model = seed(model_class.jax_model, rng_seed=0) + seeded_model = seed(orchestrator.jax_model, rng_seed=0) traced_model = trace(seeded_model) - model_trace = traced_model.get_trace(data=model_class.data, - priors=model_class.priors) + model_trace = traced_model.get_trace(data=orchestrator.data, + priors=orchestrator.priors) # ------------------------------------------------------------------------- # MAP param conversion (if needed) @@ -311,14 +311,14 @@ def predict(model_class, # Find the corresponding dimension in the TensorManager dim_idx = None - for i, name in enumerate(model_class.growth_tm.tensor_dim_names): + for i, name in enumerate(orchestrator.growth_tm.tensor_dim_names): if name.lower() in plate_name: dim_idx = i break if dim_idx is not None: - old_labels = model_class.growth_tm.tensor_dim_labels[dim_idx] - new_labels = new_mc.growth_tm.tensor_dim_labels[dim_idx] + old_labels = orchestrator.growth_tm.tensor_dim_labels[dim_idx] + new_labels = new_orchestrator.growth_tm.tensor_dim_labels[dim_idx] if not np.array_equal(old_labels, new_labels): try: @@ -337,7 +337,7 @@ def predict(model_class, # ------------------------------------------------------------------------- # Run Prediction - predictive = Predictive(new_mc.jax_model, + predictive = Predictive(new_orchestrator.jax_model, posterior_samples=sliced_samples, return_sites=predict_sites) @@ -345,7 +345,7 @@ def predict(model_class, predict_key = jax.random.PRNGKey(0) predictions = predictive(predict_key, data=subset_data, - priors=new_mc.priors) + priors=new_orchestrator.priors) # ------------------------------------------------------------------------- # Calculate Quantiles and Join @@ -353,23 +353,23 @@ def predict(model_class, # tm._pivot_index columns in df contain the integer codes for each dimension # (replicate_idx, time_idx, etc.) # We want a dataframe that only has the subsetted genotypes. - base_df = new_mc.growth_df.copy() + base_df = new_orchestrator.growth_df.copy() base_df = base_df[base_df["genotype"].isin(genotypes)].copy() # Replace the dummy ln_cfu/ln_cfu_std zeros with observed values from the - # original model_class.growth_df (NaN where there is no matching observation, + # original orchestrator.growth_df (NaN where there is no matching observation, # e.g. for expanded prediction grids). merge_keys = ["replicate", "condition_pre", "condition_sel", "titrant_name", "genotype", "t_pre", "t_sel", "titrant_conc"] obs_cols = merge_keys + ["ln_cfu", "ln_cfu_std"] - orig_obs = model_class.growth_df[obs_cols].drop_duplicates(subset=merge_keys) + orig_obs = orchestrator.growth_df[obs_cols].drop_duplicates(subset=merge_keys) base_df = base_df.drop(columns=["ln_cfu", "ln_cfu_std"]).merge( orig_obs, on=merge_keys, how="left" ) # Re-calculate indices for the subsetted dataframe relative to the - # using the new_mc TM but subset the df. - tm = new_mc.growth_tm + # using the new_orchestrator TM but subset the df. + tm = new_orchestrator.growth_tm indices = [base_df[f"{dim}_idx"].values for dim in tm.tensor_dim_names] # Since we used get_batch, the predictions only have the subsetted genotypes. diff --git a/src/tfscreen/tfmodel/analysis/prior_predictive.py b/src/tfscreen/tfmodel/analysis/prior_predictive.py index 65521cb6..34f960d2 100644 --- a/src/tfscreen/tfmodel/analysis/prior_predictive.py +++ b/src/tfscreen/tfmodel/analysis/prior_predictive.py @@ -6,7 +6,7 @@ from tfscreen.tfmodel.analysis.prediction import predict -def draw_prior(model_class, rng_key=0, num_draws=1): +def draw_prior(orchestrator, rng_key=0, num_draws=1): """ Draw samples from the model prior (no data conditioning). @@ -17,7 +17,7 @@ def draw_prior(model_class, rng_key=0, num_draws=1): Parameters ---------- - model_class : ModelOrchestrator + orchestrator : ModelOrchestrator Fully initialised model, as returned by ``read_configuration``. rng_key : int or jax.random.PRNGKey, optional Random seed. An ``int`` is converted to a PRNGKey. Default 0. @@ -41,8 +41,8 @@ def draw_prior(model_class, rng_key=0, num_draws=1): rng_key = jax.random.PRNGKey(rng_key) # Trace the model once to classify all sites. - seeded = seed(model_class.jax_model, rng_seed=0) - tr = trace(seeded).get_trace(data=model_class.data, priors=model_class.priors) + seeded = seed(orchestrator.jax_model, rng_seed=0) + tr = trace(seeded).get_trace(data=orchestrator.data, priors=orchestrator.priors) observed = { name for name, site in tr.items() @@ -56,12 +56,12 @@ def draw_prior(model_class, rng_key=0, num_draws=1): return_sites = sorted(set(tr.keys()) - observed) predictive = Predictive( - model_class.jax_model, + orchestrator.jax_model, posterior_samples=None, num_samples=num_draws, return_sites=return_sites, ) - raw = predictive(rng_key, data=model_class.data, priors=model_class.priors) + raw = predictive(rng_key, data=orchestrator.data, priors=orchestrator.priors) predictions = {k: np.array(v) for k, v in raw.items() if k in deterministic} latent_params = {k: np.array(v) for k, v in raw.items() if k not in deterministic} @@ -69,23 +69,23 @@ def draw_prior(model_class, rng_key=0, num_draws=1): return predictions, latent_params -def growth_df_from_prior(model_class, latent_params, draw_idx=0, noise_rng=None): +def growth_df_from_prior(orchestrator, latent_params, draw_idx=0, noise_rng=None): """ Build a synthetic growth DataFrame from one prior draw. Maps ``latent_params`` through the forward model to obtain the deterministic ``growth_pred`` values, then writes them into a copy of - ``model_class.growth_df``. The result has the same rows and structure + ``orchestrator.growth_df``. The result has the same rows and structure as the original DataFrame and is ready for ``tfs-fit-model``. - Noise is added using ``model_class.growth_df['ln_cfu_std']`` as the + Noise is added using ``orchestrator.growth_df['ln_cfu_std']`` as the per-observation standard deviation. This preserves the original measurement-error structure while replacing the signal with synthetic predictions. Parameters ---------- - model_class : ModelOrchestrator + orchestrator : ModelOrchestrator latent_params : dict Output of ``draw_prior``. Values have shape ``(num_draws, ...)``. draw_idx : int, optional @@ -97,7 +97,7 @@ def growth_df_from_prior(model_class, latent_params, draw_idx=0, noise_rng=None) Returns ------- pandas.DataFrame - Copy of ``model_class.growth_df`` with ``ln_cfu`` replaced by + Copy of ``orchestrator.growth_df`` with ``ln_cfu`` replaced by synthetic values. """ # Slice to single draw so predict() treats it as one "posterior sample". @@ -109,7 +109,7 @@ def growth_df_from_prior(model_class, latent_params, draw_idx=0, noise_rng=None) ] pred_df = predict( - model_class, + orchestrator, single_draw, predict_sites=["growth_pred"], q_to_get={"_prior_ln_cfu": 0.5}, @@ -117,7 +117,7 @@ def growth_df_from_prior(model_class, latent_params, draw_idx=0, noise_rng=None) ) # pred_df may contain extra rows (expanded grid); merge to keep original rows. - out_df = model_class.growth_df.drop(columns=["ln_cfu"]).merge( + out_df = orchestrator.growth_df.drop(columns=["ln_cfu"]).merge( pred_df[merge_keys + ["_prior_ln_cfu"]], on=merge_keys, how="left", diff --git a/src/tfscreen/tfmodel/configuration_io.py b/src/tfscreen/tfmodel/configuration_io.py index 0c1a06c5..22177266 100644 --- a/src/tfscreen/tfmodel/configuration_io.py +++ b/src/tfscreen/tfmodel/configuration_io.py @@ -113,7 +113,7 @@ def _update_dataclass(dc, prefix, flat_dict): return dataclasses.replace(dc, **updates) return dc -def write_configuration(gm, +def write_configuration(orchestrator, out_prefix, growth_df_path=None, binding_df_path=None, @@ -123,7 +123,7 @@ def write_configuration(gm, Parameters ---------- - gm : ModelOrchestrator + orchestrator : ModelOrchestrator Initialized ModelOrchestrator object. out_prefix : str Root filename for output files. @@ -137,12 +137,12 @@ def write_configuration(gm, guesses_list = [] # Extract priors - priors_raw = _extract_scalars(gm.priors) + priors_raw = _extract_scalars(orchestrator.priors) for k, v in priors_raw.items(): priors_list.append(pd.DataFrame({"parameter": [k], "value": [v]})) # Extract guesses - for k, v in gm.init_params.items(): + for k, v in orchestrator.init_params.items(): if not hasattr(v, 'shape') or len(v.shape) == 0: guesses_list.append(pd.DataFrame({"parameter": [k], "value": [v]})) @@ -161,7 +161,7 @@ def write_configuration(gm, config = { "tfscreen_version": __version__, "data": data_paths, - "components": gm.settings, + "components": orchestrator.settings, "priors_file": os.path.basename(priors_path), "guesses_file": os.path.basename(guesses_path) } @@ -172,7 +172,7 @@ def write_configuration(gm, print(f"Wrote configuration to {yaml_path}") # Process array guesses and any others - tm = gm.growth_tm + tm = orchestrator.growth_tm if tm is not None: cond_rep_map = tm.map_groups.get("condition_rep", pd.DataFrame()) geno_map = tm.map_groups.get("genotype", pd.DataFrame()) @@ -184,7 +184,7 @@ def write_configuration(gm, theta_map = pd.DataFrame() ln_cfu0_map = pd.DataFrame() - for k, v in gm.init_params.items(): + for k, v in orchestrator.init_params.items(): if not hasattr(v, 'shape') or len(v.shape) == 0: continue @@ -261,7 +261,7 @@ def read_configuration(config_file): Returns ------- - gm : ModelOrchestrator + orchestrator : ModelOrchestrator Initialized ModelOrchestrator object. init_params : dict Dictionary of initial parameters for the model. @@ -293,7 +293,7 @@ def read_configuration(config_file): # ended up in settings (older configs that serialised it there). settings.pop("presplit_df", None) - gm = ModelOrchestrator(growth_df_path, + orchestrator = ModelOrchestrator(growth_df_path, binding_df_path, batch_size=batch_size, presplit_df=presplit_df_path, @@ -316,8 +316,8 @@ def read_configuration(config_file): except (ValueError, TypeError): flat_priors[k] = v - new_priors = _update_dataclass(gm.priors, "", flat_priors) - gm._priors = new_priors + new_priors = _update_dataclass(orchestrator.priors, "", flat_priors) + orchestrator._priors = new_priors # Construct init_params from CSV guesses_file = config.get("guesses_file") @@ -336,8 +336,8 @@ def read_configuration(config_file): sorted_group = df_group.sort_values("flat_index") if "flat_index" in df_group else df_group val_array = sorted_group["value"].values - if param_name in gm.init_params: - orig_val = gm.init_params[param_name] + if param_name in orchestrator.init_params: + orig_val = orchestrator.init_params[param_name] if hasattr(orig_val, 'shape') and orig_val.shape != (): orig_shape = orig_val.shape init_params[param_name] = jnp.array(val_array.reshape(orig_shape)) @@ -347,11 +347,11 @@ def read_configuration(config_file): init_params[param_name] = float(df_group["value"].iloc[0]) missing_params = [] - for k in gm.init_params.keys(): + for k in orchestrator.init_params.keys(): if k not in init_params: missing_params.append(k) if len(missing_params) > 0: raise ValueError(f"Missing initial guesses for parameters: {missing_params}") - return gm, init_params + return orchestrator, init_params diff --git a/src/tfscreen/tfmodel/generative/components/transformation/_congression.py b/src/tfscreen/tfmodel/generative/components/transformation/_congression.py index 57609bc8..ac13f750 100644 --- a/src/tfscreen/tfmodel/generative/components/transformation/_congression.py +++ b/src/tfscreen/tfmodel/generative/components/transformation/_congression.py @@ -462,7 +462,7 @@ def get_guesses(name,data): # Only add these if we are not in empirical mode. # NOTE: get_guesses currently doesn't know the mode from the config easily - # during initialization in model_class.py unless we pass it. + # during initialization in model_orchestrator.py unless we pass it. # For now, we'll keep them as guesses; they just won't be used if not # in the model/guide. guesses[f"{name}_mu"] = 0.0 diff --git a/src/tfscreen/tfmodel/inference/checkpoint_io.py b/src/tfscreen/tfmodel/inference/checkpoint_io.py index a3fa4133..f9cc06e5 100644 --- a/src/tfscreen/tfmodel/inference/checkpoint_io.py +++ b/src/tfscreen/tfmodel/inference/checkpoint_io.py @@ -2,7 +2,7 @@ import dill -def resolve_param_file(param_file, gm, out_prefix): +def resolve_param_file(param_file, orchestrator, out_prefix): """ Return a posterior .h5 path, converting a MAP checkpoint on the fly if needed. @@ -22,7 +22,7 @@ def resolve_param_file(param_file, gm, out_prefix): ---------- param_file : str Path to a posterior .h5 file or a MAP checkpoint .pkl file. - gm : ModelOrchestrator + orchestrator : ModelOrchestrator Already-loaded model instance (from ``read_configuration``). out_prefix : str Output prefix for the calling script. The intermediate map posterior @@ -52,7 +52,7 @@ def resolve_param_file(param_file, gm, out_prefix): from tfscreen.tfmodel.inference.run_inference import RunInference - ri = RunInference(gm, seed=0) + ri = RunInference(orchestrator, seed=0) temp_svi = ri.setup_svi(guide_type="delta") map_params = temp_svi.optim.get_params(chk_data["svi_state"].optim_state) diff --git a/src/tfscreen/tfmodel/scripts/configure_model_cli.py b/src/tfscreen/tfmodel/scripts/configure_model_cli.py index aeeb9244..e52348a7 100644 --- a/src/tfscreen/tfmodel/scripts/configure_model_cli.py +++ b/src/tfscreen/tfmodel/scripts/configure_model_cli.py @@ -188,7 +188,7 @@ def configure_model(binding_df, check_component_compatibility(condition_growth_model, theta_rescale_model) # Initialize model to build mappings and get guesses - gm = ModelOrchestrator(growth_df, + orchestrator = ModelOrchestrator(growth_df, binding_df, presplit_df=presplit_df, binding_only=binding_only, @@ -214,7 +214,7 @@ def configure_model(binding_df, # names, the data file paths, and the parameter guesses/priors. growth_path = None if binding_only else (growth_df if isinstance(growth_df, str) else "growth.csv") presplit_path = presplit_df if isinstance(presplit_df, str) else None - write_configuration(gm=gm, + write_configuration(orchestrator=orchestrator, out_prefix=out_prefix, growth_df_path=growth_path, binding_df_path=binding_df if isinstance(binding_df, str) else "binding.csv", diff --git a/src/tfscreen/tfmodel/scripts/diagnose_nan_cli.py b/src/tfscreen/tfmodel/scripts/diagnose_nan_cli.py index 78a0f911..8846da9d 100644 --- a/src/tfscreen/tfmodel/scripts/diagnose_nan_cli.py +++ b/src/tfscreen/tfmodel/scripts/diagnose_nan_cli.py @@ -82,8 +82,8 @@ def diagnose_nan(config_file, jax.config.update("jax_debug_nans", True) jax.config.update("jax_disable_jit", True) - gm, init_params = read_configuration(config_file) - ri = RunInference(gm, seed=seed) + orchestrator, init_params = read_configuration(config_file) + ri = RunInference(orchestrator, seed=seed) svi_obj = ri.setup_svi(adam_step_size=1e-6, guide_type="component") diff --git a/src/tfscreen/tfmodel/scripts/extract_params_cli.py b/src/tfscreen/tfmodel/scripts/extract_params_cli.py index 970251eb..e55c4d7d 100644 --- a/src/tfscreen/tfmodel/scripts/extract_params_cli.py +++ b/src/tfscreen/tfmodel/scripts/extract_params_cli.py @@ -34,15 +34,15 @@ def extract_params(config_file, param_file, out_prefix="tfs_params"): out_prefix : str, optional Prefix for output CSV files. Default ``'tfs_params'``. """ - gm, _ = read_configuration(config_file) + orchestrator, _ = read_configuration(config_file) if param_file.endswith(".pkl"): - _extract_from_checkpoint(gm, param_file, out_prefix) + _extract_from_checkpoint(orchestrator, param_file, out_prefix) else: - _extract_from_posteriors(gm, param_file, out_prefix) + _extract_from_posteriors(orchestrator, param_file, out_prefix) -def _extract_from_checkpoint(gm, ckpt_path, out_prefix): +def _extract_from_checkpoint(orchestrator, ckpt_path, out_prefix): if not os.path.isfile(ckpt_path): raise FileNotFoundError(f"Checkpoint file not found: '{ckpt_path}'") @@ -56,7 +56,7 @@ def _extract_from_checkpoint(gm, ckpt_path, out_prefix): "tfs-extract-params only supports MAP (AutoDelta) checkpoints." ) - ri = RunInference(gm, seed=0) + ri = RunInference(orchestrator, seed=0) temp_svi = ri.setup_svi(guide_type="delta") map_params = temp_svi.optim.get_params(chk_data["svi_state"].optim_state) @@ -70,7 +70,7 @@ def _extract_from_checkpoint(gm, ckpt_path, out_prefix): posteriors = {k: np.expand_dims(np.asarray(v), 0) for k, v in constrained.items()} print(f"Writing parameter CSVs to {out_prefix}_*.csv...", flush=True) - param_dfs = extract_parameters(gm, posteriors, q_to_get={"point_est": 0.5}) + param_dfs = extract_parameters(orchestrator, posteriors, q_to_get={"point_est": 0.5}) for p_name, df in param_dfs.items(): out_file = f"{out_prefix}_{p_name}.csv" df.to_csv(out_file, index=False) @@ -78,7 +78,7 @@ def _extract_from_checkpoint(gm, ckpt_path, out_prefix): print("Done.", flush=True) -def _extract_from_posteriors(gm, posterior_file, out_prefix): +def _extract_from_posteriors(orchestrator, posterior_file, out_prefix): if not os.path.exists(posterior_file): if os.path.exists(f"{posterior_file}.h5"): posterior_file = f"{posterior_file}.h5" @@ -103,7 +103,7 @@ def _extract_from_posteriors(gm, posterior_file, out_prefix): "Extraction of natural parameters may fail.", flush=True) print(f"Extracting parameters to {out_prefix}_*.csv...", flush=True) - params = extract_parameters(gm, posteriors) + params = extract_parameters(orchestrator, posteriors) for p_name, p_df in params.items(): p_df.to_csv(f"{out_prefix}_{p_name}.csv", index=False) diff --git a/src/tfscreen/tfmodel/scripts/fit_model_cli.py b/src/tfscreen/tfmodel/scripts/fit_model_cli.py index 9dbc558b..15c505bf 100644 --- a/src/tfscreen/tfmodel/scripts/fit_model_cli.py +++ b/src/tfscreen/tfmodel/scripts/fit_model_cli.py @@ -432,14 +432,14 @@ def fit_model(config_file, "overwrite, delete the file or change out_prefix." ) - gm, init_params = read_configuration(config_file) + orchestrator, init_params = read_configuration(config_file) # For posterior mode the seed is optional: the checkpoint restores the PRNG # key for SVI checkpoints, and any valid key works for MAP/Laplace sampling. effective_seed = seed if seed is not None else 0 # Run SVI / MAP - ri = RunInference(gm, effective_seed) + ri = RunInference(orchestrator, effective_seed) if analysis_method == "svi": if pre_map_num_epoch > 0 and checkpoint_file is None: diff --git a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py index 389fcd8c..d38fef23 100644 --- a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py @@ -80,29 +80,29 @@ def predict_growth(config_file, file_concs = [float(x) for x in read_lines(titrant_concs_file)] if titrant_concs_file else [] print(f"Loading configuration from {config_file}...", flush=True) - gm, _ = read_configuration(config_file) + orchestrator, _ = read_configuration(config_file) is_map = param_file.endswith(".pkl") - param_file = resolve_param_file(param_file, gm, out_prefix) + param_file = resolve_param_file(param_file, orchestrator, out_prefix) # Build training-data membership set for in_training_data column. training_tuples = set( - zip(gm.growth_df["genotype"], - gm.growth_df["titrant_name"], - gm.growth_df["titrant_conc"]) + zip(orchestrator.growth_df["genotype"], + orchestrator.growth_df["titrant_name"], + orchestrator.growth_df["titrant_conc"]) ) if only_files: genotypes = file_genotypes if file_genotypes else None titrant_concs = file_concs if file_concs else None else: - training_genotypes = list(gm.growth_df["genotype"].unique()) + training_genotypes = list(orchestrator.growth_df["genotype"].unique()) genotypes = list(dict.fromkeys(training_genotypes + file_genotypes)) if file_genotypes else None - training_concs = list(gm.growth_df["titrant_conc"].unique()) + training_concs = list(orchestrator.growth_df["titrant_conc"].unique()) titrant_concs = sorted(set(training_concs) | set(file_concs)) if file_concs else None q_to_get = {"point_est": 0.5} if is_map else None print("Running growth predictions...", flush=True) - result_df = predict(model_class=gm, + result_df = predict(orchestrator=orchestrator, param_posteriors=param_file, predict_sites=["growth_pred"], num_samples=num_samples, diff --git a/src/tfscreen/tfmodel/scripts/predict_theta_cli.py b/src/tfscreen/tfmodel/scripts/predict_theta_cli.py index 3f89069e..5b808bb1 100644 --- a/src/tfscreen/tfmodel/scripts/predict_theta_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_theta_cli.py @@ -97,18 +97,18 @@ def predict_theta(config_file, ) print(f"Loading configuration from {config_file}...", flush=True) - gm, _ = read_configuration(config_file) + orchestrator, _ = read_configuration(config_file) is_map = param_file.endswith(".pkl") - param_file = resolve_param_file(param_file, gm, out_prefix) + param_file = resolve_param_file(param_file, orchestrator, out_prefix) # Determine training genotypes and (genotype, titrant_name, titrant_conc) set. # growth_tm is preferred (more genotypes); binding_tm is the fallback for # binding-only runs where growth_tm is None. - training_genotypes = set(gm.training_tm.df["genotype"].unique()) + training_genotypes = set(orchestrator.training_tm.df["genotype"].unique()) training_tuples = set( - zip(gm.training_tm.df["genotype"], - gm.training_tm.df["titrant_name"], - gm.training_tm.df["titrant_conc"]) + zip(orchestrator.training_tm.df["genotype"], + orchestrator.training_tm.df["titrant_name"], + orchestrator.training_tm.df["titrant_conc"]) ) # Resolve requested genotypes. @@ -141,7 +141,7 @@ def predict_theta(config_file, manual_titrant_df = file_titrant_df else: training_titrant_df = ( - gm.training_tm.df[["titrant_name", "titrant_conc"]] + orchestrator.training_tm.df[["titrant_name", "titrant_conc"]] .drop_duplicates() .reset_index(drop=True) ) @@ -157,10 +157,10 @@ def predict_theta(config_file, out_of_training = [g for g in requested_genotypes if g not in training_genotypes] if out_of_training: - module = model_registry.get("theta", {}).get(gm._theta) + module = model_registry.get("theta", {}).get(orchestrator._theta) if module is None or not hasattr(module, "predict_unmeasured"): raise ValueError( - f"The theta component '{gm._theta}' does not support prediction " + f"The theta component '{orchestrator._theta}' does not support prediction " "for genotypes not seen during training. Remove out-of-training " "genotypes from genotypes_file, or use a theta component that " "implements predict_unmeasured (e.g. 'hill')." @@ -170,13 +170,13 @@ def predict_theta(config_file, if manual_titrant_df is None: # Use unique (titrant_name, titrant_conc) pairs from training data. manual_titrant_df = ( - gm.training_tm.df[["titrant_name", "titrant_conc"]] + orchestrator.training_tm.df[["titrant_name", "titrant_conc"]] .drop_duplicates() .reset_index(drop=True) ) q_to_get = {"point_est": 0.5} if is_map else None result_df = extract_theta_unmeasured( - model=gm, + orchestrator=orchestrator, posteriors=param_file, target_genotypes=requested_genotypes, manual_titrant_df=manual_titrant_df, @@ -188,7 +188,7 @@ def predict_theta(config_file, flush=True) q_to_get = {"point_est": 0.5} if is_map else None result_df = extract_theta_curves( - model=gm, + orchestrator=orchestrator, posteriors=param_file, manual_titrant_df=manual_titrant_df, num_samples=num_samples, diff --git a/src/tfscreen/tfmodel/scripts/prior_predictive_cli.py b/src/tfscreen/tfmodel/scripts/prior_predictive_cli.py index 94723de7..3572ee4d 100644 --- a/src/tfscreen/tfmodel/scripts/prior_predictive_cli.py +++ b/src/tfscreen/tfmodel/scripts/prior_predictive_cli.py @@ -67,7 +67,7 @@ def sample_prior(config_file, independence. Default 0. """ print(f"Loading configuration from {config_file}...", flush=True) - gm, _ = read_configuration(config_file) + orchestrator, _ = read_configuration(config_file) width = len(str(num_datasets)) @@ -75,10 +75,10 @@ def sample_prior(config_file, tag = str(i).zfill(max(width, 3)) print(f"Drawing prior sample {i + 1}/{num_datasets}...", flush=True) - predictions, latent_params = draw_prior(gm, rng_key=seed + i, num_draws=1) + predictions, latent_params = draw_prior(orchestrator, rng_key=seed + i, num_draws=1) rng = np.random.default_rng(seed + i) if noise else None - growth_df = growth_df_from_prior(gm, latent_params, draw_idx=0, noise_rng=rng) + growth_df = growth_df_from_prior(orchestrator, latent_params, draw_idx=0, noise_rng=rng) csv_path = f"{out_prefix}_{tag}_growth.csv" growth_df.to_csv(csv_path, index=False) diff --git a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py index 4c4ff55e..9c889ff0 100644 --- a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py @@ -171,7 +171,7 @@ def _intersect_data(growth_df, binding_df): # theta_values for the simple component # --------------------------------------------------------------------------- -def _compute_theta_values(gm_cal, binding_df_cal): +def _compute_theta_values(orchestrator_cal, binding_df_cal): """ Build a (num_titrant_name, num_titrant_conc) theta tensor for the simple-theta component of the calibration model. @@ -183,13 +183,13 @@ def _compute_theta_values(gm_cal, binding_df_cal): cell is set to 0.5 (uninformative midpoint). The dimension ordering is taken from - ``gm_cal.binding_tm.tensor_dim_labels`` so the resulting array + ``orchestrator_cal.binding_tm.tensor_dim_labels`` so the resulting array matches the layout the simple-theta component expects. """ - tn_idx = gm_cal.binding_tm.tensor_dim_names.index("titrant_name") - tc_idx = gm_cal.binding_tm.tensor_dim_names.index("titrant_conc") - titrant_name_labels = list(gm_cal.binding_tm.tensor_dim_labels[tn_idx]) - titrant_conc_labels = list(gm_cal.binding_tm.tensor_dim_labels[tc_idx]) + tn_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("titrant_name") + tc_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("titrant_conc") + titrant_name_labels = list(orchestrator_cal.binding_tm.tensor_dim_labels[tn_idx]) + titrant_conc_labels = list(orchestrator_cal.binding_tm.tensor_dim_labels[tc_idx]) n_name = len(titrant_name_labels) n_conc = len(titrant_conc_labels) @@ -226,7 +226,7 @@ def _compute_theta_values(gm_cal, binding_df_cal): # Calibration model construction # --------------------------------------------------------------------------- -def _build_calibration_model(gm_prod, growth_df_cal, binding_df_cal): +def _build_calibration_model(orchestrator_prod, growth_df_cal, binding_df_cal): """ Construct the calibration ``ModelOrchestrator`` with hardcoded calibration overrides applied on top of the production component selections. @@ -237,7 +237,7 @@ def _build_calibration_model(gm_prod, growth_df_cal, binding_df_cal): sees the (calibration-genotype × calibration-condition) intersection, which by construction does not include the production's spiked rows. """ - settings = dict(gm_prod.settings) + settings = dict(orchestrator_prod.settings) for k, v in _CALIBRATION_OVERRIDES.items(): settings[k] = v settings["spiked_genotypes"] = None @@ -254,7 +254,7 @@ def _build_calibration_model(gm_prod, growth_df_cal, binding_df_cal): **settings) -def _inject_calibration_priors(gm_cal, gm_prod, theta_values): +def _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values): """ Wire the calibration model's priors into shape: @@ -268,10 +268,10 @@ def _inject_calibration_priors(gm_cal, gm_prod, theta_values): those hyperparameters to their prior loc / scale defaults so the MAP doesn't try to learn them from calibration-only data. - Mutates ``gm_cal._priors`` in place. + Mutates ``orchestrator_cal._priors`` in place. """ - prod_growth = gm_prod.priors.growth - cal_growth = gm_cal.priors.growth + prod_growth = orchestrator_prod.priors.growth + cal_growth = orchestrator_cal.priors.growth cg_prod = prod_growth.condition_growth cg_cal = cal_growth.condition_growth @@ -335,21 +335,21 @@ def _inject_calibration_priors(gm_cal, gm_prod, theta_values): new_growth = cal_growth.replace(**growth_updates) # theta priors live on PriorsClass.theta - theta_priors = gm_cal.priors.theta + theta_priors = orchestrator_cal.priors.theta if hasattr(theta_priors, "replace"): new_theta = theta_priors.replace(theta_values=theta_values) else: new_theta = theta_priors - new_priors = gm_cal.priors.replace(growth=new_growth, theta=new_theta) - gm_cal._priors = new_priors + new_priors = orchestrator_cal.priors.replace(growth=new_growth, theta=new_theta) + orchestrator_cal._priors = new_priors # --------------------------------------------------------------------------- # Field mapping introspection # --------------------------------------------------------------------------- -def _identify_field_mapping(gm_cal): +def _identify_field_mapping(orchestrator_cal): """ Enumerate the ``condition_growth`` and ``growth_transition`` sample sites and derive their ``ModelPriors`` field names. @@ -374,8 +374,8 @@ def _identify_field_mapping(gm_cal): dict[str, dict] Site name → ``{"component", "dist_class", "is_array", ...field names...}``. """ - model_trace = trace(seed(gm_cal.jax_model, rng_seed=0)).get_trace( - data=gm_cal.data, priors=gm_cal.priors + model_trace = trace(seed(orchestrator_cal.jax_model, rng_seed=0)).get_trace( + data=orchestrator_cal.data, priors=orchestrator_cal.priors ) out = {} @@ -693,7 +693,7 @@ def _run_calibration_map(ri, # Diagnostic plots # --------------------------------------------------------------------------- -def _run_calibration_diagnostics(gm_cal, params, out_prefix, growth_pred_std=None): +def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pred_std=None): """ Generate per-genotype trajectory PDFs and write correlation / stats outputs. @@ -703,7 +703,7 @@ def _run_calibration_diagnostics(gm_cal, params, out_prefix, growth_pred_std=Non Parameters ---------- - gm_cal : ModelOrchestrator + orchestrator_cal : ModelOrchestrator Calibration ModelOrchestrator. params : dict MAP parameter dict (``{site}_auto_loc`` keys). @@ -714,7 +714,7 @@ def _run_calibration_diagnostics(gm_cal, params, out_prefix, growth_pred_std=Non shape ``(R, T, CP, CS, TN, TC, G)``. """ growth_df_out = plot_geno_trajectory( - gm_cal, + orchestrator_cal, out_prefix, params=params, growth_pred_std=growth_pred_std, @@ -850,7 +850,7 @@ def _make_correlation_plot(df, out_prefix): # Laplace-based prediction uncertainty # --------------------------------------------------------------------------- -def _compute_growth_pred_std(ri, params, gm_cal, n_samples=100): +def _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=100): """ Estimate per-cell growth_pred standard deviation via Laplace samples. @@ -879,17 +879,17 @@ def _compute_growth_pred_std(ri, params, gm_cal, n_samples=100): if not unconstrained: return None - data_on_gpu = jax.device_put(gm_cal.data) - all_indices = jnp.arange(gm_cal.data.num_genotype) - full_data = gm_cal.get_batch(data_on_gpu, all_indices) - model_kwargs = {"priors": gm_cal.priors, "data": full_data} + data_on_gpu = jax.device_put(orchestrator_cal.data) + all_indices = jnp.arange(orchestrator_cal.data.num_genotype) + full_data = orchestrator_cal.get_batch(data_on_gpu, all_indices) + model_kwargs = {"priors": orchestrator_cal.priors, "data": full_data} flat_map, unravel = jax.flatten_util.ravel_pytree(unconstrained) D = int(flat_map.shape[0]) def pe_fn(flat_p): return potential_energy( - gm_cal.jax_model, [], model_kwargs, unravel(flat_p) + orchestrator_cal.jax_model, [], model_kwargs, unravel(flat_p) ) hessian = jax.hessian(pe_fn)(flat_map) @@ -901,7 +901,7 @@ def pe_fn(flat_p): L = jnp.array(L_np, dtype=jnp.float32) # Per-site unconstrained → constrained bijections. - model_trace = trace(seed(gm_cal.jax_model, rng_seed=0)).get_trace(**model_kwargs) + model_trace = trace(seed(orchestrator_cal.jax_model, rng_seed=0)).get_trace(**model_kwargs) site_transforms = { name: biject_to(site["fn"].support) for name, site in model_trace.items() @@ -919,8 +919,8 @@ def pe_fn(flat_p): for k, v in unc_samples.items() } - pred_fn = Predictive(gm_cal.jax_model, posterior_samples=constrained_samples) - pred = pred_fn(ri.get_key(), data=full_data, priors=gm_cal.priors) + pred_fn = Predictive(orchestrator_cal.jax_model, posterior_samples=constrained_samples) + pred = pred_fn(ri.get_key(), data=full_data, priors=orchestrator_cal.priors) if "growth_pred" not in pred: return None @@ -1042,11 +1042,11 @@ def run_prefit_calibration(config_file, # 1. Resolve production config and CSV targets. priors_path, guesses_path = _resolve_csv_paths(config_file) - gm_prod, _ = read_configuration(config_file) + orchestrator_prod, _ = read_configuration(config_file) # 2. Filter to the calibration (genotype, titrant_name, titrant_conc) # intersection. read_configuration already loaded the production - # data paths into gm_prod; pull them straight off the config so we + # data paths into orchestrator_prod; pull them straight off the config so we # don't depend on an internal attribute. with open(config_file, "r") as fh: cfg = yaml.safe_load(fh) @@ -1055,17 +1055,17 @@ def run_prefit_calibration(config_file, growth_df_cal, binding_df_cal = _intersect_data(growth_path, binding_path) # 3. Build the calibration model with overrides applied. - gm_cal = _build_calibration_model(gm_prod, growth_df_cal, binding_df_cal) - theta_values = _compute_theta_values(gm_cal, binding_df_cal) - _inject_calibration_priors(gm_cal, gm_prod, theta_values) + orchestrator_cal = _build_calibration_model(orchestrator_prod, growth_df_cal, binding_df_cal) + theta_values = _compute_theta_values(orchestrator_cal, binding_df_cal) + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) # 4. MAP fit + Hessian sigmas. effective_seed = seed if seed is not None else 0 - ri = RunInference(gm_cal, effective_seed) + ri = RunInference(orchestrator_cal, effective_seed) svi_state, params, converged = _run_calibration_map( ri, - init_params=gm_cal.init_params, + init_params=orchestrator_cal.init_params, out_prefix=out_prefix, checkpoint_file=checkpoint_file, adam_step_size=adam_step_size, @@ -1086,7 +1086,7 @@ def run_prefit_calibration(config_file, hessian_results = ri.compute_hessian_sigmas(params) # 5. Map sample sites → CSV fields and apply in-place updates. - field_mapping = _identify_field_mapping(gm_cal) + field_mapping = _identify_field_mapping(orchestrator_cal) prior_updates, guess_updates = _build_csv_updates(field_mapping, hessian_results) _apply_priors_updates(priors_path, prior_updates) @@ -1094,8 +1094,8 @@ def run_prefit_calibration(config_file, # 6. Write per-genotype diagnostic plots with Laplace-based prediction uncertainty. print("Computing growth_pred uncertainty via Laplace samples ...", flush=True) - growth_pred_std = _compute_growth_pred_std(ri, params, gm_cal) - _run_calibration_diagnostics(gm_cal, params, out_prefix, + growth_pred_std = _compute_growth_pred_std(ri, params, orchestrator_cal) + _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pred_std=growth_pred_std) return svi_state, params, converged diff --git a/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py b/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py index b0cbf089..11ef71bf 100644 --- a/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py +++ b/src/tfscreen/tfmodel/scripts/sample_posterior_cli.py @@ -62,8 +62,8 @@ def sample_posterior(config_file, "Run tfs-fit-model first to produce a checkpoint." ) - gm, init_params = read_configuration(config_file) - ri = RunInference(gm, seed) + orchestrator, init_params = read_configuration(config_file) + ri = RunInference(orchestrator, seed) with open(checkpoint_file, "rb") as f: chk_data = dill.load(f) diff --git a/tests/smoke-tests/test_run_growth_analysis_smoke.py b/tests/smoke-tests/test_run_growth_analysis_smoke.py index 1a465807..e1d69499 100644 --- a/tests/smoke-tests/test_run_growth_analysis_smoke.py +++ b/tests/smoke-tests/test_run_growth_analysis_smoke.py @@ -7,11 +7,11 @@ from tfscreen.tfmodel.scripts.sample_posterior_cli import sample_posterior -def _build_config(gm, tmpdir, growth_smoke_csv, binding_smoke_csv): - """Write a config file for gm and return its path.""" +def _build_config(orchestrator, tmpdir, growth_smoke_csv, binding_smoke_csv): + """Write a config file for orchestrator and return its path.""" config_root = os.path.join(tmpdir, "smoke") write_configuration( - gm=gm, + orchestrator=orchestrator, out_prefix=config_root, growth_df_path=str(growth_smoke_csv), binding_df_path=str(binding_smoke_csv), @@ -26,7 +26,7 @@ def test_run_growth_analysis_smoke(growth_smoke_csv, binding_smoke_csv, tmpdir): out_prefix = os.path.join(tmpdir, "run_smoke") # 1. Initialize a model and write its configuration - gm = ModelOrchestrator( + orchestrator = ModelOrchestrator( growth_df=growth_smoke_csv, binding_df=binding_smoke_csv, condition_growth="linear", @@ -37,7 +37,7 @@ def test_run_growth_analysis_smoke(growth_smoke_csv, binding_smoke_csv, tmpdir): config_root = os.path.join(tmpdir, "smoke") write_configuration( - gm=gm, + orchestrator=orchestrator, out_prefix=config_root, growth_df_path=str(growth_smoke_csv), binding_df_path=str(binding_smoke_csv) @@ -85,7 +85,7 @@ def test_run_growth_analysis_nuts_smoke(growth_smoke_csv, binding_smoke_csv, tmp - a posterior HDF5 is written with the expected keys - a checkpoint pkl is written containing 'mcmc_samples' """ - gm = ModelOrchestrator( + orchestrator = ModelOrchestrator( growth_df=growth_smoke_csv, binding_df=binding_smoke_csv, condition_growth="linear", @@ -93,7 +93,7 @@ def test_run_growth_analysis_nuts_smoke(growth_smoke_csv, binding_smoke_csv, tmp theta="hill_geno", batch_size=None, ) - config_file = _build_config(gm, tmpdir, growth_smoke_csv, binding_smoke_csv) + config_file = _build_config(orchestrator, tmpdir, growth_smoke_csv, binding_smoke_csv) assert os.path.exists(config_file) out_prefix = os.path.join(tmpdir, "nuts_smoke") @@ -140,7 +140,7 @@ def test_run_growth_analysis_nuts_posterior_smoke(growth_smoke_csv, binding_smok Runs a tiny NUTS chain, then re-generates posteriors from the checkpoint and verifies that the output HDF5 is recreated correctly. """ - gm = ModelOrchestrator( + orchestrator = ModelOrchestrator( growth_df=growth_smoke_csv, binding_df=binding_smoke_csv, condition_growth="linear", @@ -148,7 +148,7 @@ def test_run_growth_analysis_nuts_posterior_smoke(growth_smoke_csv, binding_smok theta="hill_geno", batch_size=None, ) - config_file = _build_config(gm, tmpdir, growth_smoke_csv, binding_smoke_csv) + config_file = _build_config(orchestrator, tmpdir, growth_smoke_csv, binding_smoke_csv) # Step 1: run NUTS to produce a checkpoint nuts_root = os.path.join(tmpdir, "nuts_for_post") diff --git a/tests/tfscreen/plot/test_geno_trajectory.py b/tests/tfscreen/plot/test_geno_trajectory.py index 3bcb11b2..83b22fcb 100644 --- a/tests/tfscreen/plot/test_geno_trajectory.py +++ b/tests/tfscreen/plot/test_geno_trajectory.py @@ -8,7 +8,7 @@ (b) matplotlib PDF rendering. Both are mocked out here so tests run fast and without GPU/display dependencies. -A helper ``_make_fake_gm`` constructs a minimal mock ModelOrchestrator +A helper ``_make_fake_orchestrator`` constructs a minimal mock ModelOrchestrator whose attributes match what plot_geno_trajectory reads. The mock returns deterministic numpy arrays for all data tensors and a small but realistic dimension structure (2 replicates, 3 timepoints, 1 condition_pre, @@ -52,7 +52,7 @@ # Fake ModelOrchestrator # --------------------------------------------------------------------------- -def _make_fake_gm(with_presplit=False): +def _make_fake_orchestrator(with_presplit=False): """ Return a MagicMock that quacks like a ModelOrchestrator for the attributes accessed by plot_geno_trajectory. @@ -133,12 +133,12 @@ def _make_fake_gm(with_presplit=False): "theta_obs": [0.5, 0.3, 0.5, 0.3], }) - gm = MagicMock() - gm.data = data - gm.growth_tm = tm - gm.binding_df = binding_df + orchestrator = MagicMock() + orchestrator.data = data + orchestrator.growth_tm = tm + orchestrator.binding_df = binding_df - return gm + return orchestrator def _fake_map_predictions(shape): @@ -159,15 +159,15 @@ def _fake_map_predictions(shape): class TestArgumentValidation: def test_raises_when_neither_provided(self, tmp_path): - gm = _make_fake_gm() + orchestrator = _make_fake_orchestrator() with pytest.raises(ValueError, match="Exactly one"): - plot_geno_trajectory(gm, str(tmp_path / "out")) + plot_geno_trajectory(orchestrator, str(tmp_path / "out")) def test_raises_when_both_provided(self, tmp_path): - gm = _make_fake_gm() + orchestrator = _make_fake_orchestrator() with pytest.raises(ValueError, match="Exactly one"): plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, posterior_file="some_file.h5", @@ -182,46 +182,46 @@ class TestMapPath: @pytest.fixture def patched_plot(self, tmp_path): """Patch _compute_map_predictions and matplotlib.pyplot.savefig.""" - gm = _make_fake_gm() + orchestrator = _make_fake_orchestrator() fake_preds = _fake_map_predictions(SHAPE) with patch( "tfscreen.plot.geno_trajectory._compute_map_predictions", return_value=fake_preds, ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): - yield gm, tmp_path + yield orchestrator, tmp_path def test_returns_dataframe(self, patched_plot): - gm, tmp_path = patched_plot + orchestrator, tmp_path = patched_plot result = plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, ) assert isinstance(result, pd.DataFrame) def test_df_has_ln_cfu_pred(self, patched_plot): - gm, tmp_path = patched_plot + orchestrator, tmp_path = patched_plot result = plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, ) assert "ln_cfu_pred" in result.columns def test_csv_written_by_default(self, patched_plot): - gm, tmp_path = patched_plot + orchestrator, tmp_path = patched_plot plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, ) assert os.path.exists(tmp_path / "out_calib_growth_df.csv") def test_csv_not_written_when_disabled(self, patched_plot): - gm, tmp_path = patched_plot + orchestrator, tmp_path = patched_plot plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, write_csv=False, @@ -229,10 +229,10 @@ def test_csv_not_written_when_disabled(self, patched_plot): assert not os.path.exists(tmp_path / "out_calib_growth_df.csv") def test_growth_pred_std_in_csv(self, patched_plot): - gm, tmp_path = patched_plot + orchestrator, tmp_path = patched_plot gps = np.ones(SHAPE) * 0.1 plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, growth_pred_std=gps, @@ -247,46 +247,46 @@ def test_growth_pred_std_in_csv(self, patched_plot): class TestGenotypeFilter: @pytest.fixture - def gm_and_path(self, tmp_path): - gm = _make_fake_gm() - return gm, tmp_path + def orchestrator_and_path(self, tmp_path): + orchestrator = _make_fake_orchestrator() + return orchestrator, tmp_path - def _run(self, gm, tmp_path, **kwargs): + def _run(self, orchestrator, tmp_path, **kwargs): fake_preds = _fake_map_predictions(SHAPE) with patch( "tfscreen.plot.geno_trajectory._compute_map_predictions", return_value=fake_preds, ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): return plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, **kwargs, ) - def test_no_filter_plots_all_genotypes(self, gm_and_path, capsys): - gm, tmp_path = gm_and_path - self._run(gm, tmp_path) + def test_no_filter_plots_all_genotypes(self, orchestrator_and_path, capsys): + orchestrator, tmp_path = orchestrator_and_path + self._run(orchestrator, tmp_path) captured = capsys.readouterr() for name in GENO_NAMES: assert name in captured.out - def test_genotype_subset(self, gm_and_path, capsys): - gm, tmp_path = gm_and_path - self._run(gm, tmp_path, genotypes=["WT"]) + def test_genotype_subset(self, orchestrator_and_path, capsys): + orchestrator, tmp_path = orchestrator_and_path + self._run(orchestrator, tmp_path, genotypes=["WT"]) captured = capsys.readouterr() assert "WT" in captured.out assert "mut1" not in captured.out - def test_unknown_genotype_warns(self, gm_and_path, capsys): - gm, tmp_path = gm_and_path - self._run(gm, tmp_path, genotypes=["WT", "nonexistent"]) + def test_unknown_genotype_warns(self, orchestrator_and_path, capsys): + orchestrator, tmp_path = orchestrator_and_path + self._run(orchestrator, tmp_path, genotypes=["WT", "nonexistent"]) captured = capsys.readouterr() assert "nonexistent" in captured.err - def test_all_unknown_returns_df(self, gm_and_path): - gm, tmp_path = gm_and_path - result = self._run(gm, tmp_path, genotypes=["nonexistent"]) + def test_all_unknown_returns_df(self, orchestrator_and_path): + orchestrator, tmp_path = orchestrator_and_path + result = self._run(orchestrator, tmp_path, genotypes=["nonexistent"]) # Should still return a DataFrame (from the full data), just no PDFs. assert isinstance(result, pd.DataFrame) @@ -296,29 +296,29 @@ def test_all_unknown_returns_df(self, gm_and_path): # --------------------------------------------------------------------------- class TestTitrantNameFilter: - def _run(self, gm, tmp_path, **kwargs): + def _run(self, orchestrator, tmp_path, **kwargs): fake_preds = _fake_map_predictions(SHAPE) with patch( "tfscreen.plot.geno_trajectory._compute_map_predictions", return_value=fake_preds, ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): return plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, **kwargs, ) def test_matching_titrant_produces_output(self, tmp_path, capsys): - gm = _make_fake_gm() - self._run(gm, tmp_path, titrant_names=["IPTG"]) + orchestrator = _make_fake_orchestrator() + self._run(orchestrator, tmp_path, titrant_names=["IPTG"]) captured = capsys.readouterr() # PDFs should still be saved (the filter matches). assert "Saved" in captured.out def test_nonmatching_titrant_no_pdfs(self, tmp_path, capsys): - gm = _make_fake_gm() - self._run(gm, tmp_path, titrant_names=["arabinose"]) + orchestrator = _make_fake_orchestrator() + self._run(orchestrator, tmp_path, titrant_names=["arabinose"]) captured = capsys.readouterr() # No valid condition combos → no PDF files saved (the CSV may still be). assert ".pdf" not in captured.out @@ -329,7 +329,7 @@ def test_nonmatching_titrant_no_pdfs(self, tmp_path, capsys): # --------------------------------------------------------------------------- class TestPresplit: - def _run(self, gm, tmp_path): + def _run(self, orchestrator, tmp_path): fake_preds = _fake_map_predictions(SHAPE) ax_mock = MagicMock() # Capture all errorbar calls to verify presplit is rendered. @@ -354,7 +354,7 @@ def _run(self, gm, tmp_path): return_value=(MagicMock(), axes_array), ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), params={"x_auto_loc": np.array(1.0)}, ) @@ -372,8 +372,8 @@ def _is_scalar_negative(x): return a.ndim == 0 and float(a) < 0 def test_presplit_errorbar_called_when_data_present(self, tmp_path): - gm = _make_fake_gm(with_presplit=True) - ax_mock = self._run(gm, tmp_path) + orchestrator = _make_fake_orchestrator(with_presplit=True) + ax_mock = self._run(orchestrator, tmp_path) # The presplit errorbar is called with a scalar negative x (−t_pre); # selection-phase errorbars use array x values. neg_x_calls = [ @@ -386,8 +386,8 @@ def test_presplit_errorbar_called_when_data_present(self, tmp_path): ) def test_no_presplit_errorbar_at_negative_x(self, tmp_path): - gm = _make_fake_gm(with_presplit=False) - ax_mock = self._run(gm, tmp_path) + orchestrator = _make_fake_orchestrator(with_presplit=False) + ax_mock = self._run(orchestrator, tmp_path) neg_x_calls = [ c for c in ax_mock.errorbar.call_args_list if c.args and self._is_scalar_negative(c.args[0]) @@ -422,8 +422,8 @@ def _make_h5(self, tmp_path): def test_compute_posterior_predictions_shapes(self, tmp_path): h5_path = self._make_h5(tmp_path) - gm = _make_fake_gm() - med, lo, hi, ln_cfu0 = _compute_posterior_predictions(gm, h5_path) + orchestrator = _make_fake_orchestrator() + med, lo, hi, ln_cfu0 = _compute_posterior_predictions(orchestrator, h5_path) assert med.shape == SHAPE assert lo.shape == SHAPE assert hi.shape == SHAPE @@ -432,17 +432,17 @@ def test_compute_posterior_predictions_shapes(self, tmp_path): def test_compute_posterior_predictions_ordering(self, tmp_path): """5th percentile ≤ median ≤ 95th percentile everywhere.""" h5_path = self._make_h5(tmp_path) - gm = _make_fake_gm() - med, lo, hi, _ = _compute_posterior_predictions(gm, h5_path) + orchestrator = _make_fake_orchestrator() + med, lo, hi, _ = _compute_posterior_predictions(orchestrator, h5_path) assert np.all(lo <= med + 1e-9) assert np.all(med <= hi + 1e-9) def test_posterior_path_returns_df_with_lo_hi(self, tmp_path): h5_path = self._make_h5(tmp_path) - gm = _make_fake_gm() + orchestrator = _make_fake_orchestrator() with patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): result = plot_geno_trajectory( - gm, + orchestrator, str(tmp_path / "out"), posterior_file=h5_path, ) @@ -457,9 +457,9 @@ def test_posterior_raises_on_missing_growth_pred(self, tmp_path): with h5py.File(h5_path, "w") as fh: fh.create_dataset("something_else", data=np.zeros(3)) - gm = _make_fake_gm() + orchestrator = _make_fake_orchestrator() with pytest.raises(ValueError, match="growth_pred not found"): - _compute_posterior_predictions(gm, h5_path) + _compute_posterior_predictions(orchestrator, h5_path) def test_params_path_loads_npz(self, tmp_path): """Passing a string path to params loads the .npz file.""" @@ -467,7 +467,7 @@ def test_params_path_loads_npz(self, tmp_path): np.savez(npz_path, x_auto_loc=np.array(1.0)) fake_preds = _fake_map_predictions(SHAPE) - gm = _make_fake_gm() + orchestrator = _make_fake_orchestrator() with patch( "tfscreen.plot.geno_trajectory._compute_map_predictions", @@ -475,7 +475,7 @@ def test_params_path_loads_npz(self, tmp_path): ) as mock_fn, patch("matplotlib.pyplot.savefig"), patch( "matplotlib.pyplot.close" ): - plot_geno_trajectory(gm, str(tmp_path / "out"), params=npz_path) + plot_geno_trajectory(orchestrator, str(tmp_path / "out"), params=npz_path) # The helper should have been called with a dict (loaded from npz), # not the raw string path. diff --git a/tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py b/tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py index 1fd7db70..88970f33 100644 --- a/tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py +++ b/tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py @@ -325,7 +325,7 @@ def test_get_priors_no_data_returns_defaults(): def test_get_priors_accepts_data_keyword(): - """get_priors must accept an optional `data` keyword (model_class detects this).""" + """get_priors must accept an optional `data` keyword (ModelOrchestrator detects this).""" sig = inspect.signature(get_priors) assert "data" in sig.parameters assert sig.parameters["data"].default is None diff --git a/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py b/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py index c79744dd..f9bebfd8 100644 --- a/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py @@ -117,7 +117,7 @@ def test_passes_point_est_q_to_get(self, tmp_path): out_prefix = str(tmp_path / "out") captured = {} - def fake_extract(gm, posteriors, q_to_get=None): + def fake_extract(orchestrator, posteriors, q_to_get=None): captured["q_to_get"] = q_to_get return {"activity": pd.DataFrame({"genotype": ["wt"], "point_est": [0.5]})} @@ -140,7 +140,7 @@ def test_posteriors_have_leading_sample_dim(self, tmp_path): ckpt_path = _make_map_checkpoint(tmp_path, model, step=100) captured = {} - def fake_extract(gm, posteriors, q_to_get=None): + def fake_extract(orchestrator, posteriors, q_to_get=None): captured["posteriors"] = posteriors return {"activity": pd.DataFrame({"genotype": ["wt"], "point_est": [0.5]})} @@ -175,7 +175,7 @@ def test_calls_extract_with_default_quantiles(self, tmp_path): with h5py.File(posterior_file, "w") as f: f.create_dataset("dummy_param", data=np.ones((10, 4))) - def fake_extract(gm, posteriors, q_to_get=None): + def fake_extract(orchestrator, posteriors, q_to_get=None): captured["q_to_get"] = q_to_get return {"activity": pd.DataFrame({"genotype": ["wt"], "median": [0.5]})} diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py index 293bd5fe..3f3596ca 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py @@ -30,21 +30,21 @@ def _write_lines(path, lines): @pytest.fixture -def mock_gm(): - gm = MagicMock() - gm.growth_df = _make_growth_df(["wt", "A1B"], [0.0, 1.0]) - return gm +def mock_orchestrator(): + orchestrator = MagicMock() + orchestrator.growth_df = _make_growth_df(["wt", "A1B"], [0.0, 1.0]) + return orchestrator @pytest.fixture -def mock_predict(mock_gm): +def mock_predict(mock_orchestrator): """Patch read_configuration, resolve_param_file, and predict; return captured call kwargs.""" calls = {} def fake_predict(**kwargs): calls.update(kwargs) - genotypes = kwargs.get("genotypes") or mock_gm.growth_df["genotype"].unique().tolist() - concs = kwargs.get("titrant_conc") or mock_gm.growth_df["titrant_conc"].unique().tolist() + genotypes = kwargs.get("genotypes") or mock_orchestrator.growth_df["genotype"].unique().tolist() + concs = kwargs.get("titrant_conc") or mock_orchestrator.growth_df["titrant_conc"].unique().tolist() rows = [{"genotype": g, "titrant_name": "IPTG", "titrant_conc": c, "median": 10.0} for g in genotypes for c in concs] @@ -53,11 +53,11 @@ def fake_predict(**kwargs): with patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.read_configuration", - return_value=(mock_gm, {}), + return_value=(mock_orchestrator, {}), ), patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.resolve_param_file", - side_effect=lambda pf, gm, op: pf, # pass-through + side_effect=lambda pf, orchestrator, op: pf, # pass-through ), patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.predict", @@ -87,7 +87,7 @@ def test_no_files_passes_none_concs(self, mock_predict, tmp_path): class TestPredictGrowthUnion: - def test_genotypes_file_unions_with_training(self, mock_predict, mock_gm, tmp_path): + def test_genotypes_file_unions_with_training(self, mock_predict, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["C2D"]) # novel genotype predict_growth("cfg.yaml", "post.h5", @@ -97,7 +97,7 @@ def test_genotypes_file_unions_with_training(self, mock_predict, mock_gm, tmp_pa assert "A1B" in mock_predict["genotypes"] assert "C2D" in mock_predict["genotypes"] - def test_genotypes_file_union_preserves_order(self, mock_predict, mock_gm, tmp_path): + def test_genotypes_file_union_preserves_order(self, mock_predict, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["C2D"]) predict_growth("cfg.yaml", "post.h5", @@ -107,7 +107,7 @@ def test_genotypes_file_union_preserves_order(self, mock_predict, mock_gm, tmp_p idx_c2d = mock_predict["genotypes"].index("C2D") assert idx_c2d > 0 - def test_concs_file_unions_with_training(self, mock_predict, mock_gm, tmp_path): + def test_concs_file_unions_with_training(self, mock_predict, mock_orchestrator, tmp_path): cf = str(tmp_path / "concs.txt") _write_lines(cf, [5.0]) # novel concentration predict_growth("cfg.yaml", "post.h5", @@ -117,7 +117,7 @@ def test_concs_file_unions_with_training(self, mock_predict, mock_gm, tmp_path): assert 1.0 in mock_predict["titrant_conc"] assert 5.0 in mock_predict["titrant_conc"] - def test_duplicate_concs_not_repeated(self, mock_predict, mock_gm, tmp_path): + def test_duplicate_concs_not_repeated(self, mock_predict, mock_orchestrator, tmp_path): cf = str(tmp_path / "concs.txt") _write_lines(cf, [1.0]) # already in training predict_growth("cfg.yaml", "post.h5", @@ -126,7 +126,7 @@ def test_duplicate_concs_not_repeated(self, mock_predict, mock_gm, tmp_path): concs = mock_predict["titrant_conc"] assert concs.count(1.0) == 1 - def test_duplicate_genotypes_not_repeated(self, mock_predict, mock_gm, tmp_path): + def test_duplicate_genotypes_not_repeated(self, mock_predict, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["wt"]) # already in training predict_growth("cfg.yaml", "post.h5", @@ -176,7 +176,7 @@ def test_only_files_no_file_falls_through_to_none(self, mock_predict, tmp_path): class TestPredictGrowthTitrantNamesFilter: - def test_titrant_names_file_filters_output_rows(self, mock_predict, mock_gm, tmp_path): + def test_titrant_names_file_filters_output_rows(self, mock_predict, mock_orchestrator, tmp_path): nf = str(tmp_path / "names.txt") _write_lines(nf, ["IPTG"]) out = str(tmp_path / "out") @@ -203,11 +203,11 @@ def test_titrant_names_file_does_not_affect_predict_call(self, mock_predict, tmp class TestPredictGrowthCheckpointInput: - def _make_fixtures(self, mock_gm, resolved_path="resolved.h5"): + def _make_fixtures(self, mock_orchestrator, resolved_path="resolved.h5"): """Return patch stack that intercepts resolve_param_file.""" def fake_predict(**kwargs): - genotypes = kwargs.get("genotypes") or mock_gm.growth_df["genotype"].unique().tolist() - concs = kwargs.get("titrant_conc") or mock_gm.growth_df["titrant_conc"].unique().tolist() + genotypes = kwargs.get("genotypes") or mock_orchestrator.growth_df["genotype"].unique().tolist() + concs = kwargs.get("titrant_conc") or mock_orchestrator.growth_df["titrant_conc"].unique().tolist() rows = [{"genotype": g, "titrant_name": "IPTG", "titrant_conc": c, "median": 10.0} for g in genotypes for c in concs] @@ -217,7 +217,7 @@ def fake_predict(**kwargs): patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.read_configuration", - return_value=(mock_gm, {}), + return_value=(mock_orchestrator, {}), ), patch( "tfscreen.tfmodel.scripts" @@ -226,15 +226,15 @@ def fake_predict(**kwargs): ), ] - def test_pkl_param_file_calls_resolve(self, mock_gm, tmp_path): + def test_pkl_param_file_calls_resolve(self, mock_orchestrator, tmp_path): """resolve_param_file is called when param_file ends with .pkl.""" resolve_calls = [] - def fake_resolve(pf, gm, op): + def fake_resolve(pf, orchestrator, op): resolve_calls.append(pf) return "resolved.h5" - patches = self._make_fixtures(mock_gm) + patches = self._make_fixtures(mock_orchestrator) with patches[0], patches[1], patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.resolve_param_file", @@ -245,15 +245,15 @@ def fake_resolve(pf, gm, op): assert resolve_calls == ["myrun_checkpoint.pkl"] - def test_h5_param_file_calls_resolve(self, mock_gm, tmp_path): + def test_h5_param_file_calls_resolve(self, mock_orchestrator, tmp_path): """resolve_param_file is called for .h5 files too (pass-through).""" resolve_calls = [] - def fake_resolve(pf, gm, op): + def fake_resolve(pf, orchestrator, op): resolve_calls.append(pf) return pf - patches = self._make_fixtures(mock_gm) + patches = self._make_fixtures(mock_orchestrator) with patches[0], patches[1], patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.resolve_param_file", @@ -264,7 +264,7 @@ def fake_resolve(pf, gm, op): assert resolve_calls == ["post.h5"] - def test_resolved_path_passed_to_predict(self, mock_gm, tmp_path): + def test_resolved_path_passed_to_predict(self, mock_orchestrator, tmp_path): """The path returned by resolve_param_file is what predict receives.""" predict_calls = {} @@ -273,7 +273,7 @@ def fake_predict(**kwargs): return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], "titrant_conc": [0.0], "median": [10.0]}) - patches = self._make_fixtures(mock_gm) + patches = self._make_fixtures(mock_orchestrator) with patches[0], patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.predict", @@ -288,7 +288,7 @@ def fake_predict(**kwargs): assert predict_calls["param_posteriors"] == "resolved_map.h5" - def test_pkl_passes_point_est_q_to_get(self, mock_gm, tmp_path): + def test_pkl_passes_point_est_q_to_get(self, mock_orchestrator, tmp_path): """q_to_get={"point_est": 0.5} is passed to predict when param_file is .pkl.""" predict_calls = {} @@ -297,7 +297,7 @@ def fake_predict(**kwargs): return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], "titrant_conc": [0.0], "point_est": [10.0]}) - patches = self._make_fixtures(mock_gm) + patches = self._make_fixtures(mock_orchestrator) with patches[0], patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.predict", @@ -312,7 +312,7 @@ def fake_predict(**kwargs): assert predict_calls["q_to_get"] == {"point_est": 0.5} - def test_h5_passes_none_q_to_get(self, mock_gm, tmp_path): + def test_h5_passes_none_q_to_get(self, mock_orchestrator, tmp_path): """q_to_get=None is passed to predict when param_file is .h5.""" predict_calls = {} @@ -321,7 +321,7 @@ def fake_predict(**kwargs): return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], "titrant_conc": [0.0], "median": [10.0]}) - patches = self._make_fixtures(mock_gm) + patches = self._make_fixtures(mock_orchestrator) with patches[0], patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.predict", @@ -329,7 +329,7 @@ def fake_predict(**kwargs): ), patch( "tfscreen.tfmodel.scripts" ".predict_growth_cli.resolve_param_file", - side_effect=lambda pf, gm, op: pf, + side_effect=lambda pf, orchestrator, op: pf, ): predict_growth("cfg.yaml", "post.h5", out_prefix=str(tmp_path / "out")) diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py index 30ec0334..7e9c6dbc 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py @@ -28,16 +28,16 @@ def _make_tm_df(genotypes, titrant_names, titrant_concs): @pytest.fixture -def mock_gm(): - gm = MagicMock() - gm._theta = "hill_geno" - gm.growth_tm.df = _make_tm_df( +def mock_orchestrator(): + orchestrator = MagicMock() + orchestrator._theta = "hill_geno" + orchestrator.growth_tm.df = _make_tm_df( ["wt", "A1B"], ["IPTG", "IPTG"], [0.0, 1.0], ) - gm.training_tm.df = gm.growth_tm.df - return gm + orchestrator.training_tm.df = orchestrator.growth_tm.df + return orchestrator def _fake_extract(model, posteriors, **kwargs): @@ -54,15 +54,15 @@ def _fake_extract(model, posteriors, **kwargs): @pytest.fixture -def mock_extract(mock_gm): +def mock_extract(mock_orchestrator): with patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.read_configuration", - return_value=(mock_gm, {}), + return_value=(mock_orchestrator, {}), ), patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.resolve_param_file", - side_effect=lambda pf, gm, op: pf, # pass-through + side_effect=lambda pf, orchestrator, op: pf, # pass-through ), patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.extract_theta_curves", @@ -94,7 +94,7 @@ def test_mismatched_titrant_files_raises(mock_extract, tmp_path): class TestPredictThetaDefaults: - def test_no_files_uses_all_training_genotypes(self, mock_extract, mock_gm, tmp_path): + def test_no_files_uses_all_training_genotypes(self, mock_extract, mock_orchestrator, tmp_path): out = str(tmp_path / "out") with patch( "tfscreen.tfmodel.scripts" @@ -114,7 +114,7 @@ def test_no_files_uses_all_training_genotypes(self, mock_extract, mock_gm, tmp_p class TestPredictThetaUnion: - def test_genotypes_file_unions_with_training(self, mock_extract, mock_gm, tmp_path): + def test_genotypes_file_unions_with_training(self, mock_extract, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["C2D"]) # out-of-training out = str(tmp_path / "out") @@ -135,7 +135,7 @@ def test_genotypes_file_unions_with_training(self, mock_extract, mock_gm, tmp_pa assert "A1B" in target assert "C2D" in target - def test_titrant_files_union_with_training_grid(self, mock_extract, mock_gm, tmp_path): + def test_titrant_files_union_with_training_grid(self, mock_extract, mock_orchestrator, tmp_path): nf = str(tmp_path / "names.txt") cf = str(tmp_path / "concs.txt") _write_lines(nf, ["IPTG"]) @@ -158,7 +158,7 @@ def test_titrant_files_union_with_training_grid(self, mock_extract, mock_gm, tmp assert 1.0 in titrant_df["titrant_conc"].values assert 5.0 in titrant_df["titrant_conc"].values - def test_single_titrant_name_broadcast_across_concs(self, mock_extract, mock_gm, tmp_path): + def test_single_titrant_name_broadcast_across_concs(self, mock_extract, mock_orchestrator, tmp_path): nf = str(tmp_path / "names.txt") cf = str(tmp_path / "concs.txt") _write_lines(nf, ["IPTG"]) @@ -180,7 +180,7 @@ def test_single_titrant_name_broadcast_across_concs(self, mock_extract, mock_gm, assert list(titrant_df["titrant_name"]) == ["IPTG", "IPTG", "IPTG", "IPTG"] assert set(titrant_df["titrant_conc"]) >= {0.0, 10.0, 100.0} - def test_mismatched_titrant_file_lengths_raises(self, mock_extract, mock_gm, tmp_path): + def test_mismatched_titrant_file_lengths_raises(self, mock_extract, mock_orchestrator, tmp_path): nf = str(tmp_path / "names.txt") cf = str(tmp_path / "concs.txt") _write_lines(nf, ["IPTG", "ATC"]) @@ -191,7 +191,7 @@ def test_mismatched_titrant_file_lengths_raises(self, mock_extract, mock_gm, tmp titrant_concs_file=cf, out_prefix=str(tmp_path / "out")) - def test_duplicate_titrant_pairs_not_repeated(self, mock_extract, mock_gm, tmp_path): + def test_duplicate_titrant_pairs_not_repeated(self, mock_extract, mock_orchestrator, tmp_path): nf = str(tmp_path / "names.txt") cf = str(tmp_path / "concs.txt") _write_lines(nf, ["IPTG"]) @@ -223,7 +223,7 @@ def test_duplicate_titrant_pairs_not_repeated(self, mock_extract, mock_gm, tmp_p class TestPredictThetaOnlyFiles: - def test_only_files_restricts_genotypes_to_file(self, mock_extract, mock_gm, tmp_path): + def test_only_files_restricts_genotypes_to_file(self, mock_extract, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["A1B"]) out = str(tmp_path / "out") @@ -244,7 +244,7 @@ def test_only_files_restricts_genotypes_to_file(self, mock_extract, mock_gm, tmp df = pd.read_csv(f"{out}.csv") assert set(df["genotype"].unique()) <= {"A1B"} - def test_only_files_restricts_titrant_grid_to_file(self, mock_extract, mock_gm, tmp_path): + def test_only_files_restricts_titrant_grid_to_file(self, mock_extract, mock_orchestrator, tmp_path): nf = str(tmp_path / "names.txt") cf = str(tmp_path / "concs.txt") _write_lines(nf, ["IPTG"]) @@ -275,12 +275,12 @@ def test_only_files_restricts_titrant_grid_to_file(self, mock_extract, mock_gm, class TestPredictThetaCheckpointInput: - def _base_patches(self, mock_gm): + def _base_patches(self, mock_orchestrator): return [ patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.read_configuration", - return_value=(mock_gm, {}), + return_value=(mock_orchestrator, {}), ), patch( "tfscreen.tfmodel.scripts" @@ -300,15 +300,15 @@ def _base_patches(self, mock_gm): ), ] - def test_pkl_calls_resolve_param_file(self, mock_gm, tmp_path): + def test_pkl_calls_resolve_param_file(self, mock_orchestrator, tmp_path): """resolve_param_file is invoked when param_file ends with .pkl.""" resolve_calls = [] - def fake_resolve(pf, gm, op): + def fake_resolve(pf, orchestrator, op): resolve_calls.append(pf) return "resolved.h5" - p = self._base_patches(mock_gm) + p = self._base_patches(mock_orchestrator) with p[0], p[1], p[2], patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.resolve_param_file", @@ -319,7 +319,7 @@ def fake_resolve(pf, gm, op): assert resolve_calls == ["run_checkpoint.pkl"] - def test_resolved_path_passed_to_extract(self, mock_gm, tmp_path): + def test_resolved_path_passed_to_extract(self, mock_orchestrator, tmp_path): """The path returned by resolve_param_file reaches extract_theta_curves.""" extract_calls = {} @@ -330,7 +330,7 @@ def fake_curves(**kwargs): "titrant_conc": [0.0], "median": [0.5], }) - p = self._base_patches(mock_gm) + p = self._base_patches(mock_orchestrator) with p[0], patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.extract_theta_curves", @@ -353,7 +353,7 @@ def fake_curves(**kwargs): class TestGenotypeBatchSize: """genotype_batch_size is forwarded to extract_theta_unmeasured.""" - def test_custom_batch_size_forwarded(self, mock_extract, mock_gm, tmp_path): + def test_custom_batch_size_forwarded(self, mock_extract, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["C2D"]) # out-of-training → unmeasured path out = str(tmp_path / "out") @@ -373,7 +373,7 @@ def test_custom_batch_size_forwarded(self, mock_extract, mock_gm, tmp_path): out_prefix=out) assert mock_unmeas.call_args.kwargs["genotype_batch_size"] == 42 - def test_default_batch_size_is_2000(self, mock_extract, mock_gm, tmp_path): + def test_default_batch_size_is_2000(self, mock_extract, mock_orchestrator, tmp_path): gf = str(tmp_path / "genos.txt") _write_lines(gf, ["C2D"]) out = str(tmp_path / "out") @@ -399,7 +399,7 @@ def test_default_batch_size_is_2000(self, mock_extract, mock_gm, tmp_path): class TestPredictThetaQToGet: - def test_pkl_passes_point_est_q_to_get(self, mock_gm, tmp_path): + def test_pkl_passes_point_est_q_to_get(self, mock_orchestrator, tmp_path): """q_to_get={"point_est": 0.5} is passed to extract_theta_curves for .pkl input.""" extract_calls = {} @@ -411,7 +411,7 @@ def fake_curves(**kwargs): with patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.read_configuration", - return_value=(mock_gm, {}), + return_value=(mock_orchestrator, {}), ), patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.resolve_param_file", @@ -431,7 +431,7 @@ def fake_curves(**kwargs): assert extract_calls["q_to_get"] == {"point_est": 0.5} - def test_h5_passes_none_q_to_get(self, mock_gm, tmp_path): + def test_h5_passes_none_q_to_get(self, mock_orchestrator, tmp_path): """q_to_get=None is passed to extract_theta_curves for .h5 input.""" extract_calls = {} @@ -443,11 +443,11 @@ def fake_curves(**kwargs): with patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.read_configuration", - return_value=(mock_gm, {}), + return_value=(mock_orchestrator, {}), ), patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.resolve_param_file", - side_effect=lambda pf, gm, op: pf, + side_effect=lambda pf, orchestrator, op: pf, ), patch( "tfscreen.tfmodel.scripts" ".predict_theta_cli.extract_theta_curves", diff --git a/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py b/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py index 4718acb5..3ec7e029 100644 --- a/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py @@ -138,7 +138,7 @@ def test_each_dataset_uses_different_seed(self, tmp_path): latent_params = {"activity": np.array([[1.0]])} fake_growth_df = pd.DataFrame({"ln_cfu": [1.0]}) - def fake_draw_prior(gm, rng_key, num_draws): + def fake_draw_prior(orchestrator, rng_key, num_draws): seeds_used.append(rng_key) return {}, latent_params @@ -161,7 +161,7 @@ def test_no_noise_passes_none_rng(self, tmp_path): captured = {} latent_params = {"activity": np.array([[1.0]])} - def fake_growth_df(gm, params, draw_idx, noise_rng): + def fake_growth_df(orchestrator, params, draw_idx, noise_rng): captured["noise_rng"] = noise_rng return pd.DataFrame({"ln_cfu": [1.0]}) @@ -185,7 +185,7 @@ def test_with_noise_passes_rng(self, tmp_path): captured = {} latent_params = {"activity": np.array([[1.0]])} - def fake_growth_df(gm, params, draw_idx, noise_rng): + def fake_growth_df(orchestrator, params, draw_idx, noise_rng): captured["noise_rng"] = noise_rng return pd.DataFrame({"ln_cfu": [1.0]}) diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py index 3645a178..ac2d5f9e 100644 --- a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py +++ b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py @@ -227,17 +227,17 @@ def test_missing_column_in_binding_raises(self): class TestComputeThetaValues: - def _make_gm_cal(self, titrant_names, titrant_concs): + def _make_orchestrator_cal(self, titrant_names, titrant_concs): """Return a MagicMock with the binding_tm shape used by the helper.""" - gm = MagicMock() + orchestrator = MagicMock() # Order matters; the helper reads names then concs by index lookup. - gm.binding_tm.tensor_dim_names = ["titrant_name", "titrant_conc"] - gm.binding_tm.tensor_dim_labels = [list(titrant_names), - list(titrant_concs)] - return gm + orchestrator.binding_tm.tensor_dim_names = ["titrant_name", "titrant_conc"] + orchestrator.binding_tm.tensor_dim_labels = [list(titrant_names), + list(titrant_concs)] + return orchestrator def test_inverse_variance_weighted_mean(self): - gm = self._make_gm_cal(["IPTG"], [1.0]) + orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0]) # Two genotypes: theta=0.2 (sigma=0.1) and theta=0.8 (sigma=0.01) # Inverse-variance weights heavily favor the second, so the # consensus should be ~0.8 (not the unweighted 0.5). @@ -247,24 +247,24 @@ def test_inverse_variance_weighted_mean(self): "theta_obs": [0.2, 0.8], "theta_std": [0.1, 0.01], }) - theta = np.asarray(_compute_theta_values(gm, binding_df)) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) assert theta.shape == (1, 1) # Closer to 0.8 than to the midpoint 0.5 assert theta[0, 0] > 0.7 def test_falls_back_to_plain_mean_when_all_stds_zero(self): - gm = self._make_gm_cal(["IPTG"], [1.0]) + orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0]) binding_df = pd.DataFrame({ "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [1.0, 1.0], "theta_obs": [0.3, 0.7], "theta_std": [0.0, 0.0], # invalid weights → fallback to mean }) - theta = np.asarray(_compute_theta_values(gm, binding_df)) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) assert theta[0, 0] == pytest.approx(0.5) def test_unobserved_cell_defaults_to_midpoint(self): - gm = self._make_gm_cal(["IPTG"], [0.0, 1.0]) + orchestrator = self._make_orchestrator_cal(["IPTG"], [0.0, 1.0]) # Only the IPTG=1.0 cell is observed. binding_df = pd.DataFrame({ "titrant_name": ["IPTG"], @@ -272,19 +272,19 @@ def test_unobserved_cell_defaults_to_midpoint(self): "theta_obs": [0.9], "theta_std": [0.01], }) - theta = np.asarray(_compute_theta_values(gm, binding_df)) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) assert theta[0, 0] == pytest.approx(0.5) assert theta[0, 1] > 0.85 def test_clip_to_open_interval(self): - gm = self._make_gm_cal(["IPTG"], [0.0, 1.0]) + orchestrator = self._make_orchestrator_cal(["IPTG"], [0.0, 1.0]) binding_df = pd.DataFrame({ "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [0.0, 1.0], "theta_obs": [0.0, 1.0], "theta_std": [0.01, 0.01], }) - theta = np.asarray(_compute_theta_values(gm, binding_df)) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) # Zero and one would blow up the downstream logit; expect clipping. assert theta[0, 0] > 0.0 assert theta[0, 1] < 1.0 @@ -591,8 +591,8 @@ def test_missing_guesses_file_raises(self, tmp_path): class TestBuildCalibrationModel: def test_overrides_replace_production_components(self): - gm_prod = MagicMock() - gm_prod.settings = { + orchestrator_prod = MagicMock() + orchestrator_prod.settings = { "theta": "categorical_geno", # → simple "activity": "horseshoe_geno", # → hierarchical "dk_geno": "fixed", # → hierarchical @@ -613,7 +613,7 @@ def test_overrides_replace_production_components(self): MockGM.return_value = MagicMock() growth_df = pd.DataFrame() binding_df = pd.DataFrame() - _build_calibration_model(gm_prod, growth_df, binding_df) + _build_calibration_model(orchestrator_prod, growth_df, binding_df) kwargs = MockGM.call_args.kwargs # Overrides applied @@ -635,8 +635,8 @@ def test_binding_weight_reset_from_large_production_value(self): # Reproduces the weighting bug: production YAML stores a large # binding_weight (N_growth / N_binding); the calibration model # must use 1.0 instead. - gm_prod = MagicMock() - gm_prod.settings = { + orchestrator_prod = MagicMock() + orchestrator_prod.settings = { "theta": "categorical_geno", "activity": "horseshoe_geno", "dk_geno": "fixed", @@ -656,14 +656,14 @@ def test_binding_weight_reset_from_large_production_value(self): ".run_prefit_calibration_cli.ModelOrchestrator" ) as MockGM: MockGM.return_value = MagicMock() - _build_calibration_model(gm_prod, pd.DataFrame(), pd.DataFrame()) + _build_calibration_model(orchestrator_prod, pd.DataFrame(), pd.DataFrame()) assert MockGM.call_args.kwargs["binding_weight"] == 1.0 def test_does_not_mutate_production_settings(self): # Ensure we work on a copy. - gm_prod = MagicMock() - gm_prod.settings = {"theta": "categorical_geno", + orchestrator_prod = MagicMock() + orchestrator_prod.settings = {"theta": "categorical_geno", "activity": "horseshoe_geno", "dk_geno": "fixed", "ln_cfu0": "fixed", @@ -673,16 +673,16 @@ def test_does_not_mutate_production_settings(self): "spiked_genotypes": None, "batch_size": None, "binding_weight": 150.0} - original_settings = dict(gm_prod.settings) + original_settings = dict(orchestrator_prod.settings) with patch( "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli.ModelOrchestrator" ) as MockGM: MockGM.return_value = MagicMock() - _build_calibration_model(gm_prod, pd.DataFrame(), pd.DataFrame()) + _build_calibration_model(orchestrator_prod, pd.DataFrame(), pd.DataFrame()) - assert gm_prod.settings == original_settings + assert orchestrator_prod.settings == original_settings # --------------------------------------------------------------------------- @@ -692,9 +692,9 @@ def test_does_not_mutate_production_settings(self): class TestInjectCalibrationPriors: def _make_models(self, gt_has_pinned=True): - gm_cal = MagicMock() - gm_prod = MagicMock() - gm_cal._priors_history = [] + orchestrator_cal = MagicMock() + orchestrator_prod = MagicMock() + orchestrator_cal._priors_history = [] cal_priors = _make_fake_priors(gt_has_pinned=gt_has_pinned) prod_priors = _make_fake_priors(gt_has_pinned=True) @@ -710,60 +710,60 @@ def _make_models(self, gt_has_pinned=True): ), ), ) - gm_cal.priors = cal_priors - gm_prod.priors = prod_priors + orchestrator_cal.priors = cal_priors + orchestrator_prod.priors = prod_priors # Capture all writes to _priors so the test can introspect them. def _setter(value): - gm_cal._priors_history.append(value) - type(gm_cal)._priors = property( + orchestrator_cal._priors_history.append(value) + type(orchestrator_cal)._priors = property( lambda self: self._priors_history[-1] if self._priors_history else None, lambda self, v: self._priors_history.append(v), ) - return gm_cal, gm_prod + return orchestrator_cal, orchestrator_prod def test_copies_production_condition_growth_and_clears_pinned(self): - gm_cal, gm_prod = self._make_models() + orchestrator_cal, orchestrator_prod = self._make_models() theta_values = np.array([[0.5]]) - _inject_calibration_priors(gm_cal, gm_prod, theta_values) + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) - new_priors = gm_cal._priors + new_priors = orchestrator_cal._priors assert new_priors.growth.condition_growth.growth_k_hyper_loc == 2.5 assert new_priors.growth.condition_growth.k_hyper_loc_scale == 0.4 # pinned cleared regardless of production's contents assert new_priors.growth.condition_growth.pinned == {} def test_copies_growth_transition_and_clears_pinned(self): - gm_cal, gm_prod = self._make_models() + orchestrator_cal, orchestrator_prod = self._make_models() theta_values = np.array([[0.5]]) - _inject_calibration_priors(gm_cal, gm_prod, theta_values) + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) - new_priors = gm_cal._priors + new_priors = orchestrator_cal._priors assert new_priors.growth.growth_transition.pre_t_hyper_loc == 3.5 assert new_priors.growth.growth_transition.pinned == {} def test_handles_growth_transition_without_pinned(self): # The "instant" growth_transition has no pinned field at all. - gm_cal, gm_prod = self._make_models(gt_has_pinned=False) + orchestrator_cal, orchestrator_prod = self._make_models(gt_has_pinned=False) theta_values = np.array([[0.5]]) # Should not raise. - _inject_calibration_priors(gm_cal, gm_prod, theta_values) - new_priors = gm_cal._priors + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) + new_priors = orchestrator_cal._priors assert new_priors.growth.growth_transition.pre_t_hyper_loc == 3.5 def test_pins_activity_hyperparams_to_prior_locs(self): - gm_cal, gm_prod = self._make_models() + orchestrator_cal, orchestrator_prod = self._make_models() theta_values = np.array([[0.5]]) - _inject_calibration_priors(gm_cal, gm_prod, theta_values) - pinned = gm_cal._priors.growth.activity.pinned + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) + pinned = orchestrator_cal._priors.growth.activity.pinned # Both suffixes from _PINNED_COMPONENTS["activity"] populated. for suffix, _ in _PINNED_COMPONENTS["activity"]: assert suffix in pinned def test_pins_ln_cfu0_hyperparams(self): - gm_cal, gm_prod = self._make_models() - _inject_calibration_priors(gm_cal, gm_prod, np.array([[0.5]])) - pinned = gm_cal._priors.growth.ln_cfu0.pinned + orchestrator_cal, orchestrator_prod = self._make_models() + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, np.array([[0.5]])) + pinned = orchestrator_cal._priors.growth.ln_cfu0.pinned for suffix, _ in _PINNED_COMPONENTS["ln_cfu0"]: assert suffix in pinned @@ -776,15 +776,15 @@ def test_dk_geno_not_in_pinned_components(self): def test_dk_geno_not_pinned_after_inject(self): # _inject_calibration_priors must leave dk_geno.pinned untouched # (empty) because the fixed component has no hyperparameters. - gm_cal, gm_prod = self._make_models() - _inject_calibration_priors(gm_cal, gm_prod, np.array([[0.5]])) - assert gm_cal._priors.growth.dk_geno.pinned == {} + orchestrator_cal, orchestrator_prod = self._make_models() + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, np.array([[0.5]])) + assert orchestrator_cal._priors.growth.dk_geno.pinned == {} def test_theta_values_are_set(self): - gm_cal, gm_prod = self._make_models() + orchestrator_cal, orchestrator_prod = self._make_models() theta_values = np.array([[0.1, 0.9]]) - _inject_calibration_priors(gm_cal, gm_prod, theta_values) - result = gm_cal._priors.theta.theta_values + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) + result = orchestrator_cal._priors.theta.theta_values assert np.allclose(np.asarray(result), theta_values) @@ -808,7 +808,7 @@ def _write_yaml_and_csvs(self, tmp_path): }, fh) return str(cfg), str(priors), str(guesses) - def _patch_pipeline(self, mocker, gm_cal=None, params=None, + def _patch_pipeline(self, mocker, orchestrator_cal=None, params=None, hessian_results=None, field_mapping=None): """Stub out every heavy callsite of run_prefit_calibration.""" mocker.patch( @@ -821,12 +821,12 @@ def _patch_pipeline(self, mocker, gm_cal=None, params=None, ".run_prefit_calibration_cli._intersect_data", return_value=(pd.DataFrame(), pd.DataFrame()), ) - gm_cal = gm_cal or MagicMock() - gm_cal.init_params = {} + orchestrator_cal = orchestrator_cal or MagicMock() + orchestrator_cal.init_params = {} mocker.patch( "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli._build_calibration_model", - return_value=gm_cal, + return_value=orchestrator_cal, ) mocker.patch( "tfscreen.tfmodel.scripts" @@ -1139,16 +1139,16 @@ def _patch_heavy_machinery(self, mocker, n_params, n_samples, pred_output): mocker.patch("numpyro.infer.Predictive", return_value=mock_pred_inst) def _make_inputs(self, n_params=2): - """Build (ri, gm_cal, params) mocks for _compute_growth_pred_std.""" + """Build (ri, orchestrator_cal, params) mocks for _compute_growth_pred_std.""" ri = MagicMock() ri.get_key.return_value = jax.random.PRNGKey(0) - gm_cal = MagicMock() - gm_cal.data.num_genotype = 3 - gm_cal.get_batch.return_value = MagicMock() - gm_cal.priors = MagicMock() + orchestrator_cal = MagicMock() + orchestrator_cal.data.num_genotype = 3 + orchestrator_cal.get_batch.return_value = MagicMock() + orchestrator_cal.priors = MagicMock() params = {f"site{i}_auto_loc": np.float32(float(i)) for i in range(n_params)} - return ri, gm_cal, params + return ri, orchestrator_cal, params # ------------------------------------------------------------------ # Tests @@ -1157,33 +1157,33 @@ def _make_inputs(self, n_params=2): def test_returns_none_when_params_empty(self): """No _auto_loc params → early-exit None without touching JAX.""" ri = MagicMock() - gm_cal = MagicMock() - gm_cal.data.num_genotype = 2 - gm_cal.get_batch.return_value = MagicMock() - assert _compute_growth_pred_std(ri, {}, gm_cal) is None + orchestrator_cal = MagicMock() + orchestrator_cal.data.num_genotype = 2 + orchestrator_cal.get_batch.return_value = MagicMock() + assert _compute_growth_pred_std(ri, {}, orchestrator_cal) is None def test_returns_none_when_growth_pred_absent(self, mocker): """When Predictive returns no growth_pred site, result must be None.""" n_params, n_samples = 2, 4 - ri, gm_cal, params = self._make_inputs(n_params) + ri, orchestrator_cal, params = self._make_inputs(n_params) self._patch_heavy_machinery( mocker, n_params, n_samples, pred_output={"other_site": np.ones((n_samples, 3))}, ) - assert _compute_growth_pred_std(ri, params, gm_cal, + assert _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=n_samples) is None def test_returns_array_matching_growth_pred_shape(self, mocker): """Output shape must equal the (R,T,CP,CS,TN,TC,G) tensor shape.""" n_params, n_samples = 2, 4 tensor_shape = (1, 3, 1, 1, 1, 1, 2) - ri, gm_cal, params = self._make_inputs(n_params) + ri, orchestrator_cal, params = self._make_inputs(n_params) growth_pred = np.ones((n_samples,) + tensor_shape) * 5.0 self._patch_heavy_machinery( mocker, n_params, n_samples, pred_output={"growth_pred": growth_pred}, ) - result = _compute_growth_pred_std(ri, params, gm_cal, + result = _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=n_samples) assert result is not None assert result.shape == tensor_shape @@ -1192,13 +1192,13 @@ def test_std_is_zero_for_identical_samples(self, mocker): """All Laplace samples identical → elementwise std must be 0.""" n_params, n_samples = 2, 6 tensor_shape = (1, 2, 1, 1, 1, 1, 3) - ri, gm_cal, params = self._make_inputs(n_params) + ri, orchestrator_cal, params = self._make_inputs(n_params) growth_pred = np.ones((n_samples,) + tensor_shape) * 7.0 self._patch_heavy_machinery( mocker, n_params, n_samples, pred_output={"growth_pred": growth_pred}, ) - result = _compute_growth_pred_std(ri, params, gm_cal, + result = _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=n_samples) assert np.allclose(result, 0.0) @@ -1206,14 +1206,14 @@ def test_std_matches_numpy_std_of_samples(self, mocker): """Output must equal np.std(growth_pred_samples, axis=0) exactly.""" n_params, n_samples = 2, 10 tensor_shape = (1, 2, 1, 1, 1, 1, 3) - ri, gm_cal, params = self._make_inputs(n_params) + ri, orchestrator_cal, params = self._make_inputs(n_params) rng = np.random.default_rng(42) growth_pred = rng.normal(size=(n_samples,) + tensor_shape).astype(np.float32) self._patch_heavy_machinery( mocker, n_params, n_samples, pred_output={"growth_pred": growth_pred}, ) - result = _compute_growth_pred_std(ri, params, gm_cal, + result = _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=n_samples) # Compare in float64 (the function casts before computing std). expected = growth_pred.astype(np.float64).std(axis=0) @@ -1224,7 +1224,7 @@ def test_no_overflow_for_large_growth_pred_values(self, mocker): must produce finite std (not inf/nan) after float64 cast + clipping.""" n_params, n_samples = 2, 4 tensor_shape = (1, 2, 1, 1, 1, 1, 2) - ri, gm_cal, params = self._make_inputs(n_params) + ri, orchestrator_cal, params = self._make_inputs(n_params) # Values near the float32 overflow boundary (~1e38); squaring these # in float32 would overflow to inf inside np.std. growth_pred = np.full((n_samples,) + tensor_shape, 1e20, dtype=np.float32) @@ -1233,7 +1233,7 @@ def test_no_overflow_for_large_growth_pred_values(self, mocker): mocker, n_params, n_samples, pred_output={"growth_pred": growth_pred}, ) - result = _compute_growth_pred_std(ri, params, gm_cal, + result = _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=n_samples) assert result is not None assert np.all(np.isfinite(result)) diff --git a/tests/tfscreen/tfmodel/test_checkpoint_protection.py b/tests/tfscreen/tfmodel/test_checkpoint_protection.py index 4b621b3b..a3d9cc84 100644 --- a/tests/tfscreen/tfmodel/test_checkpoint_protection.py +++ b/tests/tfscreen/tfmodel/test_checkpoint_protection.py @@ -5,11 +5,11 @@ @pytest.fixture def mock_growth_model(mocker): - mock_gm_class = mocker.patch("tfscreen.tfmodel.scripts.fit_model_cli.read_configuration") - mock_gm_instance = MagicMock() + mock_orchestrator_class = mocker.patch("tfscreen.tfmodel.scripts.fit_model_cli.read_configuration") + mock_orchestrator_instance = MagicMock() mock_init_params = {"a": 1.0} - mock_gm_class.return_value = (mock_gm_instance, mock_init_params) - return mock_gm_class, mock_gm_instance + mock_orchestrator_class.return_value = (mock_orchestrator_instance, mock_init_params) + return mock_orchestrator_class, mock_orchestrator_instance @pytest.fixture def mock_run_inference(mocker): diff --git a/tests/tfscreen/tfmodel/test_configure_and_run.py b/tests/tfscreen/tfmodel/test_configure_and_run.py index 6b4bc942..c7a5f1f7 100644 --- a/tests/tfscreen/tfmodel/test_configure_and_run.py +++ b/tests/tfscreen/tfmodel/test_configure_and_run.py @@ -19,20 +19,20 @@ ) @pytest.fixture -def mock_gm(mocker): - mock_gm_class = mocker.patch("tfscreen.tfmodel.scripts.configure_model_cli.ModelOrchestrator") - mock_gm_inst = mock_gm_class.return_value +def mock_orchestrator(mocker): + mock_orchestrator_class = mocker.patch("tfscreen.tfmodel.scripts.configure_model_cli.ModelOrchestrator") + mock_orchestrator_inst = mock_orchestrator_class.return_value # Mock settings - mock_gm_inst.settings = {"batch_size": 128, "theta": "hill_geno"} + mock_orchestrator_inst.settings = {"batch_size": 128, "theta": "hill_geno"} # Mock priors mock_priors = MagicMock() mock_priors.val = 1.0 - mock_gm_inst.priors = mock_priors + mock_orchestrator_inst.priors = mock_priors # Mock init_params - mock_gm_inst.init_params = { + mock_orchestrator_inst.init_params = { "scalar_p": 0.5, "arr_p": np.array([1.0, 2.0]), "cond_p": np.array([0.1]), @@ -49,12 +49,12 @@ def mock_gm(mocker): "theta": pd.DataFrame({"titrant_name": ["T1"], "titrant_conc": [0.1], "genotype": ["G1"], "map_theta": [0]}), "ln_cfu0": pd.DataFrame({"replicate": ["R1"], "condition_pre": ["CP1"], "genotype": ["G1"], "map_ln_cfu0": [0]}) } - mock_gm_inst.growth_tm = mock_tm + mock_orchestrator_inst.growth_tm = mock_tm - return mock_gm_class, mock_gm_inst + return mock_orchestrator_class, mock_orchestrator_inst -def test_configure_growth_analysis_coverage(mock_gm, tmpdir): - _, mock_gm_inst = mock_gm +def test_configure_growth_analysis_coverage(mock_orchestrator, tmpdir): + _, mock_orchestrator_inst = mock_orchestrator out_prefix = os.path.join(tmpdir, "test") # Mock priors with dict and dataclass members for coverage @@ -66,10 +66,10 @@ def test_configure_growth_analysis_coverage(mock_gm, tmpdir): mock_priors.dc = mock_sub mock_priors.d = {"k": 2.0, "arr": np.zeros((2,))} mock_priors.scalar = 3.0 - mock_gm_inst.priors = mock_priors + mock_orchestrator_inst.priors = mock_priors # Add some specific parameter names to trigger branch coverage - mock_gm_inst.init_params = { + mock_orchestrator_inst.init_params = { "scalar": 1.0, "condition_growth_offset": np.array([0.5]), "theta_logit_low": np.array([0.6]), @@ -116,17 +116,17 @@ def test_read_configuration_logic(tmpdir, mocker): "flat_index": [0, 0, 0] }).to_csv(guesses_path, index=False) - mock_gm_class = mocker.patch("tfscreen.tfmodel.configuration_io.ModelOrchestrator") - mock_gm_inst = mock_gm_class.return_value - mock_gm_inst.init_params = {"param1": 0.0, "param2": jnp.zeros((1,)), "param3": 0.0} + mock_orchestrator_class = mocker.patch("tfscreen.tfmodel.configuration_io.ModelOrchestrator") + mock_orchestrator_inst = mock_orchestrator_class.return_value + mock_orchestrator_inst.init_params = {"param1": 0.0, "param2": jnp.zeros((1,)), "param3": 0.0} # Mock update_dataclass mocker.patch("tfscreen.tfmodel.configuration_io._update_dataclass" , return_value=MagicMock()) - gm, init_params = read_configuration(config_path) - - assert gm == mock_gm_inst + orchestrator, init_params = read_configuration(config_path) + + assert orchestrator == mock_orchestrator_inst assert init_params["param1"] == 0.5 assert isinstance(init_params["param2"], jnp.ndarray) @@ -164,9 +164,9 @@ def test_read_configuration_errors(tmpdir): with open(config_path, "w") as f: yaml.dump({"data":{"growth":"g", "binding":"b"}, "components":{}, "priors_file": "priors.csv", "guesses_file": "guesses.csv"}, f) - with patch("tfscreen.tfmodel.configuration_io.ModelOrchestrator") as mock_gm_class: - mock_gm_inst = mock_gm_class.return_value - mock_gm_inst.init_params = {"param1": 0, "param2": 0} # param2 missing in CSV + with patch("tfscreen.tfmodel.configuration_io.ModelOrchestrator") as mock_orchestrator_class: + mock_orchestrator_inst = mock_orchestrator_class.return_value + mock_orchestrator_inst.init_params = {"param1": 0, "param2": 0} # param2 missing in CSV with pytest.raises(ValueError, match="Missing initial guesses for parameters"): read_configuration(config_path) @@ -372,11 +372,11 @@ def test_fit_model_svi_full(tmpdir, mocker): with patch("os.path.exists", return_value=False): config_path = os.path.join(tmpdir, "config.yaml") # Minimal valid setup for read_configuration - mock_gm_run = MagicMock() - mock_gm_run.settings = {"batch_size": 256} - mock_gm_run._ln_cfu_df = "g.csv" - mock_gm_run._binding_df = "b.csv" - mocker.patch("tfscreen.tfmodel.scripts.fit_model_cli.read_configuration", return_value=(mock_gm_run, {"p":1.0})) + mock_orchestrator_run = MagicMock() + mock_orchestrator_run.settings = {"batch_size": 256} + mock_orchestrator_run._ln_cfu_df = "g.csv" + mock_orchestrator_run._binding_df = "b.csv" + mocker.patch("tfscreen.tfmodel.scripts.fit_model_cli.read_configuration", return_value=(mock_orchestrator_run, {"p":1.0})) # Mock write_configuration to avoid yaml problems with mocks mock_ri_class = mocker.patch("tfscreen.tfmodel.scripts.fit_model_cli.RunInference") @@ -445,7 +445,7 @@ def test_incompatible_cg_tr_values_are_strings(self): assert isinstance(tr, str) assert isinstance(reason, str) and len(reason) > 0 - def test_configure_model_raises_on_incompatible(self, mock_gm): + def test_configure_model_raises_on_incompatible(self, mock_orchestrator): """configure_model must call check_component_compatibility before building the model.""" with pytest.raises(ValueError, match="Incompatible model components"): diff --git a/tests/tfscreen/tfmodel/test_h5_indexing.py b/tests/tfscreen/tfmodel/test_h5_indexing.py index d2dd67ca..0dc58a25 100644 --- a/tests/tfscreen/tfmodel/test_h5_indexing.py +++ b/tests/tfscreen/tfmodel/test_h5_indexing.py @@ -29,7 +29,7 @@ def test_predict_h5_indexing(tmp_path): "theta_obs": [0.5], "theta_std": [0.01] }) - mc = ModelOrchestrator(growth_df, binding_df) + orchestrator = ModelOrchestrator(growth_df, binding_df) # 2. Create an H5 file with some "posteriors" h5_path = tmp_path / "posteriors.h5" @@ -61,7 +61,7 @@ def test_predict_h5_indexing(tmp_path): # This calls val = val[sample_indices] inside prediction.py try: - results = predict(mc, str(h5_path), num_samples=10) + results = predict(orchestrator, str(h5_path), num_samples=10) except TypeError as e: if "Indexing elements must be in increasing order" in str(e): pytest.fail("H5 indexing failed: elements not in increasing order") diff --git a/tests/tfscreen/tfmodel/test_needs_mut_wiring.py b/tests/tfscreen/tfmodel/test_needs_mut_wiring.py index 682f9ba1..3bf4654b 100644 --- a/tests/tfscreen/tfmodel/test_needs_mut_wiring.py +++ b/tests/tfscreen/tfmodel/test_needs_mut_wiring.py @@ -115,11 +115,11 @@ def test_needs_mut_growth_path(tmp_path, theta): growth_path = _make_growth_csv(tmp_path) binding_path = _make_binding_csv(tmp_path) - mc = ModelOrchestrator(growth_path, binding_path, theta=theta) + orchestrator = ModelOrchestrator(growth_path, binding_path, theta=theta) - assert len(mc.mut_labels) > 0, \ + assert len(orchestrator.mut_labels) > 0, \ f"{theta} (growth): mut_labels is empty — _needs_mut did not fire" - assert mc.data.growth.num_mutation > 0, \ + assert orchestrator.data.growth.num_mutation > 0, \ f"{theta} (growth): num_mutation == 0 — mutation matrices not built" @@ -141,11 +141,11 @@ def test_needs_mut_binding_only_path(tmp_path, theta): binding_path = _make_binding_csv(tmp_path) - mc = ModelOrchestrator(None, binding_path, binding_only=True, theta=theta) + orchestrator = ModelOrchestrator(None, binding_path, binding_only=True, theta=theta) - assert len(mc.mut_labels) > 0, \ + assert len(orchestrator.mut_labels) > 0, \ f"{theta} (binding_only): mut_labels is empty — _needs_mut did not fire" - assert mc.data.binding.num_mutation > 0, \ + assert orchestrator.data.binding.num_mutation > 0, \ f"{theta} (binding_only): num_mutation == 0 — mutation matrices not built" @@ -165,13 +165,13 @@ def test_offset_guesses_have_correct_shape_binding_only(tmp_path, theta): from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator binding_path = _make_binding_csv(tmp_path) - mc = ModelOrchestrator(None, binding_path, binding_only=True, theta=theta) + orchestrator = ModelOrchestrator(None, binding_path, binding_only=True, theta=theta) - M = mc.data.binding.num_mutation + M = orchestrator.data.binding.num_mutation assert M > 0 # Every *_offset key in init_params must have length M (or M as a dimension) - for key, val in mc.init_params.items(): + for key, val in orchestrator.init_params.items(): if "offset" in key and hasattr(val, "shape") and val.shape != (): assert val.shape[0] == M or val.shape[-1] == M, \ f"{key} has shape {val.shape} but expected M={M} on at least one axis" diff --git a/tests/tfscreen/tfmodel/test_prediction.py b/tests/tfscreen/tfmodel/test_prediction.py index daf2657c..63011f5e 100644 --- a/tests/tfscreen/tfmodel/test_prediction.py +++ b/tests/tfscreen/tfmodel/test_prediction.py @@ -6,10 +6,10 @@ from numpyro.distributions.transforms import biject_to from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator -from tfscreen.tfmodel.analysis.prediction import copy_model_class, _convert_map_params +from tfscreen.tfmodel.analysis.prediction import copy_orchestrator, _convert_map_params @pytest.fixture -def dummy_mc(): +def dummy_orchestrator(): """Create a dummy ModelOrchestrator instance for testing.""" growth_df = pd.DataFrame({ "genotype": ["wt", "wt", "M42V", "M42V"], @@ -34,67 +34,67 @@ def dummy_mc(): return ModelOrchestrator(growth_df, binding_df) -def test_copy_model_class_defaults(dummy_mc): - """Test copy_model_class with all None inputs.""" - new_mc = copy_model_class(dummy_mc) +def test_copy_orchestrator_defaults(dummy_orchestrator): + """Test copy_orchestrator with all None inputs.""" + new_orchestrator = copy_orchestrator(dummy_orchestrator) # Categorical combinations (1 rep * 1 cp * 1 cs * 1 tn * 2 geno) = 2 # Quantitative (1 t_pre * 2 t_sel * 2 conc) = 4 # Total = 2 * 4 = 8 - assert len(new_mc.growth_df) == 8 - assert new_mc.settings == dummy_mc.settings + assert len(new_orchestrator.growth_df) == 8 + assert new_orchestrator.settings == dummy_orchestrator.settings -def test_copy_model_class_expansion(dummy_mc): +def test_copy_orchestrator_expansion(dummy_orchestrator): """Test expanding quantitative inputs.""" - new_mc = copy_model_class( - dummy_mc, + new_orchestrator = copy_orchestrator( + dummy_orchestrator, t_sel=[0.0, 10.0, 20.0], titrant_conc=[0.0, 0.5, 1.0] ) # 2 (genotypes) * 3 (t_sel) * 3 (conc) = 18 - assert len(new_mc.growth_df) == 18 - assert set(new_mc.growth_df["t_sel"]) == {0.0, 10.0, 20.0} - assert set(new_mc.growth_df["titrant_conc"]) == {0.0, 0.5, 1.0} + assert len(new_orchestrator.growth_df) == 18 + assert set(new_orchestrator.growth_df["t_sel"]) == {0.0, 10.0, 20.0} + assert set(new_orchestrator.growth_df["titrant_conc"]) == {0.0, 0.5, 1.0} -def test_copy_model_class_list_inputs(dummy_mc): +def test_copy_orchestrator_list_inputs(dummy_orchestrator): """Test passing single values as non-lists.""" - new_mc = copy_model_class( - dummy_mc, + new_orchestrator = copy_orchestrator( + dummy_orchestrator, t_sel=30.0, titrant_conc=2.0 ) - assert 30.0 in new_mc.growth_df["t_sel"].values - assert 2.0 in new_mc.growth_df["titrant_conc"].values + assert 30.0 in new_orchestrator.growth_df["t_sel"].values + assert 2.0 in new_orchestrator.growth_df["titrant_conc"].values -def test_copy_model_class_quantitative_errors(dummy_mc): +def test_copy_orchestrator_quantitative_errors(dummy_orchestrator): """Test validation of quantitative inputs.""" with pytest.raises(ValueError, match="t_pre must be >= 0"): - copy_model_class(dummy_mc, t_pre=-1.0) + copy_orchestrator(dummy_orchestrator, t_pre=-1.0) with pytest.raises(ValueError, match="t_sel must be >= 0"): - copy_model_class(dummy_mc, t_sel=[-1.0, 1.0]) + copy_orchestrator(dummy_orchestrator, t_sel=[-1.0, 1.0]) with pytest.raises(ValueError, match="titrant_conc must be >= 0"): - copy_model_class(dummy_mc, titrant_conc=[1.0, -0.5]) + copy_orchestrator(dummy_orchestrator, titrant_conc=[1.0, -0.5]) -def test_copy_model_class_t_pre_expansion(dummy_mc): +def test_copy_orchestrator_t_pre_expansion(dummy_orchestrator): """Test expanding t_pre.""" - new_mc = copy_model_class( - dummy_mc, + new_orchestrator = copy_orchestrator( + dummy_orchestrator, t_pre=[10.0, 20.0] ) # 2 (genotypes) * 2 (t_pre) * 2 (t_sel) * 2 (conc) = 16 - assert len(new_mc.growth_df) == 16 - assert set(new_mc.growth_df["t_pre"]) == {10.0, 20.0} + assert len(new_orchestrator.growth_df) == 16 + assert set(new_orchestrator.growth_df["t_pre"]) == {10.0, 20.0} -def test_copy_model_class_no_subsetting_binding(dummy_mc): +def test_copy_orchestrator_no_subsetting_binding(dummy_orchestrator): """Test that binding_df is NOT subsetted during copy.""" # Original binding_df has wt and M42V - assert len(dummy_mc.binding_df) == 2 + assert len(dummy_orchestrator.binding_df) == 2 - new_mc = copy_model_class(dummy_mc) + new_orchestrator = copy_orchestrator(dummy_orchestrator) # New binding_df should still have all genotypes - assert len(new_mc.binding_df) == 2 + assert len(new_orchestrator.binding_df) == 2 # --------------------------------------------------------------------------- diff --git a/tests/tfscreen/tfmodel/test_presplit.py b/tests/tfscreen/tfmodel/test_presplit.py index 81f61841..17f923dc 100644 --- a/tests/tfscreen/tfmodel/test_presplit.py +++ b/tests/tfscreen/tfmodel/test_presplit.py @@ -177,8 +177,8 @@ def test_model_orchestrator_without_presplit(minimal_growth_df, "theta_obs": [0.5] * 3, "theta_std": [0.1] * 3, }) - gm = ModelOrchestrator(minimal_growth_df, binding_df, presplit_df=None) - assert gm.data.presplit is None + orchestrator = ModelOrchestrator(minimal_growth_df, binding_df, presplit_df=None) + assert orchestrator.data.presplit is None def test_model_orchestrator_with_presplit(minimal_growth_df, @@ -192,9 +192,9 @@ def test_model_orchestrator_with_presplit(minimal_growth_df, "theta_obs": [0.5] * 3, "theta_std": [0.1] * 3, }) - gm = ModelOrchestrator(minimal_growth_df, binding_df, + orchestrator = ModelOrchestrator(minimal_growth_df, binding_df, presplit_df=minimal_presplit_df) - ps = gm.data.presplit + ps = orchestrator.data.presplit assert ps is not None assert isinstance(ps, PreSplitData) assert ps.num_replicate == 2 @@ -222,11 +222,11 @@ def test_jax_model_presplit_obs_site_present(minimal_growth_df, "theta_obs": [0.5] * 3, "theta_std": [0.1] * 3, }) - gm = ModelOrchestrator(minimal_growth_df, binding_df, + orchestrator = ModelOrchestrator(minimal_growth_df, binding_df, presplit_df=minimal_presplit_df) - model_fn = gm.jax_model - tr = trace(seed(model_fn, 0)).get_trace(gm.data, gm.priors) + model_fn = orchestrator.jax_model + tr = trace(seed(model_fn, 0)).get_trace(orchestrator.data, orchestrator.priors) assert "presplit_obs" in tr @@ -240,8 +240,8 @@ def test_jax_model_no_presplit_obs_site_without_data(minimal_growth_df): "theta_obs": [0.5] * 3, "theta_std": [0.1] * 3, }) - gm = ModelOrchestrator(minimal_growth_df, binding_df) + orchestrator = ModelOrchestrator(minimal_growth_df, binding_df) - model_fn = gm.jax_model - tr = trace(seed(model_fn, 0)).get_trace(gm.data, gm.priors) + model_fn = orchestrator.jax_model + tr = trace(seed(model_fn, 0)).get_trace(orchestrator.data, orchestrator.priors) assert "presplit_obs" not in tr diff --git a/tests/tfscreen/tfmodel/test_prior_predictive.py b/tests/tfscreen/tfmodel/test_prior_predictive.py index 53913d49..89f05531 100644 --- a/tests/tfscreen/tfmodel/test_prior_predictive.py +++ b/tests/tfscreen/tfmodel/test_prior_predictive.py @@ -7,7 +7,7 @@ @pytest.fixture -def dummy_mc(): +def dummy_orchestrator(): growth_df = pd.DataFrame({ "genotype": ["wt", "wt", "M42V", "M42V"], "titrant_name": ["tit1", "tit1", "tit1", "tit1"], @@ -34,54 +34,54 @@ def dummy_mc(): # draw_prior # --------------------------------------------------------------------------- -def test_draw_prior_returns_dicts(dummy_mc): - predictions, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) +def test_draw_prior_returns_dicts(dummy_orchestrator): + predictions, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) assert isinstance(predictions, dict) assert isinstance(latent_params, dict) -def test_draw_prior_contains_growth_pred(dummy_mc): - predictions, _ = draw_prior(dummy_mc, rng_key=0, num_draws=1) +def test_draw_prior_contains_growth_pred(dummy_orchestrator): + predictions, _ = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) assert "growth_pred" in predictions -def test_draw_prior_growth_pred_shape(dummy_mc): - predictions, _ = draw_prior(dummy_mc, rng_key=0, num_draws=3) +def test_draw_prior_growth_pred_shape(dummy_orchestrator): + predictions, _ = draw_prior(dummy_orchestrator, rng_key=0, num_draws=3) gp = predictions["growth_pred"] assert gp.shape[0] == 3, "First dim should equal num_draws" assert gp.ndim > 1, "growth_pred should have spatial dims beyond num_draws" -def test_draw_prior_latent_params_shape(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=3) +def test_draw_prior_latent_params_shape(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=3) for name, val in latent_params.items(): assert val.shape[0] == 3, f"Parameter '{name}' first dim should equal num_draws" -def test_draw_prior_no_observed_in_latent(dummy_mc): +def test_draw_prior_no_observed_in_latent(dummy_orchestrator): # Observed sites (obs= in model) should not appear in latent_params. # The growth observation site is named 'final_binding_obs_growth_obs'. - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) assert "final_binding_obs_growth_obs" not in latent_params -def test_draw_prior_reproducible(dummy_mc): - _, p1 = draw_prior(dummy_mc, rng_key=42, num_draws=1) - _, p2 = draw_prior(dummy_mc, rng_key=42, num_draws=1) +def test_draw_prior_reproducible(dummy_orchestrator): + _, p1 = draw_prior(dummy_orchestrator, rng_key=42, num_draws=1) + _, p2 = draw_prior(dummy_orchestrator, rng_key=42, num_draws=1) for k in p1: np.testing.assert_array_equal(p1[k], p2[k]) -def test_draw_prior_different_seeds(dummy_mc): - _, p1 = draw_prior(dummy_mc, rng_key=0, num_draws=1) - _, p2 = draw_prior(dummy_mc, rng_key=1, num_draws=1) +def test_draw_prior_different_seeds(dummy_orchestrator): + _, p1 = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) + _, p2 = draw_prior(dummy_orchestrator, rng_key=1, num_draws=1) # At least one parameter should differ across seeds. any_diff = any(not np.array_equal(p1[k], p2[k]) for k in p1 if k in p2) assert any_diff -def test_draw_prior_multiple_independent_draws(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=5) +def test_draw_prior_multiple_independent_draws(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=5) # Draws should not all be identical. first_key = next(iter(latent_params)) v = latent_params[first_key] @@ -93,54 +93,54 @@ def test_draw_prior_multiple_independent_draws(dummy_mc): # growth_df_from_prior # --------------------------------------------------------------------------- -def test_growth_df_from_prior_returns_dataframe(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) - df = growth_df_from_prior(dummy_mc, latent_params) +def test_growth_df_from_prior_returns_dataframe(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) + df = growth_df_from_prior(dummy_orchestrator, latent_params) assert isinstance(df, pd.DataFrame) -def test_growth_df_from_prior_same_rows(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) - df = growth_df_from_prior(dummy_mc, latent_params) - assert len(df) == len(dummy_mc.growth_df) +def test_growth_df_from_prior_same_rows(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) + df = growth_df_from_prior(dummy_orchestrator, latent_params) + assert len(df) == len(dummy_orchestrator.growth_df) -def test_growth_df_from_prior_has_ln_cfu(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) - df = growth_df_from_prior(dummy_mc, latent_params) +def test_growth_df_from_prior_has_ln_cfu(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) + df = growth_df_from_prior(dummy_orchestrator, latent_params) assert "ln_cfu" in df.columns assert df["ln_cfu"].notna().all() -def test_growth_df_from_prior_preserves_metadata(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) - df = growth_df_from_prior(dummy_mc, latent_params) +def test_growth_df_from_prior_preserves_metadata(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) + df = growth_df_from_prior(dummy_orchestrator, latent_params) for col in ["genotype", "titrant_conc", "ln_cfu_std", "replicate"]: assert col in df.columns -def test_growth_df_from_prior_noise_changes_ln_cfu(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=1) - df_clean = growth_df_from_prior(dummy_mc, latent_params, noise_rng=None) +def test_growth_df_from_prior_noise_changes_ln_cfu(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=1) + df_clean = growth_df_from_prior(dummy_orchestrator, latent_params, noise_rng=None) rng = np.random.default_rng(7) - df_noisy = growth_df_from_prior(dummy_mc, latent_params, noise_rng=rng) + df_noisy = growth_df_from_prior(dummy_orchestrator, latent_params, noise_rng=rng) # Noisy and clean should differ (adding N(0, 0.1) noise almost certainly changes values). assert not np.allclose(df_clean["ln_cfu"].values, df_noisy["ln_cfu"].values) -def test_growth_df_from_prior_different_draws(dummy_mc): - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=5) - df0 = growth_df_from_prior(dummy_mc, latent_params, draw_idx=0) - df1 = growth_df_from_prior(dummy_mc, latent_params, draw_idx=1) +def test_growth_df_from_prior_different_draws(dummy_orchestrator): + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=5) + df0 = growth_df_from_prior(dummy_orchestrator, latent_params, draw_idx=0) + df1 = growth_df_from_prior(dummy_orchestrator, latent_params, draw_idx=1) # Different draws should generally produce different predictions. assert not np.allclose(df0["ln_cfu"].values, df1["ln_cfu"].values) -def test_latent_params_compatible_with_predict(dummy_mc): +def test_latent_params_compatible_with_predict(dummy_orchestrator): """latent_params from draw_prior can be passed to predict() without error.""" from tfscreen.tfmodel.analysis.prediction import predict - _, latent_params = draw_prior(dummy_mc, rng_key=0, num_draws=2) - result = predict(dummy_mc, latent_params, predict_sites=["growth_pred"], + _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=2) + result = predict(dummy_orchestrator, latent_params, predict_sites=["growth_pred"], num_samples=None, num_marginal_samples=2) assert "median" in result.columns or any(c in result.columns for c in ["lower_95", "upper_95"]) diff --git a/tests/tfscreen/tfmodel/test_spiked_genotypes.py b/tests/tfscreen/tfmodel/test_spiked_genotypes.py index 35836413..e5743ec0 100644 --- a/tests/tfscreen/tfmodel/test_spiked_genotypes.py +++ b/tests/tfscreen/tfmodel/test_spiked_genotypes.py @@ -48,14 +48,14 @@ def test_spiked_genotypes_masking(dummy_data): # but ModelOrchestrator.__init__ calls _initialize_data which does most of the heavy lifting. # We can use real data for this unit test since it's small. - gm = ModelOrchestrator(growth_df, binding_df, spiked_genotypes=spiked) + orchestrator = ModelOrchestrator(growth_df, binding_df, spiked_genotypes=spiked) # Get genotype labels from the growth tensor manager - genotype_idx = gm.growth_tm.tensor_dim_names.index("genotype") - genotype_labels = gm.growth_tm.tensor_dim_labels[genotype_idx] + genotype_idx = orchestrator.growth_tm.tensor_dim_names.index("genotype") + genotype_labels = orchestrator.growth_tm.tensor_dim_labels[genotype_idx] # Check the congression_mask in the data object - mask = np.array(gm.data.growth.congression_mask) + mask = np.array(orchestrator.data.growth.congression_mask) for i, label in enumerate(genotype_labels): if label == "A10G": assert not mask[i], f"Genotype {label} should be masked (False)" @@ -87,12 +87,12 @@ def test_setup_batching_zero_division(): def test_spiked_genotypes_single_string(dummy_data): """Test that a single string is handled correctly as spiked_genotypes.""" growth_df, binding_df = dummy_data - gm = ModelOrchestrator(growth_df, binding_df, spiked_genotypes="A10G") + orchestrator = ModelOrchestrator(growth_df, binding_df, spiked_genotypes="A10G") - genotype_idx = gm.growth_tm.tensor_dim_names.index("genotype") - genotype_labels = gm.growth_tm.tensor_dim_labels[genotype_idx] + genotype_idx = orchestrator.growth_tm.tensor_dim_names.index("genotype") + genotype_labels = orchestrator.growth_tm.tensor_dim_labels[genotype_idx] - mask = np.array(gm.data.growth.congression_mask) + mask = np.array(orchestrator.data.growth.congression_mask) for i, label in enumerate(genotype_labels): if label == "A10G": assert not mask[i] diff --git a/tests/tfscreen/tfmodel/test_struct_wiring.py b/tests/tfscreen/tfmodel/test_struct_wiring.py index 6d25374d..a0a0b313 100644 --- a/tests/tfscreen/tfmodel/test_struct_wiring.py +++ b/tests/tfscreen/tfmodel/test_struct_wiring.py @@ -148,13 +148,13 @@ def test_settings_includes_thermo_data(tmp_path): h5_path = _make_struct_ensemble_h5(tmp_path) from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator - mc = ModelOrchestrator( + orchestrator = ModelOrchestrator( growth_path, binding_path, theta="thermo.O2_C4_K3_U0_a.PnnC", thermo_data=h5_path, ) - assert "thermo_data" in mc.settings - assert mc.settings["thermo_data"] == h5_path + assert "thermo_data" in orchestrator.settings + assert orchestrator.settings["thermo_data"] == h5_path def test_default_thermo_data_is_none(tmp_path): @@ -163,8 +163,8 @@ def test_default_thermo_data_is_none(tmp_path): binding_path = _make_binding_csv(tmp_path) from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator - mc = ModelOrchestrator(growth_path, binding_path, theta="hill_geno") - assert mc.settings["thermo_data"] is None + orchestrator = ModelOrchestrator(growth_path, binding_path, theta="hill_geno") + assert orchestrator.settings["thermo_data"] is None # ────────────────────────────────────────────────────────────────────────────── @@ -186,7 +186,7 @@ def test_missing_thermo_data_raises(tmp_path): # ────────────────────────────────────────────────────────────────────────────── @pytest.fixture(scope="module") -def fitted_mc(tmp_path_factory): +def fitted_orchestrator(tmp_path_factory): """ Build a ModelOrchestrator with lac_dimer_lnK_nn_prior and a synthetic HDF5 file. Expensive to construct, so scoped to the module. @@ -205,39 +205,39 @@ def fitted_mc(tmp_path_factory): class TestStructFieldsOnGrowthData: - def test_struct_names_is_correct_tuple(self, fitted_mc): - assert fitted_mc.data.growth.struct_names == STRUCTURE_NAMES + def test_struct_names_is_correct_tuple(self, fitted_orchestrator): + assert fitted_orchestrator.data.growth.struct_names == STRUCTURE_NAMES - def test_num_struct_is_four(self, fitted_mc): - assert fitted_mc.data.growth.num_struct == 4 + def test_num_struct_is_four(self, fitted_orchestrator): + assert fitted_orchestrator.data.growth.num_struct == 4 - def test_struct_features_shape(self, fitted_mc): - M = fitted_mc.data.growth.num_mutation - S = fitted_mc.data.growth.num_struct - feat = fitted_mc.data.growth.struct_features + def test_struct_features_shape(self, fitted_orchestrator): + M = fitted_orchestrator.data.growth.num_mutation + S = fitted_orchestrator.data.growth.num_struct + feat = fitted_orchestrator.data.growth.struct_features assert feat.shape == (M, S, 60) - def test_struct_n_chains_shape(self, fitted_mc): - S = fitted_mc.data.growth.num_struct - assert fitted_mc.data.growth.struct_n_chains.shape == (S,) + def test_struct_n_chains_shape(self, fitted_orchestrator): + S = fitted_orchestrator.data.growth.num_struct + assert fitted_orchestrator.data.growth.struct_n_chains.shape == (S,) - def test_struct_n_chains_value(self, fitted_mc): + def test_struct_n_chains_value(self, fitted_orchestrator): # All structures were written with n_chains_bearing_mut=2 import jax.numpy as jnp - n = fitted_mc.data.growth.struct_n_chains + n = fitted_orchestrator.data.growth.struct_n_chains assert int(n[0]) == 2 - def test_no_contact_distances_without_epistasis(self, fitted_mc): + def test_no_contact_distances_without_epistasis(self, fitted_orchestrator): # Fixture was built without epistasis=True, so no contact arrays - assert fitted_mc.data.growth.struct_contact_distances is None - assert fitted_mc.data.growth.struct_contact_pair_idx is None + assert fitted_orchestrator.data.growth.struct_contact_distances is None + assert fitted_orchestrator.data.growth.struct_contact_pair_idx is None - def test_mut_labels_populated(self, fitted_mc): + def test_mut_labels_populated(self, fitted_orchestrator): # _needs_mut must have fired for lac_dimer_lnK_nn_prior - assert len(fitted_mc.mut_labels) > 0 + assert len(fitted_orchestrator.mut_labels) > 0 - def test_num_mutation_nonzero(self, fitted_mc): - assert fitted_mc.data.growth.num_mutation > 0 + def test_num_mutation_nonzero(self, fitted_orchestrator): + assert fitted_orchestrator.data.growth.num_mutation > 0 class TestStructFieldsWithEpistasis: @@ -288,14 +288,14 @@ def test_svi_runs_without_error(tmp_path): binding_path = _make_binding_csv(tmp_path) h5_path = _make_struct_ensemble_h5(tmp_path) - mc = ModelOrchestrator( + orchestrator = ModelOrchestrator( growth_path, binding_path, theta="thermo.O2_C4_K3_U0_a.PnnC", thermo_data=h5_path, ) out_prefix = str(tmp_path / "svi_out") - inference = RunInference(model=mc, seed=0) + inference = RunInference(model=orchestrator, seed=0) svi = inference.setup_svi(adam_step_size=1e-3) svi_state, params, converged = inference.run_optimization( svi=svi, @@ -319,7 +319,7 @@ def test_svi_with_epistasis_runs_without_error(tmp_path): binding_path = _make_binding_csv(tmp_path) h5_path = _make_struct_ensemble_h5(tmp_path) - mc = ModelOrchestrator( + orchestrator = ModelOrchestrator( growth_path, binding_path, theta="thermo.O2_C4_K3_U0_a.PnnC", thermo_data=h5_path, @@ -327,7 +327,7 @@ def test_svi_with_epistasis_runs_without_error(tmp_path): ) out_prefix = str(tmp_path / "svi_epi_out") - inference = RunInference(model=mc, seed=0) + inference = RunInference(model=orchestrator, seed=0) svi = inference.setup_svi(adam_step_size=1e-3) svi_state, params, converged = inference.run_optimization( svi=svi, @@ -391,7 +391,7 @@ def test_binding_only_struct_raises_without_thermo_data(tmp_path): @pytest.fixture(scope="module") -def binding_only_ddG_mc(tmp_path_factory): +def binding_only_ddG_orchestrator(tmp_path_factory): """ ModelOrchestrator in binding_only mode with mwc_dimer_lnK_ddG_prior. Scoped to module so it is built once for all tests below. @@ -412,30 +412,30 @@ def binding_only_ddG_mc(tmp_path_factory): class TestBindingOnlyDdGStructFields: """BindingData must carry the struct_* fields when using mwc_dimer_lnK_ddG_prior.""" - def test_struct_names_is_correct_tuple(self, binding_only_ddG_mc): - assert set(binding_only_ddG_mc.data.binding.struct_names) == set(_MWC_STRUCTURE_NAMES) + def test_struct_names_is_correct_tuple(self, binding_only_ddG_orchestrator): + assert set(binding_only_ddG_orchestrator.data.binding.struct_names) == set(_MWC_STRUCTURE_NAMES) - def test_num_struct_is_six(self, binding_only_ddG_mc): - assert binding_only_ddG_mc.data.binding.num_struct == 6 + def test_num_struct_is_six(self, binding_only_ddG_orchestrator): + assert binding_only_ddG_orchestrator.data.binding.num_struct == 6 - def test_struct_features_shape(self, binding_only_ddG_mc): - M = binding_only_ddG_mc.data.binding.num_mutation - S = binding_only_ddG_mc.data.binding.num_struct - feat = binding_only_ddG_mc.data.binding.struct_features + def test_struct_features_shape(self, binding_only_ddG_orchestrator): + M = binding_only_ddG_orchestrator.data.binding.num_mutation + S = binding_only_ddG_orchestrator.data.binding.num_struct + feat = binding_only_ddG_orchestrator.data.binding.struct_features assert feat.shape == (M, S) - def test_mut_labels_populated(self, binding_only_ddG_mc): - assert len(binding_only_ddG_mc.mut_labels) > 0 + def test_mut_labels_populated(self, binding_only_ddG_orchestrator): + assert len(binding_only_ddG_orchestrator.mut_labels) > 0 - def test_num_mutation_nonzero(self, binding_only_ddG_mc): - assert binding_only_ddG_mc.data.binding.num_mutation > 0 + def test_num_mutation_nonzero(self, binding_only_ddG_orchestrator): + assert binding_only_ddG_orchestrator.data.binding.num_mutation > 0 - def test_growth_data_is_none(self, binding_only_ddG_mc): - assert binding_only_ddG_mc.data.growth is None + def test_growth_data_is_none(self, binding_only_ddG_orchestrator): + assert binding_only_ddG_orchestrator.data.growth is None - def test_struct_n_chains_is_none_for_ddG_csv(self, binding_only_ddG_mc): + def test_struct_n_chains_is_none_for_ddG_csv(self, binding_only_ddG_orchestrator): # ddG CSV loader does not provide n_chains (it's None) - assert binding_only_ddG_mc.data.binding.struct_n_chains is None + assert binding_only_ddG_orchestrator.data.binding.struct_n_chains is None @pytest.mark.slow @@ -447,7 +447,7 @@ def test_binding_only_ddG_svi_runs(tmp_path): binding_path = _make_binding_only_csv(tmp_path) ddg_path = _make_ddG_prior_csv(tmp_path) - mc = ModelOrchestrator( + orchestrator = ModelOrchestrator( None, binding_path, binding_only=True, theta="thermo.O2_C12_K5_U0_a.PddG", @@ -455,7 +455,7 @@ def test_binding_only_ddG_svi_runs(tmp_path): ) out_prefix = str(tmp_path / "svi_binding_only_ddg") - inference = RunInference(model=mc, seed=0) + inference = RunInference(model=orchestrator, seed=0) svi = inference.setup_svi(adam_step_size=1e-3) svi_state, params, converged = inference.run_optimization( svi=svi, @@ -486,7 +486,7 @@ def test_binding_only_minibatch_get_random_idx_does_not_crash(tmp_path): binding_path = _make_binding_only_csv(tmp_path, genotypes=big_genotypes) ddg_path = _make_ddG_prior_csv(tmp_path, genotypes=big_genotypes) - mc = ModelOrchestrator( + orchestrator = ModelOrchestrator( None, binding_path, binding_only=True, theta="thermo.O2_C12_K5_U0_a.PddG", @@ -495,12 +495,12 @@ def test_binding_only_minibatch_get_random_idx_does_not_crash(tmp_path): ) # Should not raise - idx = mc.get_random_idx(batch_key=42, num_batches=1) + idx = orchestrator.get_random_idx(batch_key=42, num_batches=1) assert len(idx) == 2 # All genotypes are subsamplable; none are pinned - assert mc.data.num_binding == 0 - assert len(mc.data.not_binding_idx) == 4 + assert orchestrator.data.num_binding == 0 + assert len(orchestrator.data.not_binding_idx) == 4 @pytest.mark.slow @@ -513,7 +513,7 @@ def test_binding_only_minibatch_svi_runs(tmp_path): binding_path = _make_binding_only_csv(tmp_path, genotypes=big_genotypes) ddg_path = _make_ddG_prior_csv(tmp_path, genotypes=big_genotypes) - mc = ModelOrchestrator( + orchestrator = ModelOrchestrator( None, binding_path, binding_only=True, theta="thermo.O2_C12_K5_U0_a.PddG", @@ -522,7 +522,7 @@ def test_binding_only_minibatch_svi_runs(tmp_path): ) out_prefix = str(tmp_path / "svi_binding_only_minibatch") - inference = RunInference(model=mc, seed=0) + inference = RunInference(model=orchestrator, seed=0) svi = inference.setup_svi(adam_step_size=1e-3) svi_state, params, converged = inference.run_optimization( svi=svi, From ae343005087e660c7fb79924ad7fb323e2d10faa Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 8 Jun 2026 18:45:04 -0700 Subject: [PATCH 05/24] fixed prediction bugs --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 236 ++--- reports/junit/junit.xml | 2 +- src/tfscreen/plot/geno_trajectory.py | 852 ++++++---------- src/tfscreen/simulate/library_prediction.py | 17 +- src/tfscreen/simulate/selection_experiment.py | 2 +- src/tfscreen/simulate/thermo_to_growth.py | 16 +- src/tfscreen/tfmodel/analysis/prediction.py | 141 ++- src/tfscreen/tfmodel/configuration_io.py | 9 + .../scripts/run_prefit_calibration_cli.py | 148 ++- .../tfmodel/scripts/summarize_fit_cli.py | 60 ++ tests/tfscreen/plot/test_geno_trajectory.py | 922 ++++++++++-------- .../simulate/test_library_prediction.py | 90 ++ .../simulate/test_thermo_to_growth.py | 99 ++ .../scripts/test_run_prefit_calibration.py | 283 +++++- .../tfmodel/scripts/test_summarize_fit_cli.py | 302 ++++++ .../tfmodel/test_configure_and_run.py | 19 + tests/tfscreen/tfmodel/test_prediction.py | 186 +++- 19 files changed, 2153 insertions(+), 1235 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index a9795298..4e89ae94 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.78%coverage95.78% \ No newline at end of file +coverage: 95.86%coverage95.86% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 56cf0483..9ab97a0e 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3143tests3143 \ No newline at end of file +tests: 3188tests3188 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 0d5ffb2c..68c7efa1 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -778,36 +778,7 @@ ./src/tfscreen/plot/est_v_real_summary.py:102:1: W293 blank line contains whitespace ./src/tfscreen/plot/est_v_real_summary.py:104:1: W293 blank line contains whitespace ./src/tfscreen/plot/est_v_real_summary.py:106:1: W391 blank line at end of file -./src/tfscreen/plot/geno_trajectory.py:88:14: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:93:14: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:107:16: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:172:11: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:173:11: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:192:1: C901 'plot_geno_trajectory' is too complex (32) -./src/tfscreen/plot/geno_trajectory.py:313:14: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:316:15: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:328:15: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:329:14: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:330:14: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:331:14: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:359:18: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:361:21: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:457:15: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:458:15: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:480:19: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:495:21: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:518:20: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:555:27: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:570:26: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:571:26: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:572:25: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:573:25: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:582:29: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:583:29: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:584:28: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:585:27: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:586:27: E221 multiple spaces before operator -./src/tfscreen/plot/geno_trajectory.py:613:17: E221 multiple spaces before operator +./src/tfscreen/plot/geno_trajectory.py:29:1: C901 'plot_geno_trajectory' is too complex (14) ./src/tfscreen/plot/heatmap/aa_v_res_heatmap.py:6:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/plot/heatmap/aa_v_res_heatmap.py:15:53: E231 missing whitespace after ':' ./src/tfscreen/plot/heatmap/aa_v_res_heatmap.py:16:53: E231 missing whitespace after ':' @@ -1510,7 +1481,7 @@ ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/thermo_to_growth.py:582:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/thermo_to_growth.py:596:41: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/__init__.py:2:73: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:3:20: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:18:65: W291 trailing whitespace @@ -1542,42 +1513,36 @@ ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:99:16: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:120:13: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/prediction.py:11:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:40:73: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:77:71: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:98:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:105:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:153:1: C901 'predict' is too complex (26) -./src/tfscreen/tfmodel/analysis/prediction.py:214:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:225:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/analysis/prediction.py:227:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:240:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:242:18: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:242:18: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:295:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:298:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:303:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:306:67: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:307:76: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:311:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:318:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:327:58: E741 ambiguous variable name 'l' -./src/tfscreen/tfmodel/analysis/prediction.py:330:26: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:330:26: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:333:25: E122 continuation line missing indentation or outdented -./src/tfscreen/tfmodel/analysis/prediction.py:339:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:340:56: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:341:62: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:343:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:345:40: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:346:42: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:347:47: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:349:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:352:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:99:73: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:136:71: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:157:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:164:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/analysis/prediction.py:210:1: C901 'predict' is too complex (26) +./src/tfscreen/tfmodel/analysis/prediction.py:271:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:282:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/analysis/prediction.py:284:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:297:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:299:18: E111 indentation is not a multiple of 4 +./src/tfscreen/tfmodel/analysis/prediction.py:299:18: E117 over-indented +./src/tfscreen/tfmodel/analysis/prediction.py:353:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:356:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:361:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:364:67: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:365:76: W291 trailing whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:369:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:370:71: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:374:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:379:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:383:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:376:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:385:58: E741 ambiguous variable name 'l' +./src/tfscreen/tfmodel/analysis/prediction.py:388:26: E111 indentation is not a multiple of 4 +./src/tfscreen/tfmodel/analysis/prediction.py:388:26: E117 over-indented +./src/tfscreen/tfmodel/analysis/prediction.py:391:25: E122 continuation line missing indentation or outdented +./src/tfscreen/tfmodel/analysis/prediction.py:397:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:412:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:415:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:432:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:433:71: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:437:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:442:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:446:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prior_predictive.py:104:33: E203 whitespace before ':' ./src/tfscreen/tfmodel/analysis/sbc.py:130:1: C901 'summarize_sbc' is too complex (17) ./src/tfscreen/tfmodel/configuration_io.py:19:1: E302 expected 2 blank lines, found 1 @@ -1595,7 +1560,7 @@ ./src/tfscreen/tfmodel/configuration_io.py:237:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/configuration_io.py:251:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/configuration_io.py:251:1: E402 module level import not at top of file -./src/tfscreen/tfmodel/configuration_io.py:253:1: C901 'read_configuration' is too complex (19) +./src/tfscreen/tfmodel/configuration_io.py:253:1: C901 'read_configuration' is too complex (20) ./src/tfscreen/tfmodel/configuration_io.py:253:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/configuration_io.py:289:128: E501 line too long (139 > 127 characters) ./src/tfscreen/tfmodel/configuration_io.py:297:22: E128 continuation line under-indented for visual indent @@ -3650,17 +3615,17 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:252:24: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:253:24: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:257:1: C901 '_inject_calibration_priors' is too complex (19) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:439:1: C901 '_build_csv_updates' is too complex (13) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:541:1: C901 '_apply_guesses_updates' is too complex (12) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:255:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:258:1: C901 '_inject_calibration_priors' is too complex (21) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:449:1: C901 '_build_csv_updates' is too complex (13) +./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:551:1: C901 '_apply_guesses_updates' is too complex (12) ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:43:1: C901 '_read_all_losses' is too complex (11) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:125:1: C901 'summarize_fit' is too complex (49) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:100:1: C901 '_read_all_losses' is too complex (11) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:182:1: C901 'summarize_fit' is too complex (49) ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:47:30: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:76:1: C901 'summarize_grid' is too complex (14) ./src/tfscreen/tfmodel/tensors/batch.py:5:1: E302 expected 2 blank lines, found 1 @@ -4891,38 +4856,16 @@ ./tests/tfscreen/plot/test_est_v_real_summary.py:22:1: W293 blank line contains whitespace ./tests/tfscreen/plot/test_est_v_real_summary.py:25:1: W293 blank line contains whitespace ./tests/tfscreen/plot/test_est_v_real_summary.py:30:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/plot/test_geno_trajectory.py:21:1: F401 'json' imported but unused -./tests/tfscreen/plot/test_geno_trajectory.py:23:1: F401 'sys' imported but unused -./tests/tfscreen/plot/test_geno_trajectory.py:24:1: F401 'tempfile' imported but unused -./tests/tfscreen/plot/test_geno_trajectory.py:43:11: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:44:10: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:45:9: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:46:9: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:47:9: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:48:8: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:75:17: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:76:13: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:77:13: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:78:14: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:85:21: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:87:21: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:91:16: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:92:18: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:119:24: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:123:25: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:137:22: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:138:27: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:147:16: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:148:16: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:152:14: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:337:21: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:338:23: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:342:24: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:343:25: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:413:55: E127 continuation line over-indented for visual indent -./tests/tfscreen/plot/test_geno_trajectory.py:418:55: E127 continuation line over-indented for visual indent -./tests/tfscreen/plot/test_geno_trajectory.py:428:24: E221 multiple spaces before operator -./tests/tfscreen/plot/test_geno_trajectory.py:429:24: E221 multiple spaces before operator +./tests/tfscreen/plot/test_geno_trajectory.py:18:1: E402 module level import not at top of file +./tests/tfscreen/plot/test_geno_trajectory.py:20:1: E402 module level import not at top of file +./tests/tfscreen/plot/test_geno_trajectory.py:158:25: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:303:31: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:304:42: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:319:31: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:320:42: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:487:19: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:515:19: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:543:19: E741 ambiguous variable name 'l' ./tests/tfscreen/plot/test_helper.py:3:1: F401 'pytest' imported but unused ./tests/tfscreen/plot/test_helper.py:6:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/plot/test_helper.py:9:1: W293 blank line contains whitespace @@ -5380,11 +5323,11 @@ ./tests/tfscreen/simulate/test_thermo_to_growth.py:98:32: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:104:32: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:106:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:173:14: E131 continuation line unaligned for hanging indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:200:44: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:207:35: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:1064:1: E402 module level import not at top of file -./tests/tfscreen/simulate/test_thermo_to_growth.py:1152:9: F841 local variable 'G_out' is assigned to but never used +./tests/tfscreen/simulate/test_thermo_to_growth.py:195:14: E131 continuation line unaligned for hanging indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:222:44: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:229:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_thermo_to_growth.py:1163:1: E402 module level import not at top of file +./tests/tfscreen/simulate/test_thermo_to_growth.py:1251:9: F841 local variable 'G_out' is assigned to but never used ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 @@ -6500,31 +6443,33 @@ ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:13:1: F401 'shutil' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:14:1: F401 'sys' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.run_prefit_calibration_cli._identify_field_mapping' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:295:1: E303 too many blank lines (4) -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:299:1: E302 expected 2 blank lines, found 4 -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:667:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:668:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:669:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:670:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:299:1: E303 too many blank lines (4) +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:303:1: E302 expected 2 blank lines, found 4 ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:671:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:672:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:673:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:674:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:675:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1051:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1070:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1085:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1353:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1379:63: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1435:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:676:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:677:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:678:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:679:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1084:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1103:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1118:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1386:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1412:63: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1534:20: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:84:31: E741 ambiguous variable name 'l' ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:598:28: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:15:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:16:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:17:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:18:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:20:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:19:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:21:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:23:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:624:18: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused @@ -6673,16 +6618,20 @@ ./tests/tfscreen/tfmodel/test_configure_and_run.py:175:56: E231 missing whitespace after ':' ./tests/tfscreen/tfmodel/test_configure_and_run.py:175:75: E231 missing whitespace after ':' ./tests/tfscreen/tfmodel/test_configure_and_run.py:175:128: E501 line too long (150 > 127 characters) -./tests/tfscreen/tfmodel/test_configure_and_run.py:223:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:225:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/test_configure_and_run.py:228:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/test_configure_and_run.py:356:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:371:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_configure_and_run.py:379:124: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:379:128: E501 line too long (130 > 127 characters) -./tests/tfscreen/tfmodel/test_configure_and_run.py:382:9: F841 local variable 'mock_ri_class' is assigned to but never used -./tests/tfscreen/tfmodel/test_configure_and_run.py:385:44: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_configure_and_run.py:401:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:187:30: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:187:40: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:187:55: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:187:74: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:242:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:244:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/test_configure_and_run.py:247:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/test_configure_and_run.py:375:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:390:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_configure_and_run.py:398:124: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:398:128: E501 line too long (130 > 127 characters) +./tests/tfscreen/tfmodel/test_configure_and_run.py:401:9: F841 local variable 'mock_ri_class' is assigned to but never used +./tests/tfscreen/tfmodel/test_configure_and_run.py:404:44: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_configure_and_run.py:420:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_congression_mask.py:1:1: F401 'pytest' imported but unused ./tests/tfscreen/tfmodel/test_congression_mask.py:5:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_congression_mask.py:7:1: W293 blank line contains whitespace @@ -7125,6 +7074,9 @@ ./tests/tfscreen/tfmodel/test_posteriors.py:101:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_posteriors.py:108:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_posteriors.py:138:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_prediction.py:4:1: F401 'jax.numpy as jnp' imported but unused +./tests/tfscreen/tfmodel/test_prediction.py:6:1: F401 'unittest.mock.MagicMock' imported but unused +./tests/tfscreen/tfmodel/test_prediction.py:6:1: F401 'unittest.mock.patch' imported but unused ./tests/tfscreen/tfmodel/test_prediction.py:11:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_prediction.py:37:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_prediction.py:46:1: E302 expected 2 blank lines, found 1 @@ -7657,17 +7609,17 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -167 E127 continuation line over-indented for visual indent +165 E127 continuation line over-indented for visual indent 79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1767 E221 multiple spaces before operator +1712 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1115 E231 missing whitespace after ',' +1119 E231 missing whitespace after ',' 79 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 226 E261 at least two spaces before inline comment @@ -7681,20 +7633,20 @@ 17 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line -33 E402 module level import not at top of file +36 E402 module level import not at top of file 71 E501 line too long (145 > 127 characters) 46 E701 multiple statements on one line (colon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' 2 E713 test for membership should be 'not in' 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def -12 E741 ambiguous variable name 'l' +20 E741 ambiguous variable name 'l' 222 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -678 W291 trailing whitespace +673 W291 trailing whitespace 86 W292 no newline at end of file -1699 W293 blank line contains whitespace +1698 W293 blank line contains whitespace 29 W391 blank line at end of file -7651 +7603 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 5465f3b0..539db625 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/plot/geno_trajectory.py b/src/tfscreen/plot/geno_trajectory.py index 5f4922be..636d70b5 100644 --- a/src/tfscreen/plot/geno_trajectory.py +++ b/src/tfscreen/plot/geno_trajectory.py @@ -1,619 +1,351 @@ """ -Public function for plotting per-genotype growth trajectory fits overlaid -on experimental data. - -The core value of :func:`plot_geno_trajectory` is the interpolative step: -it runs the forward model on a fine time grid to produce smooth trajectory -curves that can be compared visually against the raw ln_cfu observations. -Simpler downstream diagnostics (correlation plots, goodness-of-fit statistics) -are intentionally left to the caller so they can be combined or skipped as -needed. -""" +Functions for plotting per-genotype growth trajectory predictions. + +plot_geno_trajectory + Pure plotting function. Accepts a prediction DataFrame and returns a + ``matplotlib.figure.Figure``. -import sys +predict_and_plot_geno_trajectory + Composite entry point. Calls ``predict()`` with a fine t_sel grid, merges + in observed data and a pre-selection anchor from the ln_cfu0 site, then + delegates to ``plot_geno_trajectory``. +""" import numpy as np import pandas as pd -import jax -import jax.numpy as jnp -# Number of equally-spaced time points used for the fine-grid selection-phase -# trajectory in the MAP prediction path. _T_FINE = 50 +_CONDITION_COLS = [ + "condition_pre", "condition_sel", "titrant_name", "titrant_conc" +] +_KEEP_COLS = [ + "replicate", "condition_pre", "condition_sel", + "titrant_name", "titrant_conc", "genotype", + "t_sel", "ln_cfu", "ln_cfu_std", "q05", "median", "q95", +] -# --------------------------------------------------------------------------- -# Prediction helpers -# --------------------------------------------------------------------------- -def _compute_map_predictions(orchestrator, params): +def plot_geno_trajectory( + pred_df, + figsize=None, + colors=None, + markers=None, +): """ - Run the forward model at the MAP point and return prediction tensors. + Plot per-genotype growth trajectories from a prediction DataFrame. - Two forward passes are performed: one at the observed timepoints and one - on a :data:`_T_FINE`-point fine grid spanning ``[0, global_max_t_sel]``. - The fine-grid pass is used to draw smooth selection-phase trajectory curves. + One subplot is generated per unique + ``(condition_pre, condition_sel, titrant_name, titrant_conc)`` combination. + Every ``(genotype, replicate)`` pair present in ``pred_df`` is drawn as a + separate series on each subplot. Genotypes are distinguished by color; + replicates are distinguished by marker and linestyle. Parameters ---------- - orchestrator : ModelOrchestrator - Fitted model orchestrator. - params : dict - MAP parameter dict with ``{site}_auto_loc`` keys (unconstrained space). + pred_df : pd.DataFrame + Prediction DataFrame. Required columns: + + * ``replicate``, ``condition_pre``, ``condition_sel``, + ``titrant_name``, ``titrant_conc``, ``genotype`` — grouping keys. + * ``t_sel`` — time coordinate (may include negative values for the + pre-selection phase). + * ``ln_cfu`` — observed ln(CFU); ``NaN`` where no observation exists. + * ``ln_cfu_std`` — observed std; ``NaN`` where no observation exists. + * ``median`` — predicted median trajectory; ``NaN`` where unavailable. + + Optional columns (both must be present together to draw a credible + interval): + + * ``q05``, ``q95`` — 5th/95th percentile of the posterior predictive. + + figsize : tuple of (float, float), optional + Figure ``(width, height)`` in inches. Defaults to + ``(5 * n_cols, 4 * n_rows)``. + colors : list of str, optional + Color cycle for genotypes. Defaults to ``DEFAULT_COLORS``. + markers : list of str, optional + Marker cycle for replicates. Defaults to ``DEFAULT_MARKERS``. Returns ------- - growth_pred : np.ndarray - Shape ``(R, T, CP, CS, TN, TC, G)`` — predictions at observed timepoints. - ln_cfu0_map : np.ndarray or None - Shape ``(R, CP, G)`` — MAP ``ln_cfu0`` deterministic site, or ``None`` - if absent from the trace. - growth_pred_fine : np.ndarray - Shape ``(R, T_FINE, CP, CS, TN, TC, G)`` — predictions on the fine grid. - t_fine_1d : np.ndarray - Shape ``(T_FINE,)`` — time values for ``growth_pred_fine``. - - Raises - ------ - ValueError - If ``growth_pred`` is not present in the model trace. + matplotlib.figure.Figure """ - from numpyro.infer import Predictive - from numpyro.infer.autoguide import AutoDelta - - guide = AutoDelta(orchestrator.jax_model) - all_indices = jnp.arange(orchestrator.data.num_genotype) - full_data = orchestrator.get_batch(orchestrator.data, all_indices) - pred_fn = Predictive(orchestrator.jax_model, guide=guide, params=params, num_samples=1) - map_samples = pred_fn( - jax.random.PRNGKey(0), data=full_data, priors=orchestrator.priors + from matplotlib import pyplot as plt + from tfscreen.plot.default_styles import DEFAULT_COLORS, DEFAULT_MARKERS + + if colors is None: + colors = DEFAULT_COLORS[:] + if markers is None: + markers = DEFAULT_MARKERS[:] + + conditions = ( + pred_df[_CONDITION_COLS] + .drop_duplicates() + .sort_values(_CONDITION_COLS) + .reset_index(drop=True) ) + genotypes = sorted(pred_df["genotype"].unique().tolist(), key=str) + replicates = sorted(pred_df["replicate"].unique().tolist(), key=str) - if "growth_pred" not in map_samples: - raise ValueError( - "growth_pred not found in the model trace; " - "cannot generate trajectory plots." - ) + n_combos = len(conditions) + n_cols = min(3, n_combos) + n_rows = (n_combos + n_cols - 1) // n_cols - # Remove leading num_samples dimension (num_samples=1). - growth_pred = np.asarray(map_samples["growth_pred"][0]) # (R,T,CP,CS,TN,TC,G) - ln_cfu0_map = ( - np.asarray(map_samples["ln_cfu0"][0]) - if "ln_cfu0" in map_samples - else None - ) + if figsize is None: + figsize = (5 * n_cols, 4 * n_rows) - # --- Fine-grid forward pass --- - gd = orchestrator.data.growth - good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) - t_sel_tensor = np.asarray(gd.t_sel) - n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno = good_mask.shape - - global_max_t_sel = float(np.nanmax(t_sel_tensor[good_mask])) - t_fine_1d = np.linspace(0.0, global_max_t_sel, _T_FINE) - fine_shape = (n_rep, _T_FINE, n_cp, n_cs, n_tn, n_tc, n_geno) - - # Broadcast a 7-D array from T=1 to T_FINE; all per-condition tensors are - # constant along the T axis so slicing any timepoint is exact. - def _bc_t(arr, *, dtype=None): - a = np.asarray(arr) - r = np.broadcast_to(a[:, 0:1, ...], fine_shape).copy() - return jnp.array(r if dtype is None else r.astype(dtype)) - - t_sel_fine = np.broadcast_to( - t_fine_1d[None, :, None, None, None, None, None], fine_shape - ).copy() - # Active wherever any observed timepoint was valid for that cell. - has_data_bc = good_mask.any(axis=1, keepdims=True) - good_mask_fine = np.broadcast_to(has_data_bc, fine_shape).copy() - - gd_full = full_data.growth - fine_gd = gd_full.replace( - num_time=_T_FINE, - t_sel=jnp.array(t_sel_fine), - t_pre=_bc_t(gd_full.t_pre), - good_mask=jnp.array(good_mask_fine), - map_condition_pre=_bc_t(gd_full.map_condition_pre, dtype=int), - map_condition_sel=_bc_t(gd_full.map_condition_sel, dtype=int), - ln_cfu=jnp.zeros(fine_shape), - ln_cfu_std=jnp.ones(fine_shape), + fig, axes = plt.subplots( + n_rows, n_cols, figsize=figsize, squeeze=False, sharey=True ) - fine_data = full_data.replace(growth=fine_gd) - fine_samples = pred_fn(jax.random.PRNGKey(1), data=fine_data, priors=orchestrator.priors) - growth_pred_fine = np.asarray(fine_samples["growth_pred"][0]) - return growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d + # One color per (genotype, replicate) pair + series_pairs = [ + (g, r) for g in genotypes for r in replicates + ] + pair_color = { + pair: colors[i % len(colors)] for i, pair in enumerate(series_pairs) + } + rep_marker = {r: markers[i % len(markers)] for i, r in enumerate(replicates)} + + has_ci = {"q05", "q95"} <= set(pred_df.columns) + + for combo_i, cond_row in conditions.iterrows(): + ax = axes[combo_i // n_cols][combo_i % n_cols] + + cp = cond_row["condition_pre"] + cs = cond_row["condition_sel"] + tn = cond_row["titrant_name"] + tc = float(cond_row["titrant_conc"]) + + cond_df = pred_df[ + (pred_df["condition_pre"] == cp) + & (pred_df["condition_sel"] == cs) + & (pred_df["titrant_name"] == tn) + & (pred_df["titrant_conc"] == tc) + ].copy() + + ax.set_title(f"{cp} → {cs}\n{tn} = {tc:.3g}", fontsize=9) + ax.set_xlabel("Time") + ax.set_ylabel("ln(CFU)") + ax.axvline(0.0, color="0.6", lw=0.8, ls="--") + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + + for geno in genotypes: + geno_df = cond_df[cond_df["genotype"] == geno] + if geno_df.empty: + continue + for rep in replicates: + rep_df = ( + geno_df[geno_df["replicate"] == rep] + .sort_values("t_sel") + ) + if rep_df.empty: + continue -def _compute_posterior_predictions(orchestrator, posterior_file): - """ - Load ``growth_pred`` from a posterior HDF5 file and compute summary statistics. + color = pair_color[(geno, rep)] + label = f"{geno} ({rep})" + mk = rep_marker[rep] + ls = "-" - The posterior file must have been produced by - ``RunInference.get_posteriors()``, ``get_laplace_posteriors()``, or - ``get_map_posteriors()``. + # Observed data points + obs = rep_df.dropna(subset=["ln_cfu"]) + if not obs.empty: + ax.errorbar( + obs["t_sel"], + obs["ln_cfu"], + yerr=obs["ln_cfu_std"], + fmt=mk, + color=color, + ms=5, + lw=1, + capsize=3, + zorder=3, + ) - Parameters - ---------- - orchestrator : ModelOrchestrator - Fitted model orchestrator (used only for shape validation; not called). - posterior_file : str or h5py.File - Path to an HDF5 posterior file or an already-open ``h5py.File`` handle. + # Predicted median line + line_df = rep_df.dropna(subset=["median"]) + if not line_df.empty: + ax.plot( + line_df["t_sel"], + line_df["median"], + ls, + color=color, + lw=1.8, + label=label, + zorder=4, + ) - Returns - ------- - growth_pred_median : np.ndarray - Shape ``(R, T, CP, CS, TN, TC, G)`` — median across posterior samples. - growth_pred_lo : np.ndarray - Shape ``(R, T, CP, CS, TN, TC, G)`` — 5th-percentile (lower bound of - 90 % credible interval). - growth_pred_hi : np.ndarray - Shape ``(R, T, CP, CS, TN, TC, G)`` — 95th-percentile (upper bound). - ln_cfu0_median : np.ndarray or None - Shape ``(R, CP, G)`` — median ``ln_cfu0`` if present in the file, - otherwise ``None``. - - Raises - ------ - ValueError - If ``growth_pred`` is not a dataset in the HDF5 file. - """ - import h5py - - def _read(fh): - if "growth_pred" not in fh: - raise ValueError( - "growth_pred not found in posterior file; " - "cannot generate trajectory plots." - ) - # Shape: (S, R, T, CP, CS, TN, TC, G) - samples = fh["growth_pred"][:] - median = np.median(samples, axis=0) - lo = np.percentile(samples, 5, axis=0) - hi = np.percentile(samples, 95, axis=0) - ln_cfu0 = ( - np.median(fh["ln_cfu0"][:], axis=0) - if "ln_cfu0" in fh - else None - ) - return median, lo, hi, ln_cfu0 + # 90 % credible interval band + if has_ci: + ci_df = rep_df.dropna(subset=["q05", "q95"]) + if not ci_df.empty: + ax.fill_between( + ci_df["t_sel"], + ci_df["q05"], + ci_df["q95"], + color=color, + alpha=0.2, + zorder=2, + ) - if isinstance(posterior_file, str): - with h5py.File(posterior_file, "r") as fh: - return _read(fh) - else: - return _read(posterior_file) + ax.legend(fontsize=8, loc="best") + for extra_i in range(n_combos, n_rows * n_cols): + axes[extra_i // n_cols][extra_i % n_cols].set_visible(False) -# --------------------------------------------------------------------------- -# Public entry point -# --------------------------------------------------------------------------- + fig.tight_layout() + return fig -def plot_geno_trajectory( + +def predict_and_plot_geno_trajectory( orchestrator, - out_prefix, - params=None, - posterior_file=None, + param_posteriors, genotypes=None, titrant_names=None, - write_csv=True, - growth_pred_std=None, + t_fine=_T_FINE, + num_marginal_samples=200, + figsize=None, + colors=None, + markers=None, ): """ - Generate per-genotype growth trajectory plots from a fitted model. - - For each genotype a PDF is written containing one subplot per - ``(condition_pre, condition_sel, titrant_name, titrant_conc)`` - combination. Each subplot shows: - - * **Observed data** — ``ln_cfu ± ln_cfu_std`` error bars at their - ``t_sel`` x-coordinates (circles, one colour per replicate). - * **Pre-split observations** *(optional)* — ``ln_cfu_t0 ± ln_cfu_t0_std`` - as square markers at ``x = −t_pre`` when ``orchestrator.data.presplit`` is not - ``None``. The same pooled aliquot value appears in every subplot that - shares the same ``(replicate, condition_pre, genotype)`` triple, which - is correct because the measurement predates the condition split. - * **Model prediction**: - - - *MAP path* — a smooth 50-point trajectory from ``−t_pre`` through - ``max(t_sel)``, computed via a fine-grid forward pass. - - *Posterior path* — a median line from ``−t_pre`` through the - observed timepoints, with a 90 % credible-interval shaded band - covering the selection phase (``t ≥ 0``). + Run forward predictions and plot per-genotype growth trajectories. + + Calls :func:`~tfscreen.tfmodel.analysis.prediction.predict` with a fine + ``t_sel`` grid to produce smooth trajectory curves with credible-interval + bands, merges in observed data, and delegates to + :func:`plot_geno_trajectory`. Only ``(condition_pre, condition_sel, + titrant_name, titrant_conc)`` combinations present in + ``orchestrator.growth_df`` are included in the output. Parameters ---------- orchestrator : ModelOrchestrator Fitted model orchestrator. - out_prefix : str - Prefix for output files. Each genotype's PDF is written to - ``{out_prefix}_calib_{genotype}.pdf``. - params : dict or str, optional - MAP parameter dict with ``{site}_auto_loc`` keys, or a path to a - ``_params.npz`` file produced by ``RunInference.write_params()``. - Exactly one of *params* or *posterior_file* must be supplied. - posterior_file : str or h5py.File, optional - Path to a ``_posterior.h5`` file (or an open ``h5py.File`` handle) - produced by ``RunInference.get_posteriors()``. The plot shows the - posterior median with a 90 % credible-interval shaded band. + param_posteriors : dict or str + MAP parameter dict (keys ending in ``_auto_loc``), path to a + ``_params.npz`` file, or path to a ``_posterior.h5`` file. genotypes : list of str, optional - Subset of genotype names to plot. If ``None``, all genotypes in - ``orchestrator`` are plotted. Names not found in the model are warned about - and silently skipped. + Subset of genotypes to include. If ``None``, uses all genotypes in + the orchestrator. titrant_names : list of str, optional - Subset of titrant names to include. If ``None``, all titrant names - are included. - write_csv : bool, optional - If ``True`` (default), write - ``{out_prefix}_calib_growth_df.csv`` containing the observed and - predicted ``ln_cfu`` values. - growth_pred_std : np.ndarray, optional - Laplace-based per-cell prediction standard deviations, same shape - as the ``growth_pred`` tensor ``(R, T, CP, CS, TN, TC, G)``. - Only used with the MAP path (ignored for posteriors). When - *write_csv* is ``True`` this is written as the ``ln_cfu_pred_std`` - column. + Subset of titrant names to include. If ``None``, uses all. + t_fine : int, optional + Number of equally-spaced selection-phase time points for the fine grid. + Defaults to ``_T_FINE`` (50). + num_marginal_samples : int, optional + Number of posterior samples to run through the model when computing + quantiles. Passed directly to :func:`~.prediction.predict`. + Defaults to 200. + figsize : tuple of (float, float), optional + Passed to :func:`plot_geno_trajectory`. + colors : list of str, optional + Passed to :func:`plot_geno_trajectory`. + markers : list of str, optional + Passed to :func:`plot_geno_trajectory`. Returns ------- - growth_df_out : pd.DataFrame or None - Merged DataFrame of observed and predicted ``ln_cfu`` values (one - row per valid tensor cell), or ``None`` if matplotlib is not - available. Columns include all columns from the growth - ``TensorManager`` DataFrame plus ``ln_cfu_pred`` and, depending on - the prediction path: - - - MAP + *growth_pred_std*: ``ln_cfu_pred_std`` - - Posterior: ``ln_cfu_pred_lo``, ``ln_cfu_pred_hi`` - - Raises - ------ - ValueError - If neither or both of *params* / *posterior_file* are supplied, or - if required data are missing from the posterior file. + matplotlib.figure.Figure """ - if (params is None) == (posterior_file is None): - raise ValueError( - "Exactly one of params or posterior_file must be provided." - ) + from tfscreen.tfmodel.analysis.prediction import predict - try: - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - except ImportError: - print( - " warning: matplotlib not available; skipping calibration plots.", - file=sys.stderr, - ) - return None - - # ----------------------------------------------------------------------- - # Compute predictions - # ----------------------------------------------------------------------- - is_map = params is not None - growth_pred_lo = growth_pred_hi = None - growth_pred_fine = t_fine_1d = None - - if is_map: - if isinstance(params, str): - params = dict(np.load(params)) - growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d = ( - _compute_map_predictions(orchestrator, params) - ) - else: - growth_pred, growth_pred_lo, growth_pred_hi, ln_cfu0_map = ( - _compute_posterior_predictions(orchestrator, posterior_file) - ) + if isinstance(param_posteriors, str) and param_posteriors.endswith(".npz"): + param_posteriors = dict(np.load(param_posteriors)) - # ----------------------------------------------------------------------- - # Data tensors - # ----------------------------------------------------------------------- + # Fine selection-phase time grid: 0 … max observed t_sel gd = orchestrator.data.growth - good_mask = np.asarray(gd.good_mask) # (R, T, CP, CS, TN, TC, G) - t_pre_tensor = np.asarray(gd.t_pre) + good_mask = np.asarray(gd.good_mask) t_sel_tensor = np.asarray(gd.t_sel) - ln_cfu_obs = np.asarray(gd.ln_cfu) - ln_cfu_std_t = np.asarray(gd.ln_cfu_std) - - n_rep, n_t, n_cp, n_cs, n_tn, n_tc, n_geno = good_mask.shape - - # ----------------------------------------------------------------------- - # Dimension labels and helper maps - # ----------------------------------------------------------------------- - tm = orchestrator.growth_tm - dn = tm.tensor_dim_names - - geno_labels = list(tm.tensor_dim_labels[dn.index("genotype")]) - rep_labels = list(tm.tensor_dim_labels[dn.index("replicate")]) - cp_labels = list(tm.tensor_dim_labels[dn.index("condition_pre")]) - tn_labels = list(tm.tensor_dim_labels[dn.index("titrant_name")]) - tc_labels = list(tm.tensor_dim_labels[dn.index("titrant_conc")]) - - df = tm.df - # (cp_idx, cs_idx) → condition_sel label string - cs_name_map = {} - for _, row in df.drop_duplicates( - ["condition_pre_idx", "condition_sel_idx"] - ).iterrows(): - cs_name_map[ - (int(row["condition_pre_idx"]), int(row["condition_sel_idx"])) - ] = str(row["condition_sel"]) - - # (genotype, titrant_name, titrant_conc) → mean observed theta - theta_map: dict = {} - for _, row in orchestrator.binding_df.iterrows(): - key = ( - str(row["genotype"]), - str(row["titrant_name"]), - float(row["titrant_conc"]), - ) - theta_map.setdefault(key, []).append(float(row["theta_obs"])) - theta_map = {k: float(np.nanmean(v)) for k, v in theta_map.items()} - - # ----------------------------------------------------------------------- - # Pre-split data (optional) - # ----------------------------------------------------------------------- - presplit = orchestrator.data.presplit - if presplit is not None: - ps_ln_cfu = np.asarray(presplit.ln_cfu_t0) # (R, CP, G) - ps_ln_cfu_std = np.asarray(presplit.ln_cfu_t0_std) # (R, CP, G) - ps_good_mask = np.asarray(presplit.good_mask) # (R, CP, G) - else: - ps_ln_cfu = ps_ln_cfu_std = ps_good_mask = None - - # ----------------------------------------------------------------------- - # Genotype filter - # ----------------------------------------------------------------------- - avail_geno = {str(g): i for i, g in enumerate(geno_labels)} - if genotypes is not None: - requested = [str(g) for g in genotypes] - missing = sorted(set(requested) - set(avail_geno)) - if missing: - print( - f" warning: requested genotype(s) not found in model: {missing}", - file=sys.stderr, - ) - geno_iter = [ - (avail_geno[g], g) - for g in requested - if g in avail_geno - ] - else: - geno_iter = [(i, str(g)) for i, g in enumerate(geno_labels)] - - # ----------------------------------------------------------------------- - # Titrant_name filter - # ----------------------------------------------------------------------- - if titrant_names is not None: - requested_tn = set(str(t) for t in titrant_names) - tn_indices = frozenset( - i for i, tn in enumerate(tn_labels) if str(tn) in requested_tn + max_t_sel = float(np.nanmax(t_sel_tensor[good_mask])) + t_sel_grid = np.linspace(0.0, max_t_sel, t_fine).tolist() + + q_spec = {"q05": 0.05, "median": 0.5, "q95": 0.95} + + all_dfs = predict( + orchestrator, + param_posteriors, + predict_sites=["growth_pred", "ln_cfu0"], + q_to_get=q_spec, + num_samples=None, + num_marginal_samples=num_marginal_samples, + t_sel=t_sel_grid, + genotypes=genotypes, + ) + fine_df = all_dfs["growth_pred"] + ln_cfu0_raw = all_dfs["ln_cfu0"] + + # Build anchor rows at t_sel = -t_pre using ln_cfu0 quantiles. + # ln_cfu0 is indexed by (replicate, condition_pre, genotype); expand across + # all valid (condition_sel, titrant_name, titrant_conc) for each condition_pre. + t_pre_df = ( + orchestrator.growth_df[["replicate", "condition_pre", "t_pre"]] + .drop_duplicates(subset=["replicate", "condition_pre"]) + ) + ln_cfu0_vals = ( + ln_cfu0_raw[["replicate", "condition_pre", "genotype", "q05", "median", "q95"]] + .drop_duplicates(subset=["replicate", "condition_pre", "genotype"]) + ) + valid_combos = orchestrator.growth_df[_CONDITION_COLS].drop_duplicates() + anchor_df = ( + ln_cfu0_vals + .merge(valid_combos, on="condition_pre", how="inner") + .merge(t_pre_df, on=["replicate", "condition_pre"], how="left") + ) + anchor_df["t_sel"] = -anchor_df["t_pre"] + anchor_df["ln_cfu"] = np.nan + anchor_df["ln_cfu_std"] = np.nan + + # Overlay observed ln_cfu0 measurements when presplit_df is available. + presplit_df = getattr(orchestrator, "presplit_df", None) + if presplit_df is not None: + ps = presplit_df[ + ["replicate", "condition_pre", "genotype", "ln_cfu", "ln_cfu_std"] + ].rename(columns={"ln_cfu": "_ps_ln", "ln_cfu_std": "_ps_ln_std"}) + anchor_df = anchor_df.merge( + ps, on=["replicate", "condition_pre", "genotype"], how="left" ) - else: - tn_indices = frozenset(range(n_tn)) - - # ----------------------------------------------------------------------- - # Build predictions DataFrame (used for CSV and returned to caller) - # ----------------------------------------------------------------------- - r_idx, t_idx, cp_idx, cs_idx, tn_idx, tc_idx, g_idx = np.where(good_mask) - flat_idx = (r_idx, t_idx, cp_idx, cs_idx, tn_idx, tc_idx, g_idx) - - pred_dict: dict = { - "replicate_idx": r_idx.astype(int), - "condition_pre_idx": cp_idx.astype(int), - "condition_sel_idx": cs_idx.astype(int), - "titrant_name_idx": tn_idx.astype(int), - "titrant_conc_idx": tc_idx.astype(int), - "genotype_idx": g_idx.astype(int), - "t_sel": t_sel_tensor[flat_idx], - "ln_cfu_pred": growth_pred[flat_idx], - } - if is_map and growth_pred_std is not None: - pred_dict["ln_cfu_pred_std"] = growth_pred_std[flat_idx] - elif not is_map: - pred_dict["ln_cfu_pred_lo"] = growth_pred_lo[flat_idx] - pred_dict["ln_cfu_pred_hi"] = growth_pred_hi[flat_idx] - - merge_cols = [ - "replicate_idx", "condition_pre_idx", "condition_sel_idx", - "titrant_name_idx", "titrant_conc_idx", "genotype_idx", "t_sel", + anchor_df["ln_cfu"] = anchor_df["_ps_ln"] + anchor_df["ln_cfu_std"] = anchor_df["_ps_ln_std"] + anchor_df = anchor_df.drop(columns=["_ps_ln", "_ps_ln_std"]) + + # Observed data from the orchestrator (no model predictions) + obs_cols = [ + "replicate", "condition_pre", "condition_sel", + "titrant_name", "titrant_conc", "genotype", + "t_sel", "ln_cfu", "ln_cfu_std", ] - growth_df_out = df.merge(pd.DataFrame(pred_dict), on=merge_cols, how="left") - - if write_csv: - csv_path = f"{out_prefix}_calib_growth_df.csv" - growth_df_out.to_csv(csv_path, index=False) - print(f" Saved {csv_path}", flush=True) - - # ----------------------------------------------------------------------- - # Per-genotype PDF plots - # ----------------------------------------------------------------------- - print("Generating calibration quality plots ...", flush=True) - - prop_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] - - for g_i, geno_name in geno_iter: - - # Collect valid (cp, cs, tn, tc) combos → list of replicates. - # Apply titrant_name filter here. - condition_combos: dict = {} - for r_i in range(n_rep): - for cp_i in range(n_cp): - for cs_i in range(n_cs): - for tn_i in range(n_tn): - if tn_i not in tn_indices: - continue - for tc_i in range(n_tc): - if good_mask[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i].any(): - condition_combos.setdefault( - (cp_i, cs_i, tn_i, tc_i), [] - ).append(r_i) - - if not condition_combos: - continue - - n_combos = len(condition_combos) - n_cols = min(3, n_combos) - n_rows = (n_combos + n_cols - 1) // n_cols - - fig, axes = plt.subplots( - n_rows, n_cols, - figsize=(5 * n_cols, 4 * n_rows), - squeeze=False, - sharey=True, - ) - fig.suptitle( - f"Calibration fit — genotype: {geno_name}", - fontsize=13, - fontweight="bold", - ) - - for combo_i, ((cp_i, cs_i, tn_i, tc_i), rep_list) in enumerate( - condition_combos.items() - ): - ax = axes[combo_i // n_cols][combo_i % n_cols] - - cp_name = str(cp_labels[cp_i]) - cs_name = cs_name_map.get((cp_i, cs_i), f"sel_{cs_i}") - tn_name = str(tn_labels[tn_i]) - tc_val = float(tc_labels[tc_i]) - - theta_obs = theta_map.get((geno_name, tn_name, tc_val)) - theta_str = f", θ = {theta_obs:.3f}" if theta_obs is not None else "" - ax.set_title( - f"{cp_name} → {cs_name}\n{tn_name} = {tc_val:.3g}{theta_str}", - fontsize=9, - ) - ax.set_xlabel("Time") - ax.set_ylabel("ln(CFU)") - ax.axvline(0.0, color="0.6", lw=0.8, ls="--") - - # Build per-replicate observation records. - rep_data = [] - for r_i in rep_list: - mask = good_mask[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i] - valid_t = np.where(mask)[0] - if not len(valid_t): - continue - rep_data.append({ - "r_i": r_i, - "valid_t": valid_t, - "t_sel": t_sel_tensor[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], - "obs": ln_cfu_obs[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], - "std": ln_cfu_std_t[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i], - "t_pre": float(np.nanmedian( - t_pre_tensor[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] - )), - }) - - if not rep_data: - ax.set_visible(False) - continue - - max_t_sel = float(max(np.nanmax(rd["t_sel"]) for rd in rep_data)) - max_t_pre = float(max(rd["t_pre"] for rd in rep_data)) - - for rd in rep_data: - r_i = rd["r_i"] - rep_color = prop_colors[r_i % len(prop_colors)] - rep_label = str(rep_labels[r_i]) - - # --- Pre-split observed point (square marker) --- - # Shown at x = −t_pre; same pooled aliquot for all conditions - # sharing this (replicate, condition_pre, genotype) triple. - if ps_good_mask is not None and ps_good_mask[r_i, cp_i, g_i]: - ax.errorbar( - -rd["t_pre"], - float(ps_ln_cfu[r_i, cp_i, g_i]), - yerr=float(ps_ln_cfu_std[r_i, cp_i, g_i]), - fmt="s", - color=rep_color, - ms=5, - lw=1, - capsize=3, - zorder=3, - ) - - # --- Selection-phase observed data (circle marker) --- - ax.errorbar( - rd["t_sel"], - rd["obs"], - yerr=rd["std"], - fmt="o", - color=rep_color, - ms=5, - lw=1, - capsize=3, - label=rep_label, - zorder=3, - ) - - # --- Model prediction --- - if is_map: - # Smooth fine-grid line (pre-selection + selection phases). - y_fine = growth_pred_fine[r_i, :, cp_i, cs_i, tn_i, tc_i, g_i] - calc_at_0 = float(y_fine[0]) - ln_cfu0_r = ( - float(ln_cfu0_map[r_i, cp_i, g_i]) - if (ln_cfu0_map is not None and ln_cfu0_map.ndim == 3) - else calc_at_0 - ) - t_smooth = np.concatenate([[-rd["t_pre"], 0.0], t_fine_1d]) - y_smooth = np.concatenate([[ln_cfu0_r, calc_at_0], y_fine]) - ax.plot(t_smooth, y_smooth, "-", color=rep_color, lw=1.8, zorder=4) - - else: - # Posterior: median line (both phases) + 90 % credible - # interval shaded band (selection phase only). - valid_t = rd["valid_t"] - t_obs = rd["t_sel"] - y_med = growth_pred[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] - y_lo = growth_pred_lo[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] - y_hi = growth_pred_hi[r_i, valid_t, cp_i, cs_i, tn_i, tc_i, g_i] - - ln_cfu0_r = ( - float(ln_cfu0_map[r_i, cp_i, g_i]) - if (ln_cfu0_map is not None and ln_cfu0_map.ndim == 3) - else float(y_med[0]) - ) - - # Sort by time for clean connected lines. - sort_idx = np.argsort(t_obs) - t_sorted = t_obs[sort_idx] - y_med_s = y_med[sort_idx] - y_lo_s = y_lo[sort_idx] - y_hi_s = y_hi[sort_idx] - - # Full line: pre-selection anchor → observed medians. - t_line = np.concatenate([[-rd["t_pre"]], t_sorted]) - y_line = np.concatenate([[ln_cfu0_r], y_med_s]) - ax.plot(t_line, y_line, "-", color=rep_color, lw=1.8, zorder=4) - - # Shaded credible interval — selection phase only. - ax.fill_between( - t_sorted, - y_lo_s, - y_hi_s, - color=rep_color, - alpha=0.2, - zorder=2, - ) - - ax.legend(fontsize=8, loc="best") - x_pad = (max_t_sel + max_t_pre) * 0.03 - ax.set_xlim(-max_t_pre - x_pad, max_t_sel + x_pad) + obs_df = orchestrator.growth_df[obs_cols].copy() + if genotypes is not None: + obs_df = obs_df[obs_df["genotype"].isin(list(genotypes))] + for col in ("q05", "median", "q95"): + obs_df[col] = np.nan - # Hide unused subplot cells. - for extra_i in range(n_combos, n_rows * n_cols): - axes[extra_i // n_cols][extra_i % n_cols].set_visible(False) + # Titrant-name filter (predict() has no titrant_name argument) + if titrant_names is not None: + tn_set = {str(t) for t in titrant_names} + fine_df = fine_df[fine_df["titrant_name"].isin(tn_set)] + obs_df = obs_df[obs_df["titrant_name"].isin(tn_set)] + anchor_df = anchor_df[anchor_df["titrant_name"].isin(tn_set)] + + pred_df = pd.concat( + [fine_df[_KEEP_COLS], obs_df[_KEEP_COLS], anchor_df[_KEEP_COLS]], + ignore_index=True, + ) - fig.tight_layout(rect=[0, 0, 1, 0.95]) - safe_name = geno_name.replace("/", "-") - pdf_path = f"{out_prefix}_calib_{safe_name}.pdf" - fig.savefig(pdf_path, format="pdf", bbox_inches="tight") - plt.close(fig) - print(f" Saved {pdf_path}", flush=True) + # Restrict to condition combos that actually exist in the training data. + # copy_orchestrator() produces a Cartesian product of categorical columns + # that can include invalid (condition_pre, condition_sel) pairings; this + # filter removes them. + valid_combos = ( + orchestrator.growth_df[_CONDITION_COLS] + .drop_duplicates() + ) + pred_df = pred_df.merge(valid_combos, on=_CONDITION_COLS, how="inner") - print("Calibration plots complete.", flush=True) - return growth_df_out + return plot_geno_trajectory( + pred_df, figsize=figsize, colors=colors, markers=markers + ) diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index 8425fb98..64758f80 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -77,6 +77,16 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], theta_rng_seed = cf.get('theta_rng_seed', 0) theta_rng_key = jax.random.PRNGKey(theta_rng_seed) + dk_geno_zero = cf.get('dk_geno_zero', False) + if dk_geno_zero: + dk_geno_hyper_loc = cf.get('dk_geno_hyper_loc', -3.5) + dk_geno_hyper_scale = cf.get('dk_geno_hyper_scale', 1.0) + dk_geno_hyper_shift = cf.get('dk_geno_hyper_shift', 0.02) + else: + dk_geno_hyper_loc = cf['dk_geno_hyper_loc'] + dk_geno_hyper_scale = cf['dk_geno_hyper_scale'] + dk_geno_hyper_shift = cf['dk_geno_hyper_shift'] + # Calculate phenotype for each genotype across all conditions in sample_df phenotype_df, genotype_theta_df, parameters_df = thermo_to_growth( genotypes=library_df["genotype"], @@ -86,9 +96,10 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], theta_rng_key=theta_rng_key, growth_params=cf['growth'], theta_priors_overrides=cf.get('theta_priors'), - dk_geno_hyper_loc=cf['dk_geno_hyper_loc'], - dk_geno_hyper_scale=cf['dk_geno_hyper_scale'], - dk_geno_hyper_shift=cf['dk_geno_hyper_shift'], + dk_geno_hyper_loc=dk_geno_hyper_loc, + dk_geno_hyper_scale=dk_geno_hyper_scale, + dk_geno_hyper_shift=dk_geno_hyper_shift, + dk_geno_zero=dk_geno_zero, activity_wt=cf.get('activity_wt', 1.0), activity_mut_scale=cf.get('activity_mut_scale', 0.0), activity_component=cf.get('activity_component', 'fixed'), diff --git a/src/tfscreen/simulate/selection_experiment.py b/src/tfscreen/simulate/selection_experiment.py index 7af1fdac..56e8168b 100644 --- a/src/tfscreen/simulate/selection_experiment.py +++ b/src/tfscreen/simulate/selection_experiment.py @@ -54,7 +54,7 @@ "theta_component", "theta_rng_seed", "thermo_data", "theta_priors", "theta_rescale", # Conditions and growth "condition_blocks", "growth", - "dk_geno_hyper_loc", "dk_geno_hyper_scale", "dk_geno_hyper_shift", + "dk_geno_hyper_loc", "dk_geno_hyper_scale", "dk_geno_hyper_shift", "dk_geno_zero", "activity_wt", "activity_mut_scale", "activity_component", "activity_priors", # Experimental simulation parameters "transform_sizes", "library_mixture", "lib_assembly_skew_sigma", diff --git a/src/tfscreen/simulate/thermo_to_growth.py b/src/tfscreen/simulate/thermo_to_growth.py index ee064e94..5f6c4d4e 100644 --- a/src/tfscreen/simulate/thermo_to_growth.py +++ b/src/tfscreen/simulate/thermo_to_growth.py @@ -219,7 +219,8 @@ def _assign_dk_geno(unique_genotypes, hyper_loc=-3.5, hyper_scale=1.0, hyper_shift=0.02, - rng: Generator | None = None): + rng: Generator | None = None, + fixed_value: float | None = None): """ Assign a pleiotropic growth-rate effect (dk_geno) to each genotype. @@ -228,7 +229,13 @@ def _assign_dk_geno(unique_genotypes, Wild-type receives dk_geno = 0. This matches the prior used in the hierarchical tfmodel inference component. + + If fixed_value is not None, all genotypes (including wild-type) receive + that value and no stochastic sampling is performed. """ + if fixed_value is not None: + return pd.Series({g: float(fixed_value) for g in unique_genotypes}) + if rng is None: rng = np.random.default_rng() @@ -291,6 +298,7 @@ def thermo_to_growth( dk_geno_hyper_loc: float = -3.5, dk_geno_hyper_scale: float = 1.0, dk_geno_hyper_shift: float = 0.02, + dk_geno_zero: bool = False, activity_wt: float = 1.0, activity_mut_scale: float = 0.0, rng: Generator | None = None, @@ -335,6 +343,11 @@ def thermo_to_growth( dk_geno_hyper_shift : float Shift applied after exponentiation: dk_geno = hyper_shift - exp(offset). Controls the maximum possible (beneficial) growth-rate effect. + dk_geno_zero : bool, default False + When True, assign dk_geno = 0 to every genotype and skip stochastic + sampling entirely. The three ``dk_geno_hyper_*`` parameters are + ignored. Useful for simulations where no pleiotropic growth effects + are desired. activity_wt : float TF activity of the wild-type genotype. Used only when ``activity_component == "fixed"``. @@ -517,6 +530,7 @@ def thermo_to_growth( genotype_dk_geno_series = _assign_dk_geno( unique_genotypes, dk_geno_hyper_loc, dk_geno_hyper_scale, dk_geno_hyper_shift, rng, + fixed_value=0.0 if dk_geno_zero else None, ) phenotype_df["dk_geno"] = phenotype_df["genotype"].map(genotype_dk_geno_series) diff --git a/src/tfscreen/tfmodel/analysis/prediction.py b/src/tfscreen/tfmodel/analysis/prediction.py index 98a6d003..be2e71d4 100644 --- a/src/tfscreen/tfmodel/analysis/prediction.py +++ b/src/tfscreen/tfmodel/analysis/prediction.py @@ -8,6 +8,65 @@ from numpyro.handlers import seed, trace from numpyro.infer import Predictive +def _align_site_to_tm_dims(spatial_shape, tm_sizes): + """ + Map each axis of a predicted site's spatial_shape to the correct + TensorManager dimension index. + + Right-to-left alignment is tried first. It is correct when the + non-broadcast site dimensions are tail-aligned with the TM ordering — + e.g. growth_pred (7-D, full TM rank) or theta_growth_pred (7-D with + broadcast size-1 dims on the left). + + For sites whose non-broadcast dims are NOT tail-aligned — notably ln_cfu0 + with shape (n_rep, n_cp, n_geno) where n_rep and n_cp sit at TM positions + 0 and 2 rather than at the tail — we fall back to a greedy left-to-right + subsequence search through tm_sizes. + + Parameters + ---------- + spatial_shape : tuple of int + Shape of the prediction site with the sample axis removed. + tm_sizes : tuple of int + Sizes of each TensorManager dimension in order. + + Returns + ------- + list of int + tm_dims[i] is the TM dimension index for spatial_shape[i]. + """ + n_tm = len(tm_sizes) + n_sp = len(spatial_shape) + + rt_dims = [n_tm - n_sp + i for i in range(n_sp)] + + # Check whether non-broadcast axes agree with their right-to-left positions. + rt_ok = all( + spatial_shape[i] == tm_sizes[rt_dims[i]] + for i in range(n_sp) + if spatial_shape[i] != 1 + ) + if rt_ok: + return rt_dims + + # Right-to-left is wrong: greedy left-to-right subsequence match on + # non-broadcast dims. Size-1 (broadcast) dims keep their rt_dims entry + # because modulo makes the exact TM position irrelevant. + result = list(rt_dims) + j = 0 + for i in range(n_sp): + if spatial_shape[i] == 1: + continue + while j < n_tm and tm_sizes[j] != spatial_shape[i]: + j += 1 + if j >= n_tm: + return rt_dims # No match — fall back to right-to-left + result[i] = j + j += 1 + + return result + + def copy_orchestrator(orchestrator, t_pre=None, t_sel=None, @@ -104,23 +163,29 @@ def _get_input(value, col_name): def _convert_map_params(map_params, model_trace): """ - Convert a raw MAP parameter dict to a constrained posterior dict. - - ``AutoDelta`` guide parameters are stored in *unconstrained* space with - ``{site}_auto_loc`` keys. ``Predictive(posterior_samples=...)`` expects - values in *constrained* space keyed by bare site name, with a leading - sample dimension. This function applies the per-site bijection and adds - that dimension so the rest of :func:`predict` can treat the MAP point as - a 1-sample posterior. + Convert a raw MAP parameter dict to a posterior dict suitable for Predictive. + + ``AutoDelta`` guide parameters are stored in *constrained* space with + ``{site}_auto_loc`` keys because ``RunInference.write_params`` calls + ``svi.get_params(svi_state)``, which applies the SVI ``constrain_fn`` + before saving. ``Predictive(posterior_samples=...)`` expects values keyed + by bare site name with a leading sample dimension. This function strips + the ``_auto_loc`` suffix and adds that dimension so the rest of + :func:`predict` can treat the MAP point as a 1-sample posterior. + + .. note:: + Do **not** apply ``biject_to`` or any other transform here. The values + are already constrained; a second transform would corrupt positive- + constrained sites (e.g. ``ln_cfu0_hyper_scale``) by applying ``exp`` + to a value that is already in positive space. Parameters ---------- map_params : dict-like Raw MAP parameter dict (e.g., from ``np.load("_params.npz")``). - All keys must end with ``_auto_loc``. + Keys end with ``_auto_loc``; values are already in constrained space. model_trace : dict - NumPyro model trace produced by ``numpyro.handlers.trace``. Used to - look up the ``fn.support`` bijection for each latent site. + NumPyro model trace (unused; retained for interface compatibility). Returns ------- @@ -128,8 +193,6 @@ def _convert_map_params(map_params, model_trace): Constrained values keyed by bare site name, each with shape ``(1, *site_shape)``. """ - from numpyro.distributions.transforms import biject_to - constrained = {} for k, v in map_params.items(): k = str(k) @@ -137,14 +200,8 @@ def _convert_map_params(map_params, model_trace): continue site_name = k[: -len("_auto_loc")] val = jnp.array(np.asarray(v)) - - site = model_trace.get(site_name) - if (site is not None - and site["type"] == "sample" - and not site.get("is_observed", False)): - val = biject_to(site["fn"].support)(val) - - # Add the leading sample dimension expected by predict(). + # Values are already constrained (svi.get_params() applied constrain_fn). + # Add the leading sample dimension expected by Predictive. constrained[site_name] = jnp.expand_dims(val, 0) return constrained @@ -260,9 +317,10 @@ def predict(orchestrator, # MAP param conversion (if needed) # # Raw MAP params from RunInference.write_params() / np.load("_params.npz") - # have keys like ``{site}_auto_loc`` in *unconstrained* space and no - # leading sample dimension. Convert them to constrained space with a - # size-1 leading dim so the rest of this function treats them as a + # have keys like ``{site}_auto_loc`` in *constrained* space (because + # write_params receives the output of svi.get_params(), which applies + # constrain_fn) and no leading sample dimension. Rename the keys and add + # a size-1 leading dim so the rest of this function treats them as a # 1-sample posterior. # # Genuine posterior files produced by get_posteriors() / get_map_posteriors() @@ -337,15 +395,20 @@ def predict(orchestrator, # ------------------------------------------------------------------------- # Run Prediction - predictive = Predictive(new_orchestrator.jax_model, - posterior_samples=sliced_samples, + predictive = Predictive(new_orchestrator.jax_model, + posterior_samples=sliced_samples, return_sites=predict_sites) - + # We need a key, even if not used for randomness in deterministic sites - predict_key = jax.random.PRNGKey(0) - predictions = predictive(predict_key, - data=subset_data, - priors=new_orchestrator.priors) + predict_key = jax.random.PRNGKey(0) + # Use the ORIGINAL orchestrator's priors so that any pinned hyperpriors + # (e.g. activity_hyper_loc/activity_hyper_scale in the calibration model) + # remain pinned at their calibrated values rather than being sampled from + # the default prior. new_orchestrator is rebuilt from settings-only + # (component names, no priors) so its priors are all-default. + predictions = predictive(predict_key, + data=subset_data, + priors=orchestrator.priors) # ------------------------------------------------------------------------- # Calculate Quantiles and Join @@ -384,17 +447,21 @@ def predict(orchestrator, # Update indices for genotype to be relative for the prediction tensor indices[-1] = relative_geno_indices + # TM dimension sizes matching the current (possibly genotype-subsetted) indices. + tm_sizes = tuple(int(idx.max()) + 1 if len(idx) > 0 else 1 for idx in indices) + all_dfs = {} for site in predict_sites: site_samples = predictions[site] # shape: (num_samples, ...) - # Compute the per-row index tuple once — shape is the same for every - # quantile and for individual sample draws. - # q_arr drops the leading sample axis, so spatial_shape == site_samples.shape[1:]. - # We align right-to-left and use modulo to handle size-1 broadcast axes. + # Map each axis of the site's spatial shape to the correct TM dimension. + # Right-to-left alignment works for full-rank and tail-aligned sites; + # _align_site_to_tm_dims falls back to subsequence matching for sites + # like ln_cfu0 whose batch dims are not at the tail of the TM ordering. spatial_shape = site_samples.shape[1:] + tm_dims = _align_site_to_tm_dims(spatial_shape, tm_sizes) aligned_indices = tuple( - indices[len(indices) - len(spatial_shape) + i] % spatial_shape[i] + indices[tm_dims[i]] % spatial_shape[i] for i in range(len(spatial_shape)) ) @@ -408,7 +475,7 @@ def predict(orchestrator, has_broadcast = any(sz == 1 for sz in spatial_shape) if has_broadcast: nontrivial_tm_idx = [ - len(indices) - len(spatial_shape) + i + tm_dims[i] for i, sz in enumerate(spatial_shape) if sz > 1 ] if nontrivial_tm_idx: diff --git a/src/tfscreen/tfmodel/configuration_io.py b/src/tfscreen/tfmodel/configuration_io.py index 22177266..06f6752d 100644 --- a/src/tfscreen/tfmodel/configuration_io.py +++ b/src/tfscreen/tfmodel/configuration_io.py @@ -340,6 +340,15 @@ def read_configuration(config_file): orig_val = orchestrator.init_params[param_name] if hasattr(orig_val, 'shape') and orig_val.shape != (): orig_shape = orig_val.shape + if val_array.size != np.prod(orig_shape): + raise ValueError( + f"Parameter '{param_name}' has {val_array.size} " + f"{'value' if val_array.size == 1 else 'values'} in " + f"'{guesses_file}' but the current model expects shape " + f"{orig_shape} ({int(np.prod(orig_shape))} values). " + f"The guesses file is likely stale — regenerate it with " + f"tfs-configure-model and re-run tfs-prefit-calibration." + ) init_params[param_name] = jnp.array(val_array.reshape(orig_shape)) else: init_params[param_name] = float(val_array[0]) diff --git a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py index 9c889ff0..06880ac0 100644 --- a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py @@ -65,6 +65,7 @@ read_configuration, ) from tfscreen.plot.geno_trajectory import plot_geno_trajectory +from tfscreen.tfmodel.analysis.prediction import predict # --------------------------------------------------------------------------- @@ -96,8 +97,8 @@ ("hyper_scale", "hyper_scale_loc"), ), "ln_cfu0": ( - ("hyper_loc", "ln_cfu0_hyper_loc_loc"), - ("hyper_scale", "ln_cfu0_hyper_scale_loc"), + ("hyper_loc", "ln_cfu0_hyper_loc_locs"), + ("hyper_scale", "ln_cfu0_hyper_scale_locs"), ), } @@ -325,7 +326,16 @@ def _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values pinned_dict = {} for suffix, field_name in suffix_field_pairs: if hasattr(comp, field_name): - pinned_dict[suffix] = float(getattr(comp, field_name)) + val = getattr(comp, field_name) + arr = np.asarray(val) + if arr.ndim == 0: + # Scalar field (e.g. activity hyper_loc_loc) + pinned_dict[suffix] = float(arr) + else: + # Array field (e.g. ln_cfu0 per-class arrays) — expand to + # per-class entries so _hyper() finds "hyper_loc_0" etc. + for i, v in enumerate(arr.flat): + pinned_dict[f"{suffix}_{i}"] = float(v) if pinned_dict and hasattr(comp, "replace"): pinned_components[comp_name] = comp.replace(pinned=pinned_dict) @@ -693,13 +703,26 @@ def _run_calibration_map(ri, # Diagnostic plots # --------------------------------------------------------------------------- +_DIAG_PLOT_COLS = [ + "replicate", "condition_pre", "condition_sel", + "titrant_name", "titrant_conc", "genotype", + "t_sel", "ln_cfu", "ln_cfu_std", "q05", "median", "q95", +] +_DIAG_CONDITION_COLS = [ + "condition_pre", "condition_sel", "titrant_name", "titrant_conc" +] + + def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pred_std=None): """ Generate per-genotype trajectory PDFs and write correlation / stats outputs. - Delegates the trajectory plots to :func:`tfscreen.plot.plot_geno_trajectory`, - then uses the returned predictions DataFrame to write the goodness-of-fit - stats JSON and the observed-vs-predicted correlation PDF. + Calls :func:`~tfscreen.tfmodel.analysis.prediction.predict` at the + observed timepoints to obtain MAP point estimates, then iterates over + genotypes to save one trajectory PDF per genotype via + :func:`~tfscreen.plot.geno_trajectory.plot_geno_trajectory`. Also + writes the goodness-of-fit stats JSON and the observed-vs-predicted + correlation PDF. Parameters ---------- @@ -713,22 +736,117 @@ def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pr Optional Laplace-based per-cell prediction standard deviations, shape ``(R, T, CP, CS, TN, TC, G)``. """ - growth_df_out = plot_geno_trajectory( + try: + import matplotlib.pyplot as plt + except ImportError: + return + + # ------------------------------------------------------------------ + # 1. MAP predictions at the observed timepoints. + # q_to_get uses "median" so the returned columns match + # plot_geno_trajectory's expected column names directly. + # ------------------------------------------------------------------ + all_dfs = predict( orchestrator_cal, - out_prefix, - params=params, - growth_pred_std=growth_pred_std, + params, + predict_sites=["growth_pred", "ln_cfu0"], + q_to_get={"median": 0.5, "q05": 0.05, "q95": 0.95}, + num_samples=None, + num_marginal_samples=1, ) - if growth_df_out is None: - return + pred_df = all_dfs["growth_pred"] + ln_cfu0_raw = all_dfs["ln_cfu0"] + + # ------------------------------------------------------------------ + # 2. Add ln_cfu_pred (alias for median) and optional Laplace std for + # the stats / correlation helpers. + # ------------------------------------------------------------------ + pred_df = pred_df.copy() + pred_df["ln_cfu_pred"] = pred_df["median"] + + if growth_pred_std is not None: + tm = orchestrator_cal.growth_tm + try: + indices = tuple( + pred_df[f"{dim}_idx"].values for dim in tm.tensor_dim_names + ) + pred_df["ln_cfu_pred_std"] = growth_pred_std[indices] + except (KeyError, IndexError): + pass + # ------------------------------------------------------------------ + # 3. Calibration stats + observed-vs-predicted correlation. + # ------------------------------------------------------------------ n_params = int(sum(np.asarray(v).size for k, v in params.items() if k.endswith("_auto_loc"))) - n_obs = int(growth_df_out["ln_cfu_pred"].notna().sum()) - _write_calibration_stats(growth_df_out, out_prefix, + n_obs = int(pred_df["ln_cfu_pred"].notna().sum()) + _write_calibration_stats(pred_df, out_prefix, n_params=n_params, n_obs=n_obs) - _make_correlation_plot(growth_df_out, out_prefix) + _make_correlation_plot(pred_df, out_prefix) + + # ------------------------------------------------------------------ + # 4. Build anchor rows (t_sel = -t_pre) from ln_cfu0 predictions. + # Mirror the logic in predict_and_plot_geno_trajectory. + # ------------------------------------------------------------------ + t_pre_df = ( + orchestrator_cal.growth_df[["replicate", "condition_pre", "t_pre"]] + .drop_duplicates(subset=["replicate", "condition_pre"]) + ) + ln_cfu0_vals = ( + ln_cfu0_raw[["replicate", "condition_pre", "genotype", + "q05", "median", "q95"]] + .drop_duplicates(subset=["replicate", "condition_pre", "genotype"]) + ) + valid_combos = ( + orchestrator_cal.growth_df[_DIAG_CONDITION_COLS].drop_duplicates() + ) + anchor_df = ( + ln_cfu0_vals + .merge(valid_combos, on="condition_pre", how="inner") + .merge(t_pre_df, on=["replicate", "condition_pre"], how="left") + ) + anchor_df["t_sel"] = -anchor_df["t_pre"] + anchor_df["ln_cfu"] = np.nan + anchor_df["ln_cfu_std"] = np.nan + + presplit_df = getattr(orchestrator_cal, "presplit_df", None) + if presplit_df is not None: + ps = presplit_df[ + ["replicate", "condition_pre", "genotype", "ln_cfu", "ln_cfu_std"] + ].rename(columns={"ln_cfu": "_ps_ln", "ln_cfu_std": "_ps_ln_std"}) + anchor_df = anchor_df.merge( + ps, on=["replicate", "condition_pre", "genotype"], how="left" + ) + anchor_df["ln_cfu"] = anchor_df["_ps_ln"] + anchor_df["ln_cfu_std"] = anchor_df["_ps_ln_std"] + anchor_df = anchor_df.drop(columns=["_ps_ln", "_ps_ln_std"]) + + # ------------------------------------------------------------------ + # 5. One trajectory PDF per genotype. + # ------------------------------------------------------------------ + genotypes = sorted(pred_df["genotype"].unique().tolist(), key=str) + for geno in genotypes: + geno_pred = pred_df[pred_df["genotype"] == geno] + geno_anchor = anchor_df[anchor_df["genotype"] == geno] + geno_df = pd.concat( + [geno_pred[_DIAG_PLOT_COLS], geno_anchor[_DIAG_PLOT_COLS]], + ignore_index=True, + ) + safe_name = str(geno).replace("/", "_").replace(" ", "_") + geno_df.to_csv(f"{out_prefix}_{safe_name}_trajectory.csv") + try: + fig = plot_geno_trajectory(geno_df) + safe_name = str(geno).replace("/", "_").replace(" ", "_") + pdf_path = f"{out_prefix}_{safe_name}_trajectory.pdf" + fig.savefig(pdf_path, format="pdf", bbox_inches="tight") + plt.close(fig) + print(f" Saved {pdf_path}", flush=True) + except Exception as exc: + print( + f" Warning: trajectory plot failed for {geno!r}: {exc}", + flush=True, + ) # --------------------------------------------------------------------------- diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index a17ff81c..a3c59ce8 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -14,6 +14,63 @@ from tfscreen.util.cli.generalized_main import generalized_main +def _find_params_or_posterior(run_dir): + """Return (kind, path) for the best available prediction source in run_dir. + + Preference order: ``*_posterior.h5`` first (richer uncertainty), then + ``*_params.npz`` (MAP fallback). Returns ``(None, None)`` when neither + is found. Warns when multiple files of the same type are present and + uses the first alphabetically. + """ + for suffix, kind in (("_posterior.h5", "posterior"), ("_params.npz", "params")): + matches = sorted(glob.glob(os.path.join(run_dir, f"*{suffix}"))) + if not matches: + continue + if len(matches) > 1: + warnings.warn( + f"Multiple {kind} files found in {run_dir}; " + f"using {os.path.basename(matches[0])}" + ) + return kind, matches[0] + return None, None + + +def _try_plot_trajectories(config_file, config_yaml, run_dir, out_prefix, binding_df): + """Attempt to generate per-genotype growth trajectory plots. + + Silently skips when the run has no growth data. Warns and returns on any + other failure so that the rest of summarize_fit output is unaffected. + """ + if config_yaml is None or not config_yaml.get("data", {}).get("growth"): + return + + kind, pred_path = _find_params_or_posterior(run_dir) + if pred_path is None: + warnings.warn( + "No *_posterior.h5 or *_params.npz found in " + f"{run_dir}; skipping trajectory plots." + ) + return + + try: + from tfscreen.tfmodel.configuration_io import read_configuration + from tfscreen.plot.geno_trajectory import plot_geno_trajectory + + orchestrator, _ = read_configuration(config_file) + + genotypes = list(binding_df["genotype"].unique()) if binding_df is not None else None + + kwargs = {"posterior_file": pred_path} if kind == "posterior" else {"params": pred_path} + plot_geno_trajectory( + orchestrator, + out_prefix, + genotypes=genotypes, + **kwargs, + ) + except Exception as exc: + warnings.warn(f"Could not generate trajectory plots: {exc}") + + def _find_unique(run_dir, suffix, label, warn_missing=True): """Return single file in run_dir matching *suffix, or None.""" matches = sorted(glob.glob(os.path.join(run_dir, f"*{suffix}"))) @@ -306,6 +363,9 @@ def summarize_fit(run_dir, except Exception as exc: warnings.warn(f"Could not compute growth training statistics: {exc}") + # --- Trajectory plots --- + _try_plot_trajectories(config_file, config_yaml, run_dir, out_prefix, binding_df) + # --- Write JSON --- results = { "metadata": metadata, diff --git a/tests/tfscreen/plot/test_geno_trajectory.py b/tests/tfscreen/plot/test_geno_trajectory.py index 83b22fcb..238427b4 100644 --- a/tests/tfscreen/plot/test_geno_trajectory.py +++ b/tests/tfscreen/plot/test_geno_trajectory.py @@ -1,484 +1,550 @@ """ -Tests for tfscreen.plot.geno_trajectory.plot_geno_trajectory. - -Strategy --------- -The public function has two expensive internal steps: (a) a JAX forward -pass via _compute_map_predictions / _compute_posterior_predictions, and -(b) matplotlib PDF rendering. Both are mocked out here so tests run fast -and without GPU/display dependencies. - -A helper ``_make_fake_orchestrator`` constructs a minimal mock ModelOrchestrator -whose attributes match what plot_geno_trajectory reads. The mock returns -deterministic numpy arrays for all data tensors and a small but realistic -dimension structure (2 replicates, 3 timepoints, 1 condition_pre, -1 condition_sel, 1 titrant, 1 concentration, 2 genotypes). - -The prediction arrays (growth_pred, growth_pred_fine, ln_cfu0_map) are -returned by mocked helpers so no JAX compilation is triggered. -""" +Tests for tfscreen.plot.geno_trajectory. + +plot_geno_trajectory is a pure plotting function — all tests pass synthetic +DataFrames and inspect the returned Figure without requiring JAX or a real +ModelOrchestrator. -import json -import os -import sys -import tempfile -from unittest.mock import MagicMock, patch +predict_and_plot_geno_trajectory requires a live orchestrator and is covered +by smoke tests; only its .npz loading path is tested here via a mock. +""" import numpy as np import pandas as pd import pytest -from tfscreen.plot.geno_trajectory import ( - _compute_posterior_predictions, - plot_geno_trajectory, -) +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +from tfscreen.plot.geno_trajectory import plot_geno_trajectory # --------------------------------------------------------------------------- -# Shared shape constants +# Synthetic DataFrame helpers # --------------------------------------------------------------------------- -N_REP, N_T, N_CP, N_CS, N_TN, N_TC, N_GENO = 2, 3, 1, 1, 1, 1, 2 -SHAPE = (N_REP, N_T, N_CP, N_CS, N_TN, N_TC, N_GENO) -GENO_NAMES = ["WT", "mut1"] -REP_NAMES = ["rep1", "rep2"] -CP_NAMES = ["preinduction"] -CS_NAMES = ["selection"] -TN_NAMES = ["IPTG"] -TC_VALS = [0.0] +def _make_pred_df( + genotypes=("geno_A", "geno_B"), + replicates=("rep1", "rep2"), + conditions=(("pre", "sel", "drug", 0.0), ("pre", "sel", "drug", 1.0)), + t_values=(-5.0, 0.0, 5.0, 10.0, 15.0), + include_ci=True, + include_obs=True, +): + """ + Build a minimal synthetic pred_df. + + Observed data (non-NaN ln_cfu) is placed at t_values that are >= 0. + Fine-grid rows have NaN ln_cfu. Rows at negative t carry ln_cfu0 quantiles + (simulating the pre-selection anchor). + """ + rng = np.random.default_rng(42) + rows = [] + for cp, cs, tn, tc in conditions: + for geno in genotypes: + for rep in replicates: + base = rng.uniform(8.0, 12.0) + slope = rng.uniform(0.1, 0.3) + for t in t_values: + med = base + slope * t + row = { + "replicate": rep, + "condition_pre": cp, + "condition_sel": cs, + "titrant_name": tn, + "titrant_conc": tc, + "genotype": geno, + "t_sel": t, + "ln_cfu": (base + slope * t) if (include_obs and t >= 0) else np.nan, + "ln_cfu_std": 0.2 if (include_obs and t >= 0) else np.nan, + "median": med, + } + if include_ci: + row["q05"] = med - 0.5 + row["q95"] = med + 0.5 + rows.append(row) + return pd.DataFrame(rows) # --------------------------------------------------------------------------- -# Fake ModelOrchestrator +# Return type # --------------------------------------------------------------------------- -def _make_fake_orchestrator(with_presplit=False): - """ - Return a MagicMock that quacks like a ModelOrchestrator for the - attributes accessed by plot_geno_trajectory. - """ - rng = np.random.default_rng(0) - - # All timepoints are valid except the last one for rep 0 genotype 0. - good_mask = np.ones(SHAPE, dtype=bool) - good_mask[0, -1, 0, 0, 0, 0, 0] = False +class TestReturnType: + def test_returns_figure(self): + df = _make_pred_df() + fig = plot_geno_trajectory(df) + assert isinstance(fig, plt.Figure) + plt.close(fig) - t_sel = np.broadcast_to( - np.linspace(0.0, 10.0, N_T)[None, :, None, None, None, None, None], - SHAPE, - ).copy() - t_pre = np.full(SHAPE, 5.0) - ln_cfu = rng.normal(10.0, 1.0, SHAPE) - ln_cfu_std = np.ones(SHAPE) * 0.5 + def test_figure_has_axes(self): + df = _make_pred_df() + fig = plot_geno_trajectory(df) + assert len(fig.axes) > 0 + plt.close(fig) - gd = MagicMock() - gd.good_mask = good_mask - gd.t_sel = t_sel - gd.t_pre = t_pre - gd.ln_cfu = ln_cfu - gd.ln_cfu_std = ln_cfu_std - - # --- presplit --- - presplit = None - if with_presplit: - ps = MagicMock() - ps.ln_cfu_t0 = rng.normal(10.0, 0.5, (N_REP, N_CP, N_GENO)) - ps.ln_cfu_t0_std = np.ones((N_REP, N_CP, N_GENO)) * 0.3 - ps.good_mask = np.ones((N_REP, N_CP, N_GENO), dtype=bool) - presplit = ps - data = MagicMock() - data.growth = gd - data.presplit = presplit - data.num_genotype = N_GENO - - # --- TensorManager --- - # Build a small DataFrame that mirrors what tm.df looks like after - # TensorManager.create_tensors(). Only the columns that plot_geno_trajectory - # accesses are required. - rows = [] - for r_i in range(N_REP): - for t_i in range(N_T): - for g_i in range(N_GENO): - rows.append({ - "replicate_idx": r_i, - "condition_pre_idx": 0, - "condition_sel_idx": 0, - "titrant_name_idx": 0, - "titrant_conc_idx": 0, - "genotype_idx": g_i, - "t_sel": t_sel[r_i, t_i, 0, 0, 0, 0, g_i], - "ln_cfu": ln_cfu[r_i, t_i, 0, 0, 0, 0, g_i], - "genotype": GENO_NAMES[g_i], - "condition_sel": "selection", - "condition_pre": "preinduction", - }) - tm_df = pd.DataFrame(rows) - - tm = MagicMock() - tm.tensor_dim_names = [ - "replicate", "time", "condition_pre", "condition_sel", - "titrant_name", "titrant_conc", "genotype", - ] - tm.tensor_dim_labels = [ - REP_NAMES, [f"t{i}" for i in range(N_T)], - CP_NAMES, CS_NAMES, TN_NAMES, TC_VALS, GENO_NAMES, - ] - tm.df = tm_df - - binding_df = pd.DataFrame({ - "genotype": GENO_NAMES * 2, - "titrant_name": ["IPTG"] * 4, - "titrant_conc": [0.0] * 4, - "theta_obs": [0.5, 0.3, 0.5, 0.3], - }) +# --------------------------------------------------------------------------- +# Subplot count +# --------------------------------------------------------------------------- - orchestrator = MagicMock() - orchestrator.data = data - orchestrator.growth_tm = tm - orchestrator.binding_df = binding_df +class TestSubplotCount: + def test_one_subplot_per_condition_combo(self): + n_conds = 3 + conditions = tuple( + ("pre", "sel", "drug", float(i)) for i in range(n_conds) + ) + df = _make_pred_df(conditions=conditions) + fig = plot_geno_trajectory(df) + visible = [ax for ax in fig.axes if ax.get_visible()] + assert len(visible) == n_conds + plt.close(fig) + + def test_single_condition_one_subplot(self): + df = _make_pred_df(conditions=(("pre", "sel", "drug", 0.0),)) + fig = plot_geno_trajectory(df) + visible = [ax for ax in fig.axes if ax.get_visible()] + assert len(visible) == 1 + plt.close(fig) + + def test_four_conditions_two_rows(self): + conditions = tuple( + ("pre", "sel", "drug", float(i)) for i in range(4) + ) + df = _make_pred_df(conditions=conditions) + fig = plot_geno_trajectory(df) + n_rows = fig.axes[0].get_subplotspec().get_gridspec().nrows + assert n_rows == 2 + plt.close(fig) + + def test_unused_cells_hidden(self): + # 4 conditions → 2×3 grid → 2 hidden cells + conditions = tuple( + ("pre", "sel", "drug", float(i)) for i in range(4) + ) + df = _make_pred_df(conditions=conditions) + fig = plot_geno_trajectory(df) + hidden = [ax for ax in fig.axes if not ax.get_visible()] + assert len(hidden) == 2 + plt.close(fig) - return orchestrator +# --------------------------------------------------------------------------- +# Observed data and predicted lines +# --------------------------------------------------------------------------- -def _fake_map_predictions(shape): - """Return plausible synthetic MAP prediction tensors.""" - rng = np.random.default_rng(1) - growth_pred = rng.normal(10.0, 1.0, shape) - ln_cfu0_map = rng.normal(10.0, 0.5, (shape[0], shape[2], shape[6])) - growth_pred_fine = rng.normal(10.0, 1.0, - (shape[0], 50, shape[2], shape[3], - shape[4], shape[5], shape[6])) - t_fine_1d = np.linspace(0.0, 10.0, 50) - return growth_pred, ln_cfu0_map, growth_pred_fine, t_fine_1d +class TestObservedAndPredicted: + def test_no_crash_all_nan_ln_cfu(self): + df = _make_pred_df() + df["ln_cfu"] = np.nan + df["ln_cfu_std"] = np.nan + fig = plot_geno_trajectory(df) + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_no_crash_all_nan_median(self): + df = _make_pred_df() + df["median"] = np.nan + fig = plot_geno_trajectory(df) + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_axvline_at_zero_present(self): + df = _make_pred_df(conditions=(("pre", "sel", "drug", 0.0),)) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + # The axvline draws a vertical line; its x-data is (0, 0). + vlines = [l for l in ax.lines if list(l.get_xdata()) == [0.0, 0.0]] + assert len(vlines) >= 1 + plt.close(fig) + + def test_predicted_lines_drawn(self): + """Each (genotype, replicate) pair should produce at least one line.""" + n_geno = 2 + n_rep = 2 + genotypes = tuple(f"g{i}" for i in range(n_geno)) + replicates = tuple(f"r{i}" for i in range(n_rep)) + df = _make_pred_df( + genotypes=genotypes, + replicates=replicates, + conditions=(("pre", "sel", "drug", 0.0),), + include_ci=False, + ) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + # axvline + one line per (geno, rep) pair + assert len(ax.lines) >= n_geno * n_rep + 1 + plt.close(fig) # --------------------------------------------------------------------------- -# Argument validation +# Credible interval # --------------------------------------------------------------------------- -class TestArgumentValidation: - def test_raises_when_neither_provided(self, tmp_path): - orchestrator = _make_fake_orchestrator() - with pytest.raises(ValueError, match="Exactly one"): - plot_geno_trajectory(orchestrator, str(tmp_path / "out")) - - def test_raises_when_both_provided(self, tmp_path): - orchestrator = _make_fake_orchestrator() - with pytest.raises(ValueError, match="Exactly one"): - plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - posterior_file="some_file.h5", - ) +class TestCredibleInterval: + def test_ci_band_present_when_columns_exist(self): + df = _make_pred_df(include_ci=True) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + # fill_between produces PolyCollection objects + assert len(ax.collections) > 0 + plt.close(fig) + + def test_no_ci_band_without_columns(self): + df = _make_pred_df(include_ci=False) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + # fill_between produces PolyCollection; errorbar may add LineCollections. + # Only PolyCollections indicate a CI band. + from matplotlib.collections import PolyCollection + poly_cols = [c for c in ax.collections if isinstance(c, PolyCollection)] + assert len(poly_cols) == 0 + plt.close(fig) + + def test_ci_spans_negative_t(self): + """CI band should cover pre-selection (t < 0) anchor rows.""" + df = _make_pred_df( + conditions=(("pre", "sel", "drug", 0.0),), + genotypes=("g0",), + replicates=("r0",), + t_values=(-5.0, 0.0, 5.0), + include_ci=True, + ) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + polys = ax.collections + assert len(polys) > 0 + # The polygon should include x-values less than zero + x_vals = np.concatenate([p.get_paths()[0].vertices[:, 0] for p in polys]) + assert np.any(x_vals < 0) + plt.close(fig) # --------------------------------------------------------------------------- -# MAP path — basic smoke test with mocked internals +# Legend # --------------------------------------------------------------------------- -class TestMapPath: - @pytest.fixture - def patched_plot(self, tmp_path): - """Patch _compute_map_predictions and matplotlib.pyplot.savefig.""" - orchestrator = _make_fake_orchestrator() - fake_preds = _fake_map_predictions(SHAPE) - - with patch( - "tfscreen.plot.geno_trajectory._compute_map_predictions", - return_value=fake_preds, - ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): - yield orchestrator, tmp_path - - def test_returns_dataframe(self, patched_plot): - orchestrator, tmp_path = patched_plot - result = plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - ) - assert isinstance(result, pd.DataFrame) - - def test_df_has_ln_cfu_pred(self, patched_plot): - orchestrator, tmp_path = patched_plot - result = plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - ) - assert "ln_cfu_pred" in result.columns - - def test_csv_written_by_default(self, patched_plot): - orchestrator, tmp_path = patched_plot - plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - ) - assert os.path.exists(tmp_path / "out_calib_growth_df.csv") - - def test_csv_not_written_when_disabled(self, patched_plot): - orchestrator, tmp_path = patched_plot - plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - write_csv=False, +class TestLegend: + def test_legend_contains_genotype_replicate(self): + df = _make_pred_df( + genotypes=("WT", "mut1"), + replicates=("rep1",), + conditions=(("pre", "sel", "drug", 0.0),), ) - assert not os.path.exists(tmp_path / "out_calib_growth_df.csv") - - def test_growth_pred_std_in_csv(self, patched_plot): - orchestrator, tmp_path = patched_plot - gps = np.ones(SHAPE) * 0.1 - plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - growth_pred_std=gps, + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + legend_texts = [t.get_text() for t in ax.get_legend().get_texts()] + assert any("WT" in t for t in legend_texts) + assert any("mut1" in t for t in legend_texts) + plt.close(fig) + + def test_legend_includes_replicate_label(self): + df = _make_pred_df( + genotypes=("WT",), + replicates=("rep1", "rep2"), + conditions=(("pre", "sel", "drug", 0.0),), ) - df = pd.read_csv(tmp_path / "out_calib_growth_df.csv") - assert "ln_cfu_pred_std" in df.columns + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + legend_texts = [t.get_text() for t in ax.get_legend().get_texts()] + assert any("rep1" in t for t in legend_texts) + assert any("rep2" in t for t in legend_texts) + plt.close(fig) # --------------------------------------------------------------------------- -# Genotype filtering +# Styling overrides # --------------------------------------------------------------------------- -class TestGenotypeFilter: - @pytest.fixture - def orchestrator_and_path(self, tmp_path): - orchestrator = _make_fake_orchestrator() - return orchestrator, tmp_path - - def _run(self, orchestrator, tmp_path, **kwargs): - fake_preds = _fake_map_predictions(SHAPE) - with patch( - "tfscreen.plot.geno_trajectory._compute_map_predictions", - return_value=fake_preds, - ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): - return plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - **kwargs, - ) - - def test_no_filter_plots_all_genotypes(self, orchestrator_and_path, capsys): - orchestrator, tmp_path = orchestrator_and_path - self._run(orchestrator, tmp_path) - captured = capsys.readouterr() - for name in GENO_NAMES: - assert name in captured.out - - def test_genotype_subset(self, orchestrator_and_path, capsys): - orchestrator, tmp_path = orchestrator_and_path - self._run(orchestrator, tmp_path, genotypes=["WT"]) - captured = capsys.readouterr() - assert "WT" in captured.out - assert "mut1" not in captured.out - - def test_unknown_genotype_warns(self, orchestrator_and_path, capsys): - orchestrator, tmp_path = orchestrator_and_path - self._run(orchestrator, tmp_path, genotypes=["WT", "nonexistent"]) - captured = capsys.readouterr() - assert "nonexistent" in captured.err - - def test_all_unknown_returns_df(self, orchestrator_and_path): - orchestrator, tmp_path = orchestrator_and_path - result = self._run(orchestrator, tmp_path, genotypes=["nonexistent"]) - # Should still return a DataFrame (from the full data), just no PDFs. - assert isinstance(result, pd.DataFrame) +class TestStylingOverrides: + def test_custom_figsize(self): + df = _make_pred_df(conditions=(("pre", "sel", "drug", 0.0),)) + fig = plot_geno_trajectory(df, figsize=(10, 8)) + w, h = fig.get_size_inches() + assert pytest.approx(w) == 10 + assert pytest.approx(h) == 8 + plt.close(fig) + + def test_default_figsize_scales_with_columns(self): + # 3 conditions → n_cols=3 → width=15 + conditions = tuple( + ("pre", "sel", "drug", float(i)) for i in range(3) + ) + df = _make_pred_df(conditions=conditions) + fig = plot_geno_trajectory(df) + w, _ = fig.get_size_inches() + assert pytest.approx(w) == 15 + plt.close(fig) + + def test_custom_colors_accepted(self): + df = _make_pred_df(genotypes=("g0", "g1")) + fig = plot_geno_trajectory(df, colors=["#ff0000", "#0000ff"]) + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_custom_markers_accepted(self): + df = _make_pred_df(replicates=("r0", "r1")) + fig = plot_geno_trajectory(df, markers=["o", "s"]) + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_genotype_replicate_pairs_get_distinct_colors(self): + """Each (genotype, replicate) pair should get a distinct color.""" + df = _make_pred_df( + genotypes=("g0", "g1"), + replicates=("r0", "r1"), + conditions=(("pre", "sel", "drug", 0.0),), + include_ci=False, + ) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + # Predicted lines are drawn with lw=1.8; exclude axvline + series_lines = [l for l in ax.lines if l.get_linewidth() == pytest.approx(1.8)] + colors_used = {l.get_color() for l in series_lines} + # 2 genotypes × 2 replicates = 4 pairs → 4 distinct colors + assert len(colors_used) == 4 + plt.close(fig) + + def test_same_genotype_different_replicates_different_colors(self): + """Two replicates of the same genotype must get different colors.""" + df = _make_pred_df( + genotypes=("g0",), + replicates=("r0", "r1"), + conditions=(("pre", "sel", "drug", 0.0),), + include_ci=False, + ) + fig = plot_geno_trajectory(df) + ax = next(ax for ax in fig.axes if ax.get_visible()) + series_lines = [l for l in ax.lines if l.get_linewidth() == pytest.approx(1.8)] + colors_used = {l.get_color() for l in series_lines} + assert len(colors_used) == 2 + plt.close(fig) # --------------------------------------------------------------------------- -# Titrant_name filtering +# predict_and_plot_geno_trajectory — .npz loading path (mocked) # --------------------------------------------------------------------------- -class TestTitrantNameFilter: - def _run(self, orchestrator, tmp_path, **kwargs): - fake_preds = _fake_map_predictions(SHAPE) - with patch( - "tfscreen.plot.geno_trajectory._compute_map_predictions", - return_value=fake_preds, - ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): - return plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - **kwargs, - ) - - def test_matching_titrant_produces_output(self, tmp_path, capsys): - orchestrator = _make_fake_orchestrator() - self._run(orchestrator, tmp_path, titrant_names=["IPTG"]) - captured = capsys.readouterr() - # PDFs should still be saved (the filter matches). - assert "Saved" in captured.out - - def test_nonmatching_titrant_no_pdfs(self, tmp_path, capsys): - orchestrator = _make_fake_orchestrator() - self._run(orchestrator, tmp_path, titrant_names=["arabinose"]) - captured = capsys.readouterr() - # No valid condition combos → no PDF files saved (the CSV may still be). - assert ".pdf" not in captured.out +class TestPredictAndPlotNpzLoading: + def test_npz_path_converted_to_dict(self, tmp_path): + """Passing a .npz path loads the file before calling predict().""" + from unittest.mock import MagicMock, patch + import matplotlib + matplotlib.use("Agg") + npz_path = str(tmp_path / "params.npz") + np.savez(npz_path, x_auto_loc=np.array(1.0)) -# --------------------------------------------------------------------------- -# Presplit overlay -# --------------------------------------------------------------------------- + # Build a minimal fake orchestrator + gd = MagicMock() + gd.good_mask = np.ones((1, 3, 1, 1, 1, 1, 1), dtype=bool) + gd.t_sel = np.broadcast_to( + np.array([0.0, 5.0, 10.0])[None, :, None, None, None, None, None], + (1, 3, 1, 1, 1, 1, 1), + ).copy() + data = MagicMock() + data.growth = gd + + orchestrator = MagicMock() + orchestrator.data = data + orchestrator.presplit_df = None + + # Synthetic fine_df / ln_cfu0_df to return from mocked predict() + fine_df = pd.DataFrame({ + "replicate": ["r0"] * 5, + "condition_pre": ["pre"] * 5, + "condition_sel": ["sel"] * 5, + "titrant_name": ["drug"] * 5, + "titrant_conc": [0.0] * 5, + "genotype": ["g0"] * 5, + "t_sel": np.linspace(0, 10, 5), + "ln_cfu": [np.nan] * 5, + "ln_cfu_std": [np.nan] * 5, + "q05": np.linspace(8, 12, 5) - 0.5, + "median": np.linspace(8, 12, 5), + "q95": np.linspace(8, 12, 5) + 0.5, + }) + orchestrator.growth_df = pd.DataFrame({ + "replicate": ["r0"], + "condition_pre": ["pre"], + "condition_sel": ["sel"], + "titrant_name": ["drug"], + "titrant_conc": [0.0], + "genotype": ["g0"], + "t_pre": [5.0], + "t_sel": [5.0], + "ln_cfu": [10.0], + "ln_cfu_std": [0.2], + }) + + captured_params = {} + + def _mock_predict(orch, params, predict_sites, **kwargs): + captured_params["value"] = params + if len(predict_sites) > 1: + ln0 = pd.DataFrame({ + "replicate": ["r0"], + "condition_pre": ["pre"], + "genotype": ["g0"], + "q05": [9.5], + "median": [10.0], + "q95": [10.5], + }) + return {"growth_pred": fine_df, "ln_cfu0": ln0} + return fine_df -class TestPresplit: - def _run(self, orchestrator, tmp_path): - fake_preds = _fake_map_predictions(SHAPE) - ax_mock = MagicMock() - # Capture all errorbar calls to verify presplit is rendered. - ax_mock.errorbar = MagicMock() - ax_mock.plot = MagicMock() - ax_mock.legend = MagicMock() - ax_mock.set_title = MagicMock() - ax_mock.set_xlabel = MagicMock() - ax_mock.set_ylabel = MagicMock() - ax_mock.axvline = MagicMock() - ax_mock.set_xlim = MagicMock() - ax_mock.set_visible = MagicMock() - - axes_array = np.empty((1, 1), dtype=object) - axes_array[0, 0] = ax_mock - - with patch( - "tfscreen.plot.geno_trajectory._compute_map_predictions", - return_value=fake_preds, - ), patch( - "matplotlib.pyplot.subplots", - return_value=(MagicMock(), axes_array), - ), patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): - plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - params={"x_auto_loc": np.array(1.0)}, - ) - return ax_mock - - @staticmethod - def _is_scalar_negative(x): - """True iff x is a 0-d value strictly less than zero. - - The presplit errorbar passes a scalar (−t_pre). Selection-phase - errorbars pass an array of t_sel values. Using ndim prevents - float() from raising on multi-element arrays. - """ - a = np.asarray(x) - return a.ndim == 0 and float(a) < 0 - - def test_presplit_errorbar_called_when_data_present(self, tmp_path): - orchestrator = _make_fake_orchestrator(with_presplit=True) - ax_mock = self._run(orchestrator, tmp_path) - # The presplit errorbar is called with a scalar negative x (−t_pre); - # selection-phase errorbars use array x values. - neg_x_calls = [ - c for c in ax_mock.errorbar.call_args_list - if c.args and self._is_scalar_negative(c.args[0]) - ] - assert len(neg_x_calls) > 0, ( - "Expected at least one scalar-negative-x errorbar call (presplit) " - f"but got: {ax_mock.errorbar.call_args_list}" - ) + from tfscreen.plot import geno_trajectory + with patch.object(geno_trajectory, "predict_and_plot_geno_trajectory", + wraps=geno_trajectory.predict_and_plot_geno_trajectory): + with patch("tfscreen.tfmodel.analysis.prediction.predict", + side_effect=_mock_predict): + from tfscreen.plot.geno_trajectory import predict_and_plot_geno_trajectory + fig = predict_and_plot_geno_trajectory(orchestrator, npz_path) - def test_no_presplit_errorbar_at_negative_x(self, tmp_path): - orchestrator = _make_fake_orchestrator(with_presplit=False) - ax_mock = self._run(orchestrator, tmp_path) - neg_x_calls = [ - c for c in ax_mock.errorbar.call_args_list - if c.args and self._is_scalar_negative(c.args[0]) - ] - assert len(neg_x_calls) == 0 + assert isinstance(captured_params["value"], dict), \ + "npz path should be loaded to a dict before predict() is called" + assert "x_auto_loc" in captured_params["value"] + plt.close(fig) # --------------------------------------------------------------------------- -# Posterior path +# presplit_df overlay # --------------------------------------------------------------------------- -class TestPosteriorPath: - def _make_h5(self, tmp_path): - """Write a minimal synthetic posterior HDF5 file.""" - import h5py - - h5_path = str(tmp_path / "posterior.h5") - n_samples = 5 - with h5py.File(h5_path, "w") as fh: - fh.create_dataset( - "growth_pred", - data=np.random.default_rng(2).normal(10.0, 1.0, - (n_samples,) + SHAPE), - ) - fh.create_dataset( - "ln_cfu0", - data=np.random.default_rng(3).normal(10.0, 0.5, - (n_samples, N_REP, - N_CP, N_GENO)), - ) - return h5_path - - def test_compute_posterior_predictions_shapes(self, tmp_path): - h5_path = self._make_h5(tmp_path) - orchestrator = _make_fake_orchestrator() - med, lo, hi, ln_cfu0 = _compute_posterior_predictions(orchestrator, h5_path) - assert med.shape == SHAPE - assert lo.shape == SHAPE - assert hi.shape == SHAPE - assert ln_cfu0.shape == (N_REP, N_CP, N_GENO) - - def test_compute_posterior_predictions_ordering(self, tmp_path): - """5th percentile ≤ median ≤ 95th percentile everywhere.""" - h5_path = self._make_h5(tmp_path) - orchestrator = _make_fake_orchestrator() - med, lo, hi, _ = _compute_posterior_predictions(orchestrator, h5_path) - assert np.all(lo <= med + 1e-9) - assert np.all(med <= hi + 1e-9) - - def test_posterior_path_returns_df_with_lo_hi(self, tmp_path): - h5_path = self._make_h5(tmp_path) - orchestrator = _make_fake_orchestrator() - with patch("matplotlib.pyplot.savefig"), patch("matplotlib.pyplot.close"): - result = plot_geno_trajectory( - orchestrator, - str(tmp_path / "out"), - posterior_file=h5_path, - ) - assert isinstance(result, pd.DataFrame) - assert "ln_cfu_pred_lo" in result.columns - assert "ln_cfu_pred_hi" in result.columns - - def test_posterior_raises_on_missing_growth_pred(self, tmp_path): - import h5py - - h5_path = str(tmp_path / "empty.h5") - with h5py.File(h5_path, "w") as fh: - fh.create_dataset("something_else", data=np.zeros(3)) - - orchestrator = _make_fake_orchestrator() - with pytest.raises(ValueError, match="growth_pred not found"): - _compute_posterior_predictions(orchestrator, h5_path) - - def test_params_path_loads_npz(self, tmp_path): - """Passing a string path to params loads the .npz file.""" - npz_path = str(tmp_path / "params.npz") - np.savez(npz_path, x_auto_loc=np.array(1.0)) +def _make_mock_orchestrator_with_presplit(presplit_df=None): + """Return a minimal mock orchestrator for predict_and_plot tests.""" + from unittest.mock import MagicMock - fake_preds = _fake_map_predictions(SHAPE) - orchestrator = _make_fake_orchestrator() - - with patch( - "tfscreen.plot.geno_trajectory._compute_map_predictions", - return_value=fake_preds, - ) as mock_fn, patch("matplotlib.pyplot.savefig"), patch( - "matplotlib.pyplot.close" - ): - plot_geno_trajectory(orchestrator, str(tmp_path / "out"), params=npz_path) - - # The helper should have been called with a dict (loaded from npz), - # not the raw string path. - called_params = mock_fn.call_args[0][1] - assert isinstance(called_params, dict) - assert "x_auto_loc" in called_params + gd = MagicMock() + gd.good_mask = np.ones((1, 3, 1, 1, 1, 1, 1), dtype=bool) + gd.t_sel = np.broadcast_to( + np.array([0.0, 5.0, 10.0])[None, :, None, None, None, None, None], + (1, 3, 1, 1, 1, 1, 1), + ).copy() + data = MagicMock() + data.growth = gd + + orch = MagicMock() + orch.data = data + orch.growth_df = pd.DataFrame({ + "replicate": ["r0"], + "condition_pre": ["pre"], + "condition_sel": ["sel"], + "titrant_name": ["drug"], + "titrant_conc": [0.0], + "genotype": ["g0"], + "t_pre": [5.0], + "t_sel": [5.0], + "ln_cfu": [10.0], + "ln_cfu_std": [0.2], + }) + orch.presplit_df = presplit_df + return orch + + +def _mock_predict_two_sites(orch, params, predict_sites, **kwargs): + fine_df = pd.DataFrame({ + "replicate": ["r0"] * 5, + "condition_pre": ["pre"] * 5, + "condition_sel": ["sel"] * 5, + "titrant_name": ["drug"] * 5, + "titrant_conc": [0.0] * 5, + "genotype": ["g0"] * 5, + "t_sel": np.linspace(0, 10, 5), + "ln_cfu": [np.nan] * 5, + "ln_cfu_std": [np.nan] * 5, + "q05": np.linspace(8, 12, 5) - 0.5, + "median": np.linspace(8, 12, 5), + "q95": np.linspace(8, 12, 5) + 0.5, + }) + ln0 = pd.DataFrame({ + "replicate": ["r0"], + "condition_pre": ["pre"], + "genotype": ["g0"], + "q05": [9.5], + "median": [10.0], + "q95": [10.5], + }) + return {"growth_pred": fine_df, "ln_cfu0": ln0} + + +class TestPresplitOverlay: + def test_no_presplit_anchor_has_nan_ln_cfu(self): + """Without presplit_df the anchor row has NaN ln_cfu.""" + from unittest.mock import patch + orch = _make_mock_orchestrator_with_presplit(presplit_df=None) + + with patch("tfscreen.tfmodel.analysis.prediction.predict", + side_effect=_mock_predict_two_sites): + from tfscreen.plot.geno_trajectory import predict_and_plot_geno_trajectory + fig = predict_and_plot_geno_trajectory(orch, {"dummy": np.array(1.0)}) + + ax = next(a for a in fig.axes if a.get_visible()) + # Only observed data plotted via errorbar comes from non-NaN ln_cfu. + # With no presplit and obs rows at t_sel>=0 only, t<0 should have no errorbar. + obs_lines_neg = [ + l for l in ax.lines + if l.get_linewidth() != pytest.approx(1.8) # exclude predicted lines + and list(l.get_xdata()) != [0.0, 0.0] # exclude axvline + and any(x < 0 for x in l.get_xdata()) + ] + assert len(obs_lines_neg) == 0 + plt.close(fig) + + def test_presplit_anchor_shows_observed_point(self): + """With presplit_df the anchor row has observed ln_cfu rendered as an errorbar.""" + from unittest.mock import patch + presplit = pd.DataFrame({ + "replicate": ["r0"], + "condition_pre": ["pre"], + "genotype": ["g0"], + "ln_cfu": [9.8], + "ln_cfu_std": [0.15], + }) + orch = _make_mock_orchestrator_with_presplit(presplit_df=presplit) + + with patch("tfscreen.tfmodel.analysis.prediction.predict", + side_effect=_mock_predict_two_sites): + from tfscreen.plot.geno_trajectory import predict_and_plot_geno_trajectory + fig = predict_and_plot_geno_trajectory(orch, {"dummy": np.array(1.0)}) + + ax = next(a for a in fig.axes if a.get_visible()) + # errorbar lines at negative t should exist now + obs_lines_neg = [ + l for l in ax.lines + if l.get_linewidth() != pytest.approx(1.8) + and list(l.get_xdata()) != [0.0, 0.0] + and len(l.get_xdata()) > 0 + and any(x < 0 for x in l.get_xdata()) + ] + assert len(obs_lines_neg) > 0 + plt.close(fig) + + def test_presplit_unmatched_genotype_stays_nan(self): + """presplit row for a different genotype does not affect anchor ln_cfu.""" + from unittest.mock import patch + presplit = pd.DataFrame({ + "replicate": ["r0"], + "condition_pre": ["pre"], + "genotype": ["other_geno"], # not "g0" + "ln_cfu": [9.8], + "ln_cfu_std": [0.15], + }) + orch = _make_mock_orchestrator_with_presplit(presplit_df=presplit) + + with patch("tfscreen.tfmodel.analysis.prediction.predict", + side_effect=_mock_predict_two_sites): + from tfscreen.plot.geno_trajectory import predict_and_plot_geno_trajectory + fig = predict_and_plot_geno_trajectory(orch, {"dummy": np.array(1.0)}) + + ax = next(a for a in fig.axes if a.get_visible()) + obs_lines_neg = [ + l for l in ax.lines + if l.get_linewidth() != pytest.approx(1.8) + and list(l.get_xdata()) != [0.0, 0.0] + and len(l.get_xdata()) > 0 + and any(x < 0 for x in l.get_xdata()) + ] + assert len(obs_lines_neg) == 0 + plt.close(fig) diff --git a/tests/tfscreen/simulate/test_library_prediction.py b/tests/tfscreen/simulate/test_library_prediction.py index c6bc5d74..31b8cdfe 100644 --- a/tests/tfscreen/simulate/test_library_prediction.py +++ b/tests/tfscreen/simulate/test_library_prediction.py @@ -86,3 +86,93 @@ def test_library_prediction_success(mocker, mock_config): def test_library_prediction_config_error(): with pytest.raises(FileNotFoundError): library_prediction("nonexistent_config.yaml") + + +def test_library_prediction_dk_geno_zero_passes_flag(mocker, mock_config): + """dk_geno_zero=True in config must be forwarded to thermo_to_growth.""" + config = {**mock_config, "dk_geno_zero": True} + mocker.patch("tfscreen.util.read_yaml", return_value=config) + mocker.patch( + "tfscreen.simulate.library_prediction.library_manager.LibraryManager" + ).return_value.build_library_df.return_value = pd.DataFrame({"genotype": ["wt", "M1"]}) + mocker.patch( + "tfscreen.simulate.library_prediction.build_sample_dataframes", + return_value=pd.DataFrame({"titrant_conc": [0.0]}), + ) + mocker.patch("tfscreen.simulate.library_prediction.build_sim_data", return_value=MagicMock()) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + + mock_thermo = mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + return_value=( + pd.DataFrame({"theta": [0.5]}), + pd.DataFrame({"genotype": ["wt"]}), + pd.DataFrame({"genotype": ["wt"], "dk_geno": [0.0], "activity": [1.0]}), + ), + ) + + library_prediction(cf="config.yaml") + + _, kwargs = mock_thermo.call_args + assert kwargs["dk_geno_zero"] is True + + +def test_library_prediction_dk_geno_zero_makes_hyper_params_optional(mocker): + """When dk_geno_zero=True the three dk_geno_hyper_* keys may be absent.""" + config = { + "condition_blocks": [{"some": "block"}], + "theta_component": "mock_theta", + "theta_rng_seed": 0, + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + "dk_geno_zero": True, + # dk_geno_hyper_* intentionally absent + } + mocker.patch("tfscreen.util.read_yaml", return_value=config) + mocker.patch( + "tfscreen.simulate.library_prediction.library_manager.LibraryManager" + ).return_value.build_library_df.return_value = pd.DataFrame({"genotype": ["wt"]}) + mocker.patch( + "tfscreen.simulate.library_prediction.build_sample_dataframes", + return_value=pd.DataFrame({"titrant_conc": [0.0]}), + ) + mocker.patch("tfscreen.simulate.library_prediction.build_sim_data", return_value=MagicMock()) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + return_value=( + pd.DataFrame({"theta": [0.5]}), + pd.DataFrame({"genotype": ["wt"]}), + pd.DataFrame({"genotype": ["wt"], "dk_geno": [0.0], "activity": [1.0]}), + ), + ) + + # Must not raise KeyError despite missing dk_geno_hyper_* keys + library_prediction(cf="config.yaml") + + +def test_library_prediction_missing_hyper_params_raises_without_dk_geno_zero(mocker): + """Without dk_geno_zero, missing dk_geno_hyper_* must raise KeyError.""" + config = { + "condition_blocks": [{"some": "block"}], + "theta_component": "mock_theta", + "theta_rng_seed": 0, + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + # dk_geno_hyper_* absent and dk_geno_zero not set + } + mocker.patch("tfscreen.util.read_yaml", return_value=config) + mocker.patch( + "tfscreen.simulate.library_prediction.library_manager.LibraryManager" + ).return_value.build_library_df.return_value = pd.DataFrame({"genotype": ["wt"]}) + mocker.patch( + "tfscreen.simulate.library_prediction.build_sample_dataframes", + return_value=pd.DataFrame({"titrant_conc": [0.0]}), + ) + mocker.patch("tfscreen.simulate.library_prediction.build_sim_data", return_value=MagicMock()) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + + with pytest.raises(KeyError): + library_prediction(cf="config.yaml") diff --git a/tests/tfscreen/simulate/test_thermo_to_growth.py b/tests/tfscreen/simulate/test_thermo_to_growth.py index 31a6808a..e83b3954 100644 --- a/tests/tfscreen/simulate/test_thermo_to_growth.py +++ b/tests/tfscreen/simulate/test_thermo_to_growth.py @@ -143,6 +143,28 @@ def test_assign_dk_geno_distribution(rng): assert (result <= 0.02).all() +def test_assign_dk_geno_fixed_value_zero(): + genotypes = ["wt", "A1B", "C2D", "A1B/C2D"] + result = _assign_dk_geno(genotypes, fixed_value=0.0) + assert isinstance(result, pd.Series) + assert set(result.index) == set(genotypes) + np.testing.assert_array_equal(result.values, 0.0) + + +def test_assign_dk_geno_fixed_value_nonzero(): + genotypes = ["wt", "A1B", "C2D"] + result = _assign_dk_geno(genotypes, fixed_value=-0.05) + np.testing.assert_array_equal(result.values, -0.05) + + +def test_assign_dk_geno_fixed_value_skips_rng(): + """fixed_value must not call the RNG — result is identical regardless of seed.""" + genotypes = ["wt", "A1B", "C2D"] + r1 = _assign_dk_geno(genotypes, fixed_value=0.0, rng=np.random.default_rng(1)) + r2 = _assign_dk_geno(genotypes, fixed_value=0.0, rng=np.random.default_rng(2)) + np.testing.assert_array_equal(r1.values, r2.values) + + # ---------------------------------------------------------------------------- # test _apply_growth_params # ---------------------------------------------------------------------------- @@ -434,6 +456,83 @@ def test_thermo_to_growth_propagates_rng( assert passed_rng is rng +def test_thermo_to_growth_dk_geno_zero_sets_all_to_zero( + mocker, test_genotypes, test_sample_df, simple_growth_params +): + """dk_geno_zero=True must set dk_geno=0 for every genotype.""" + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.5) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + phenotype_df, _, parameters_df = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + dk_geno_zero=True, + ) + + np.testing.assert_array_equal(phenotype_df["dk_geno"].values, 0.0) + np.testing.assert_array_equal(parameters_df["dk_geno"].values, 0.0) + + +def test_thermo_to_growth_dk_geno_zero_passes_fixed_value( + mocker, test_genotypes, test_sample_df, simple_growth_params +): + """dk_geno_zero=True must pass fixed_value=0.0 to _assign_dk_geno.""" + concs = np.array([10.0, 100.0]) + theta_gc = np.zeros((3, 2)) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + mock_assign_dk = mocker.patch( + "tfscreen.simulate.thermo_to_growth._assign_dk_geno", + return_value=pd.Series({"wt": 0.0, "A1B": 0.0, "A1B/C2D": 0.0}), + ) + + thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + dk_geno_zero=True, + ) + + assert mock_assign_dk.call_args.kwargs.get("fixed_value") == 0.0 + + +def test_thermo_to_growth_dk_geno_zero_false_passes_none( + mocker, test_genotypes, test_sample_df, simple_growth_params +): + """dk_geno_zero=False (default) must pass fixed_value=None to _assign_dk_geno.""" + concs = np.array([10.0, 100.0]) + theta_gc = np.zeros((3, 2)) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + mock_assign_dk = mocker.patch( + "tfscreen.simulate.thermo_to_growth._assign_dk_geno", + return_value=pd.Series({"wt": 0.0, "A1B": 0.0, "A1B/C2D": 0.0}), + ) + + thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + dk_geno_zero=False, + ) + + assert mock_assign_dk.call_args.kwargs.get("fixed_value") is None + + # ============================================================================ # test _sample_horseshoe_activity # ============================================================================ diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py index ac2d5f9e..9331351b 100644 --- a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py +++ b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py @@ -95,8 +95,9 @@ class _FakeDkGeno: @fstruct.dataclass class _FakeLnCfu0: - ln_cfu0_hyper_loc_loc: float = 5.0 - ln_cfu0_hyper_scale_loc: float = 1.0 + # Array fields (one element per library class) matching the real ModelPriors. + ln_cfu0_hyper_loc_locs: object = None + ln_cfu0_hyper_scale_locs: object = None pinned: dict = fstruct.field(default_factory=dict, pytree_node=False) @@ -128,7 +129,10 @@ def _make_fake_priors(gt_has_pinned=True): growth_transition=gt, activity=_FakeActivity(), dk_geno=_FakeDkGeno(), - ln_cfu0=_FakeLnCfu0(), + ln_cfu0=_FakeLnCfu0( + ln_cfu0_hyper_loc_locs=jnp.array([5.0]), + ln_cfu0_hyper_scale_locs=jnp.array([1.0]), + ), ), theta=_FakeTheta(), ) @@ -760,12 +764,41 @@ def test_pins_activity_hyperparams_to_prior_locs(self): for suffix, _ in _PINNED_COMPONENTS["activity"]: assert suffix in pinned - def test_pins_ln_cfu0_hyperparams(self): + def test_pins_ln_cfu0_hyperparams_single_class(self): + # Single-class case: array fields of length 1 must produce + # "hyper_loc_0" and "hyper_scale_0" in the pinned dict, matching + # the site suffix "{name}_hyper_loc_0" used by define_model in + # the hierarchical ln_cfu0 component. orchestrator_cal, orchestrator_prod = self._make_models() _inject_calibration_priors(orchestrator_cal, orchestrator_prod, np.array([[0.5]])) pinned = orchestrator_cal._priors.growth.ln_cfu0.pinned - for suffix, _ in _PINNED_COMPONENTS["ln_cfu0"]: - assert suffix in pinned + assert "hyper_loc_0" in pinned, f"pinned keys: {list(pinned)}" + assert "hyper_scale_0" in pinned, f"pinned keys: {list(pinned)}" + assert pinned["hyper_loc_0"] == pytest.approx(5.0) + assert pinned["hyper_scale_0"] == pytest.approx(1.0) + + def test_pins_ln_cfu0_hyperparams_multi_class(self): + # Multi-class case: 2-element arrays must produce per-class entries + # "hyper_loc_0", "hyper_loc_1", "hyper_scale_0", "hyper_scale_1". + orchestrator_cal, orchestrator_prod = self._make_models() + # Replace the ln_cfu0 component in orchestrator_cal.priors (the + # MagicMock attribute that _inject_calibration_priors reads). + two_class_ln_cfu0 = _FakeLnCfu0( + ln_cfu0_hyper_loc_locs=jnp.array([5.0, 7.0]), + ln_cfu0_hyper_scale_locs=jnp.array([1.0, 2.0]), + ) + cal_priors = orchestrator_cal.priors + new_growth = cal_priors.growth.replace(ln_cfu0=two_class_ln_cfu0) + orchestrator_cal.priors = cal_priors.replace(growth=new_growth) + + _inject_calibration_priors(orchestrator_cal, orchestrator_prod, np.array([[0.5]])) + pinned = orchestrator_cal._priors.growth.ln_cfu0.pinned + for key, expected in [ + ("hyper_loc_0", 5.0), ("hyper_loc_1", 7.0), + ("hyper_scale_0", 1.0), ("hyper_scale_1", 2.0), + ]: + assert key in pinned, f"missing '{key}'; pinned keys: {list(pinned)}" + assert pinned[key] == pytest.approx(expected) def test_dk_geno_not_in_pinned_components(self): # dk_geno uses the fixed component during calibration (all zeros). @@ -1399,9 +1432,79 @@ def fake_import(name, *args, **kwargs): _make_correlation_plot(df, str(tmp_path / "run")) assert not (tmp_path / "run_calib_correlation.pdf").exists() + # ----------------------------------------------------------------- + # Helpers shared by the _run_calibration_diagnostics tests below. + # ----------------------------------------------------------------- + + @staticmethod + def _make_fake_predict_dfs(genotypes=("gA", "gB"), n_t=3): + """Return (all_dfs, orchestrator_mock) for diagnostics tests.""" + rng = np.random.default_rng(0) + rows = [] + for i, g in enumerate(genotypes): + for t_i, t in enumerate(np.linspace(0, 10, n_t)): + rows.append({ + "replicate": "r1", + "condition_pre": "cp1", + "condition_sel": "cs1", + "titrant_name": "tn1", + "titrant_conc": 0.0, + "genotype": g, + "t_sel": t, + "ln_cfu": rng.uniform(8, 12), + "ln_cfu_std": 0.2, + "q05": rng.uniform(7, 9), + "median": rng.uniform(9, 11), + "q95": rng.uniform(11, 13), + "replicate_idx": 0, + "time_idx": t_i, + "condition_pre_idx": 0, + "condition_sel_idx": 0, + "titrant_name_idx": 0, + "titrant_conc_idx": 0, + "genotype_idx": i, + }) + + pred_df = pd.DataFrame(rows) + + ln_cfu0_rows = [] + for i, g in enumerate(genotypes): + ln_cfu0_rows.append({ + "replicate": "r1", + "condition_pre": "cp1", + "genotype": g, + "q05": 8.5, + "median": 9.0, + "q95": 9.5, + }) + ln_cfu0_df = pd.DataFrame(ln_cfu0_rows) + + all_dfs = {"growth_pred": pred_df, "ln_cfu0": ln_cfu0_df} + + orch = MagicMock() + orch.growth_df = pd.DataFrame({ + "replicate": ["r1"] * len(genotypes), + "condition_pre": ["cp1"] * len(genotypes), + "condition_sel": ["cs1"] * len(genotypes), + "titrant_name": ["tn1"] * len(genotypes), + "titrant_conc": [0.0] * len(genotypes), + "genotype": list(genotypes), + "t_pre": [5.0] * len(genotypes), + "t_sel": [10.0] * len(genotypes), + "ln_cfu": [10.0] * len(genotypes), + "ln_cfu_std": [0.2] * len(genotypes), + }) + orch.presplit_df = None + orch.growth_tm.tensor_dim_names = [ + "replicate", "time", "condition_pre", "condition_sel", + "titrant_name", "titrant_conc", "genotype", + ] + + return all_dfs, orch + def test_run_calibration_diagnostics_calls_both_helpers(self, tmp_path, mocker): - """_run_calibration_diagnostics must call _write_calibration_stats and - _make_correlation_plot after plot_geno_trajectory returns a DataFrame.""" + """_run_calibration_diagnostics calls _write_calibration_stats and + _make_correlation_plot with a DataFrame that has ln_cfu_pred.""" mock_stats = mocker.patch( "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli._write_calibration_stats", @@ -1410,35 +1513,167 @@ def test_run_calibration_diagnostics_calls_both_helpers(self, tmp_path, mocker): "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli._make_correlation_plot", ) - - # plot_geno_trajectory returns a fake predictions DataFrame. - fake_df = pd.DataFrame({ - "ln_cfu": [1.0, 2.0, 3.0], - "ln_cfu_pred": [1.1, 2.2, np.nan], # one NaN → n_obs == 2 - }) mocker.patch( "tfscreen.tfmodel.scripts" ".run_prefit_calibration_cli.plot_geno_trajectory", - return_value=fake_df, + return_value=MagicMock(), + ) + all_dfs, orch = self._make_fake_predict_dfs() + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.predict", + return_value=all_dfs, ) - # params has one _auto_loc key with 2 elements → n_params == 2. params = {"alpha_auto_loc": np.array([0.5, 0.6])} - _run_calibration_diagnostics( - MagicMock(), params, out_prefix=str(tmp_path / "run") - ) + _run_calibration_diagnostics(orch, params, out_prefix=str(tmp_path / "run")) assert mock_stats.call_count == 1 assert mock_corr.call_count == 1 - # Both receive the same DataFrame as first argument. df_arg_stats = mock_stats.call_args.args[0] df_arg_corr = mock_corr.call_args.args[0] assert isinstance(df_arg_stats, pd.DataFrame) assert isinstance(df_arg_corr, pd.DataFrame) - assert df_arg_stats is df_arg_corr - # n_params and n_obs must be forwarded as keyword arguments. + assert "ln_cfu_pred" in df_arg_stats.columns + # n_params and n_obs forwarded as kwargs stats_kwargs = mock_stats.call_args.kwargs - assert "n_params" in stats_kwargs - assert "n_obs" in stats_kwargs + assert "n_params" in stats_kwargs and "n_obs" in stats_kwargs assert isinstance(stats_kwargs["n_params"], int) assert isinstance(stats_kwargs["n_obs"], int) + + def test_run_calibration_diagnostics_one_pdf_per_genotype(self, tmp_path, mocker): + """One trajectory PDF must be written for each genotype.""" + genotypes = ("genoX", "genoY", "genoZ") + all_dfs, orch = self._make_fake_predict_dfs(genotypes=genotypes) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.predict", + return_value=all_dfs, + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._write_calibration_stats", + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._make_correlation_plot", + ) + # plot_geno_trajectory returns a real Figure so savefig works. + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.plot_geno_trajectory", + side_effect=lambda df, **kw: plt.subplots(1)[0].get_figure(), + ) + + prefix = str(tmp_path / "run") + _run_calibration_diagnostics(orch, {}, out_prefix=prefix) + + pdfs = [f for f in tmp_path.iterdir() if f.suffix == ".pdf" + and "trajectory" in f.name] + assert len(pdfs) == len(genotypes), \ + f"expected {len(genotypes)} PDFs, got {len(pdfs)}: {sorted(pdfs)}" + + def test_run_calibration_diagnostics_pdf_named_by_genotype(self, tmp_path, mocker): + """Each PDF filename must contain the genotype name.""" + all_dfs, orch = self._make_fake_predict_dfs(genotypes=("WT", "V44A")) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.predict", + return_value=all_dfs, + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._write_calibration_stats", + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._make_correlation_plot", + ) + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.plot_geno_trajectory", + side_effect=lambda df, **kw: plt.subplots(1)[0].get_figure(), + ) + + prefix = str(tmp_path / "run") + _run_calibration_diagnostics(orch, {}, out_prefix=prefix) + + names = [f.name for f in tmp_path.iterdir() if f.suffix == ".pdf"] + assert any("WT" in n for n in names) + assert any("V44A" in n for n in names) + + def test_run_calibration_diagnostics_genotype_slash_sanitized(self, tmp_path, mocker): + """Slashes in genotype names are replaced with underscores in filenames.""" + all_dfs, orch = self._make_fake_predict_dfs(genotypes=("a/b",)) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.predict", + return_value=all_dfs, + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._write_calibration_stats", + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._make_correlation_plot", + ) + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.plot_geno_trajectory", + side_effect=lambda df, **kw: plt.subplots(1)[0].get_figure(), + ) + + prefix = str(tmp_path / "run") + _run_calibration_diagnostics(orch, {}, out_prefix=prefix) + + names = [f.name for f in tmp_path.iterdir() if f.suffix == ".pdf"] + assert any("a_b" in n for n in names), \ + f"Expected 'a_b' in a filename, got: {names}" + assert not any("a/b" in n for n in names) + + def test_run_calibration_diagnostics_growth_pred_std_attached(self, tmp_path, mocker): + """When growth_pred_std is supplied, ln_cfu_pred_std appears in the + DataFrame passed to _write_calibration_stats.""" + all_dfs, orch = self._make_fake_predict_dfs(genotypes=("g0",)) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.predict", + return_value=all_dfs, + ) + mock_stats = mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._write_calibration_stats", + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli._make_correlation_plot", + ) + mocker.patch( + "tfscreen.tfmodel.scripts" + ".run_prefit_calibration_cli.plot_geno_trajectory", + return_value=MagicMock(), + ) + + # Shape (1,3,1,1,1,1,1) matches 1 replicate × 3 times × ... × 1 geno + gp_std = np.full((1, 3, 1, 1, 1, 1, 1), 0.42) + _run_calibration_diagnostics( + orch, {}, out_prefix=str(tmp_path / "run"), + growth_pred_std=gp_std, + ) + + df_arg = mock_stats.call_args.args[0] + assert "ln_cfu_pred_std" in df_arg.columns + assert np.allclose(df_arg["ln_cfu_pred_std"].dropna(), 0.42) diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index be76a636..0894926e 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -8,6 +8,7 @@ import json import os import warnings +from pathlib import Path import matplotlib matplotlib.use("Agg") @@ -17,12 +18,16 @@ import pytest import yaml +from unittest.mock import MagicMock, patch + from tfscreen.tfmodel.scripts.summarize_fit_cli import ( + _find_params_or_posterior, _find_unique, _json_safe, _read_all_losses, _read_final_loss, _resolve_path, + _try_plot_trajectories, summarize_fit, ) @@ -553,3 +558,300 @@ def test_empty_run_dir_writes_json_with_nulls(self, tmp_path): assert data["growth"]["training"] is None assert data["metadata"]["n_parameters"] is None assert data["metadata"]["final_loss"] is None + + +# --------------------------------------------------------------------------- +# _find_params_or_posterior +# --------------------------------------------------------------------------- + +class TestFindParamsOrPosterior: + + def test_returns_none_when_neither_present(self, tmp_path): + kind, path = _find_params_or_posterior(str(tmp_path)) + assert kind is None + assert path is None + + def test_finds_posterior_h5(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + kind, path = _find_params_or_posterior(str(tmp_path)) + assert kind == "posterior" + assert path == str(tmp_path / "run_posterior.h5") + + def test_finds_params_npz(self, tmp_path): + (tmp_path / "run_params.npz").touch() + kind, path = _find_params_or_posterior(str(tmp_path)) + assert kind == "params" + assert path == str(tmp_path / "run_params.npz") + + def test_prefers_posterior_over_params(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + (tmp_path / "run_params.npz").touch() + kind, path = _find_params_or_posterior(str(tmp_path)) + assert kind == "posterior" + + def test_warns_on_multiple_posterior_files(self, tmp_path): + (tmp_path / "a_posterior.h5").touch() + (tmp_path / "b_posterior.h5").touch() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + kind, path = _find_params_or_posterior(str(tmp_path)) + assert kind == "posterior" + assert any("posterior" in str(x.message).lower() for x in w) + + def test_warns_on_multiple_params_files_falls_back_to_first(self, tmp_path): + (tmp_path / "a_params.npz").touch() + (tmp_path / "b_params.npz").touch() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + kind, path = _find_params_or_posterior(str(tmp_path)) + assert kind == "params" + assert path == str(tmp_path / "a_params.npz") + assert any("params" in str(x.message).lower() for x in w) + + +# --------------------------------------------------------------------------- +# _try_plot_trajectories +# --------------------------------------------------------------------------- + +_MOCK_BINDING_DF = pd.DataFrame({ + "genotype": ["wt", "A1B", "C2D"], + "titrant_name": ["IPTG"] * 3, + "titrant_conc": [0.0] * 3, + "theta_obs": [0.1, 0.5, 0.9], +}) + +_CONFIG_WITH_GROWTH = {"data": {"growth": "growth.csv", "binding": "binding.csv"}} +_CONFIG_NO_GROWTH = {"data": {"binding": "binding.csv"}} + + +class TestTryPlotTrajectories: + + # ------------------------------------------------------------------ + # Guard: no growth data in config + # ------------------------------------------------------------------ + + def test_skips_silently_when_no_growth_key(self, tmp_path): + with patch("tfscreen.plot.geno_trajectory.plot_geno_trajectory") as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_NO_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + mock_pg.assert_not_called() + + def test_skips_silently_when_config_yaml_is_none(self, tmp_path): + with patch("tfscreen.plot.geno_trajectory.plot_geno_trajectory") as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=None, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + mock_pg.assert_not_called() + + # ------------------------------------------------------------------ + # Guard: no params / posterior file + # ------------------------------------------------------------------ + + def test_warns_and_skips_when_no_pred_file(self, tmp_path): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch("tfscreen.plot.geno_trajectory.plot_geno_trajectory") as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + mock_pg.assert_not_called() + assert any("posterior" in str(x.message).lower() or + "params" in str(x.message).lower() for x in w) + + # ------------------------------------------------------------------ + # Guard: read_configuration raises + # ------------------------------------------------------------------ + + def test_warns_and_skips_when_orchestrator_load_fails(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + side_effect=RuntimeError("load failed"), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + mock_pg.assert_not_called() + assert any("trajectory" in str(x.message).lower() for x in w) + + # ------------------------------------------------------------------ + # Happy path: posterior.h5 present + # ------------------------------------------------------------------ + + def test_calls_plot_with_posterior_file_when_h5_present(self, tmp_path): + h5_path = str(tmp_path / "run_posterior.h5") + (tmp_path / "run_posterior.h5").touch() + mock_orch = MagicMock() + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + return_value=(mock_orch, {}), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + mock_pg.assert_called_once() + _, kwargs = mock_pg.call_args + assert kwargs.get("posterior_file") == h5_path + assert "params" not in kwargs + + # ------------------------------------------------------------------ + # Happy path: only params.npz present (fallback) + # ------------------------------------------------------------------ + + def test_calls_plot_with_params_when_only_npz_present(self, tmp_path): + npz_path = str(tmp_path / "run_params.npz") + (tmp_path / "run_params.npz").touch() + mock_orch = MagicMock() + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + return_value=(mock_orch, {}), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + mock_pg.assert_called_once() + _, kwargs = mock_pg.call_args + assert kwargs.get("params") == npz_path + assert "posterior_file" not in kwargs + + # ------------------------------------------------------------------ + # Genotype list derived from binding_df + # ------------------------------------------------------------------ + + def test_passes_genotypes_from_binding_df(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + mock_orch = MagicMock() + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + return_value=(mock_orch, {}), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + _, kwargs = mock_pg.call_args + assert set(kwargs["genotypes"]) == {"wt", "A1B", "C2D"} + + def test_passes_none_genotypes_when_binding_df_is_none(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + mock_orch = MagicMock() + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + return_value=(mock_orch, {}), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=None, + ) + _, kwargs = mock_pg.call_args + assert kwargs["genotypes"] is None + + # ------------------------------------------------------------------ + # Guard: plot_geno_trajectory itself raises + # ------------------------------------------------------------------ + + def test_warns_gracefully_when_plot_raises(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + mock_orch = MagicMock() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + return_value=(mock_orch, {}), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory", + side_effect=RuntimeError("plot failed"), + ): + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=str(tmp_path / "out"), + binding_df=_MOCK_BINDING_DF, + ) + assert any("trajectory" in str(x.message).lower() for x in w) + + # ------------------------------------------------------------------ + # Integration: summarize_fit calls _try_plot_trajectories + # ------------------------------------------------------------------ + + def test_summarize_fit_calls_plot_when_h5_and_growth_config_present(self, run_dir): + # Add a growth key to the config and a fake posterior file. + config_path = os.path.join(run_dir, "run_config.yaml") + with open(config_path) as fh: + cfg = yaml.safe_load(fh) + cfg["data"]["growth"] = "growth.csv" + with open(config_path, "w") as fh: + yaml.dump(cfg, fh) + (Path(run_dir) / "run_posterior.h5").touch() + + with patch( + "tfscreen.tfmodel.configuration_io.read_configuration", + return_value=(MagicMock(), {}), + ), patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + summarize_fit(run_dir) + + mock_pg.assert_called_once() + + def test_summarize_fit_skips_plot_when_no_pred_file(self, run_dir): + config_path = os.path.join(run_dir, "run_config.yaml") + with open(config_path) as fh: + cfg = yaml.safe_load(fh) + cfg["data"]["growth"] = "growth.csv" + with open(config_path, "w") as fh: + yaml.dump(cfg, fh) + # No posterior.h5 or params.npz written. + + with patch( + "tfscreen.plot.geno_trajectory.plot_geno_trajectory" + ) as mock_pg: + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + summarize_fit(run_dir) + + mock_pg.assert_not_called() diff --git a/tests/tfscreen/tfmodel/test_configure_and_run.py b/tests/tfscreen/tfmodel/test_configure_and_run.py index c7a5f1f7..1d1a5269 100644 --- a/tests/tfscreen/tfmodel/test_configure_and_run.py +++ b/tests/tfscreen/tfmodel/test_configure_and_run.py @@ -176,6 +176,25 @@ def test_read_configuration_errors(tmpdir): with pytest.raises(FileNotFoundError, match="Guesses file not found"): read_configuration(config_path) + # 7. Stale guesses file: array parameter has wrong number of values + guesses_path2 = os.path.join(tmpdir, "guesses_stale.csv") + pd.DataFrame({ + "parameter": ["param_arr", "param_arr"], + "value": [1.0, 2.0], + "flat_index": [0, 1] + }).to_csv(guesses_path2, index=False) + with open(config_path, "w") as f: + yaml.dump({"data":{"growth":"g", "binding":"b"}, "components":{}, + "priors_file": "priors.csv", "guesses_file": "guesses_stale.csv"}, f) + with patch("tfscreen.tfmodel.configuration_io.ModelOrchestrator") as mock_orch_cls: + mock_orch_inst = mock_orch_cls.return_value + # Model expects shape (5,) but CSV only has 2 values + mock_orch_inst.init_params = {"param_arr": jnp.zeros((5,))} + with patch("tfscreen.tfmodel.configuration_io._update_dataclass", + return_value=MagicMock()): + with pytest.raises(ValueError, match="stale"): + read_configuration(config_path) + # ---------------------------------------------------------------------------- # test TFMODEL_KNOWN_KEYS / unknown-key validation in read_configuration diff --git a/tests/tfscreen/tfmodel/test_prediction.py b/tests/tfscreen/tfmodel/test_prediction.py index 63011f5e..d8eb5624 100644 --- a/tests/tfscreen/tfmodel/test_prediction.py +++ b/tests/tfscreen/tfmodel/test_prediction.py @@ -3,10 +3,10 @@ import numpy as np import jax.numpy as jnp import numpyro.distributions as dist -from numpyro.distributions.transforms import biject_to +from unittest.mock import MagicMock, patch from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator -from tfscreen.tfmodel.analysis.prediction import copy_orchestrator, _convert_map_params +from tfscreen.tfmodel.analysis.prediction import copy_orchestrator, _convert_map_params, _align_site_to_tm_dims, predict @pytest.fixture def dummy_orchestrator(): @@ -107,10 +107,10 @@ def fake_trace(): Minimal model trace for testing _convert_map_params. Includes: - - an unconstrained site (Normal → real support → identity bijection) - - a positive-constrained site (HalfNormal → positive support → softplus bijection) - - an observed site (must be ignored) - - a deterministic site (must be ignored) + - an unconstrained site (Normal → real support) + - a positive-constrained site (HalfNormal → positive support) + - an observed site (must be passed through with raw value) + - a deterministic site (must be ignored — no _auto_loc key) """ return { "real_site": { @@ -157,7 +157,7 @@ def test_leading_sample_dim_added(self, fake_trace): ) def test_identity_for_unconstrained_site(self, fake_trace): - """Normal (real support) site value is unchanged by the bijection.""" + """Normal (real support) site value is passed through unchanged.""" raw_val = np.array(-2.3) map_params = {"real_site_auto_loc": raw_val} out = _convert_map_params(map_params, fake_trace) @@ -167,17 +167,35 @@ def test_identity_for_unconstrained_site(self, fake_trace): rtol=1e-5, ) - def test_bijection_for_positive_site(self, fake_trace): - """HalfNormal (positive support) site value is transformed to be positive.""" - raw_val = np.array(-3.0) # arbitrary unconstrained value + def test_positive_site_passes_through_unchanged(self, fake_trace): + """HalfNormal (positive support) site value is passed through as-is. + + RunInference.write_params() calls svi.get_params(), which applies + constrain_fn before saving, so stored _auto_loc values are already in + constrained (positive) space. _convert_map_params must NOT apply a + second bijection. + + Regression: a previous version incorrectly called biject_to(positive) + (== ExpTransform) on the stored value, mapping e.g. 2.09 → exp(2.09) + = 8.09 instead of leaving it as 2.09. This corrupted ln_cfu0 for + library genotypes (which use a sampled HalfNormal scale) while leaving + wt predictions unaffected (wt uses a fixed prior scale). + """ + # Use the real-world value from prefit9 that exposed the bug. + raw_val = np.array(2.09) # already constrained (positive) map_params = {"positive_site_auto_loc": raw_val} out = _convert_map_params(map_params, fake_trace) constrained = float(out["positive_site"][0]) - # Result must be strictly positive. - assert constrained > 0, f"Expected positive constrained value, got {constrained}" - # Must match what biject_to gives. - expected = float(biject_to(dist.HalfNormal(1.0).support)(jnp.array(raw_val))) - np.testing.assert_allclose(constrained, expected, rtol=1e-5) + + # Value must pass through unchanged — no bijection should be applied. + np.testing.assert_allclose(constrained, float(raw_val), rtol=1e-5, + err_msg="positive site value should not be re-transformed") + + # Explicit guard against the old bug: exp(2.09) ≈ 8.09, not 2.09. + assert abs(constrained - float(np.exp(raw_val))) > 1.0, ( + f"Output ({constrained:.4f}) looks like exp(input) " + f"({float(np.exp(raw_val)):.4f}) — biject_to was probably re-introduced" + ) def test_observed_site_ignored(self, fake_trace): """Keys whose site is marked is_observed are not converted.""" @@ -186,13 +204,8 @@ def test_observed_site_ignored(self, fake_trace): "observed_site_auto_loc": np.array(1.0), } out = _convert_map_params(map_params, fake_trace) - # observed_site should still appear (key is not filtered by _convert_map_params - # — it has no _auto_loc guard in the trace loop, so the bijection path is - # simply skipped and the raw value is passed through with leading dim). - # The important thing: real_site IS bijected, observed_site is NOT blocked. + # Both sites should appear with their raw values and a leading sample dim. assert "real_site" in out - # observed_site: site is_observed=True so bijection branch is skipped; - # value should be expanded raw value. assert "observed_site" in out assert out["observed_site"].shape[0] == 1 @@ -222,3 +235,134 @@ def test_unknown_site_passes_value_through(self, fake_trace): out = _convert_map_params(map_params, fake_trace) assert "mystery_site" in out np.testing.assert_allclose(float(out["mystery_site"][0]), 5.0) + + +# --------------------------------------------------------------------------- +# _align_site_to_tm_dims +# --------------------------------------------------------------------------- + +# TM dim order: rep=2, t=3, cp=2, cs=1, tn=1, tc=8, geno=473 +_TM_SIZES = (2, 3, 2, 1, 1, 8, 473) + + +class TestAlignSiteToTmDims: + def test_full_rank_identity(self): + """A 7-D site matching TM exactly maps to [0,1,2,3,4,5,6].""" + spatial = (2, 3, 2, 1, 1, 8, 473) + assert _align_site_to_tm_dims(spatial, _TM_SIZES) == [0, 1, 2, 3, 4, 5, 6] + + def test_tail_aligned_1d(self): + """A 1-D site of size 473 should map to the last TM dim (geno).""" + assert _align_site_to_tm_dims((473,), _TM_SIZES) == [6] + + def test_tail_aligned_2d(self): + """(8, 473) should map to TM dims [5, 6] (titrant_conc, geno).""" + assert _align_site_to_tm_dims((8, 473), _TM_SIZES) == [5, 6] + + def test_broadcast_left_padding(self): + """theta_growth_pred shape (1,1,1,1,1,8,473) — non-broadcast tail match.""" + spatial = (1, 1, 1, 1, 1, 8, 473) + result = _align_site_to_tm_dims(spatial, _TM_SIZES) + # Size-1 dims use right-to-left (modulo-safe); non-broadcast dims are [5, 6]. + assert result[5] == 5 + assert result[6] == 6 + + def test_ln_cfu0_shape(self): + """ln_cfu0 shape (2, 2, 473) should map to TM dims [0, 2, 6] = rep, cp, geno.""" + assert _align_site_to_tm_dims((2, 2, 473), _TM_SIZES) == [0, 2, 6] + + def test_ln_cfu0_tube_offset_shape(self): + """(n_rep, n_cp) = (2, 2) maps to TM dims [0, 2].""" + assert _align_site_to_tm_dims((2, 2), _TM_SIZES) == [0, 2] + + def test_single_geno_dim(self): + """1-D (473,) still maps to geno regardless of other TM sizes.""" + tm = (5, 3, 7, 473) + assert _align_site_to_tm_dims((473,), tm) == [3] + + def test_fallback_to_right_to_left_on_no_match(self): + """If subsequence matching fails, right-to-left is returned.""" + # spatial size 999 is not in any TM dim + result = _align_site_to_tm_dims((999,), _TM_SIZES) + assert result == [6] # right-to-left: len=7, n_sp=1 → 7-1+0=6 + + +# --------------------------------------------------------------------------- +# predict() — priors propagation +# --------------------------------------------------------------------------- + +class TestPredictPriorsPropagate: + """ + Verify that predict() uses orchestrator.priors (the original calibration + priors, which may contain pinned hyperpriors) when calling Predictive, + not new_orchestrator.priors (the copy's default/reset priors). + + Regression test for the bug where copy_orchestrator() always creates + priors with default values (e.g. theta_values=[[0.5]]) which would be + used instead of the fitted calibration priors. + """ + + def test_predict_passes_original_priors_to_predictive(self, dummy_orchestrator, mocker): + """ + predict() must forward orchestrator.priors (not copy's priors) to + the Predictive call. We patch Predictive to capture what priors + argument was passed and assert it is the ORIGINAL orchestrator's + priors object. + """ + original_priors = dummy_orchestrator.priors + # Sanity-check: copy_orchestrator creates an orchestrator whose priors + # differ from the original (defaults are reset on copy). + copy_orch = copy_orchestrator(dummy_orchestrator) + # Priors are not the same object (copy builds fresh defaults). + assert copy_orch.priors is not original_priors + + # Capture the priors argument that reach Predictive.__call__. + captured = {} + + real_predictive_class = __import__( + "numpyro.infer", fromlist=["Predictive"] + ).Predictive + + class CapturingPredictive: + """Thin wrapper that records the priors kwarg then delegates.""" + def __init__(self, *args, **kwargs): + self._inner = real_predictive_class(*args, **kwargs) + + def __call__(self, rng_key, **kwargs): + captured["priors"] = kwargs.get("priors") + return self._inner(rng_key, **kwargs) + + mocker.patch( + "tfscreen.tfmodel.analysis.prediction.Predictive", + CapturingPredictive, + ) + + # Build a trivial 1-sample posterior dict (MAP-style, no _auto_loc). + # Just need the latent sites that exist in the dummy model trace. + from numpyro.handlers import seed, trace as nptrace + seeded = seed(dummy_orchestrator.jax_model, rng_seed=0) + model_tr = nptrace(seeded).get_trace( + data=dummy_orchestrator.data, + priors=dummy_orchestrator.priors, + ) + fake_posteriors = { + name: np.zeros((1,) + site["value"].shape) + for name, site in model_tr.items() + if site["type"] == "sample" and not site.get("is_observed", False) + } + + predict( + dummy_orchestrator, + fake_posteriors, + predict_sites=["growth_pred"], + num_samples=None, + ) + + assert "priors" in captured, "CapturingPredictive was never called" + assert captured["priors"] is original_priors, ( + "predict() passed the copy's priors to Predictive instead of " + "the original orchestrator's priors. This regression means " + "pinned hyperpriors (e.g. activity_hyper_loc/scale in the " + "calibration model) will be randomly re-sampled from the " + "default prior, producing wildly incorrect predictions." + ) From 42c5d50afa06c1ebc9ae353de3019564b379477c Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 8 Jun 2026 20:55:40 -0700 Subject: [PATCH 06/24] improved fit summary plotting --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 65 ++-- reports/junit/junit.xml | 2 +- src/tfscreen/plot/geno_trajectory.py | 81 ++++- src/tfscreen/tfmodel/analysis/prediction.py | 103 +++--- .../tfmodel/scripts/summarize_fit_cli.py | 57 ++- .../tfmodel/scripts/test_summarize_fit_cli.py | 324 +++++++++++++----- tests/tfscreen/tfmodel/test_prediction.py | 127 +++++++ 9 files changed, 572 insertions(+), 191 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 4e89ae94..e5ef2397 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.86%coverage95.86% \ No newline at end of file +coverage: 95.91%coverage95.91% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 9ab97a0e..33dffddd 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3188tests3188 \ No newline at end of file +tests: 3213tests3213 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 68c7efa1..92d62a3e 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1513,36 +1513,22 @@ ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:99:16: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:120:13: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/prediction.py:11:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:99:73: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:136:71: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:157:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:164:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:210:1: C901 'predict' is too complex (26) -./src/tfscreen/tfmodel/analysis/prediction.py:271:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:282:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/analysis/prediction.py:284:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:297:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:299:18: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:299:18: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:353:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:356:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:361:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:364:67: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:365:76: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:369:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:70:1: C901 'copy_orchestrator' is too complex (11) +./src/tfscreen/tfmodel/analysis/prediction.py:195:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/analysis/prediction.py:241:1: C901 'predict' is too complex (22) +./src/tfscreen/tfmodel/analysis/prediction.py:302:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:368:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:371:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prediction.py:376:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:385:58: E741 ambiguous variable name 'l' -./src/tfscreen/tfmodel/analysis/prediction.py:388:26: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:388:26: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:391:25: E122 continuation line missing indentation or outdented -./src/tfscreen/tfmodel/analysis/prediction.py:397:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:379:67: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:380:76: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:384:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:391:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:400:58: E741 ambiguous variable name 'l' +./src/tfscreen/tfmodel/analysis/prediction.py:403:26: E111 indentation is not a multiple of 4 +./src/tfscreen/tfmodel/analysis/prediction.py:403:26: E117 over-indented +./src/tfscreen/tfmodel/analysis/prediction.py:406:25: E122 continuation line missing indentation or outdented ./src/tfscreen/tfmodel/analysis/prediction.py:412:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:415:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:432:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:433:71: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:437:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:442:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:446:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prior_predictive.py:104:33: E203 whitespace before ':' ./src/tfscreen/tfmodel/analysis/sbc.py:130:1: C901 'summarize_sbc' is too complex (17) ./src/tfscreen/tfmodel/configuration_io.py:19:1: E302 expected 2 blank lines, found 1 @@ -3624,8 +3610,8 @@ ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:100:1: C901 '_read_all_losses' is too complex (11) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:182:1: C901 'summarize_fit' is too complex (49) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:144:1: C901 '_read_all_losses' is too complex (11) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:226:1: C901 'summarize_fit' is too complex (49) ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:47:30: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:76:1: C901 'summarize_grid' is too complex (14) ./src/tfscreen/tfmodel/tensors/batch.py:5:1: E302 expected 2 blank lines, found 1 @@ -6469,7 +6455,8 @@ ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:19:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:21:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:23:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:624:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:791:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:794:17: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused @@ -7601,11 +7588,11 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -45 C901 'cat_fit' is too complex (19) -16 E111 indentation is not a multiple of 4 +46 C901 'cat_fit' is too complex (19) +15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) -12 E117 over-indented +11 E117 over-indented 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line @@ -7616,7 +7603,7 @@ 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1712 E221 multiple spaces before operator +1713 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1119 E231 missing whitespace after ',' @@ -7629,7 +7616,7 @@ 146 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 802 E302 expected 2 blank lines, found 1 -51 E303 too many blank lines (2) +50 E303 too many blank lines (2) 17 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line @@ -7645,8 +7632,8 @@ 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -673 W291 trailing whitespace +670 W291 trailing whitespace 86 W292 no newline at end of file -1698 W293 blank line contains whitespace +1689 W293 blank line contains whitespace 29 W391 blank line at end of file -7603 +7590 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 539db625..e622a07a 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/plot/geno_trajectory.py b/src/tfscreen/plot/geno_trajectory.py index 636d70b5..26f5b7d3 100644 --- a/src/tfscreen/plot/geno_trajectory.py +++ b/src/tfscreen/plot/geno_trajectory.py @@ -200,26 +200,23 @@ def plot_geno_trajectory( return fig -def predict_and_plot_geno_trajectory( +def predict_geno_trajectory_df( orchestrator, param_posteriors, genotypes=None, titrant_names=None, t_fine=_T_FINE, num_marginal_samples=200, - figsize=None, - colors=None, - markers=None, ): """ - Run forward predictions and plot per-genotype growth trajectories. + Run forward predictions and return the merged trajectory DataFrame. Calls :func:`~tfscreen.tfmodel.analysis.prediction.predict` with a fine - ``t_sel`` grid to produce smooth trajectory curves with credible-interval - bands, merges in observed data, and delegates to + ``t_sel`` grid, merges in observed data and a pre-selection anchor from + the ``ln_cfu0`` site, and returns a single DataFrame ready for :func:`plot_geno_trajectory`. Only ``(condition_pre, condition_sel, titrant_name, titrant_conc)`` combinations present in - ``orchestrator.growth_df`` are included in the output. + ``orchestrator.growth_df`` are included. Parameters ---------- @@ -240,16 +237,11 @@ def predict_and_plot_geno_trajectory( Number of posterior samples to run through the model when computing quantiles. Passed directly to :func:`~.prediction.predict`. Defaults to 200. - figsize : tuple of (float, float), optional - Passed to :func:`plot_geno_trajectory`. - colors : list of str, optional - Passed to :func:`plot_geno_trajectory`. - markers : list of str, optional - Passed to :func:`plot_geno_trajectory`. Returns ------- - matplotlib.figure.Figure + pd.DataFrame + Merged prediction DataFrame with columns matching ``_KEEP_COLS``. """ from tfscreen.tfmodel.analysis.prediction import predict @@ -345,7 +337,66 @@ def predict_and_plot_geno_trajectory( .drop_duplicates() ) pred_df = pred_df.merge(valid_combos, on=_CONDITION_COLS, how="inner") + return pred_df + + +def predict_and_plot_geno_trajectory( + orchestrator, + param_posteriors, + genotypes=None, + titrant_names=None, + t_fine=_T_FINE, + num_marginal_samples=200, + figsize=None, + colors=None, + markers=None, +): + """ + Run forward predictions and plot per-genotype growth trajectories. + + Calls :func:`predict_geno_trajectory_df` to build the merged prediction + DataFrame, then delegates to :func:`plot_geno_trajectory`. Only + ``(condition_pre, condition_sel, titrant_name, titrant_conc)`` combinations + present in ``orchestrator.growth_df`` are included in the output. + Parameters + ---------- + orchestrator : ModelOrchestrator + Fitted model orchestrator. + param_posteriors : dict or str + MAP parameter dict (keys ending in ``_auto_loc``), path to a + ``_params.npz`` file, or path to a ``_posterior.h5`` file. + genotypes : list of str, optional + Subset of genotypes to include. If ``None``, uses all genotypes in + the orchestrator. + titrant_names : list of str, optional + Subset of titrant names to include. If ``None``, uses all. + t_fine : int, optional + Number of equally-spaced selection-phase time points for the fine grid. + Defaults to ``_T_FINE`` (50). + num_marginal_samples : int, optional + Number of posterior samples to run through the model when computing + quantiles. Passed directly to :func:`~.prediction.predict`. + Defaults to 200. + figsize : tuple of (float, float), optional + Passed to :func:`plot_geno_trajectory`. + colors : list of str, optional + Passed to :func:`plot_geno_trajectory`. + markers : list of str, optional + Passed to :func:`plot_geno_trajectory`. + + Returns + ------- + matplotlib.figure.Figure + """ + pred_df = predict_geno_trajectory_df( + orchestrator, + param_posteriors, + genotypes=genotypes, + titrant_names=titrant_names, + t_fine=t_fine, + num_marginal_samples=num_marginal_samples, + ) return plot_geno_trajectory( pred_df, figsize=figsize, colors=colors, markers=markers ) diff --git a/src/tfscreen/tfmodel/analysis/prediction.py b/src/tfscreen/tfmodel/analysis/prediction.py index be2e71d4..ab0d05f7 100644 --- a/src/tfscreen/tfmodel/analysis/prediction.py +++ b/src/tfscreen/tfmodel/analysis/prediction.py @@ -70,7 +70,8 @@ def _align_site_to_tm_dims(spatial_shape, tm_sizes): def copy_orchestrator(orchestrator, t_pre=None, t_sel=None, - titrant_conc=None): + titrant_conc=None, + genotypes=None): """ Generate a fresh ModelOrchestrator instance with model components from an old ModelOrchestrator and new quantitative data passed in by the user. @@ -92,11 +93,14 @@ def copy_orchestrator(orchestrator, titrant_conc : list, optional List of titrant concentrations. Must be >= 0. If None, uses values from `orchestrator.growth_df`. + genotypes : list, optional + Subset of genotypes to include. Must be a subset of those in + ``orchestrator.growth_df``. If None, uses all genotypes. Returns ------- ModelOrchestrator - A new ModelOrchestrator instance initialized with the exhaustive + A new ModelOrchestrator instance initialized with the exhaustive combinations of all inputs. """ @@ -133,10 +137,23 @@ def _get_input(value, col_name): titrant_conc_list = _get_input(titrant_conc, "titrant_conc") # Get all unique categorical combinations - categorical_cols = ["replicate", "condition_pre", "condition_sel", + categorical_cols = ["replicate", "condition_pre", "condition_sel", "titrant_name", "genotype"] unique_cats = df[categorical_cols].drop_duplicates() + # Subset genotypes when requested. Doing this here ensures the new + # orchestrator's TM labels differ from the original's, which causes the + # parameter-slicing loop in predict() to fire correctly. + if genotypes is not None: + all_genos = set(str(g) for g in unique_cats["genotype"].tolist()) + geno_strs = [str(g) for g in genotypes] + missing = [g for g in geno_strs if g not in all_genos] + if missing: + raise ValueError(f"Genotype(s) not found in orchestrator: {missing}") + unique_cats = unique_cats[ + unique_cats["genotype"].astype(str).isin(set(geno_strs)) + ] + # Build exhaustive combinations of quantitative values quant_combos = list(itertools.product(t_pre_list, t_sel_list, titrant_conc_list)) quant_df = pd.DataFrame(quant_combos, columns=["t_pre", "t_sel", "titrant_conc"]) @@ -154,7 +171,21 @@ def _get_input(value, col_name): # Create new ModelOrchestrator using settings from the old one settings = orchestrator.settings.copy() - + + # When subsetting genotypes, restrict spiked_genotypes in settings to + # only those present in the subset. Spiked genotypes affect per-genotype + # masks (spiked_mask) which control per_geno_loc/scale at prediction time + # via the non-centred parameterisation — so any spiked genotype that IS in + # the subset must remain marked as spiked; those outside the subset are + # simply omitted. Dropping all spiked_genotypes would silently mis-classify + # in-subset spiked genotypes as library genotypes and corrupt predictions. + if genotypes is not None: + geno_set = set(geno_strs) + orig_spiked = settings.get("spiked_genotypes") or [] + settings["spiked_genotypes"] = [ + g for g in orig_spiked if str(g) in geno_set + ] + return ModelOrchestrator( growth_df=new_growth_df, binding_df=new_binding_df, @@ -272,36 +303,20 @@ def predict(orchestrator, if isinstance(predict_sites, str): predict_sites = [predict_sites] - # Create the expanded prediction model + # Create the expanded prediction model, subsetting genotypes if requested. + # Passing genotypes to copy_orchestrator ensures the new orchestrator's TM + # labels are a strict subset of the original's, which triggers parameter + # slicing in the loop below. Validation of unknown genotype names is also + # handled inside copy_orchestrator. new_orchestrator = copy_orchestrator(orchestrator, t_pre=t_pre, t_sel=t_sel, - titrant_conc=titrant_conc) - - - # ------------------------------------------------------------------------- - # Genotype subsetting - - all_genotypes = new_orchestrator.growth_tm.tensor_dim_labels[-1] - if genotypes is None: - genotypes = all_genotypes.tolist() - genotype_indices = np.arange(len(all_genotypes)) - else: - # Validate and find indices - genotype_indices = [] - for g in genotypes: - matches = np.where(all_genotypes == g)[0] - if len(matches) == 0: - # Try string fallback - matches = np.where(all_genotypes.astype(str) == str(g))[0] - - if len(matches) == 0: - raise ValueError(f"Genotype {g} not found in model.") - genotype_indices.append(matches[0]) - genotype_indices = np.array(genotype_indices) + titrant_conc=titrant_conc, + genotypes=genotypes) - # Use existing get_batch to subset the data tensors - subset_data = new_orchestrator.get_batch(new_orchestrator.data, jnp.array(genotype_indices)) + # After copy_orchestrator the new TM contains exactly the requested + # genotypes (or all genotypes when genotypes=None). + genotypes = new_orchestrator.growth_tm.tensor_dim_labels[-1].tolist() # ------------------------------------------------------------------------- # Model trace — run first so bijections are available for MAP detection. @@ -407,17 +422,15 @@ def predict(orchestrator, # the default prior. new_orchestrator is rebuilt from settings-only # (component names, no priors) so its priors are all-default. predictions = predictive(predict_key, - data=subset_data, + data=new_orchestrator.data, priors=orchestrator.priors) - + # ------------------------------------------------------------------------- # Calculate Quantiles and Join - - # tm._pivot_index columns in df contain the integer codes for each dimension - # (replicate_idx, time_idx, etc.) - # We want a dataframe that only has the subsetted genotypes. + + # new_orchestrator already contains exactly the requested genotypes, so + # growth_df needs no further filtering. base_df = new_orchestrator.growth_df.copy() - base_df = base_df[base_df["genotype"].isin(genotypes)].copy() # Replace the dummy ln_cfu/ln_cfu_std zeros with observed values from the # original orchestrator.growth_df (NaN where there is no matching observation, @@ -429,23 +442,9 @@ def predict(orchestrator, base_df = base_df.drop(columns=["ln_cfu", "ln_cfu_std"]).merge( orig_obs, on=merge_keys, how="left" ) - - # Re-calculate indices for the subsetted dataframe relative to the - # using the new_orchestrator TM but subset the df. + tm = new_orchestrator.growth_tm indices = [base_df[f"{dim}_idx"].values for dim in tm.tensor_dim_names] - - # Since we used get_batch, the predictions only have the subsetted genotypes. - # The last dimension of predictions[site] will match the length of genotype_indices. - # To map back to the tensor indices, we need to map genotype_idx in base_df - # to its relative position in the genotype_indices array. - - # Mapping from original genotype index to new relative index - geno_map = {orig_idx: i for i, orig_idx in enumerate(genotype_indices)} - relative_geno_indices = base_df["genotype_idx"].map(geno_map).values - - # Update indices for genotype to be relative for the prediction tensor - indices[-1] = relative_geno_indices # TM dimension sizes matching the current (possibly genotype-subsetted) indices. tm_sizes = tuple(int(idx.max()) + 1 if len(idx) > 0 else 1 for idx in indices) diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index a3c59ce8..0e38d198 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -35,6 +35,44 @@ def _find_params_or_posterior(run_dir): return None, None +def _try_plot_theta_fits(binding_df, pred_df, out_prefix): + """Generate per-genotype theta fit plots merging binding observations with predictions. + + Silently skips when binding_df or pred_df is None. Warns and returns on + any failure so that the rest of summarize_fit output is unaffected. + """ + if binding_df is None or pred_df is None: + return + + if "theta_std" not in binding_df.columns: + warnings.warn( + "binding_df has no 'theta_std' column; skipping theta fit plots." + ) + return + + try: + from tfscreen.plot.plot_theta_fits import plot_theta_fits + + join_cols = ["genotype", "titrant_name", "titrant_conc"] + binding_sel = [c for c in join_cols + ["theta_obs", "theta_std"] + if c in binding_df.columns] + merged = pred_df.merge(binding_df[binding_sel], on=join_cols, how="inner") + if len(merged) == 0: + return + + for geno in sorted(merged["genotype"].unique().tolist(), key=str): + geno_df = merged[merged["genotype"] == geno] + safe_name = str(geno).replace("/", "_").replace(" ", "_") + ax = plot_theta_fits(geno_df, title=str(geno)) + fig = ax.get_figure() + pdf_path = f"{out_prefix}_{safe_name}_theta_fits.pdf" + fig.savefig(pdf_path, format="pdf", bbox_inches="tight") + plt.close(fig) + print(f"Wrote theta fit plot to {pdf_path}") + except Exception as exc: + warnings.warn(f"Could not generate theta fit plots: {exc}") + + def _try_plot_trajectories(config_file, config_yaml, run_dir, out_prefix, binding_df): """Attempt to generate per-genotype growth trajectory plots. @@ -54,19 +92,25 @@ def _try_plot_trajectories(config_file, config_yaml, run_dir, out_prefix, bindin try: from tfscreen.tfmodel.configuration_io import read_configuration - from tfscreen.plot.geno_trajectory import plot_geno_trajectory + from tfscreen.plot.geno_trajectory import predict_geno_trajectory_df, plot_geno_trajectory orchestrator, _ = read_configuration(config_file) genotypes = list(binding_df["genotype"].unique()) if binding_df is not None else None - kwargs = {"posterior_file": pred_path} if kind == "posterior" else {"params": pred_path} - plot_geno_trajectory( + pred_df = predict_geno_trajectory_df( orchestrator, - out_prefix, + pred_path, genotypes=genotypes, - **kwargs, ) + for geno in sorted(pred_df["genotype"].unique().tolist(), key=str): + geno_df = pred_df[pred_df["genotype"] == geno] + safe_name = str(geno).replace("/", "_").replace(" ", "_") + fig = plot_geno_trajectory(geno_df) + pdf_path = f"{out_prefix}_{safe_name}_trajectory.pdf" + fig.savefig(pdf_path, format="pdf", bbox_inches="tight") + plt.close(fig) + print(f"Wrote trajectory plot to {pdf_path}") except Exception as exc: warnings.warn(f"Could not generate trajectory plots: {exc}") @@ -366,6 +410,9 @@ def summarize_fit(run_dir, # --- Trajectory plots --- _try_plot_trajectories(config_file, config_yaml, run_dir, out_prefix, binding_df) + # --- Per-genotype theta fit plots --- + _try_plot_theta_fits(binding_df, pred_df, out_prefix) + # --- Write JSON --- results = { "metadata": metadata, diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index 0894926e..22741324 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -27,6 +27,7 @@ _read_all_losses, _read_final_loss, _resolve_path, + _try_plot_theta_fits, _try_plot_trajectories, summarize_fit, ) @@ -609,6 +610,172 @@ def test_warns_on_multiple_params_files_falls_back_to_first(self, tmp_path): assert any("params" in str(x.message).lower() for x in w) +# --------------------------------------------------------------------------- +# _try_plot_theta_fits +# --------------------------------------------------------------------------- + +_PATCH_PLOT_THETA_FITS = "tfscreen.plot.plot_theta_fits.plot_theta_fits" + +_MOCK_BINDING_WITH_STD = pd.DataFrame({ + "genotype": ["wt", "A1B", "C2D"], + "titrant_name": ["IPTG"] * 3, + "titrant_conc": [10.0] * 3, + "theta_obs": [0.1, 0.5, 0.9], + "theta_std": [0.02, 0.02, 0.02], +}) + +_MOCK_PRED_FOR_THETA = pd.DataFrame({ + "genotype": ["wt", "A1B", "C2D"], + "titrant_name": ["IPTG"] * 3, + "titrant_conc": [10.0] * 3, + "median": [0.12, 0.48, 0.88], + "in_training_data": [1, 1, 1], +}) + + +class TestTryPlotThetaFits: + + def _make_mock_ax(self): + mock_ax = MagicMock() + mock_ax.get_figure.return_value = MagicMock() + return mock_ax + + # ------------------------------------------------------------------ + # Guard: None inputs + # ------------------------------------------------------------------ + + def test_skips_silently_when_binding_df_is_none(self, tmp_path): + with patch(_PATCH_PLOT_THETA_FITS) as mock_plot: + _try_plot_theta_fits(None, _MOCK_PRED_FOR_THETA, str(tmp_path / "out")) + mock_plot.assert_not_called() + + def test_skips_silently_when_pred_df_is_none(self, tmp_path): + with patch(_PATCH_PLOT_THETA_FITS) as mock_plot: + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, None, str(tmp_path / "out")) + mock_plot.assert_not_called() + + # ------------------------------------------------------------------ + # Guard: missing theta_std column + # ------------------------------------------------------------------ + + def test_warns_and_skips_when_theta_std_missing(self, tmp_path): + binding_no_std = _MOCK_BINDING_WITH_STD.drop(columns=["theta_std"]) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch(_PATCH_PLOT_THETA_FITS) as mock_plot: + _try_plot_theta_fits(binding_no_std, _MOCK_PRED_FOR_THETA, + str(tmp_path / "out")) + mock_plot.assert_not_called() + assert any("theta_std" in str(x.message).lower() for x in w) + + # ------------------------------------------------------------------ + # Happy path: one call per genotype, savefig called with correct paths + # ------------------------------------------------------------------ + + def test_calls_plot_once_per_genotype(self, tmp_path): + mock_ax = self._make_mock_ax() + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax) as mock_plot: + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, _MOCK_PRED_FOR_THETA, + str(tmp_path / "out")) + assert mock_plot.call_count == 3 + + def test_savefig_called_with_correct_paths(self, tmp_path): + out_prefix = str(tmp_path / "out") + mock_ax = self._make_mock_ax() + mock_fig = mock_ax.get_figure.return_value + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax): + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, _MOCK_PRED_FOR_THETA, out_prefix) + saved_paths = {call.args[0] for call in mock_fig.savefig.call_args_list} + for geno in ["wt", "A1B", "C2D"]: + assert f"{out_prefix}_{geno}_theta_fits.pdf" in saved_paths + + def test_title_set_to_genotype_name(self, tmp_path): + mock_ax = self._make_mock_ax() + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax) as mock_plot: + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, _MOCK_PRED_FOR_THETA, + str(tmp_path / "out")) + titles = {call.kwargs.get("title") or call.args[1] + for call in mock_plot.call_args_list + if mock_plot.call_args_list} + # title is passed as kwarg + titles = {call.kwargs["title"] for call in mock_plot.call_args_list} + assert titles == {"wt", "A1B", "C2D"} + + # ------------------------------------------------------------------ + # Filename sanitization + # ------------------------------------------------------------------ + + def test_slash_in_genotype_sanitized_in_filename(self, tmp_path): + out_prefix = str(tmp_path / "out") + binding = _MOCK_BINDING_WITH_STD.copy() + binding["genotype"] = binding["genotype"].replace("wt", "wt/mut") + pred = _MOCK_PRED_FOR_THETA.copy() + pred["genotype"] = pred["genotype"].replace("wt", "wt/mut") + mock_ax = self._make_mock_ax() + mock_fig = mock_ax.get_figure.return_value + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax): + _try_plot_theta_fits(binding, pred, out_prefix) + saved_paths = {call.args[0] for call in mock_fig.savefig.call_args_list} + assert f"{out_prefix}_wt_mut_theta_fits.pdf" in saved_paths + + def test_space_in_genotype_sanitized_in_filename(self, tmp_path): + out_prefix = str(tmp_path / "out") + binding = _MOCK_BINDING_WITH_STD.copy() + binding["genotype"] = binding["genotype"].replace("wt", "wt mut") + pred = _MOCK_PRED_FOR_THETA.copy() + pred["genotype"] = pred["genotype"].replace("wt", "wt mut") + mock_ax = self._make_mock_ax() + mock_fig = mock_ax.get_figure.return_value + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax): + _try_plot_theta_fits(binding, pred, out_prefix) + saved_paths = {call.args[0] for call in mock_fig.savefig.call_args_list} + assert f"{out_prefix}_wt_mut_theta_fits.pdf" in saved_paths + + # ------------------------------------------------------------------ + # Only genotypes present in both dfs are plotted + # ------------------------------------------------------------------ + + def test_only_genotypes_in_both_dfs_are_plotted(self, tmp_path): + pred_subset = _MOCK_PRED_FOR_THETA[_MOCK_PRED_FOR_THETA["genotype"] != "C2D"].copy() + mock_ax = self._make_mock_ax() + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax) as mock_plot: + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, pred_subset, + str(tmp_path / "out")) + assert mock_plot.call_count == 2 + + # ------------------------------------------------------------------ + # Guard: plot raises + # ------------------------------------------------------------------ + + def test_warns_gracefully_when_plot_raises(self, tmp_path): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch(_PATCH_PLOT_THETA_FITS, side_effect=RuntimeError("render failed")): + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, _MOCK_PRED_FOR_THETA, + str(tmp_path / "out")) + assert any("theta fit" in str(x.message).lower() for x in w) + + # ------------------------------------------------------------------ + # Integration: summarize_fit calls _try_plot_theta_fits + # ------------------------------------------------------------------ + + def test_summarize_fit_calls_plot_theta_fits_per_training_genotype(self, run_dir): + mock_ax = MagicMock() + mock_ax.get_figure.return_value = MagicMock() + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax) as mock_plot: + summarize_fit(run_dir) + # run_dir fixture has 3 training genotypes + assert mock_plot.call_count == 3 + + def test_summarize_fit_skips_theta_fits_when_no_pred_csv(self, run_dir): + os.remove(os.path.join(run_dir, "run_theta_pred.csv")) + with patch(_PATCH_PLOT_THETA_FITS) as mock_plot: + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + summarize_fit(run_dir) + mock_plot.assert_not_called() + + # --------------------------------------------------------------------------- # _try_plot_trajectories # --------------------------------------------------------------------------- @@ -623,6 +790,16 @@ def test_warns_on_multiple_params_files_falls_back_to_first(self, tmp_path): _CONFIG_WITH_GROWTH = {"data": {"growth": "growth.csv", "binding": "binding.csv"}} _CONFIG_NO_GROWTH = {"data": {"binding": "binding.csv"}} +_PATCH_PREDICT_DF = "tfscreen.plot.geno_trajectory.predict_geno_trajectory_df" +_PATCH_PLOT_GENO = "tfscreen.plot.geno_trajectory.plot_geno_trajectory" +_PATCH_READ_CONFIG = "tfscreen.tfmodel.configuration_io.read_configuration" + +# DataFrame returned by the mock predict_geno_trajectory_df — one row per +# genotype so the per-genotype loop fires exactly len(GENOTYPES) times. +_MOCK_PRED_DF = pd.DataFrame({ + "genotype": list(_MOCK_BINDING_DF["genotype"]), +}) + class TestTryPlotTrajectories: @@ -631,7 +808,7 @@ class TestTryPlotTrajectories: # ------------------------------------------------------------------ def test_skips_silently_when_no_growth_key(self, tmp_path): - with patch("tfscreen.plot.geno_trajectory.plot_geno_trajectory") as mock_pg: + with patch(_PATCH_PREDICT_DF) as mock_pdf: _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_NO_GROWTH, @@ -639,10 +816,10 @@ def test_skips_silently_when_no_growth_key(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - mock_pg.assert_not_called() + mock_pdf.assert_not_called() def test_skips_silently_when_config_yaml_is_none(self, tmp_path): - with patch("tfscreen.plot.geno_trajectory.plot_geno_trajectory") as mock_pg: + with patch(_PATCH_PREDICT_DF) as mock_pdf: _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=None, @@ -650,7 +827,7 @@ def test_skips_silently_when_config_yaml_is_none(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - mock_pg.assert_not_called() + mock_pdf.assert_not_called() # ------------------------------------------------------------------ # Guard: no params / posterior file @@ -659,7 +836,7 @@ def test_skips_silently_when_config_yaml_is_none(self, tmp_path): def test_warns_and_skips_when_no_pred_file(self, tmp_path): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - with patch("tfscreen.plot.geno_trajectory.plot_geno_trajectory") as mock_pg: + with patch(_PATCH_PREDICT_DF) as mock_pdf: _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -667,7 +844,7 @@ def test_warns_and_skips_when_no_pred_file(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - mock_pg.assert_not_called() + mock_pdf.assert_not_called() assert any("posterior" in str(x.message).lower() or "params" in str(x.message).lower() for x in w) @@ -679,12 +856,8 @@ def test_warns_and_skips_when_orchestrator_load_fails(self, tmp_path): (tmp_path / "run_posterior.h5").touch() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - side_effect=RuntimeError("load failed"), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_READ_CONFIG, side_effect=RuntimeError("load failed")), \ + patch(_PATCH_PREDICT_DF) as mock_pdf: _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -692,23 +865,19 @@ def test_warns_and_skips_when_orchestrator_load_fails(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - mock_pg.assert_not_called() + mock_pdf.assert_not_called() assert any("trajectory" in str(x.message).lower() for x in w) # ------------------------------------------------------------------ - # Happy path: posterior.h5 present + # Happy path: posterior.h5 present — passes h5 path as second arg # ------------------------------------------------------------------ - def test_calls_plot_with_posterior_file_when_h5_present(self, tmp_path): + def test_calls_predict_df_with_h5_path(self, tmp_path): h5_path = str(tmp_path / "run_posterior.h5") (tmp_path / "run_posterior.h5").touch() - mock_orch = MagicMock() - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - return_value=(mock_orch, {}), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF) as mock_pdf, \ + patch(_PATCH_PLOT_GENO, return_value=MagicMock()): _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -716,25 +885,20 @@ def test_calls_plot_with_posterior_file_when_h5_present(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - mock_pg.assert_called_once() - _, kwargs = mock_pg.call_args - assert kwargs.get("posterior_file") == h5_path - assert "params" not in kwargs + mock_pdf.assert_called_once() + args, _ = mock_pdf.call_args + assert args[1] == h5_path # ------------------------------------------------------------------ - # Happy path: only params.npz present (fallback) + # Happy path: only params.npz present (fallback) — passes npz path # ------------------------------------------------------------------ - def test_calls_plot_with_params_when_only_npz_present(self, tmp_path): + def test_calls_predict_df_with_npz_path(self, tmp_path): npz_path = str(tmp_path / "run_params.npz") (tmp_path / "run_params.npz").touch() - mock_orch = MagicMock() - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - return_value=(mock_orch, {}), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF) as mock_pdf, \ + patch(_PATCH_PLOT_GENO, return_value=MagicMock()): _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -742,10 +906,34 @@ def test_calls_plot_with_params_when_only_npz_present(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - mock_pg.assert_called_once() - _, kwargs = mock_pg.call_args - assert kwargs.get("params") == npz_path - assert "posterior_file" not in kwargs + mock_pdf.assert_called_once() + args, _ = mock_pdf.call_args + assert args[1] == npz_path + + # ------------------------------------------------------------------ + # Happy path: one PDF saved per genotype + # ------------------------------------------------------------------ + + def test_saves_per_genotype_pdfs(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + out_prefix = str(tmp_path / "out") + mock_fig = MagicMock() + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF), \ + patch(_PATCH_PLOT_GENO, return_value=mock_fig) as mock_plot: + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=out_prefix, + binding_df=_MOCK_BINDING_DF, + ) + genotypes = sorted(_MOCK_PRED_DF["genotype"].unique().tolist(), key=str) + assert mock_plot.call_count == len(genotypes) + saved_paths = {call.args[0] for call in mock_fig.savefig.call_args_list} + for geno in genotypes: + safe = geno.replace("/", "_").replace(" ", "_") + assert f"{out_prefix}_{safe}_trajectory.pdf" in saved_paths # ------------------------------------------------------------------ # Genotype list derived from binding_df @@ -753,13 +941,9 @@ def test_calls_plot_with_params_when_only_npz_present(self, tmp_path): def test_passes_genotypes_from_binding_df(self, tmp_path): (tmp_path / "run_posterior.h5").touch() - mock_orch = MagicMock() - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - return_value=(mock_orch, {}), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF) as mock_pdf, \ + patch(_PATCH_PLOT_GENO, return_value=MagicMock()): _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -767,18 +951,14 @@ def test_passes_genotypes_from_binding_df(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=_MOCK_BINDING_DF, ) - _, kwargs = mock_pg.call_args + _, kwargs = mock_pdf.call_args assert set(kwargs["genotypes"]) == {"wt", "A1B", "C2D"} def test_passes_none_genotypes_when_binding_df_is_none(self, tmp_path): (tmp_path / "run_posterior.h5").touch() - mock_orch = MagicMock() - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - return_value=(mock_orch, {}), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF) as mock_pdf, \ + patch(_PATCH_PLOT_GENO, return_value=MagicMock()): _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -786,25 +966,19 @@ def test_passes_none_genotypes_when_binding_df_is_none(self, tmp_path): out_prefix=str(tmp_path / "out"), binding_df=None, ) - _, kwargs = mock_pg.call_args + _, kwargs = mock_pdf.call_args assert kwargs["genotypes"] is None # ------------------------------------------------------------------ - # Guard: plot_geno_trajectory itself raises + # Guard: predict_geno_trajectory_df raises # ------------------------------------------------------------------ def test_warns_gracefully_when_plot_raises(self, tmp_path): (tmp_path / "run_posterior.h5").touch() - mock_orch = MagicMock() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - return_value=(mock_orch, {}), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory", - side_effect=RuntimeError("plot failed"), - ): + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, side_effect=RuntimeError("plot failed")): _try_plot_trajectories( config_file=str(tmp_path / "cfg.yaml"), config_yaml=_CONFIG_WITH_GROWTH, @@ -818,8 +992,9 @@ def test_warns_gracefully_when_plot_raises(self, tmp_path): # Integration: summarize_fit calls _try_plot_trajectories # ------------------------------------------------------------------ - def test_summarize_fit_calls_plot_when_h5_and_growth_config_present(self, run_dir): - # Add a growth key to the config and a fake posterior file. + def test_summarize_fit_calls_predict_df_when_h5_and_growth_config_present( + self, run_dir + ): config_path = os.path.join(run_dir, "run_config.yaml") with open(config_path) as fh: cfg = yaml.safe_load(fh) @@ -828,15 +1003,12 @@ def test_summarize_fit_calls_plot_when_h5_and_growth_config_present(self, run_di yaml.dump(cfg, fh) (Path(run_dir) / "run_posterior.h5").touch() - with patch( - "tfscreen.tfmodel.configuration_io.read_configuration", - return_value=(MagicMock(), {}), - ), patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF) as mock_pdf, \ + patch(_PATCH_PLOT_GENO, return_value=MagicMock()): summarize_fit(run_dir) - mock_pg.assert_called_once() + mock_pdf.assert_called_once() def test_summarize_fit_skips_plot_when_no_pred_file(self, run_dir): config_path = os.path.join(run_dir, "run_config.yaml") @@ -847,11 +1019,9 @@ def test_summarize_fit_skips_plot_when_no_pred_file(self, run_dir): yaml.dump(cfg, fh) # No posterior.h5 or params.npz written. - with patch( - "tfscreen.plot.geno_trajectory.plot_geno_trajectory" - ) as mock_pg: + with patch(_PATCH_PREDICT_DF) as mock_pdf: with warnings.catch_warnings(record=True): warnings.simplefilter("always") summarize_fit(run_dir) - mock_pg.assert_not_called() + mock_pdf.assert_not_called() diff --git a/tests/tfscreen/tfmodel/test_prediction.py b/tests/tfscreen/tfmodel/test_prediction.py index d8eb5624..d069a870 100644 --- a/tests/tfscreen/tfmodel/test_prediction.py +++ b/tests/tfscreen/tfmodel/test_prediction.py @@ -97,6 +97,133 @@ def test_copy_orchestrator_no_subsetting_binding(dummy_orchestrator): assert len(new_orchestrator.binding_df) == 2 +def test_copy_orchestrator_genotypes_subset(dummy_orchestrator): + """Requesting a subset of genotypes restricts growth_df rows.""" + new_orch = copy_orchestrator(dummy_orchestrator, genotypes=["wt"]) + assert set(new_orch.growth_df["genotype"]) == {"wt"} + + +def test_copy_orchestrator_genotypes_subset_tm_labels(dummy_orchestrator): + """TM genotype labels match the requested subset.""" + new_orch = copy_orchestrator(dummy_orchestrator, genotypes=["wt"]) + labels = new_orch.growth_tm.tensor_dim_labels[-1].tolist() + assert labels == ["wt"] + + +def test_copy_orchestrator_genotypes_subset_row_count(dummy_orchestrator): + """Row count matches the single-genotype cross-product.""" + new_orch = copy_orchestrator(dummy_orchestrator, genotypes=["wt"]) + # 1 genotype * 1 t_pre * 2 t_sel * 2 titrant_conc = 4 + assert len(new_orch.growth_df) == 4 + + +def test_copy_orchestrator_genotypes_all_explicit(dummy_orchestrator): + """Passing all genotypes explicitly is equivalent to genotypes=None.""" + new_all = copy_orchestrator(dummy_orchestrator) + new_explicit = copy_orchestrator(dummy_orchestrator, genotypes=["wt", "M42V"]) + assert set(new_explicit.growth_df["genotype"]) == set(new_all.growth_df["genotype"]) + + +def test_copy_orchestrator_genotypes_unknown_raises(dummy_orchestrator): + """Requesting an unknown genotype raises ValueError.""" + with pytest.raises(ValueError, match="not found"): + copy_orchestrator(dummy_orchestrator, genotypes=["no_such_geno"]) + + +def test_copy_orchestrator_genotypes_partial_unknown_raises(dummy_orchestrator): + """A mix of valid and unknown genotypes still raises.""" + with pytest.raises(ValueError, match="not found"): + copy_orchestrator(dummy_orchestrator, genotypes=["wt", "no_such_geno"]) + + +def test_copy_orchestrator_genotypes_tm_labels_differ_from_original(dummy_orchestrator): + """Subset TM labels must differ from the original so slicing fires in predict().""" + new_orch = copy_orchestrator(dummy_orchestrator, genotypes=["wt"]) + orig_labels = dummy_orchestrator.growth_tm.tensor_dim_labels[-1].tolist() + new_labels = new_orch.growth_tm.tensor_dim_labels[-1].tolist() + assert orig_labels != new_labels + + +@pytest.fixture +def spiked_orchestrator(): + """ModelOrchestrator with one spiked genotype (A1G) and two library genotypes.""" + growth_df = pd.DataFrame({ + "genotype": ["wt", "wt", "M42V", "M42V", "A1G", "A1G"], + "titrant_name": ["tit1"] * 6, + "titrant_conc": [0.0, 1.0] * 3, + "condition_pre": ["pre1"] * 6, + "condition_sel": ["sel1"] * 6, + "t_pre": [10.0] * 6, + "t_sel": [0.0, 20.0] * 3, + "ln_cfu": [0.0, 5.0, 0.0, 3.0, 0.0, 4.0], + "ln_cfu_std": [0.1] * 6, + "replicate": [1] * 6, + }) + binding_df = pd.DataFrame({ + "genotype": ["wt", "M42V", "A1G"], + "titrant_name": ["tit1"] * 3, + "titrant_conc": [0.5] * 3, + "theta_obs": [0.5, 0.2, 0.9], + "theta_std": [0.01] * 3, + }) + return ModelOrchestrator(growth_df, binding_df, spiked_genotypes=["A1G"]) + + +def test_copy_orchestrator_spiked_outside_subset_dropped(spiked_orchestrator): + """Spiked genotypes not in the requested subset are removed from settings.""" + new_orch = copy_orchestrator(spiked_orchestrator, genotypes=["wt", "M42V"]) + assert new_orch.settings.get("spiked_genotypes") == [] + + +def test_copy_orchestrator_spiked_inside_subset_retained(spiked_orchestrator): + """Spiked genotypes that are in the requested subset are kept in settings.""" + new_orch = copy_orchestrator(spiked_orchestrator, genotypes=["wt", "A1G"]) + assert "A1G" in (new_orch.settings.get("spiked_genotypes") or []) + + +def test_copy_orchestrator_spiked_partial_overlap(): + """Only spiked genotypes present in the subset survive.""" + # Build a fresh orchestrator with two spiked genotypes: A1G and S5T + growth_df = pd.DataFrame({ + "genotype": ["wt", "wt", "M42V", "M42V", "A1G", "A1G", "S5T", "S5T"], + "titrant_name": ["tit1"] * 8, + "titrant_conc": [0.0, 1.0] * 4, + "condition_pre": ["pre1"] * 8, + "condition_sel": ["sel1"] * 8, + "t_pre": [10.0] * 8, + "t_sel": [0.0, 20.0] * 4, + "ln_cfu": [0.0, 5.0, 0.0, 3.0, 0.0, 4.0, 0.0, 4.5], + "ln_cfu_std": [0.1] * 8, + "replicate": [1] * 8, + }) + binding_df = pd.DataFrame({ + "genotype": ["wt", "M42V", "A1G"], + "titrant_name": ["tit1"] * 3, + "titrant_conc": [0.5] * 3, + "theta_obs": [0.5, 0.2, 0.9], + "theta_std": [0.01] * 3, + }) + orch2 = ModelOrchestrator(growth_df, binding_df, spiked_genotypes=["A1G", "S5T"]) + new_orch = copy_orchestrator(orch2, genotypes=["wt", "A1G"]) + spiked_kept = new_orch.settings.get("spiked_genotypes") or [] + assert "A1G" in spiked_kept + assert "S5T" not in spiked_kept + + +def test_copy_orchestrator_no_spiked_in_settings_unaffected(dummy_orchestrator): + """When spiked_genotypes is absent from settings, subsetting doesn't break.""" + assert not dummy_orchestrator.settings.get("spiked_genotypes") + new_orch = copy_orchestrator(dummy_orchestrator, genotypes=["wt"]) + assert set(new_orch.growth_df["genotype"]) == {"wt"} + + +def test_copy_orchestrator_spiked_no_subset_unchanged(spiked_orchestrator): + """Without genotype subsetting, spiked_genotypes in settings are unchanged.""" + new_orch = copy_orchestrator(spiked_orchestrator) + assert new_orch.settings.get("spiked_genotypes") == \ + spiked_orchestrator.settings.get("spiked_genotypes") + + # --------------------------------------------------------------------------- # _convert_map_params # --------------------------------------------------------------------------- From f34ac6347c9abd7b3401253541ff32e2aea1b68e Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 9 Jun 2026 08:00:00 -0700 Subject: [PATCH 07/24] added hill model perturbative simulation --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 453 +++++++++++------- reports/junit/junit.xml | 2 +- src/tfscreen/plot/plot_theta_fits.py | 1 + src/tfscreen/simulate/library_prediction.py | 1 + src/tfscreen/simulate/sample_theta.py | 56 ++- src/tfscreen/simulate/thermo_to_growth.py | 9 +- .../generative/components/theta/hill_geno.py | 208 +++++++- .../generative/components/theta/hill_mut.py | 232 ++++++++- tests/tfscreen/simulate/test_sample_theta.py | 65 +++ .../components/theta/test_hill_geno.py | 146 ++++++ .../tfmodel/components/theta/test_hill_mut.py | 212 ++++++++ 13 files changed, 1172 insertions(+), 217 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index e5ef2397..0b567b26 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.91%coverage95.91% \ No newline at end of file +coverage: 95.95%coverage95.95% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 33dffddd..3204b615 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3213tests3213 \ No newline at end of file +tests: 3242tests3242 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 92d62a3e..4171a853 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -955,7 +955,8 @@ ./src/tfscreen/plot/plot_theta_fits.py:134:1: W293 blank line contains whitespace ./src/tfscreen/plot/plot_theta_fits.py:139:38: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:141:1: W293 blank line contains whitespace -./src/tfscreen/plot/plot_theta_fits.py:152:1: W391 blank line at end of file +./src/tfscreen/plot/plot_theta_fits.py:144:18: E231 missing whitespace after ',' +./src/tfscreen/plot/plot_theta_fits.py:153:1: W391 blank line at end of file ./src/tfscreen/plot/uncertainty_calibration.py:12:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/plot/uncertainty_calibration.py:21:5: E125 continuation line with same indent as next logical line ./src/tfscreen/plot/uncertainty_calibration.py:24:63: W291 trailing whitespace @@ -1481,7 +1482,7 @@ ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/thermo_to_growth.py:596:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/thermo_to_growth.py:603:41: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/__init__.py:2:73: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:3:20: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:18:65: W291 trailing whitespace @@ -1925,151 +1926,220 @@ ./src/tfscreen/tfmodel/generative/components/theta/categorical_geno.py:553:20: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/categorical_geno.py:567:15: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/categorical_geno.py:573:18: W292 no newline at end of file -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:105:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:111:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:112:12: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:115:12: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:121:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:123:8: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:124:8: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:127:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:131:7: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:204:29: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:206:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:207:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:212:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:212:53: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:212:74: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:214:21: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:215:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:216:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:219:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:221:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:257:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:262:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:266:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:271:22: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:275:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:280:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:284:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:289:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:294:28: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:298:30: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:302:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:306:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:314:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:317:22: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:320:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:323:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:329:29: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:333:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:335:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:342:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:342:53: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:342:74: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:344:21: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:345:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:346:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:348:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:350:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:390:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:393:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:396:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:397:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:423:48: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:424:50: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:425:46: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:427:50: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:428:52: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:429:48: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:431:49: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:432:51: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:433:47: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:435:49: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:436:51: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:437:47: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:464:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:472:43: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:473:45: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:474:45: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:476:44: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:478:44: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:481:40: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:483:41: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:484:41: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:569:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:571:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:572:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:575:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:577:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:578:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:590:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:618:40: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:680:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:683:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:135:76: E261 at least two spaces before inline comment -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:235:31: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:236:31: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:237:31: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:275:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:275:30: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:277:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:277:28: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:278:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:278:28: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:289:22: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:289:38: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:291:23: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:291:36: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:292:23: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:292:36: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:302:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:304:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:305:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:308:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:308:44: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:310:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:310:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:311:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:311:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:543:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:545:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:546:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:552:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:552:44: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:552:73: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:554:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:554:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:554:71: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:555:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:555:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:555:71: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:784:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:786:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:787:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:788:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:789:12: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:790:8: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:791:8: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:794:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:794:43: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:796:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:796:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:797:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:797:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:800:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:802:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:803:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:804:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:806:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:807:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:811:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:813:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:814:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:857:40: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:912:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:925:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:928:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:160:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:166:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:167:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:170:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:176:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:178:8: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:179:8: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:182:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:186:7: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:259:29: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:261:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:262:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:267:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:267:53: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:267:74: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:269:21: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:270:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:271:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:274:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:276:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:312:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:317:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:321:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:326:22: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:330:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:335:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:339:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:344:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:349:28: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:353:30: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:357:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:361:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:369:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:372:22: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:375:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:378:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:384:29: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:388:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:390:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:397:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:397:53: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:397:74: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:399:21: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:400:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:401:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:403:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:405:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:464:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:466:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:469:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:482:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:484:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:485:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:490:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:492:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:493:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:498:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:500:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:501:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:506:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:508:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:509:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:514:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:516:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:517:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:520:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:521:70: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:522:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:526:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:529:18: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:529:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:529:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:530:19: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:530:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:531:19: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:531:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:532:15: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:532:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:532:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:533:11: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:533:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:533:16: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:534:14: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:534:16: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:565:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:568:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:571:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:572:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:598:48: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:599:50: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:600:46: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:602:50: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:603:52: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:604:48: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:606:49: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:607:51: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:608:47: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:610:49: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:611:51: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:612:47: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:632:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:666:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:674:43: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:675:45: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:676:45: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:678:44: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:680:44: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:683:40: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:685:41: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:686:41: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:771:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:773:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:774:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:777:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:779:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:780:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:792:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:820:40: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:882:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:885:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:193:76: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:293:31: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:294:31: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:295:31: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:333:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:333:30: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:335:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:335:28: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:336:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:336:28: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:347:22: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:347:38: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:349:23: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:349:36: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:350:23: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:350:36: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:360:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:362:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:363:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:366:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:366:44: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:368:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:368:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:369:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:369:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:601:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:603:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:604:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:44: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:73: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:612:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:612:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:612:71: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:613:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:613:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:613:71: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:693:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:695:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:698:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:705:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:705:31: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:707:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:708:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:708:27: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:741:22: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:743:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:744:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:748:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:749:70: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:750:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:754:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:18: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:758:19: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:758:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:759:19: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:759:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:760:15: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:760:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:760:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:11: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:16: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:762:14: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:762:16: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:839:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1006:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1008:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1009:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1010:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1011:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1012:8: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1013:8: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1016:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1016:43: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1018:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1018:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1019:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1019:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1022:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1024:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1025:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1026:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1028:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1029:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1033:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1035:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1036:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1079:40: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1134:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1147:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1150:14: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/PK.py:38:1: F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused ./src/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/PK.py:38:1: F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.get_population_moments' imported but unused ./src/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/PK.py:171:6: E221 multiple spaces before operator @@ -5703,38 +5773,51 @@ ./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:379:44: W292 no newline at end of file ./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:4:1: F401 'numpyro.distributions as dist' imported but unused ./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:5:1: F401 'numpyro.handlers.trace' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:36:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:171:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:174:36: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:175:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:200:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:243:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:247:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:248:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:273:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:274:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:280:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:280:64: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:299:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:315:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:324:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:326:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:350:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:352:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:353:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:472:51: E222 multiple spaces after operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:487:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:9:1: F401 'tfscreen.tfmodel.generative.components.theta.hill_mut._population_moments' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:49:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:55:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:62:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:119:15: F841 local variable 'G' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:130:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:134:15: F841 local variable 'G' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:134:18: F841 local variable 'P' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:240:9: F841 local variable 'M' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:248:62: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:447:56: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:41:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:176:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:179:36: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:180:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:205:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:248:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:252:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:253:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:278:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:279:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:285:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:285:64: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:304:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:320:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:329:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:331:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:355:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:357:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:358:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:477:51: E222 multiple spaces after operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:492:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:537:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:540:36: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:541:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:10:1: F401 'tfscreen.tfmodel.generative.components.theta.hill_mut._population_moments' imported but unused +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:53:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:59:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:66:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:123:15: F841 local variable 'G' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:134:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:138:15: F841 local variable 'G' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:138:18: F841 local variable 'P' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:244:9: F841 local variable 'M' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:252:62: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:451:56: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:478:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:480:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:490:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:492:26: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:507:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:510:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:511:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:526:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:528:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:528:52: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:25:11: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:30:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:155:31: E221 multiple spaces before operator @@ -7603,17 +7686,17 @@ 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1713 E221 multiple spaces before operator +1767 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1119 E231 missing whitespace after ',' -79 E251 unexpected spaces around keyword / parameter equals +1120 E231 missing whitespace after ',' +103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals -226 E261 at least two spaces before inline comment +228 E261 at least two spaces before inline comment 1 E262 inline comment should start with '# ' 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment -146 E272 multiple spaces before keyword +148 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 802 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) @@ -7636,4 +7719,4 @@ 86 W292 no newline at end of file 1689 W293 blank line contains whitespace 29 W391 blank line at end of file -7590 +7673 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index e622a07a..fd256f6f 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/plot/plot_theta_fits.py b/src/tfscreen/plot/plot_theta_fits.py index 1dcff60c..85d9971c 100644 --- a/src/tfscreen/plot/plot_theta_fits.py +++ b/src/tfscreen/plot/plot_theta_fits.py @@ -141,6 +141,7 @@ def plot_theta_fits(df, ax.set_xscale("log") ax.set_xlabel("titrant conc (mM)") + ax.set_ylim(0,1) ax.set_ylabel("$\\theta$") ax.legend() ax.spines['top'].set_visible(False) diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index 64758f80..82f07e4d 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -96,6 +96,7 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], theta_rng_key=theta_rng_key, growth_params=cf['growth'], theta_priors_overrides=cf.get('theta_priors'), + theta_sim_priors_overrides=cf.get('theta_sim_priors'), dk_geno_hyper_loc=dk_geno_hyper_loc, dk_geno_hyper_scale=dk_geno_hyper_scale, dk_geno_hyper_shift=dk_geno_hyper_shift, diff --git a/src/tfscreen/simulate/sample_theta.py b/src/tfscreen/simulate/sample_theta.py index 4f112716..a4faaa81 100644 --- a/src/tfscreen/simulate/sample_theta.py +++ b/src/tfscreen/simulate/sample_theta.py @@ -8,6 +8,7 @@ model family being inferred. """ +import inspect import numpy as np from numpyro import handlers @@ -20,36 +21,44 @@ def sample_theta_prior(component_name, sim_data, rng_key, - priors_overrides=None): + priors_overrides=None, + sim_priors_overrides=None): """ - Draw one sample of theta from the prior of a registered theta component. + Draw one sample of theta from a registered theta component. + + If the component defines a ``simulate`` function (detected via + ``inspect.isfunction``), the perturbation-based path is used: wildtype + reference parameters are drawn from ``SimPriors`` built from + ``get_sim_hyperparameters()`` plus any ``sim_priors_overrides``. + + Otherwise the prior-predictive path is used: ``define_model`` is called + inside a seeded NumPyro trace, and ``run_model`` produces ``theta_gc``. Parameters ---------- component_name : str - A key in ``model_registry["theta"]`` (e.g. ``"hill"``, - ``"mwc_dimer_lnK_mut"``). ``"simple"`` is not supported. + A key in ``model_registry["theta"]`` (e.g. ``"hill_geno"``, + ``"mwc_dimer_lnK_mut"``). ``"_simple"`` is not supported. sim_data : SimData Built by ``build_sim_data``. Must have all fields required by the chosen component. rng_key : jax.random.PRNGKey - Seed for the NumPyro sampler. + Seed for sampling (NumPyro or NumPy depending on the path taken). priors_overrides : dict or None - Key-value overrides applied to ``get_hyperparameters()`` before - constructing the priors object. Use to set physical constants such - as ``theta_tf_total_M`` or concentration unit scales without - modifying the component defaults. + Overrides for the inference ``ModelPriors`` (prior-predictive path + only). Ignored when the component provides ``simulate``. + sim_priors_overrides : dict or None + Overrides for ``SimPriors`` (perturbation path only). Ignored when + the component does not provide ``simulate``. Returns ------- theta : np.ndarray, shape (num_genotype, num_titrant_conc) Ground-truth fractional occupancy for each genotype at each unique - effector concentration. Store alongside phenotype_df to enable - comparison with fitted posteriors. + effector concentration. theta_param : ThetaParam - Sampled parameter pytree (structure is model-specific). Contains - the underlying equilibrium constants, Hill parameters, etc. that - generated ``theta``. + Sampled parameter pytree. When the perturbation path is used, + ``mu`` and ``sigma`` fields are ``None``. Raises ------ @@ -71,17 +80,24 @@ def sample_theta_prior(component_name, module = theta_registry[component_name] - # Build priors: start from module defaults, then apply any overrides. - params = module.get_hyperparameters() - if priors_overrides: - params.update(priors_overrides) - priors = module.ModelPriors(**params) + # Perturbation-based path: component defines a simulate() function. + if inspect.isfunction(getattr(module, "simulate", None)): + sim_params = module.get_sim_hyperparameters() + if sim_priors_overrides: + sim_params.update(sim_priors_overrides) + sim_priors = module.SimPriors(**sim_params) + return module.simulate("theta", sim_data, sim_priors, rng_key) - # Seed the NumPyro trace and call define_model directly. + # Prior-predictive fallback: seed the NumPyro trace and call define_model. # handlers.seed gives every pyro.sample site a reproducible draw # without needing a full Predictive wrapper. pyro.param sites return # their init_value (module default) when no param_store is active, # which is the correct behaviour for prior-predictive simulation. + params = module.get_hyperparameters() + if priors_overrides: + params.update(priors_overrides) + priors = module.ModelPriors(**params) + with handlers.seed(rng_seed=rng_key): theta_param = module.define_model("theta", sim_data, priors) diff --git a/src/tfscreen/simulate/thermo_to_growth.py b/src/tfscreen/simulate/thermo_to_growth.py index 5f6c4d4e..4557e524 100644 --- a/src/tfscreen/simulate/thermo_to_growth.py +++ b/src/tfscreen/simulate/thermo_to_growth.py @@ -294,6 +294,7 @@ def thermo_to_growth( theta_rng_key, growth_params: dict, theta_priors_overrides: Optional[dict] = None, + theta_sim_priors_overrides: Optional[dict] = None, theta_noise_sigma_logit: float = 0.0, dk_geno_hyper_loc: float = -3.5, dk_geno_hyper_scale: float = 1.0, @@ -325,7 +326,12 @@ def thermo_to_growth( growth_params : dict Per-condition growth model parameters keyed by condition string. theta_priors_overrides : dict or None - Overrides to the theta component's default hyperparameters. + Overrides to the theta component's default ``ModelPriors`` + hyperparameters (prior-predictive path only; ignored when the + component defines ``simulate``). + theta_sim_priors_overrides : dict or None + Overrides to the theta component's ``SimPriors`` defaults + (perturbation path only; ignored for components without ``simulate``). theta_noise_sigma_logit : float, default 0.0 Standard deviation of additive Normal noise applied on the logit scale after prior-predictive theta sampling, giving: @@ -435,6 +441,7 @@ def thermo_to_growth( sim_data=sim_data, rng_key=theta_rng_key, priors_overrides=theta_priors_overrides, + sim_priors_overrides=theta_sim_priors_overrides, ) print("Done.", flush=True) diff --git a/src/tfscreen/tfmodel/generative/components/theta/hill_geno.py b/src/tfscreen/tfmodel/generative/components/theta/hill_geno.py index dd9a5ee0..3fef9861 100644 --- a/src/tfscreen/tfmodel/generative/components/theta/hill_geno.py +++ b/src/tfscreen/tfmodel/generative/components/theta/hill_geno.py @@ -4,7 +4,7 @@ import numpyro as pyro import numpyro.distributions as dist from flax.struct import dataclass -from typing import Dict, Any +from typing import Dict, Any, Optional from tfscreen.tfmodel.data_class import DataClass @@ -74,8 +74,63 @@ class ThetaParam: theta_high: jnp.ndarray log_hill_K: jnp.ndarray hill_n: jnp.ndarray - mu: jnp.ndarray - sigma: jnp.ndarray + mu: Optional[jnp.ndarray] + sigma: Optional[jnp.ndarray] + + +@dataclass(frozen=True) +class SimPriors: + """ + Parameters controlling perturbation-based theta simulation for hill_geno. + + Genotypes are drawn from one of four phenotype categories: + + * **normal** (probability ``1 - p_stuck_bound - p_never_binds - p_inverted``): + Hill curve perturbed around the wildtype reference. + * **stuck-bound** (``p_stuck_bound``): theta ≈ ``wt_theta_low`` throughout. + * **never-binds** (``p_never_binds``): theta ≈ ``wt_theta_high`` throughout. + * **inverted** (``p_inverted``): curve runs from ``wt_theta_high`` to ``wt_theta_low``. + + Attributes + ---------- + wt_theta_low : float + Wildtype theta at zero ligand concentration. + wt_theta_high : float + Wildtype theta at saturating ligand concentration. + wt_log_K : float + Wildtype log(K_D); sets the Hill-curve midpoint. + wt_hill_n : float + Wildtype Hill coefficient (cooperativity). + sigma_logit_low : float + Std dev of additive Normal noise on logit(theta_low) for normal genotypes. + sigma_logit_delta : float + Std dev of additive Normal noise on logit_delta (= logit_high - logit_low) + for normal and inverted genotypes. + sigma_log_K : float + Std dev of additive Normal noise on log_K for all genotypes. + sigma_log_n : float + Std dev of additive Normal noise on log(hill_n) for all genotypes. + p_stuck_bound : float + Fraction of genotypes assigned to the stuck-bound category. + p_never_binds : float + Fraction of genotypes assigned to the never-binds category. + p_inverted : float + Fraction of genotypes assigned to the inverted category. + """ + + wt_theta_low: float + wt_theta_high: float + wt_log_K: float + wt_hill_n: float + + sigma_logit_low: float + sigma_logit_delta: float + sigma_log_K: float + sigma_log_n: float + + p_stuck_bound: float + p_never_binds: float + p_inverted: float def _population_moments( @@ -362,6 +417,126 @@ def guide(name: str, mu=mu, sigma=sigma) +def simulate(name: str, + data: DataClass, + sim_priors: SimPriors, + rng_key) -> tuple: + """ + Perturbation-based theta simulation for hill_geno. + + Generates per-genotype Hill curves by perturbing a wildtype reference. + Genotypes are assigned to one of four phenotype categories (normal, + stuck-bound, never-binds, inverted) and their parameters drawn from + Normal distributions centered on the wildtype reference. + + The returned ``ThetaParam`` has ``mu=None`` and ``sigma=None`` because + population-moment hyperparameters are not sampled in this path. Downstream + ``run_model`` and ``_theta_param_to_df`` only read the per-genotype fields + (``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n``), so this is safe. + + Parameters + ---------- + name : str + Unused; present for interface consistency with ``define_model``. + data : DataClass + Must expose ``num_genotype`` and ``log_titrant_conc``. + sim_priors : SimPriors + Wildtype reference and perturbation distributions. + rng_key : jax.random.PRNGKey + Seed for NumPy RNG (converted internally). + + Returns + ------- + theta_gc : np.ndarray, shape (num_genotype, num_titrant_conc) + Fractional occupancy for each genotype at each unique concentration. + theta_param : ThetaParam + Per-genotype fields shape ``(1, num_genotype)``; ``mu`` and ``sigma`` + are ``None``. + """ + _eps = 1e-6 + seed = int(jax.random.randint(rng_key, shape=(), minval=0, maxval=2**30)) + rng = np.random.default_rng(seed) + + G = data.num_genotype + log_conc = np.array(data.log_titrant_conc) # (C,) + + # Wildtype reference in logit space + wt_logit_low = float(np.log(np.clip(sim_priors.wt_theta_low, _eps, 1 - _eps) + / (1 - np.clip(sim_priors.wt_theta_low, _eps, 1 - _eps)))) + wt_logit_high = float(np.log(np.clip(sim_priors.wt_theta_high, _eps, 1 - _eps) + / (1 - np.clip(sim_priors.wt_theta_high, _eps, 1 - _eps)))) + wt_logit_delta = wt_logit_high - wt_logit_low + wt_log_n = float(np.log(sim_priors.wt_hill_n)) + + # Assign phenotype categories + p_normal = 1.0 - sim_priors.p_stuck_bound - sim_priors.p_never_binds - sim_priors.p_inverted + if p_normal < 0: + raise ValueError( + "SimPriors mixture probabilities (p_stuck_bound + p_never_binds + p_inverted) " + "sum to more than 1.0" + ) + probs = [p_normal, sim_priors.p_stuck_bound, sim_priors.p_never_binds, sim_priors.p_inverted] + categories = rng.choice(4, size=G, p=probs) + + # Per-genotype parameters, initialised to wildtype + logit_low = np.full(G, wt_logit_low) + logit_delta = np.full(G, wt_logit_delta) + log_K = np.full(G, sim_priors.wt_log_K) + log_n = np.full(G, wt_log_n) + + # Category 0: normal (wildtype-like, full perturbation on all parameters) + m0 = categories == 0 + n0 = int(m0.sum()) + logit_low[m0] += rng.normal(0.0, sim_priors.sigma_logit_low, n0) + logit_delta[m0] += rng.normal(0.0, sim_priors.sigma_logit_delta, n0) + log_K[m0] += rng.normal(0.0, sim_priors.sigma_log_K, n0) + log_n[m0] += rng.normal(0.0, sim_priors.sigma_log_n, n0) + + # Category 1: stuck-bound (theta ≈ wt_theta_low, near-zero transition) + m1 = categories == 1 + n1 = int(m1.sum()) + logit_low[m1] = wt_logit_low + rng.normal(0.0, sim_priors.sigma_logit_low, n1) + logit_delta[m1] = rng.normal(0.0, 0.1, n1) + log_K[m1] += rng.normal(0.0, sim_priors.sigma_log_K, n1) + log_n[m1] += rng.normal(0.0, sim_priors.sigma_log_n, n1) + + # Category 2: never-binds (theta ≈ wt_theta_high, near-zero transition) + m2 = categories == 2 + n2 = int(m2.sum()) + logit_low[m2] = wt_logit_high + rng.normal(0.0, sim_priors.sigma_logit_low, n2) + logit_delta[m2] = rng.normal(0.0, 0.1, n2) + log_K[m2] += rng.normal(0.0, sim_priors.sigma_log_K, n2) + log_n[m2] += rng.normal(0.0, sim_priors.sigma_log_n, n2) + + # Category 3: inverted (theta goes from wt_theta_high to wt_theta_low) + m3 = categories == 3 + n3 = int(m3.sum()) + logit_low[m3] = wt_logit_high + rng.normal(0.0, sim_priors.sigma_logit_low, n3) + logit_delta[m3] = -wt_logit_delta + rng.normal(0.0, sim_priors.sigma_logit_delta, n3) + log_K[m3] += rng.normal(0.0, sim_priors.sigma_log_K, n3) + log_n[m3] += rng.normal(0.0, sim_priors.sigma_log_n, n3) + + # Convert to probability space + theta_low_arr = 1.0 / (1.0 + np.exp(-logit_low)) # (G,) + theta_high_arr = 1.0 / (1.0 + np.exp(-(logit_low + logit_delta))) # (G,) + hill_n_arr = np.exp(log_n) # (G,) + + # Compute theta: (G, C) + occupancy = 1.0 / (1.0 + np.exp(-hill_n_arr[:, None] * (log_conc[None, :] - log_K[:, None]))) + theta_gc = theta_low_arr[:, None] + (theta_high_arr - theta_low_arr)[:, None] * occupancy + + theta_param = ThetaParam( + theta_low = jnp.array(theta_low_arr)[None, :], # (1, G) + theta_high = jnp.array(theta_high_arr)[None, :], # (1, G) + log_hill_K = jnp.array(log_K)[None, :], # (1, G) + hill_n = jnp.array(hill_n_arr)[None, :], # (1, G) + mu = None, + sigma = None, + ) + + return theta_gc, theta_param + + def run_model(theta_param: ThetaParam, data: DataClass) -> jnp.ndarray: """ Calculates fractional occupancy (theta) using the Hill equation. @@ -439,6 +614,33 @@ def get_hyperparameters() -> Dict[str, Any]: return parameters +def get_sim_hyperparameters() -> Dict[str, Any]: + """ + Default hyperparameters for perturbation-based simulation (``SimPriors``). + + Wildtype reference is a full-range decreasing Hill curve (theta: ~1→~0) + matching the lac/IPTG system. Perturbation sigmas allow realistic spread + across the genotype library. + + Returns + ------- + dict[str, Any] + """ + return { + "wt_theta_low": 0.99, + "wt_theta_high": 0.01, + "wt_log_K": -4.1, # ln(0.017 mM) — lac/IPTG default + "wt_hill_n": 2.0, + "sigma_logit_low": 0.5, + "sigma_logit_delta": 0.5, + "sigma_log_K": 0.5, + "sigma_log_n": 0.3, + "p_stuck_bound": 0.05, + "p_never_binds": 0.05, + "p_inverted": 0.02, + } + + def get_guesses(name: str, data: DataClass) -> Dict[str, Any]: """ Gets initial guess values for model parameters. diff --git a/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py b/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py index 35bf0272..b8beb30b 100644 --- a/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py +++ b/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py @@ -32,10 +32,8 @@ import numpyro.distributions as dist import pandas as pd from flax.struct import dataclass -from typing import Dict, Any - +from typing import Dict, Any, Optional, Union from functools import partial -from typing import Union from tfscreen.tfmodel.data_class import GrowthData, BindingData from tfscreen.genetics.build_mut_geno_matrix import apply_pair_matrix, apply_mut_matrix @@ -78,8 +76,68 @@ class ThetaParam: theta_high: jnp.ndarray log_hill_K: jnp.ndarray hill_n: jnp.ndarray - mu: jnp.ndarray - sigma: jnp.ndarray + mu: Optional[jnp.ndarray] + sigma: Optional[jnp.ndarray] + + +@dataclass(frozen=True) +class SimPriors: + """ + Parameters controlling perturbation-based theta simulation for hill_mut. + + Mutations perturb each Hill parameter by additive Normal noise in + transformed space. The wildtype genotype (no mutations) receives exactly + the reference curve. Per-genotype values are assembled by summing per-mutation + deltas over each genotype's mutation set, optionally plus pairwise epistasis. + + Attributes + ---------- + wt_theta_low : float + Wildtype theta at zero ligand concentration. + wt_theta_high : float + Wildtype theta at saturating ligand concentration. + wt_log_K : float + Wildtype log(K_D); sets the Hill-curve midpoint. + wt_hill_n : float + Wildtype Hill coefficient (cooperativity). + sigma_d_logit_low : float + Std dev of per-mutation additive delta on logit(theta_low). + sigma_d_logit_delta : float + Std dev of per-mutation additive delta on logit_delta + (= logit_high - logit_low). + sigma_d_log_K : float + Std dev of per-mutation additive delta on log_K. Primary binding- + affinity effect; typically the largest perturbation. + sigma_d_log_n : float + Std dev of per-mutation additive delta on log(hill_n). + epi_tau_scale : float + HalfCauchy scale for the global sparsity parameter τ of the + regularized horseshoe prior on pairwise epistasis. Setting this to + ``0.0`` makes τ=0 exactly, disabling all epistasis. Ignored when + ``data.num_pair == 0``. + epi_slab_scale : float + Typical magnitude of a large (non-shrunk) epistasis effect; sets the + scale of the InvGamma slab (c²). + epi_slab_df : float + Degrees of freedom for the InvGamma slab prior on c². Usually 4. + """ + + wt_theta_low: float + wt_theta_high: float + wt_log_K: float + wt_hill_n: float + + sigma_d_logit_low: float + sigma_d_logit_delta: float + sigma_d_log_K: float + sigma_d_log_n: float + + # Regularized horseshoe prior parameters for pairwise epistasis. + # Used only when ``data.num_pair > 0``. + # Set ``epi_tau_scale=0.0`` to disable epistasis exactly (τ=0 → zero effects). + epi_tau_scale: float # HalfCauchy scale for global τ + epi_slab_scale: float # typical magnitude of a large epistasis effect + epi_slab_df: float # InvGamma degrees of freedom (slab; usually 4) # --------------------------------------------------------------------------- @@ -571,6 +629,142 @@ def _lam_tilde(lam): sigma=sigma) +# --------------------------------------------------------------------------- +# Perturbation-based simulation +# --------------------------------------------------------------------------- + +def simulate(name: str, + data, + sim_priors: SimPriors, + rng_key) -> tuple: + """ + Perturbation-based theta simulation for hill_mut. + + Generates per-genotype Hill curves by assigning a wildtype reference curve + and adding per-mutation additive deltas in transformed parameter space. + The wildtype genotype (no mutations present) receives the reference curve + exactly. Pairwise epistasis is sampled and applied when ``data.num_pair > 0``. + + Pairwise epistasis is sampled from a regularized horseshoe prior when + ``data.num_pair > 0``. Setting ``sim_priors.epi_tau_scale=0.0`` makes + the global scale τ=0 exactly, which forces all epistasis effects to zero + (a clean "no epistasis" toggle). + + The returned ``ThetaParam`` has ``mu=None`` and ``sigma=None``; downstream + ``run_model`` only reads the per-genotype fields. + + Parameters + ---------- + name : str + Unused; present for interface consistency with ``define_model``. + data : SimData or compatible + Must expose ``num_genotype``, ``num_mutation``, ``num_pair``, + ``log_titrant_conc``, ``mut_nnz_mut_idx``, ``mut_nnz_geno_idx``, + and (when ``num_pair > 0``) ``pair_nnz_pair_idx``, + ``pair_nnz_geno_idx``. + sim_priors : SimPriors + Wildtype reference and perturbation distributions. + rng_key : jax.random.PRNGKey + Seed for NumPy RNG (converted internally). + + Returns + ------- + theta_gc : np.ndarray, shape (num_genotype, num_titrant_conc) + Fractional occupancy for each genotype at each unique concentration. + theta_param : ThetaParam + Per-genotype fields shape ``(1, num_genotype)``; ``mu`` and ``sigma`` + are ``None``. + """ + _eps = 1e-6 + seed = int(jax.random.randint(rng_key, shape=(), minval=0, maxval=2**30)) + rng = np.random.default_rng(seed) + + G = data.num_genotype + M = data.num_mutation + log_conc = np.array(data.log_titrant_conc) # (C,) + + # Scatter callables: accept (T, M) or (T, P), return (T, G) + mut_scatter = partial(apply_mut_matrix, + mut_nnz_mut_idx=jnp.array(data.mut_nnz_mut_idx), + mut_nnz_geno_idx=jnp.array(data.mut_nnz_geno_idx), + num_genotype=G) + + # Wildtype reference in logit space + wt_logit_low = float(np.log(np.clip(sim_priors.wt_theta_low, _eps, 1 - _eps) + / (1 - np.clip(sim_priors.wt_theta_low, _eps, 1 - _eps)))) + wt_logit_high = float(np.log(np.clip(sim_priors.wt_theta_high, _eps, 1 - _eps) + / (1 - np.clip(sim_priors.wt_theta_high, _eps, 1 - _eps)))) + wt_logit_delta = wt_logit_high - wt_logit_low + wt_log_n = float(np.log(sim_priors.wt_hill_n)) + + def _scatter_mut(d_1d): + """Scatter (M,) deltas to (G,) via the mutation-genotype COO map.""" + return np.array(mut_scatter(jnp.array(d_1d)[None, :]))[0] + + # Sample per-mutation deltas and assemble per-genotype parameters: (G,) + logit_low = wt_logit_low + _scatter_mut(rng.normal(0.0, sim_priors.sigma_d_logit_low, M)) + logit_delta = wt_logit_delta + _scatter_mut(rng.normal(0.0, sim_priors.sigma_d_logit_delta, M)) + log_K = sim_priors.wt_log_K + _scatter_mut(rng.normal(0.0, sim_priors.sigma_d_log_K, M)) + log_n = wt_log_n + _scatter_mut(rng.normal(0.0, sim_priors.sigma_d_log_n, M)) + + # Optional pairwise epistasis — regularized horseshoe prior + # epi_tau_scale=0.0 → tau=0 exactly → all epistasis effects are zero. + if data.num_pair > 0: + pair_scatter = partial(apply_pair_matrix, + pair_nnz_pair_idx=jnp.array(data.pair_nnz_pair_idx), + pair_nnz_geno_idx=jnp.array(data.pair_nnz_geno_idx), + num_genotype=G) + P = data.num_pair + + def _scatter_pair(e_1d): + """Scatter (P,) epistasis to (G,) via the pair-genotype COO map.""" + return np.array(pair_scatter(jnp.array(e_1d)[None, :]))[0] + + # Global sparsity scale τ ~ HalfCauchy(epi_tau_scale). + # When epi_tau_scale == 0.0, tau == 0 and all effects are exactly zero. + tau = float(np.abs(rng.standard_cauchy())) * sim_priors.epi_tau_scale + + if tau > 0.0: + # Slab variance c² ~ InvGamma(slab_df/2, slab_df*slab_scale²/2). + # Sampled as 1/Gamma(α, scale=1/β) with α=slab_df/2, β=slab_df*slab_scale²/2. + c2 = 1.0 / rng.gamma( + shape=sim_priors.epi_slab_df / 2.0, + scale=2.0 / (sim_priors.epi_slab_df * sim_priors.epi_slab_scale ** 2)) + + def _horseshoe(size): + """Draw horseshoe-shrunk epistasis effects of shape (size,).""" + lam = np.abs(rng.standard_cauchy(size)) # per-pair local scale + lam_tilde = np.sqrt(c2 * lam**2 + / (c2 + tau**2 * lam**2)) # slab-regularised scale + return rng.standard_normal(size) * tau * lam_tilde + + logit_low += _scatter_pair(_horseshoe(P)) + logit_delta += _scatter_pair(_horseshoe(P)) + log_K += _scatter_pair(_horseshoe(P)) + log_n += _scatter_pair(_horseshoe(P)) + # else: tau == 0.0 → all epistasis effects are exactly zero; nothing to add + + # Convert to probability space + theta_low_arr = 1.0 / (1.0 + np.exp(-logit_low)) # (G,) + theta_high_arr = 1.0 / (1.0 + np.exp(-(logit_low + logit_delta))) # (G,) + hill_n_arr = np.exp(log_n) # (G,) + + # Compute theta: (G, C) + occupancy = 1.0 / (1.0 + np.exp(-hill_n_arr[:, None] * (log_conc[None, :] - log_K[:, None]))) + theta_gc = theta_low_arr[:, None] + (theta_high_arr - theta_low_arr)[:, None] * occupancy + + theta_param = ThetaParam( + theta_low = jnp.array(theta_low_arr)[None, :], # (1, G) + theta_high = jnp.array(theta_high_arr)[None, :], + log_hill_K = jnp.array(log_K)[None, :], + hill_n = jnp.array(hill_n_arr)[None, :], + mu = None, + sigma = None, + ) + + return theta_gc, theta_param + + # --------------------------------------------------------------------------- # run_model – identical to hill.py # --------------------------------------------------------------------------- @@ -626,6 +820,34 @@ def get_hyperparameters() -> Dict[str, Any]: return p +def get_sim_hyperparameters() -> Dict[str, Any]: + """ + Default hyperparameters for perturbation-based simulation (``SimPriors``). + + The wildtype reference is a full-range decreasing Hill curve matching the + lac/IPTG system. Per-mutation effects primarily shift K_D (``sigma_d_log_K``) + with smaller effects on cooperativity and the baseline/ceiling occupancy. + Epistasis is small by default. + + Returns + ------- + dict[str, Any] + """ + return { + "wt_theta_low": 0.99, + "wt_theta_high": 0.01, + "wt_log_K": -4.1, # ln(0.017 mM) — lac/IPTG default + "wt_hill_n": 2.0, + "sigma_d_logit_low": 0.3, + "sigma_d_logit_delta": 0.5, + "sigma_d_log_K": 0.5, + "sigma_d_log_n": 0.3, + "epi_tau_scale": 0.1, # matches theta_epi_tau_scale in get_hyperparameters() + "epi_slab_scale": 2.0, # matches theta_epi_slab_scale + "epi_slab_df": 4.0, # matches theta_epi_slab_df + } + + def get_guesses(name: str, data: GrowthData) -> Dict[str, Any]: T = data.num_titrant_name M = data.num_mutation diff --git a/tests/tfscreen/simulate/test_sample_theta.py b/tests/tfscreen/simulate/test_sample_theta.py index 5b073c2c..4d1c9a56 100644 --- a/tests/tfscreen/simulate/test_sample_theta.py +++ b/tests/tfscreen/simulate/test_sample_theta.py @@ -105,3 +105,68 @@ def test_define_model_called_with_correct_args(mock_sim_data): sample_theta_prior("hill_geno", mock_sim_data, rng_key=0) mock_module.define_model.assert_called_once_with("theta", mock_sim_data, mock_priors) + + +# ---------------------------------------------------------------------------- +# Perturbation-path dispatch (simulate function present) +# ---------------------------------------------------------------------------- + +def _make_simulate_module(G, C): + """Return a mock theta module that exposes a real simulate() function.""" + mock_module = MagicMock() + mock_module.get_sim_hyperparameters.return_value = {"alpha": 1.0} + mock_module.SimPriors.return_value = MagicMock() + + expected_theta = np.ones((G, C)) * 0.7 + expected_param = MagicMock(name="theta_param") + + def fake_simulate(name, data, sim_priors, rng_key): + return expected_theta, expected_param + + mock_module.simulate = fake_simulate + mock_module._expected_theta = expected_theta + mock_module._expected_param = expected_param + return mock_module + + +def test_dispatch_uses_simulate_when_present(mock_sim_data): + G = mock_sim_data.num_genotype + C = mock_sim_data.num_titrant_conc + mock_module = _make_simulate_module(G, C) + + with patch("tfscreen.simulate.sample_theta.model_registry", + {"theta": {"hill_geno": mock_module}}): + theta_gc, theta_param = sample_theta_prior("hill_geno", mock_sim_data, rng_key=0) + + np.testing.assert_array_equal(theta_gc, mock_module._expected_theta) + assert theta_param is mock_module._expected_param + mock_module.define_model.assert_not_called() + + +def test_dispatch_prior_path_when_no_simulate(mock_sim_data): + """Prior-predictive path is used when simulate is not a real function.""" + G, C = mock_sim_data.num_genotype, mock_sim_data.num_titrant_conc + mock_module = _make_mock_module(G, C) + # MagicMock.simulate is a MagicMock, not a function — should fall through to prior path + + with patch("tfscreen.simulate.sample_theta.model_registry", + {"theta": {"hill_geno": mock_module}}): + with patch("tfscreen.simulate.sample_theta.handlers"): + sample_theta_prior("hill_geno", mock_sim_data, rng_key=0) + + mock_module.define_model.assert_called_once() + + +def test_sim_priors_overrides_applied(mock_sim_data): + G = mock_sim_data.num_genotype + C = mock_sim_data.num_titrant_conc + mock_module = _make_simulate_module(G, C) + + overrides = {"alpha": 9.9} + with patch("tfscreen.simulate.sample_theta.model_registry", + {"theta": {"hill_geno": mock_module}}): + sample_theta_prior("hill_geno", mock_sim_data, rng_key=0, + sim_priors_overrides=overrides) + + call_kwargs = mock_module.SimPriors.call_args[1] + assert call_kwargs["alpha"] == 9.9 diff --git a/tests/tfscreen/tfmodel/components/theta/test_hill_geno.py b/tests/tfscreen/tfmodel/components/theta/test_hill_geno.py index e9d127f0..0fa74c2c 100644 --- a/tests/tfscreen/tfmodel/components/theta/test_hill_geno.py +++ b/tests/tfscreen/tfmodel/components/theta/test_hill_geno.py @@ -6,14 +6,19 @@ from collections import namedtuple # --- Import Module Under Test (MUT) --- +import jax + from tfscreen.tfmodel.generative.components.theta.hill_geno import ( ModelPriors, ThetaParam, + SimPriors, define_model, guide, + simulate, run_model, get_population_moments, get_hyperparameters, + get_sim_hyperparameters, get_guesses, get_priors, _population_moments, @@ -487,3 +492,144 @@ def test_different_titrants_get_different_predictions(self): iptg_pred = result.loc[result["titrant_name"] == "IPTG", "median"].values[0] tmaipp_pred = result.loc[result["titrant_name"] == "TMAIPP", "median"].values[0] assert abs(iptg_pred - tmaipp_pred) > 0.05 + + +# --------------------------------------------------------------------------- +# SimPriors / get_sim_hyperparameters +# --------------------------------------------------------------------------- + +def test_get_sim_hyperparameters_returns_dict(): + params = get_sim_hyperparameters() + assert isinstance(params, dict) + for key in ["wt_theta_low", "wt_theta_high", "wt_log_K", "wt_hill_n", + "sigma_logit_low", "sigma_logit_delta", "sigma_log_K", "sigma_log_n", + "p_stuck_bound", "p_never_binds", "p_inverted"]: + assert key in params + + +def test_sim_priors_constructs(): + sp = SimPriors(**get_sim_hyperparameters()) + assert isinstance(sp, SimPriors) + assert 0.0 < sp.wt_theta_low < 1.0 + assert 0.0 < sp.wt_theta_high < 1.0 + assert sp.p_stuck_bound + sp.p_never_binds + sp.p_inverted < 1.0 + + +# --------------------------------------------------------------------------- +# simulate +# --------------------------------------------------------------------------- + +@pytest.fixture +def sim_mock_data(mock_data): + """Mock data compatible with simulate() — only needs num_genotype and log_titrant_conc.""" + return mock_data._replace(scatter_theta=0) + + +def test_simulate_output_shapes(sim_mock_data): + rng_key = jax.random.PRNGKey(0) + sim_priors = SimPriors(**get_sim_hyperparameters()) + theta_gc, theta_param = simulate("theta", sim_mock_data, sim_priors, rng_key) + + G = sim_mock_data.num_genotype + C = sim_mock_data.num_titrant_conc + + assert theta_gc.shape == (G, C) + assert theta_param.theta_low.shape == (1, G) + assert theta_param.theta_high.shape == (1, G) + assert theta_param.log_hill_K.shape == (1, G) + assert theta_param.hill_n.shape == (1, G) + assert theta_param.mu is None + assert theta_param.sigma is None + + +def test_simulate_theta_in_unit_interval(sim_mock_data): + rng_key = jax.random.PRNGKey(1) + sim_priors = SimPriors(**get_sim_hyperparameters()) + theta_gc, _ = simulate("theta", sim_mock_data, sim_priors, rng_key) + assert np.all(theta_gc >= 0.0) + assert np.all(theta_gc <= 1.0) + + +def test_simulate_normal_genotypes_wide_range(mock_data): + """With many genotypes and all normal, most curves should span > 0.5 dynamic range.""" + WideData = namedtuple("WideData", mock_data._fields) + data = WideData(**{ + **mock_data._asdict(), + "num_genotype": 200, + "log_titrant_conc": jnp.linspace(-8, 2, 20), + "num_titrant_conc": 20, + "scatter_theta": 0, + }) + + params = get_sim_hyperparameters() + params.update({"p_stuck_bound": 0.0, "p_never_binds": 0.0, "p_inverted": 0.0}) + sim_priors = SimPriors(**params) + + rng_key = jax.random.PRNGKey(42) + theta_gc, _ = simulate("theta", data, sim_priors, rng_key) + + dynamic_range = np.ptp(theta_gc, axis=1) # max - min per genotype + assert np.median(dynamic_range) > 0.5, ( + f"Median dynamic range {np.median(dynamic_range):.3f} is too small" + ) + + +def test_simulate_stuck_bound_genotypes(mock_data): + """Stuck-bound genotypes should have near-zero dynamic range.""" + LargeData = namedtuple("LargeData", mock_data._fields) + data = LargeData(**{ + **mock_data._asdict(), + "num_genotype": 500, + "log_titrant_conc": jnp.linspace(-8, 2, 20), + "num_titrant_conc": 20, + "scatter_theta": 0, + }) + + params = get_sim_hyperparameters() + params.update({"p_stuck_bound": 1.0, "p_never_binds": 0.0, "p_inverted": 0.0}) + sim_priors = SimPriors(**params) + + rng_key = jax.random.PRNGKey(7) + theta_gc, _ = simulate("theta", data, sim_priors, rng_key) + + dynamic_range = np.ptp(theta_gc, axis=1) + assert np.all(dynamic_range < 0.3), ( + f"Stuck-bound genotypes should have small dynamic range; max was {dynamic_range.max():.3f}" + ) + + +def test_simulate_mixture_probabilities_error(sim_mock_data): + """SimPriors with mixture probabilities summing > 1 should raise.""" + params = get_sim_hyperparameters() + params.update({"p_stuck_bound": 0.5, "p_never_binds": 0.5, "p_inverted": 0.2}) + sim_priors = SimPriors(**params) + with pytest.raises(ValueError, match="sum to more than 1"): + simulate("theta", sim_mock_data, sim_priors, jax.random.PRNGKey(0)) + + +def test_simulate_deterministic_with_same_key(sim_mock_data): + """Same rng_key should produce identical results.""" + sim_priors = SimPriors(**get_sim_hyperparameters()) + t1, _ = simulate("theta", sim_mock_data, sim_priors, jax.random.PRNGKey(99)) + t2, _ = simulate("theta", sim_mock_data, sim_priors, jax.random.PRNGKey(99)) + np.testing.assert_array_equal(t1, t2) + + +def test_simulate_different_keys_differ(sim_mock_data): + """Different rng_keys should (almost always) produce different results.""" + sim_priors = SimPriors(**get_sim_hyperparameters()) + t1, _ = simulate("theta", sim_mock_data, sim_priors, jax.random.PRNGKey(0)) + t2, _ = simulate("theta", sim_mock_data, sim_priors, jax.random.PRNGKey(1)) + assert not np.allclose(t1, t2) + + +def test_simulate_theta_param_compatible_with_run_model(mock_data): + """ThetaParam from simulate() should be usable by run_model without error.""" + data = mock_data._replace(scatter_theta=0) + sim_priors = SimPriors(**get_sim_hyperparameters()) + _, theta_param = simulate("theta", data, sim_priors, jax.random.PRNGKey(5)) + result = run_model(theta_param, data) + G = data.num_genotype + C = data.num_titrant_conc + # simulate() always produces T=1 (no per-titrant structure); run_model reflects that + assert result.shape == (1, C, G) diff --git a/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py b/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py index 24244e17..5c88d720 100644 --- a/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py +++ b/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py @@ -5,15 +5,19 @@ from numpyro.handlers import trace, substitute, seed from collections import namedtuple +import jax from functools import partial from tfscreen.tfmodel.generative.components.theta.hill_mut import ( ModelPriors, ThetaParam, + SimPriors, define_model, guide, + simulate, run_model, get_population_moments, get_hyperparameters, + get_sim_hyperparameters, get_guesses, get_priors, _assemble, @@ -452,3 +456,211 @@ def test_get_population_moments_shape(mock_data_no_epi): assert mu.shape == (T, C, 1) assert sigma.shape == (T, C, 1) assert jnp.all(sigma > 0) + + +# --------------------------------------------------------------------------- +# SimPriors / get_sim_hyperparameters +# --------------------------------------------------------------------------- + +def test_get_sim_hyperparameters_returns_dict(): + params = get_sim_hyperparameters() + assert isinstance(params, dict) + for key in ["wt_theta_low", "wt_theta_high", "wt_log_K", "wt_hill_n", + "sigma_d_logit_low", "sigma_d_logit_delta", + "sigma_d_log_K", "sigma_d_log_n", + "epi_tau_scale", "epi_slab_scale", "epi_slab_df"]: + assert key in params, f"Missing key: {key}" + + +def test_get_sim_hyperparameters_horseshoe_defaults_positive(): + """Horseshoe defaults must all be strictly positive.""" + params = get_sim_hyperparameters() + assert params["epi_tau_scale"] > 0.0 + assert params["epi_slab_scale"] > 0.0 + assert params["epi_slab_df"] > 0.0 + + +def test_sim_priors_constructs(): + sp = SimPriors(**get_sim_hyperparameters()) + assert isinstance(sp, SimPriors) + assert 0.0 < sp.wt_theta_low < 1.0 + assert 0.0 < sp.wt_theta_high < 1.0 + assert sp.wt_hill_n > 0.0 + # Horseshoe attributes must exist and be non-negative + assert sp.epi_tau_scale >= 0.0 + assert sp.epi_slab_scale >= 0.0 + assert sp.epi_slab_df >= 0.0 + + +# --------------------------------------------------------------------------- +# simulate – no epistasis +# --------------------------------------------------------------------------- + +class TestSimulateNoEpi: + + def test_output_shapes(self, mock_data_no_epi): + sp = SimPriors(**get_sim_hyperparameters()) + theta_gc, theta_param = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(0)) + G = mock_data_no_epi.num_genotype + C = mock_data_no_epi.num_titrant_conc + assert theta_gc.shape == (G, C) + assert theta_param.theta_low.shape == (1, G) + assert theta_param.theta_high.shape == (1, G) + assert theta_param.log_hill_K.shape == (1, G) + assert theta_param.hill_n.shape == (1, G) + assert theta_param.mu is None + assert theta_param.sigma is None + + def test_theta_in_unit_interval(self, mock_data_no_epi): + sp = SimPriors(**get_sim_hyperparameters()) + theta_gc, _ = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(1)) + assert np.all(theta_gc >= 0.0) + assert np.all(theta_gc <= 1.0) + + def test_wt_genotype_exact_reference(self, mock_data_no_epi): + """Wildtype (column 0, no mutations) receives zero additive deltas, so + its assembled parameters must exactly match the WT SimPriors reference.""" + sp = SimPriors(**get_sim_hyperparameters()) + _, theta_param = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(2)) + # Column 0 is WT: no mutation indicator → zero additive delta on every param + wt_theta_low = float(theta_param.theta_low[0, 0]) + wt_theta_high = float(theta_param.theta_high[0, 0]) + assert abs(wt_theta_low - sp.wt_theta_low) < 1e-5 + assert abs(wt_theta_high - sp.wt_theta_high) < 1e-5 + # WT curve must be decreasing: theta goes from ~1 at low conc to ~0 at high conc + assert wt_theta_low > wt_theta_high + + def test_mutant_differs_from_wt(self, mock_data_no_epi): + """With non-zero sigma_d_log_K, mutant genotypes should have a shifted K.""" + params = get_sim_hyperparameters() + params["sigma_d_log_K"] = 2.0 # large effect to ensure visible shift + sp = SimPriors(**params) + theta_gc, _ = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(3)) + # Single mutants (columns 1 and 2) should differ from wt (column 0) at mid-conc + assert not np.allclose(theta_gc[0], theta_gc[1], atol=1e-3) or \ + not np.allclose(theta_gc[0], theta_gc[2], atol=1e-3) + + def test_deterministic_same_key(self, mock_data_no_epi): + sp = SimPriors(**get_sim_hyperparameters()) + t1, _ = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(99)) + t2, _ = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(99)) + np.testing.assert_array_equal(t1, t2) + + def test_different_keys_differ(self, mock_data_no_epi): + sp = SimPriors(**get_sim_hyperparameters()) + t1, _ = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(0)) + t2, _ = simulate("theta", mock_data_no_epi, sp, jax.random.PRNGKey(1)) + assert not np.allclose(t1, t2) + + def test_compatible_with_run_model(self, mock_data_no_epi): + """ThetaParam from simulate() should be usable by run_model.""" + data = mock_data_no_epi._replace(scatter_theta=0) + sp = SimPriors(**get_sim_hyperparameters()) + _, theta_param = simulate("theta", data, sp, jax.random.PRNGKey(5)) + result = run_model(theta_param, data) + G = data.num_genotype + C = data.num_titrant_conc + # simulate() always produces T=1 + assert result.shape == (1, C, G) + + +# --------------------------------------------------------------------------- +# simulate – with epistasis +# --------------------------------------------------------------------------- + +class TestSimulateEpi: + + def test_output_shapes(self, mock_data_epi): + sp = SimPriors(**get_sim_hyperparameters()) + theta_gc, theta_param = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(0)) + G = mock_data_epi.num_genotype + C = mock_data_epi.num_titrant_conc + assert theta_gc.shape == (G, C) + assert theta_param.theta_low.shape == (1, G) + + def test_theta_in_unit_interval(self, mock_data_epi): + sp = SimPriors(**get_sim_hyperparameters()) + theta_gc, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(7)) + assert np.all(theta_gc >= 0.0) + assert np.all(theta_gc <= 1.0) + + def test_epistasis_only_shifts_double_mutant(self, mock_data_epi): + """ + With zero mutation deltas but non-zero epistasis, only the double-mutant + column (index 3) can differ from the wildtype column (index 0). + Single mutants (indices 1 and 2) have no pair membership, so they must + equal the wildtype reference regardless of the epistasis draw. + """ + params = get_sim_hyperparameters() + params.update({ + "sigma_d_logit_low": 0.0, + "sigma_d_logit_delta": 0.0, + "sigma_d_log_K": 0.0, + "sigma_d_log_n": 0.0, + "epi_tau_scale": 2.0, # large τ → large horseshoe effects + "epi_slab_scale": 2.0, + "epi_slab_df": 4.0, + }) + sp = SimPriors(**params) + theta_gc, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(42)) + # Columns 0, 1, 2 have no pair membership → must equal WT reference + np.testing.assert_allclose(theta_gc[0], theta_gc[1], atol=1e-6) + np.testing.assert_allclose(theta_gc[0], theta_gc[2], atol=1e-6) + # Column 3 (double mutant M42I/K84L) may differ due to epistasis + + def test_epi_tau_zero_produces_no_epistasis(self, mock_data_epi): + """ + epi_tau_scale=0.0 makes τ=0 exactly, so all epistasis effects are zero. + With zero mutation deltas too, all four genotypes must be identical. + """ + params = get_sim_hyperparameters() + params.update({ + "sigma_d_logit_low": 0.0, + "sigma_d_logit_delta": 0.0, + "sigma_d_log_K": 0.0, + "sigma_d_log_n": 0.0, + "epi_tau_scale": 0.0, # hard off-switch: τ = 0 exactly + }) + sp = SimPriors(**params) + theta_gc, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(42)) + # No mutation delta, no epistasis → all columns are the WT reference + np.testing.assert_allclose(theta_gc[0], theta_gc[1], atol=1e-10) + np.testing.assert_allclose(theta_gc[0], theta_gc[2], atol=1e-10) + np.testing.assert_allclose(theta_gc[0], theta_gc[3], atol=1e-10) + + def test_horseshoe_larger_tau_produces_larger_effects(self, mock_data_epi): + """ + Over many random seeds, a larger epi_tau_scale should produce larger + median absolute epistasis effects on the double-mutant log_K. + """ + base = get_sim_hyperparameters() + base.update({ + "sigma_d_logit_low": 0.0, + "sigma_d_logit_delta": 0.0, + "sigma_d_log_K": 0.0, + "sigma_d_log_n": 0.0, + }) + + def median_abs_epi(tau_scale, n=200): + params = dict(base) + params["epi_tau_scale"] = tau_scale + sp = SimPriors(**params) + effects = [] + for seed_val in range(n): + _, tp = simulate("theta", mock_data_epi, sp, + jax.random.PRNGKey(seed_val)) + # Epistasis affects col 3 (double mutant); col 0 is pure WT + effects.append(float(tp.log_hill_K[0, 3] - tp.log_hill_K[0, 0])) + return float(np.median(np.abs(effects))) + + small_epi = median_abs_epi(0.1) + large_epi = median_abs_epi(1.0) + assert large_epi > small_epi, ( + f"Expected larger tau to produce larger effects, but " + f"small={small_epi:.4f}, large={large_epi:.4f}") + + def test_deterministic_same_key(self, mock_data_epi): + sp = SimPriors(**get_sim_hyperparameters()) + t1, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(11)) + t2, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(11)) + np.testing.assert_array_equal(t1, t2) From f9483a0c677809e6d3cb4d801e5d6327dbc647a6 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 9 Jun 2026 10:56:22 -0700 Subject: [PATCH 08/24] cleaned up summarize fit output --- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 8 +- reports/junit/junit.xml | 2 +- src/tfscreen/plot/plot_theta_fits.py | 2 +- .../tfmodel/scripts/summarize_fit_cli.py | 74 +++++++- .../tfmodel/scripts/test_summarize_fit_cli.py | 178 +++++++++++++++--- 6 files changed, 230 insertions(+), 36 deletions(-) diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 3204b615..8f6d58ee 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3242tests3242 \ No newline at end of file +tests: 3253tests3253 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 4171a853..60070bcf 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -3680,8 +3680,8 @@ ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:144:1: C901 '_read_all_losses' is too complex (11) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:226:1: C901 'summarize_fit' is too complex (49) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:152:1: C901 '_read_all_losses' is too complex (11) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:234:1: C901 'summarize_fit' is too complex (59) ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:47:30: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:76:1: C901 'summarize_grid' is too complex (14) ./src/tfscreen/tfmodel/tensors/batch.py:5:1: E302 expected 2 blank lines, found 1 @@ -6538,8 +6538,8 @@ ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:19:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:21:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:23:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:791:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:794:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:900:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:903:17: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index fd256f6f..3dbc041c 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/plot/plot_theta_fits.py b/src/tfscreen/plot/plot_theta_fits.py index 85d9971c..ad5bf9de 100644 --- a/src/tfscreen/plot/plot_theta_fits.py +++ b/src/tfscreen/plot/plot_theta_fits.py @@ -141,7 +141,7 @@ def plot_theta_fits(df, ax.set_xscale("log") ax.set_xlabel("titrant conc (mM)") - ax.set_ylim(0,1) + ax.set_ylim(-0.05,1.05) ax.set_ylabel("$\\theta$") ax.legend() ax.spines['top'].set_visible(False) diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index 0e38d198..e117a28e 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -10,6 +10,8 @@ from matplotlib import pyplot as plt from tfscreen.mle.stats_test_suite import stats_test_suite +from tfscreen.plot import default_styles # noqa: F401 — applies global rcParams on import +from tfscreen.plot.default_styles import DEFAULT_COLORS from tfscreen.plot.xy_corr import xy_corr from tfscreen.util.cli.generalized_main import generalized_main @@ -63,6 +65,9 @@ def _try_plot_theta_fits(binding_df, pred_df, out_prefix): for geno in sorted(merged["genotype"].unique().tolist(), key=str): geno_df = merged[merged["genotype"] == geno] safe_name = str(geno).replace("/", "_").replace(" ", "_") + csv_path = f"{out_prefix}_{safe_name}_theta_fits.csv" + geno_df.to_csv(csv_path, index=False) + print(f"Wrote theta fit data to {csv_path}") ax = plot_theta_fits(geno_df, title=str(geno)) fig = ax.get_figure() pdf_path = f"{out_prefix}_{safe_name}_theta_fits.pdf" @@ -106,6 +111,9 @@ def _try_plot_trajectories(config_file, config_yaml, run_dir, out_prefix, bindin for geno in sorted(pred_df["genotype"].unique().tolist(), key=str): geno_df = pred_df[pred_df["genotype"] == geno] safe_name = str(geno).replace("/", "_").replace(" ", "_") + csv_path = f"{out_prefix}_{safe_name}_trajectory.csv" + geno_df.to_csv(csv_path, index=False) + print(f"Wrote trajectory data to {csv_path}") fig = plot_geno_trajectory(geno_df) pdf_path = f"{out_prefix}_{safe_name}_trajectory.pdf" fig.savefig(pdf_path, format="pdf", bbox_inches="tight") @@ -231,15 +239,34 @@ def summarize_fit(run_dir, Scans run_dir for a *_config.yaml, *_theta_pred.csv, *_growth_pred.csv (optional), and *_losses.txt (optional). Computes prediction statistics - and writes three output files: + and writes output files to the summary directory. + + **CSV files are the authoritative outputs.** Every plot written by this + function has a matched CSV containing the exact data used to generate it. + The PDFs are human-readable summaries; the CSVs are the record of truth + for downstream quantitative analysis. + + Output files written: - ``{out_prefix}_fit_summary.json`` — nested statistics with top-level keys ``metadata``, ``theta`` (training / test sub-keys), and ``growth`` (training sub-key). - - ``{out_prefix}_theta_corr.pdf`` — two-panel hexbin correlation plot for - theta (training left, test right). - - ``{out_prefix}_growth_corr.pdf`` — hexbin correlation plot for ln_cfu - (only written when *_growth_pred.csv is present). + - ``{out_prefix}_theta_corr_training.csv`` — joined (observed, predicted) + theta pairs for training genotypes. + - ``{out_prefix}_theta_corr_test.csv`` — same for test genotypes (only + written when *ground_truth_file* is provided and has matching rows). + - ``{out_prefix}_theta_corr.pdf`` — two-panel correlation plot for theta + (training left, test right). + - ``{out_prefix}_growth_corr.csv`` — relative symlink to the + *_growth_pred.csv in run_dir (only created when that file is present). + - ``{out_prefix}_growth_corr.pdf`` — correlation plot for ln_cfu (only + written when *_growth_pred.csv is present). + - ``{out_prefix}_{genotype}_theta_fits.csv`` / ``.pdf`` — per-genotype + joined observation + prediction data and fit plot (one pair per + genotype present in both binding and prediction CSVs). + - ``{out_prefix}_{genotype}_trajectory.csv`` / ``.pdf`` — per-genotype + predicted growth trajectory data and plot (only when growth data is + configured and a posterior/params file is found). - ``{out_prefix}_losses.pdf`` — training loss curve (only written when *_losses.txt is present). @@ -259,7 +286,8 @@ def summarize_fit(run_dir, """ run_dir = os.path.abspath(run_dir) if out_prefix is None: - out_prefix = os.path.join(run_dir, "tfs_summarize") + out_prefix = os.path.join(run_dir, "summary", "tfs_summarize") + os.makedirs(os.path.dirname(os.path.abspath(out_prefix)), exist_ok=True) metadata = { "run_dir": run_dir, @@ -432,6 +460,23 @@ def summarize_fit(run_dir, except Exception as exc: warnings.warn(f"Could not write JSON to {json_file}: {exc}") + # --- Theta correlation CSVs --- + if train_merged is not None and len(train_merged) > 0: + csv_file = f"{out_prefix}_theta_corr_training.csv" + try: + train_merged.to_csv(csv_file, index=False) + print(f"Wrote theta training data to {csv_file}") + except Exception as exc: + warnings.warn(f"Could not write theta training CSV to {csv_file}: {exc}") + + if test_merged is not None and len(test_merged) > 0: + csv_file = f"{out_prefix}_theta_corr_test.csv" + try: + test_merged.to_csv(csv_file, index=False) + print(f"Wrote theta test data to {csv_file}") + except Exception as exc: + warnings.warn(f"Could not write theta test CSV to {csv_file}: {exc}") + # --- Correlation plot --- pdf_file = f"{out_prefix}_theta_corr.pdf" try: @@ -470,6 +515,21 @@ def summarize_fit(run_dir, except Exception as exc: warnings.warn(f"Could not generate correlation plot: {exc}") + # --- Growth correlation CSV symlink --- + if growth_pred_file is not None: + growth_csv_file = f"{out_prefix}_growth_corr.csv" + try: + rel_target = os.path.relpath( + os.path.abspath(growth_pred_file), + os.path.dirname(os.path.abspath(growth_csv_file)), + ) + if os.path.islink(growth_csv_file): + os.remove(growth_csv_file) + os.symlink(rel_target, growth_csv_file) + print(f"Linked growth data to {growth_csv_file}") + except Exception as exc: + warnings.warn(f"Could not create growth CSV symlink at {growth_csv_file}: {exc}") + # --- Growth correlation plot --- growth_pdf_file = f"{out_prefix}_growth_corr.pdf" try: @@ -496,7 +556,7 @@ def summarize_fit(run_dir, try: if all_losses is not None: fig, ax = plt.subplots(figsize=(8, 4)) - ax.plot(all_epochs, all_losses, lw=1.5, color="steelblue") + ax.plot(all_epochs, all_losses, lw=1.5, color=DEFAULT_COLORS[2]) ax.set_xlabel("Epoch") ax.set_ylabel("Loss") ax.set_title("Training loss") diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index 22741324..b460fc3d 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -332,15 +332,19 @@ class TestSummarizeFitComplete: def test_json_written(self, run_dir): summarize_fit(run_dir) - assert os.path.exists(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) + assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) + + def test_summary_dir_created(self, run_dir): + summarize_fit(run_dir) + assert os.path.isdir(os.path.join(run_dir, "summary")) def test_pdf_written(self, run_dir): summarize_fit(run_dir) - assert os.path.exists(os.path.join(run_dir, "tfs_summarize_theta_corr.pdf")) + assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_theta_corr.pdf")) def test_json_is_valid(self, run_dir): summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert "metadata" in data assert "theta" in data @@ -348,7 +352,7 @@ def test_json_is_valid(self, run_dir): def test_json_theta_and_growth_have_training_subkey(self, run_dir): summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert "training" in data["theta"] assert "test" in data["theta"] @@ -356,7 +360,7 @@ def test_json_theta_and_growth_have_training_subkey(self, run_dir): def test_metadata_fields_populated(self, run_dir): summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) meta = data["metadata"] assert meta["n_parameters"] == 25 @@ -365,7 +369,7 @@ def test_metadata_fields_populated(self, run_dir): def test_theta_training_stats_populated(self, run_dir): summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) stats = data["theta"]["training"] assert stats is not None @@ -374,14 +378,14 @@ def test_theta_training_stats_populated(self, run_dir): def test_theta_test_null_when_no_ground_truth(self, run_dir): summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["test"] is None assert data["metadata"]["n_theta_test_points"] is None def test_growth_training_null_when_no_growth_pred(self, run_dir): summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["growth"]["training"] is None assert data["metadata"]["n_growth_training_points"] is None @@ -393,7 +397,7 @@ def test_growth_training_stats_populated_when_file_present(self, run_dir): "median": np.linspace(8.1, 13.1, 6), }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) stats = data["growth"]["training"] assert stats is not None @@ -409,13 +413,13 @@ def test_growth_training_drops_nan_ln_cfu(self, run_dir): }) df.to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["metadata"]["n_growth_training_points"] == 4 def test_loss_pdf_written(self, run_dir): summarize_fit(run_dir) - assert os.path.exists(os.path.join(run_dir, "tfs_summarize_losses.pdf")) + assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_losses.pdf")) def test_growth_corr_pdf_written_when_file_present(self, run_dir): # Write a minimal growth_pred CSV @@ -425,15 +429,89 @@ def test_growth_corr_pdf_written_when_file_present(self, run_dir): "median": np.linspace(8.1, 13.1, 6), }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) summarize_fit(run_dir) - assert os.path.exists(os.path.join(run_dir, "tfs_summarize_growth_corr.pdf")) + assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.pdf")) def test_growth_corr_pdf_not_written_when_file_absent(self, run_dir): summarize_fit(run_dir) - assert not os.path.exists(os.path.join(run_dir, "tfs_summarize_growth_corr.pdf")) + assert not os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.pdf")) + + # ------------------------------------------------------------------ + # CSV outputs + # ------------------------------------------------------------------ + + def test_theta_corr_training_csv_written(self, run_dir): + summarize_fit(run_dir) + csv_path = os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_training.csv") + assert os.path.exists(csv_path) + df = pd.read_csv(csv_path) + assert "theta_obs" in df.columns + assert "median" in df.columns + assert len(df) > 0 + + def test_theta_corr_training_csv_not_written_when_no_binding(self, run_dir): + config_path = os.path.join(run_dir, "run_config.yaml") + with open(config_path) as fh: + cfg = yaml.safe_load(fh) + cfg["data"]["binding"] = "/nonexistent/binding.csv" + with open(config_path, "w") as fh: + yaml.dump(cfg, fh) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + summarize_fit(run_dir) + assert not os.path.exists( + os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_training.csv") + ) + + def test_theta_corr_test_csv_not_written_without_ground_truth(self, run_dir): + summarize_fit(run_dir) + assert not os.path.exists( + os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_test.csv") + ) + + def test_theta_corr_test_csv_written_with_ground_truth(self, run_dir, ground_truth_file): + pred_path = os.path.join(run_dir, "run_theta_pred.csv") + _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) + summarize_fit(run_dir, ground_truth_file=ground_truth_file) + csv_path = os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_test.csv") + assert os.path.exists(csv_path) + df = pd.read_csv(csv_path) + assert "theta_obs" in df.columns + assert "median" in df.columns + assert len(df) == len(TITRANT_CONCS) + + def test_growth_corr_csv_symlink_created(self, run_dir): + growth_pred_path = os.path.join(run_dir, "tfs_growth_pred.csv") + pd.DataFrame({ + "genotype": ["wt"] * 6, + "ln_cfu": np.linspace(8.0, 13.0, 6), + "median": np.linspace(8.1, 13.1, 6), + }).to_csv(growth_pred_path, index=False) + summarize_fit(run_dir) + csv_path = os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") + assert os.path.islink(csv_path) + # symlink must resolve to the actual growth pred file + assert os.path.realpath(csv_path) == os.path.realpath(growth_pred_path) + + def test_growth_corr_csv_symlink_not_created_when_file_absent(self, run_dir): + summarize_fit(run_dir) + assert not os.path.exists( + os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") + ) + + def test_growth_corr_csv_symlink_overwritten_on_rerun(self, run_dir): + growth_pred_path = os.path.join(run_dir, "tfs_growth_pred.csv") + pd.DataFrame({ + "genotype": ["wt"] * 3, + "ln_cfu": [8.0, 9.0, 10.0], + "median": [8.1, 9.1, 10.1], + }).to_csv(growth_pred_path, index=False) + summarize_fit(run_dir) + summarize_fit(run_dir) # second call must not raise + csv_path = os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") + assert os.path.islink(csv_path) def test_custom_out_prefix(self, run_dir, tmp_path): custom_prefix = str(tmp_path / "custom" / "myrun") - os.makedirs(os.path.dirname(custom_prefix), exist_ok=True) summarize_fit(run_dir, out_prefix=custom_prefix) assert os.path.exists(f"{custom_prefix}_fit_summary.json") assert os.path.exists(f"{custom_prefix}_theta_corr.pdf") @@ -451,7 +529,7 @@ def test_theta_test_stats_populated(self, run_dir, ground_truth_file): _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) summarize_fit(run_dir, ground_truth_file=ground_truth_file) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["test"] is not None assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) @@ -461,7 +539,7 @@ def test_theta_test_pearson_r_reasonable(self, run_dir, ground_truth_file): _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) summarize_fit(run_dir, ground_truth_file=ground_truth_file) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) r = data["theta"]["test"]["pearson_r"] # Predictions are close to truth so r should be high @@ -476,7 +554,7 @@ def test_theta_col_fallback_to_theta(self, run_dir, tmp_path): _make_ground_truth_csv(gt_path, theta_col="theta") summarize_fit(run_dir, ground_truth_file=gt_path) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["test"] is not None assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) @@ -499,7 +577,7 @@ def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): summarize_fit(run_dir, ground_truth_file=gt_path) assert any("neither" in str(x.message).lower() for x in w) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["test"] is None assert data["metadata"]["n_theta_test_points"] is None @@ -516,7 +594,7 @@ def test_missing_theta_pred_still_writes_json(self, run_dir): with warnings.catch_warnings(record=True): warnings.simplefilter("always") summarize_fit(run_dir) - json_path = os.path.join(run_dir, "tfs_summarize_fit_summary.json") + json_path = os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json") assert os.path.exists(json_path) with open(json_path) as fh: data = json.load(fh) @@ -533,7 +611,7 @@ def test_missing_binding_csv_gives_null_theta_training(self, run_dir, tmp_path): with warnings.catch_warnings(record=True): warnings.simplefilter("always") summarize_fit(run_dir) - with open(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) as fh: + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["training"] is None @@ -542,7 +620,7 @@ def test_missing_config_still_writes_json(self, run_dir): with warnings.catch_warnings(record=True): warnings.simplefilter("always") summarize_fit(run_dir) - assert os.path.exists(os.path.join(run_dir, "tfs_summarize_fit_summary.json")) + assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) def test_empty_run_dir_writes_json_with_nulls(self, tmp_path): empty_dir = str(tmp_path / "empty") @@ -550,7 +628,7 @@ def test_empty_run_dir_writes_json_with_nulls(self, tmp_path): with warnings.catch_warnings(record=True): warnings.simplefilter("always") summarize_fit(empty_dir) - json_path = os.path.join(empty_dir, "tfs_summarize_fit_summary.json") + json_path = os.path.join(empty_dir, "summary", "tfs_summarize_fit_summary.json") assert os.path.exists(json_path) with open(json_path) as fh: data = json.load(fh) @@ -743,6 +821,37 @@ def test_only_genotypes_in_both_dfs_are_plotted(self, tmp_path): str(tmp_path / "out")) assert mock_plot.call_count == 2 + # ------------------------------------------------------------------ + # CSV output: one CSV per genotype alongside each PDF + # ------------------------------------------------------------------ + + def test_writes_csv_per_genotype(self, tmp_path): + out_prefix = str(tmp_path / "out") + mock_ax = self._make_mock_ax() + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax): + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, _MOCK_PRED_FOR_THETA, out_prefix) + for geno in ["wt", "A1B", "C2D"]: + csv_path = f"{out_prefix}_{geno}_theta_fits.csv" + assert os.path.exists(csv_path), f"Missing CSV for {geno}" + df = pd.read_csv(csv_path) + assert "theta_obs" in df.columns + assert "median" in df.columns + assert all(df["genotype"] == geno) + + def test_csv_written_before_pdf(self, tmp_path): + """CSV must exist even if savefig raises.""" + out_prefix = str(tmp_path / "out") + mock_ax = self._make_mock_ax() + mock_ax.get_figure.return_value.savefig.side_effect = RuntimeError("render failed") + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + with patch(_PATCH_PLOT_THETA_FITS, return_value=mock_ax): + _try_plot_theta_fits(_MOCK_BINDING_WITH_STD, _MOCK_PRED_FOR_THETA, out_prefix) + # savefig raised inside the try block so warn should fire, but the + # CSV for the first genotype (A1B, alphabetically first) must exist + # because it is written before savefig is called + assert os.path.exists(f"{out_prefix}_A1B_theta_fits.csv") + # ------------------------------------------------------------------ # Guard: plot raises # ------------------------------------------------------------------ @@ -969,6 +1078,31 @@ def test_passes_none_genotypes_when_binding_df_is_none(self, tmp_path): _, kwargs = mock_pdf.call_args assert kwargs["genotypes"] is None + # ------------------------------------------------------------------ + # CSV output: one CSV per genotype alongside each trajectory PDF + # ------------------------------------------------------------------ + + def test_writes_csv_per_genotype(self, tmp_path): + (tmp_path / "run_posterior.h5").touch() + out_prefix = str(tmp_path / "out") + mock_fig = MagicMock() + with patch(_PATCH_READ_CONFIG, return_value=(MagicMock(), {})), \ + patch(_PATCH_PREDICT_DF, return_value=_MOCK_PRED_DF), \ + patch(_PATCH_PLOT_GENO, return_value=mock_fig): + _try_plot_trajectories( + config_file=str(tmp_path / "cfg.yaml"), + config_yaml=_CONFIG_WITH_GROWTH, + run_dir=str(tmp_path), + out_prefix=out_prefix, + binding_df=_MOCK_BINDING_DF, + ) + for geno in sorted(_MOCK_PRED_DF["genotype"].unique().tolist(), key=str): + safe = geno.replace("/", "_").replace(" ", "_") + csv_path = f"{out_prefix}_{safe}_trajectory.csv" + assert os.path.exists(csv_path), f"Missing CSV for {geno}" + df = pd.read_csv(csv_path) + assert all(df["genotype"] == geno) + # ------------------------------------------------------------------ # Guard: predict_geno_trajectory_df raises # ------------------------------------------------------------------ From f96a5b40c0a344dbce3a1daee01da961a4f61136 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 9 Jun 2026 10:56:58 -0700 Subject: [PATCH 09/24] improved simulate examples --- examples/simulate/simulate_config.yaml | 109 ++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/examples/simulate/simulate_config.yaml b/examples/simulate/simulate_config.yaml index cde1b22a..e1f9b431 100644 --- a/examples/simulate/simulate_config.yaml +++ b/examples/simulate/simulate_config.yaml @@ -43,7 +43,29 @@ spiked_seqs: # --------------------------------------------------------------------------- # Registered theta component name. Any key in model_registry["theta"] is -# valid except "_simple" (calibration-only). Examples: "hill_geno", "thermo.O2_C12_K5_U0_a.PK". +# valid except "_simple" (calibration-only). +# +# Thermodynamic components (physics-based operator-occupancy model): +# "thermo.O2_C12_K5_U0_a.PK" — dimer with operator architecture O2, 12-mer +# cooperative lattice, 5 binding sites, etc. +# (other thermo.* keys available — see model_registry) +# +# Perturbation-based components (wildtype reference + per-genotype perturbations): +# "hill_geno" — per-genotype Hill equation; each genotype's (theta_low, +# theta_high, K, n) drawn independently around a WT reference. +# Supports four phenotype categories: normal, stuck-bound, +# never-binds, inverted-logic. +# "hill_mut" — mutation-decomposed Hill equation; per-mutation additive +# deltas in transformed parameter space, assembled via +# matrix multiplication. Pairwise epistasis sampled from a +# regularized horseshoe prior when the library includes +# double mutants. Wildtype genotype always receives the +# exact reference curve. +# +# Perturbation-based components ignore theta_priors and instead use +# theta_sim_priors (see below). They do not call define_model; tfs-simulate +# will fall back to prior-predictive sampling only for components that lack a +# simulate() function. theta_component: "thermo.O2_C12_K5_U0_a.PK" # Optional: path to structural/thermodynamic data file required by PnnC and @@ -52,12 +74,61 @@ theta_component: "thermo.O2_C12_K5_U0_a.PK" # .csv → per-mutation per-structure ddG prior means (PddG) # thermo_data: null -# Optional: overrides for the theta component's default hyperparameters. +# Optional: overrides for the theta component's default model hyperparameters. # Any key returned by the component's get_hyperparameters() can be overridden. +# Used by thermodynamic/hierarchical components; ignored when theta_component +# is "hill_geno" or "hill_mut" (those use theta_sim_priors instead). # Omit this block to use all component defaults. # theta_priors: # theta_tf_total_M: 3.3e-7 +# Optional: overrides for perturbation-based simulation priors (SimPriors). +# Used only when theta_component is "hill_geno" or "hill_mut". +# Any key returned by the component's get_sim_hyperparameters() can be overridden. +# Omit this block to use all component defaults. +# +# --- hill_geno SimPriors keys --- +# Wildtype reference curve (a full-range decreasing Hill curve by default): +# wt_theta_low: 0.99 # theta at zero ligand (near fully bound) +# wt_theta_high: 0.01 # theta at saturating ligand (near unbound) +# wt_log_K: -4.1 # ln(K_D in mM); sets midpoint (lac/IPTG default) +# wt_hill_n: 2.0 # Hill coefficient (cooperativity) +# Per-genotype Normal perturbation widths (in logit/log space): +# sigma_logit_low: 0.5 # spread on logit(theta_low) +# sigma_logit_delta: 0.5 # spread on logit_delta = logit_high − logit_low +# sigma_log_K: 0.5 # spread on log(K) — primary binding-affinity effect +# sigma_log_n: 0.3 # spread on log(hill_n) +# Mixture fractions for special phenotype categories (must sum to < 1; +# remainder is "normal"): +# p_stuck_bound: 0.05 # fraction whose theta ≈ 1 everywhere +# p_never_binds: 0.05 # fraction whose theta ≈ 0 everywhere +# p_inverted: 0.02 # fraction with inverted logic (theta rises with ligand) +# +# --- hill_mut SimPriors keys --- +# Wildtype reference (shared with hill_geno defaults): +# wt_theta_low: 0.99 +# wt_theta_high: 0.01 +# wt_log_K: -4.1 +# wt_hill_n: 2.0 +# Per-mutation delta widths (additive in logit/log space): +# sigma_d_logit_low: 0.3 +# sigma_d_logit_delta: 0.5 +# sigma_d_log_K: 0.5 # largest effect — primarily shifts binding affinity +# sigma_d_log_n: 0.3 +# Pairwise epistasis (regularized horseshoe; used when library has doubles): +# epi_tau_scale: 0.1 # HalfCauchy scale for global sparsity τ +# # set to 0.0 for exactly no epistasis +# epi_slab_scale: 2.0 # typical large-epistasis effect size +# epi_slab_df: 4.0 # InvGamma slab degrees of freedom +# +# Example — stress-test epistasis recovery: +# theta_sim_priors: +# epi_tau_scale: 1.0 # strong, dense epistasis +# +# Example — simulate with no epistasis at all: +# theta_sim_priors: +# epi_tau_scale: 0.0 + # Integer seed for the JAX RNG used when sampling theta from the prior. theta_rng_seed: 0 @@ -113,6 +184,9 @@ growth: dk_geno_hyper_loc: -3.5 dk_geno_hyper_scale: 1.0 dk_geno_hyper_shift: 0.02 +# Set dk_geno_zero: true to pin every genotype's dk_geno to 0, skipping the +# stochastic draw above. When set, the three dk_geno_hyper_* keys are optional. +# dk_geno_zero: true # Per-genotype TF activity (A) — a scalar that multiplies the theta contribution # to growth: k = b + A * m * theta. The wild-type is fixed at activity_wt=1.0 @@ -220,3 +294,34 @@ random_seed: null # titrant_name: iptg # titrant_conc: [0, 0.0001, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0] # noise: 0.02 + +# --------------------------------------------------------------------------- +# Pre-split data simulation (optional) +# --------------------------------------------------------------------------- + +# tfs-simulate writes a presplit CSV when a 'presplit_data' key is present +# in this file. To enable it, ADD the following lines (uncommented) anywhere +# at the top level of this config — for example, right here: +# +# presplit_data: +# noise: 0.0 +# +# The 'noise' sub-key is optional; omitting it (or setting noise: 0.0) uses +# sequencing-count uncertainty only. The minimal activation is just: +# +# presplit_data: +# +# ---- What the presplit CSV represents ------------------------------------- +# This simulates the sequencing aliquot taken at t = -t_pre, just before +# the culture is split into titrant-concentration conditions. Because it is +# taken before any pre-selection growth, each ln_cfu value directly +# constrains ln_cfu0 in the hierarchical model. +# +# Output columns: replicate, condition_pre, genotype, ln_cfu, ln_cfu_std, +# ln_cfu_0_true (ground truth for validation). +# One row per (replicate, condition_pre, genotype). +# +# Pass the resulting file to tfs-configure-model with --presplit_df. +# +# noise (float, default 0): extra Gaussian noise on the ln-scale, added on +# top of sequencing-count uncertainty. Useful for benchmarking. From 8d1fec6f0fb6139e66ad64c0f8baed95f93635f1 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Wed, 10 Jun 2026 12:42:17 -0400 Subject: [PATCH 10/24] fixed bug that scrambled genotypes on growth prediction --- reports/flake.txt | 568 +++++++++--------- reports/junit/junit.xml | 2 +- src/tfscreen/simulate/selection_experiment.py | 1 + src/tfscreen/tfmodel/analysis/prediction.py | 20 +- src/tfscreen/tfmodel/model_orchestrator.py | 12 +- 5 files changed, 313 insertions(+), 290 deletions(-) diff --git a/reports/flake.txt b/reports/flake.txt index 60070bcf..25db98b5 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -955,7 +955,7 @@ ./src/tfscreen/plot/plot_theta_fits.py:134:1: W293 blank line contains whitespace ./src/tfscreen/plot/plot_theta_fits.py:139:38: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:141:1: W293 blank line contains whitespace -./src/tfscreen/plot/plot_theta_fits.py:144:18: E231 missing whitespace after ',' +./src/tfscreen/plot/plot_theta_fits.py:144:22: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:153:1: W391 blank line at end of file ./src/tfscreen/plot/uncertainty_calibration.py:12:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/plot/uncertainty_calibration.py:21:5: E125 continuation line with same indent as next logical line @@ -1341,144 +1341,144 @@ ./src/tfscreen/simulate/selection_experiment.py:44:36: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:45:36: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:46:36: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:70:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:71:1: C901 '_check_dict_number' is too complex (13) -./src/tfscreen/simulate/selection_experiment.py:71:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:114:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:157:1: C901 '_check_cf' is too complex (26) -./src/tfscreen/simulate/selection_experiment.py:179:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:201:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:203:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:203:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:203:65: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:203:85: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:226:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:71:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:72:1: C901 '_check_dict_number' is too complex (13) +./src/tfscreen/simulate/selection_experiment.py:72:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:115:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:158:1: C901 '_check_cf' is too complex (26) +./src/tfscreen/simulate/selection_experiment.py:180:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:202:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:204:48: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:204:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:204:65: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:204:85: E231 missing whitespace after ',' ./src/tfscreen/simulate/selection_experiment.py:227:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:293:1: C901 '_check_lib_spec' is too complex (12) -./src/tfscreen/simulate/selection_experiment.py:293:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:345:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:350:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:372:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:425:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:441:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:449:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:449:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:459:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:469:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:512:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:516:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:517:75: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:527:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:530:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:576:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:587:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:588:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:591:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:595:55: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:601:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:606:30: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:607:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:611:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:653:51: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:659:35: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:664:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:681:50: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:681:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:683:75: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:685:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:686:46: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:687:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:688:67: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:738:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:755:79: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:761:29: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:803:43: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:809:74: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:810:18: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:816:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:818:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:823:80: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:828:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:834:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:845:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:888:73: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:893:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:910:25: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1011:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1019:26: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1019:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1019:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1019:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1019:60: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1027:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1028:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1036:42: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1045:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1046:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1048:63: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1054:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1056:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1063:28: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1065:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1066:24: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1073:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1078:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1079:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1081:36: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1083:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1087:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1093:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:228:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:294:1: C901 '_check_lib_spec' is too complex (12) +./src/tfscreen/simulate/selection_experiment.py:294:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:346:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:351:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:373:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:426:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:442:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:450:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:450:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:460:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:470:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:513:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:517:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:518:75: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:528:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:531:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:577:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:588:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:589:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:592:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:596:55: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:602:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:607:30: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:608:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:612:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:654:51: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:660:35: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:665:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:682:50: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:682:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:684:75: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:686:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:687:46: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:688:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:689:67: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:739:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:756:79: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:762:29: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:804:43: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:810:74: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:811:18: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:817:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:819:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:824:80: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:829:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:835:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:846:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:889:73: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:894:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:911:25: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1012:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1020:26: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1020:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1020:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1020:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1020:60: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1028:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1029:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1037:42: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1046:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1047:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1049:63: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1055:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1057:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1064:28: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1066:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1067:24: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1074:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1079:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1080:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1082:36: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1084:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1088:1: W293 blank line contains whitespace ./src/tfscreen/simulate/selection_experiment.py:1094:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1098:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1099:72: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1103:34: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1110:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1112:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1112:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1095:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1099:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1100:72: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1104:34: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1111:1: W293 blank line contains whitespace ./src/tfscreen/simulate/selection_experiment.py:1113:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1113:56: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1115:32: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1116:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1128:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1136:41: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1143:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1145:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1150:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1151:37: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1151:45: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1151:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1151:61: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1156:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:1191:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1212:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1213:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1216:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1216:39: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1221:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1227:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1230:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1238:33: E261 at least two spaces before inline comment -./src/tfscreen/simulate/selection_experiment.py:1239:49: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1240:37: E231 missing whitespace after ':' -./src/tfscreen/simulate/selection_experiment.py:1242:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1243:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1245:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1247:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1255:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1256:43: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1260:41: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/selection_experiment.py:1263:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1287:71: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1295:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1296:76: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1301:11: F541 f-string is missing placeholders -./src/tfscreen/simulate/selection_experiment.py:1306:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1323:6: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/simulate/selection_experiment.py:1330:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1331:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1332:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1333:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1336:1: W391 blank line at end of file +./src/tfscreen/simulate/selection_experiment.py:1113:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1114:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1114:56: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1116:32: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1117:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1129:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1137:41: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1144:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1146:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1151:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1152:37: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1152:45: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1152:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1152:61: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1157:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:1192:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1213:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1214:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1217:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1217:39: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1222:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1228:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1231:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1239:33: E261 at least two spaces before inline comment +./src/tfscreen/simulate/selection_experiment.py:1240:49: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1241:37: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:1243:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1244:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1246:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1248:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1256:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1257:43: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1261:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/selection_experiment.py:1264:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1288:71: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1296:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1297:76: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1302:11: F541 f-string is missing placeholders +./src/tfscreen/simulate/selection_experiment.py:1307:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1324:6: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/simulate/selection_experiment.py:1331:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1332:48: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1333:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1334:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1337:1: W391 blank line at end of file ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent @@ -1513,23 +1513,23 @@ ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:45:12: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:99:16: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:120:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/analysis/prediction.py:11:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:70:1: C901 'copy_orchestrator' is too complex (11) -./src/tfscreen/tfmodel/analysis/prediction.py:195:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/prediction.py:241:1: C901 'predict' is too complex (22) -./src/tfscreen/tfmodel/analysis/prediction.py:302:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:368:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:371:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:376:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:379:67: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:380:76: W291 trailing whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:384:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:391:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/analysis/prediction.py:400:58: E741 ambiguous variable name 'l' -./src/tfscreen/tfmodel/analysis/prediction.py:403:26: E111 indentation is not a multiple of 4 -./src/tfscreen/tfmodel/analysis/prediction.py:403:26: E117 over-indented -./src/tfscreen/tfmodel/analysis/prediction.py:406:25: E122 continuation line missing indentation or outdented -./src/tfscreen/tfmodel/analysis/prediction.py:412:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:12:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/analysis/prediction.py:71:1: C901 'copy_orchestrator' is too complex (11) +./src/tfscreen/tfmodel/analysis/prediction.py:196:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/analysis/prediction.py:242:1: C901 'predict' is too complex (22) +./src/tfscreen/tfmodel/analysis/prediction.py:303:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:369:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:372:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:377:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:380:67: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:381:76: W291 trailing whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:385:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:392:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/analysis/prediction.py:401:58: E741 ambiguous variable name 'l' +./src/tfscreen/tfmodel/analysis/prediction.py:404:26: E111 indentation is not a multiple of 4 +./src/tfscreen/tfmodel/analysis/prediction.py:404:26: E117 over-indented +./src/tfscreen/tfmodel/analysis/prediction.py:407:25: E122 continuation line missing indentation or outdented +./src/tfscreen/tfmodel/analysis/prediction.py:413:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prior_predictive.py:104:33: E203 whitespace before ':' ./src/tfscreen/tfmodel/analysis/sbc.py:130:1: C901 'summarize_sbc' is too complex (17) ./src/tfscreen/tfmodel/configuration_io.py:19:1: E302 expected 2 blank lines, found 1 @@ -3488,137 +3488,135 @@ ./src/tfscreen/tfmodel/model_orchestrator.py:461:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:466:52: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:467:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:472:80: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:474:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:478:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:494:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:495:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:515:82: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:520:87: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:523:84: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:528:79: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:610:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) -./src/tfscreen/tfmodel/model_orchestrator.py:610:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:635:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:636:28: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:638:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:639:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:639:48: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:639:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:640:46: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:640:66: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:484:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:500:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:501:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:521:82: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:526:87: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:529:84: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:534:79: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:616:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) +./src/tfscreen/tfmodel/model_orchestrator.py:616:5: E303 too many blank lines (2) ./src/tfscreen/tfmodel/model_orchestrator.py:641:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:645:39: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:646:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:651:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:658:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E116 unexpected indentation (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:662:76: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:665:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:667:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:669:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:681:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:689:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:711:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:712:41: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:712:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:713:44: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:713:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:714:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:714:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:715:46: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:715:71: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:717:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:724:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:729:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:729:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:729:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:736:40: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:782:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:783:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:784:26: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:785:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:825:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:828:78: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:829:32: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:830:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:834:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:835:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:836:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:837:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:839:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:850:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:850:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:853:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:854:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:859:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:860:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:861:58: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:863:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:868:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:872:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:876:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:879:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:885:86: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:887:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:894:77: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:950:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:951:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:952:35: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:953:39: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:960:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:961:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:962:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1038:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:1039:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1041:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1114:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) -./src/tfscreen/tfmodel/model_orchestrator.py:1134:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1136:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1137:71: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1139:68: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1140:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1141:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1142:40: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1159:42: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1160:43: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1209:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1212:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1216:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:642:28: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:644:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:645:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:645:48: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:645:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:646:46: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:646:66: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:647:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:651:39: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:652:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:657:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:664:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:666:10: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:666:10: E116 unexpected indentation (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:668:76: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:671:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:673:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:675:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:687:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:695:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:717:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:718:41: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:718:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:719:44: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:719:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:720:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:720:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:721:46: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:721:71: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:723:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:730:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:735:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:735:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:735:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:742:40: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:788:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:789:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:790:26: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:791:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:831:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:834:78: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:835:32: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:836:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:840:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:841:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:842:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:843:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:845:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:856:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:856:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:859:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:860:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:865:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:866:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:867:58: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:869:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:874:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:878:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:882:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:885:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:891:86: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:893:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:900:77: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:956:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:957:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:958:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:959:39: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:966:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:967:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:968:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1044:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:1045:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1047:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1120:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) +./src/tfscreen/tfmodel/model_orchestrator.py:1140:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1142:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1143:71: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1145:68: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1146:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1147:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1148:40: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1165:42: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1166:43: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1167:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1167:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1167:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1167:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1167:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1215:75: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:1218:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1228:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1230:9: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1283:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1288:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1302:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1312:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1322:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1336:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1339:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1368:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1380:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1392:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1398:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1399:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1400:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1401:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1402:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1403:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1404:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1405:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1406:29: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1407:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1408:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1409:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1410:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1411:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1412:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1222:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1224:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1234:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1236:9: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1289:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1294:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1308:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1318:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1328:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1342:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1345:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1374:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1386:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1398:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1404:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1405:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1406:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1407:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1408:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1409:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1410:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1411:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1412:29: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1413:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1414:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1415:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1416:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1417:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1418:31: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:192:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:193:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:194:22: E128 continuation line under-indented for visual indent @@ -7689,7 +7687,7 @@ 1767 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1120 E231 missing whitespace after ',' +1119 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 228 E261 at least two spaces before inline comment @@ -7715,8 +7713,8 @@ 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -670 W291 trailing whitespace +669 W291 trailing whitespace 86 W292 no newline at end of file 1689 W293 blank line contains whitespace 29 W391 blank line at end of file -7673 +7671 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 3dbc041c..945ceb74 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/selection_experiment.py b/src/tfscreen/simulate/selection_experiment.py index 56e8168b..12bae72f 100644 --- a/src/tfscreen/simulate/selection_experiment.py +++ b/src/tfscreen/simulate/selection_experiment.py @@ -52,6 +52,7 @@ "sub_libraries", "library_combos", "spiked_seqs", "expected_5p", "expected_3p", # Phenotype / theta calculation "theta_component", "theta_rng_seed", "thermo_data", "theta_priors", "theta_rescale", + "theta_sim_priors", # Conditions and growth "condition_blocks", "growth", "dk_geno_hyper_loc", "dk_geno_hyper_scale", "dk_geno_hyper_shift", "dk_geno_zero", diff --git a/src/tfscreen/tfmodel/analysis/prediction.py b/src/tfscreen/tfmodel/analysis/prediction.py index ab0d05f7..090b6093 100644 --- a/src/tfscreen/tfmodel/analysis/prediction.py +++ b/src/tfscreen/tfmodel/analysis/prediction.py @@ -3,6 +3,7 @@ import itertools from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator from tfscreen.tfmodel.inference.posteriors import load_posteriors, get_posterior_samples +from tfscreen.tfmodel.tensors.batch import get_batch as _get_batch import jax from jax import numpy as jnp from numpyro.handlers import seed, trace @@ -416,13 +417,30 @@ def predict(orchestrator, # We need a key, even if not used for randomness in deterministic sites predict_key = jax.random.PRNGKey(0) + + # Build a canonical-indexed data view for prediction. The static + # new_orchestrator.data.growth.batch_idx uses a binding-first ordering + # (binding genotypes at positions 0..num_binding-1, then library genotypes + # at positions num_binding..N-1). Posterior samples, however, are always + # stored in canonical genotype order (index g = genotype g). Passing the + # static data to Predictive would misalign posterior params vs. mask + # lookups for all non-canonical positions. + # + # get_batch with arange(num_genotype) sets batch_idx = arange(num_genotype) + # (canonical), which is exactly what get_posteriors uses when saving + # samples. This makes per-genotype lookups (masks, offsets) consistent + # with the stored posterior values. + num_geno = new_orchestrator.data.growth.num_genotype + all_indices = jnp.arange(num_geno, dtype=jnp.int32) + pred_data = _get_batch(new_orchestrator.data, all_indices) + # Use the ORIGINAL orchestrator's priors so that any pinned hyperpriors # (e.g. activity_hyper_loc/activity_hyper_scale in the calibration model) # remain pinned at their calibrated values rather than being sampled from # the default prior. new_orchestrator is rebuilt from settings-only # (component names, no priors) so its priors are all-default. predictions = predictive(predict_key, - data=new_orchestrator.data, + data=pred_data, priors=orchestrator.priors) # ------------------------------------------------------------------------- diff --git a/src/tfscreen/tfmodel/model_orchestrator.py b/src/tfscreen/tfmodel/model_orchestrator.py index e5fb77ea..3dc2dbb5 100644 --- a/src/tfscreen/tfmodel/model_orchestrator.py +++ b/src/tfscreen/tfmodel/model_orchestrator.py @@ -469,10 +469,16 @@ def _setup_batching(growth_genotypes, num_not_binding = len(not_binding_idx) not_binding_batch_size = batch_size - num_binding - # Build idx array. The first entries correspond to the binding data and are - # the same for all rounds - idx = np.zeros(batch_size,dtype=int) + # Build idx array. The first entries correspond to the binding data and are + # the same for all rounds. Non-binding positions are filled with + # not_binding_idx[:not_binding_batch_size] as a deterministic default. + # During training, get_random_idx() overrides these entries with a fresh + # random sample each step. During prediction (no get_random_idx call), + # the static batch_idx is used directly, so it must contain valid indices. + idx = np.zeros(batch_size, dtype=int) idx[:num_binding] = binding_idx + if not_binding_batch_size > 0: + idx[num_binding:] = not_binding_idx[:not_binding_batch_size] # Calculate scale vector, which will be the same for all rounds scale_vector = np.ones(batch_size,dtype=float) From a60ca70878871b18b0f77a5477808304325965da Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Thu, 11 Jun 2026 16:31:04 -0400 Subject: [PATCH 11/24] mid development of err calibration/quantilfe refactor --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 346 ++++----- reports/junit/junit.xml | 2 +- .../analysis/cat_response/cat_response_cli.py | 14 +- .../analysis/cat_response/fit_response_cli.py | 14 +- src/tfscreen/plot/geno_trajectory.py | 26 +- src/tfscreen/plot/plot_theta_fits.py | 22 +- .../tfmodel/analysis/error_calibration.py | 642 ++++++++++++++++ .../tfmodel/analysis/prior_predictive.py | 6 +- src/tfscreen/tfmodel/analysis/sbc.py | 331 -------- src/tfscreen/tfmodel/inference/posteriors.py | 93 ++- src/tfscreen/tfmodel/model_orchestrator.py | 10 +- .../tfmodel/scripts/extract_params_cli.py | 2 +- .../tfmodel/scripts/predict_growth_cli.py | 2 +- .../tfmodel/scripts/predict_theta_cli.py | 4 +- .../scripts/run_prefit_calibration_cli.py | 10 +- .../tfmodel/scripts/summarize_fit_cli.py | 260 ++++++- .../tfmodel/scripts/summarize_sbc_cli.py | 2 +- tests/smoke-tests/test_prediction_smoke.py | 6 +- .../cat_response/test_cat_response_cli.py | 24 +- .../cat_response/test_fit_response_cli.py | 28 +- tests/tfscreen/plot/test_geno_trajectory.py | 32 +- tests/tfscreen/plot/test_plot_theta_fits.py | 20 +- tests/tfscreen/tfmodel/analysis/__init__.py | 0 .../analysis/test_error_calibration.py | 722 ++++++++++++++++++ .../scripts/test_extract_params_cli.py | 5 +- .../scripts/test_predict_growth_cli.py | 14 +- .../tfmodel/scripts/test_predict_theta_cli.py | 38 +- .../scripts/test_run_prefit_calibration.py | 12 +- .../tfmodel/scripts/test_summarize_fit_cli.py | 464 ++++++++++- .../tfmodel/scripts/test_summarize_sbc_cli.py | 2 +- .../test_extract_growth_predictions.py | 35 +- .../test_extract_parameters_congression.py | 6 +- .../tfscreen/tfmodel/test_extract_samples.py | 16 +- .../tfmodel/test_extract_theta_curves.py | 16 +- .../tfmodel/test_extraction_new_models.py | 40 +- .../tfmodel/test_extraction_robustness.py | 25 +- .../tfmodel/test_model_orchestrator.py | 16 +- tests/tfscreen/tfmodel/test_posteriors.py | 26 +- .../tfscreen/tfmodel/test_prior_predictive.py | 4 +- tests/tfscreen/tfmodel/test_sbc.py | 284 ------- 42 files changed, 2539 insertions(+), 1086 deletions(-) create mode 100644 src/tfscreen/tfmodel/analysis/error_calibration.py delete mode 100644 src/tfscreen/tfmodel/analysis/sbc.py create mode 100644 tests/tfscreen/tfmodel/analysis/__init__.py create mode 100644 tests/tfscreen/tfmodel/analysis/test_error_calibration.py delete mode 100644 tests/tfscreen/tfmodel/test_sbc.py diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 0b567b26..fca926e5 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.95%coverage95.95% \ No newline at end of file +coverage: 95.97%coverage95.97% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 8f6d58ee..e40398f3 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3253tests3253 \ No newline at end of file +tests: 3347tests3347 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 25db98b5..ff12b2a4 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -948,11 +948,9 @@ ./src/tfscreen/plot/plot_theta_fits.py:108:46: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:115:42: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:121:21: E231 missing whitespace after ',' -./src/tfscreen/plot/plot_theta_fits.py:124:28: E231 missing whitespace after ',' -./src/tfscreen/plot/plot_theta_fits.py:125:1: W293 blank line contains whitespace +./src/tfscreen/plot/plot_theta_fits.py:124:25: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:130:38: E231 missing whitespace after ',' -./src/tfscreen/plot/plot_theta_fits.py:133:27: E231 missing whitespace after ',' -./src/tfscreen/plot/plot_theta_fits.py:134:1: W293 blank line contains whitespace +./src/tfscreen/plot/plot_theta_fits.py:133:25: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:139:38: E231 missing whitespace after ',' ./src/tfscreen/plot/plot_theta_fits.py:141:1: W293 blank line contains whitespace ./src/tfscreen/plot/plot_theta_fits.py:144:22: E231 missing whitespace after ',' @@ -1496,6 +1494,8 @@ ./src/tfscreen/tfmodel/__init__.py:41:77: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:43:56: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:46:1: W391 blank line at end of file +./src/tfscreen/tfmodel/analysis/error_calibration.py:472:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/analysis/error_calibration.py:509:1: C901 'summarize_sbc' is too complex (16) ./src/tfscreen/tfmodel/analysis/extraction.py:81:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:85:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:103:1: E302 expected 2 blank lines, found 1 @@ -1531,7 +1531,6 @@ ./src/tfscreen/tfmodel/analysis/prediction.py:407:25: E122 continuation line missing indentation or outdented ./src/tfscreen/tfmodel/analysis/prediction.py:413:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/prior_predictive.py:104:33: E203 whitespace before ':' -./src/tfscreen/tfmodel/analysis/sbc.py:130:1: C901 'summarize_sbc' is too complex (17) ./src/tfscreen/tfmodel/configuration_io.py:19:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/configuration_io.py:37:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/configuration_io.py:116:1: C901 'write_configuration' is too complex (31) @@ -3318,11 +3317,9 @@ ./src/tfscreen/tfmodel/generative/registry.py:131:18: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/generative/registry.py:133:22: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/generative/registry.py:134:21: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/inference/posteriors.py:4:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/inference/posteriors.py:42:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/inference/posteriors.py:50:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/inference/posteriors.py:84:17: F841 local variable 'e' is assigned to but never used -./src/tfscreen/tfmodel/inference/posteriors.py:115:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/inference/posteriors.py:72:1: C901 'load_posteriors' is too complex (12) +./src/tfscreen/tfmodel/inference/posteriors.py:72:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/inference/posteriors.py:117:17: F841 local variable 'e' is assigned to but never used ./src/tfscreen/tfmodel/inference/run_inference.py:21:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/inference/run_inference.py:28:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/inference/run_inference.py:28:1: E402 module level import not at top of file @@ -3488,135 +3485,136 @@ ./src/tfscreen/tfmodel/model_orchestrator.py:461:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:466:52: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:467:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:484:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:500:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:501:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:521:82: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:526:87: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:529:84: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:534:79: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:616:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) -./src/tfscreen/tfmodel/model_orchestrator.py:616:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:474:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:478:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:494:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:495:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:515:82: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:520:87: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:523:84: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:528:79: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:610:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) +./src/tfscreen/tfmodel/model_orchestrator.py:610:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:635:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:636:28: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:638:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:639:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:639:48: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:639:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:640:46: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:640:66: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:641:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:642:28: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:644:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:645:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:645:48: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:645:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:646:46: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:646:66: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:647:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:651:39: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:652:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:657:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:664:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:666:10: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:666:10: E116 unexpected indentation (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:668:76: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:671:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:673:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:675:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:687:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:695:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:717:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:718:41: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:718:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:719:44: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:719:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:720:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:720:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:721:46: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:721:71: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:723:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:730:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:735:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:735:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:735:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:742:40: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:788:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:789:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:790:26: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:791:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:831:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:834:78: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:835:32: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:836:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:840:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:841:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:842:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:843:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:845:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:856:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:856:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:859:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:860:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:865:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:866:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:867:58: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:869:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:874:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:878:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:882:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:885:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:891:86: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:893:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:900:77: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:956:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:957:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:958:35: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:959:39: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:966:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:967:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:968:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1044:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:1045:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1047:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1120:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) -./src/tfscreen/tfmodel/model_orchestrator.py:1140:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1142:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1143:71: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1145:68: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1146:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1147:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1148:40: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1165:42: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1166:43: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1167:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1167:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1167:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1167:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1167:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1215:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:645:39: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:646:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:651:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:658:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E116 unexpected indentation (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:662:76: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:665:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:667:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:669:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:681:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:689:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:711:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:712:41: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:712:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:713:44: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:713:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:714:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:714:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:715:46: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:715:71: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:717:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:724:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:729:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:729:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:729:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:736:40: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:782:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:783:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:784:26: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:785:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:825:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:828:78: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:829:32: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:830:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:834:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:835:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:836:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:837:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:839:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:850:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:850:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:853:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:854:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:859:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:860:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:861:58: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:863:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:868:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:872:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:876:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:879:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:885:86: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:887:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:894:77: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:950:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:951:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:952:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:953:39: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:960:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:961:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:962:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1038:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:1039:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1041:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1114:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) +./src/tfscreen/tfmodel/model_orchestrator.py:1134:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1136:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1137:71: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1139:68: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1140:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1141:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1142:40: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1159:42: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1160:43: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1161:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1209:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1212:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1216:75: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:1218:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1222:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1224:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1234:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1236:9: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1289:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1294:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1308:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1318:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1328:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1342:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1345:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1374:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1386:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1398:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1404:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1405:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1406:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1407:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1408:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1409:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1410:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1411:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1412:29: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1413:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1414:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1415:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1416:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1417:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1418:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1228:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1230:9: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1283:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1288:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1302:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1312:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1322:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1336:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1339:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1368:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1380:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1392:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1398:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1399:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1400:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1401:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1402:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1403:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1404:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1405:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1406:29: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1407:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1408:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1409:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1410:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1411:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1412:31: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:192:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:193:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:194:22: E128 continuation line under-indented for visual indent @@ -3678,8 +3676,9 @@ ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:152:1: C901 '_read_all_losses' is too complex (11) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:234:1: C901 'summarize_fit' is too complex (59) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:153:1: C901 '_read_all_losses' is too complex (11) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:370:1: C901 '_summarize_params' is too complex (23) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:477:1: C901 'summarize_fit' is too complex (59) ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:47:30: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:76:1: C901 'summarize_grid' is too complex (14) ./src/tfscreen/tfmodel/tensors/batch.py:5:1: E302 expected 2 blank lines, found 1 @@ -5382,6 +5381,8 @@ ./tests/tfscreen/simulate/test_thermo_to_growth.py:229:35: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:1163:1: E402 module level import not at top of file ./tests/tfscreen/simulate/test_thermo_to_growth.py:1251:9: F841 local variable 'G_out' is assigned to but never used +./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:312:9: F401 'matplotlib.axes.Axes' imported but unused +./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:354:9: F401 'matplotlib.axes.Axes' imported but unused ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 @@ -6538,6 +6539,8 @@ ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:23:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:900:18: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:903:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1168:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1348:14: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused @@ -6728,15 +6731,12 @@ ./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:50:47: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:51:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:56:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:64:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:71:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:78:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:84:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:89:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:94:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:99:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:120:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:81:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:86:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:91:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:96:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extract_growth_predictions.py:117:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_extract_parameters_congression.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_extract_parameters_congression.py:30:128: E501 line too long (130 > 127 characters) ./tests/tfscreen/tfmodel/test_extract_parameters_congression.py:37:1: E302 expected 2 blank lines, found 1 @@ -6766,8 +6766,6 @@ ./tests/tfscreen/tfmodel/test_extract_theta_curves.py:38:71: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/test_extract_theta_curves.py:43:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_extract_theta_curves.py:46:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extract_theta_curves.py:53:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extract_theta_curves.py:57:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_extract_theta_curves.py:65:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_extract_theta_curves.py:71:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_extract_theta_curves.py:73:1: W293 blank line contains whitespace @@ -6799,7 +6797,7 @@ ./tests/tfscreen/tfmodel/test_extraction_new_models.py:209:17: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_extraction_new_models.py:211:13: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_extraction_new_models.py:399:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/test_extraction_new_models.py:407:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/test_extraction_new_models.py:407:41: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_extraction_new_models.py:479:12: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_extraction_new_models.py:480:12: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_extraction_new_models.py:481:12: E221 multiple spaces before operator @@ -6822,18 +6820,18 @@ ./tests/tfscreen/tfmodel/test_extraction_robustness.py:224:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_extraction_robustness.py:268:5: F841 local variable 'first_param' is assigned to but never used ./tests/tfscreen/tfmodel/test_extraction_robustness.py:270:5: F841 local variable 'param_name' is assigned to but never used -./tests/tfscreen/tfmodel/test_extraction_robustness.py:281:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extraction_robustness.py:284:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extraction_robustness.py:287:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extraction_robustness.py:293:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extraction_robustness.py:298:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extraction_robustness.py:311:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extraction_robustness.py:315:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extraction_robustness.py:334:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extraction_robustness.py:374:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extraction_robustness.py:380:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_extraction_robustness.py:387:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_extraction_robustness.py:390:9: F841 local variable 'ds' is assigned to but never used +./tests/tfscreen/tfmodel/test_extraction_robustness.py:282:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extraction_robustness.py:285:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extraction_robustness.py:288:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extraction_robustness.py:294:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extraction_robustness.py:299:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extraction_robustness.py:312:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extraction_robustness.py:316:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extraction_robustness.py:335:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extraction_robustness.py:375:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extraction_robustness.py:381:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_extraction_robustness.py:388:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_extraction_robustness.py:391:9: F841 local variable 'ds' is assigned to but never used ./tests/tfscreen/tfmodel/test_fit_model_helpers.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_fit_model_helpers.py:42:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_fit_model_helpers.py:78:1: E302 expected 2 blank lines, found 1 @@ -7083,7 +7081,6 @@ ./tests/tfscreen/tfmodel/test_model_orchestrator.py:584:25: W291 trailing whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:586:34: W291 trailing whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:587:33: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:594:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:599:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:43: E231 missing whitespace after ',' ./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:45: E231 missing whitespace after ',' @@ -7119,7 +7116,6 @@ ./tests/tfscreen/tfmodel/test_populate_dataclass.py:133:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_populate_dataclass.py:137:44: W292 no newline at end of file ./tests/tfscreen/tfmodel/test_posteriors.py:6:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_posteriors.py:10:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_posteriors.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_posteriors.py:20:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_posteriors.py:22:1: W293 blank line contains whitespace @@ -7128,10 +7124,7 @@ ./tests/tfscreen/tfmodel/test_posteriors.py:37:14: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/test_posteriors.py:38:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_posteriors.py:45:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_posteriors.py:50:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_posteriors.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_posteriors.py:57:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_posteriors.py:60:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_posteriors.py:65:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_posteriors.py:72:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_posteriors.py:75:1: W293 blank line contains whitespace @@ -7365,7 +7358,6 @@ ./tests/tfscreen/tfmodel/test_run_inference.py:903:25: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/test_run_inference.py:903:26: E201 whitespace after '[' ./tests/tfscreen/tfmodel/test_run_inference.py:904:26: E201 whitespace after '[' -./tests/tfscreen/tfmodel/test_sbc.py:6:1: F401 'tempfile' imported but unused ./tests/tfscreen/tfmodel/test_share_replicates.py:1:1: F401 'pytest' imported but unused ./tests/tfscreen/tfmodel/test_share_replicates.py:3:1: F401 'numpy as np' imported but unused ./tests/tfscreen/tfmodel/test_share_replicates.py:7:1: E302 expected 2 blank lines, found 1 @@ -7669,7 +7661,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -46 C901 'cat_fit' is too complex (19) +48 C901 'cat_fit' is too complex (19) 15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7677,17 +7669,17 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -165 E127 continuation line over-indented for visual indent +166 E127 continuation line over-indented for visual indent 79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1767 E221 multiple spaces before operator +1768 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1119 E231 missing whitespace after ',' +1120 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 228 E261 at least two spaces before inline comment @@ -7696,12 +7688,12 @@ 2 E266 too many leading '#' for block comment 148 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -802 E302 expected 2 blank lines, found 1 +801 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) 17 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line -36 E402 module level import not at top of file +37 E402 module level import not at top of file 71 E501 line too long (145 > 127 characters) 46 E701 multiple statements on one line (colon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' @@ -7709,12 +7701,12 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 20 E741 ambiguous variable name 'l' -222 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +223 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used 669 W291 trailing whitespace 86 W292 no newline at end of file -1689 W293 blank line contains whitespace +1675 W293 blank line contains whitespace 29 W391 blank line at end of file -7671 +7663 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 945ceb74..330d456c 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/cat_response_cli.py b/src/tfscreen/analysis/cat_response/cat_response_cli.py index 8747e559..236f58fb 100644 --- a/src/tfscreen/analysis/cat_response/cat_response_cli.py +++ b/src/tfscreen/analysis/cat_response/cat_response_cli.py @@ -65,25 +65,25 @@ def cat_response(theta_file, df = pd.read_csv(theta_file) if theta_col is None: - if "median" in df.columns: - theta_col = "median" + if "q0.5" in df.columns: + theta_col = "q0.5" elif "point_est" in df.columns: theta_col = "point_est" else: raise ValueError( - "No theta column found. Expected 'median' (posterior) or " + "No theta column found. Expected 'q0.5' (posterior median) or " "'point_est' (MAP). Use --theta_col to specify a column explicitly." ) if sigma_col is None: - if "upper_std" in df.columns and "lower_std" in df.columns: + if "q0.841" in df.columns and "q0.159" in df.columns: df = df.copy() - df["_sigma"] = (df["upper_std"] - df["lower_std"]) / 2 + df["_sigma"] = (df["q0.841"] - df["q0.159"]) / 2 sigma_col = "_sigma" else: raise ValueError( - "No sigma_col specified and df lacks upper_std/lower_std columns. " - "Provide --sigma_col or ensure upper_std and lower_std are present." + "No sigma_col specified and df lacks q0.841/q0.159 columns. " + "Provide --sigma_col or ensure q0.841 and q0.159 are present." ) work_items = [] diff --git a/src/tfscreen/analysis/cat_response/fit_response_cli.py b/src/tfscreen/analysis/cat_response/fit_response_cli.py index 4f9541cc..edfa2f30 100644 --- a/src/tfscreen/analysis/cat_response/fit_response_cli.py +++ b/src/tfscreen/analysis/cat_response/fit_response_cli.py @@ -61,25 +61,25 @@ def fit_response(df, raise ValueError(f"Unknown model(s): {bad}. Valid: {list(MODEL_LIBRARY)}") if theta_col is None: - if "median" in df.columns: - theta_col = "median" + if "q0.5" in df.columns: + theta_col = "q0.5" elif "point_est" in df.columns: theta_col = "point_est" else: raise ValueError( - "No theta column found. Expected 'median' (posterior) or " + "No theta column found. Expected 'q0.5' (posterior median) or " "'point_est' (MAP). Pass theta_col explicitly to override." ) if sigma_col is None: - if "upper_std" in df.columns and "lower_std" in df.columns: + if "q0.841" in df.columns and "q0.159" in df.columns: df = df.copy() - df["_sigma"] = (df["upper_std"] - df["lower_std"]) / 2 + df["_sigma"] = (df["q0.841"] - df["q0.159"]) / 2 sigma_col = "_sigma" else: raise ValueError( - "No sigma_col specified and df lacks upper_std/lower_std columns. " - "Provide --sigma_col or ensure upper_std and lower_std are present." + "No sigma_col specified and df lacks q0.841/q0.159 columns. " + "Provide --sigma_col or ensure q0.841 and q0.159 are present." ) work_items = [] diff --git a/src/tfscreen/plot/geno_trajectory.py b/src/tfscreen/plot/geno_trajectory.py index 26f5b7d3..9662be97 100644 --- a/src/tfscreen/plot/geno_trajectory.py +++ b/src/tfscreen/plot/geno_trajectory.py @@ -22,7 +22,7 @@ _KEEP_COLS = [ "replicate", "condition_pre", "condition_sel", "titrant_name", "titrant_conc", "genotype", - "t_sel", "ln_cfu", "ln_cfu_std", "q05", "median", "q95", + "t_sel", "ln_cfu", "ln_cfu_std", "q0.05", "q0.5", "q0.95", ] @@ -52,12 +52,12 @@ def plot_geno_trajectory( pre-selection phase). * ``ln_cfu`` — observed ln(CFU); ``NaN`` where no observation exists. * ``ln_cfu_std`` — observed std; ``NaN`` where no observation exists. - * ``median`` — predicted median trajectory; ``NaN`` where unavailable. + * ``q0.5`` — predicted median trajectory; ``NaN`` where unavailable. Optional columns (both must be present together to draw a credible interval): - * ``q05``, ``q95`` — 5th/95th percentile of the posterior predictive. + * ``q0.05``, ``q0.95`` — 5th/95th percentile of the posterior predictive. figsize : tuple of (float, float), optional Figure ``(width, height)`` in inches. Defaults to @@ -108,7 +108,7 @@ def plot_geno_trajectory( } rep_marker = {r: markers[i % len(markers)] for i, r in enumerate(replicates)} - has_ci = {"q05", "q95"} <= set(pred_df.columns) + has_ci = {"q0.05", "q0.95"} <= set(pred_df.columns) for combo_i, cond_row in conditions.iterrows(): ax = axes[combo_i // n_cols][combo_i % n_cols] @@ -166,11 +166,11 @@ def plot_geno_trajectory( ) # Predicted median line - line_df = rep_df.dropna(subset=["median"]) + line_df = rep_df.dropna(subset=["q0.5"]) if not line_df.empty: ax.plot( line_df["t_sel"], - line_df["median"], + line_df["q0.5"], ls, color=color, lw=1.8, @@ -180,12 +180,12 @@ def plot_geno_trajectory( # 90 % credible interval band if has_ci: - ci_df = rep_df.dropna(subset=["q05", "q95"]) + ci_df = rep_df.dropna(subset=["q0.05", "q0.95"]) if not ci_df.empty: ax.fill_between( ci_df["t_sel"], - ci_df["q05"], - ci_df["q95"], + ci_df["q0.05"], + ci_df["q0.95"], color=color, alpha=0.2, zorder=2, @@ -255,13 +255,11 @@ def predict_geno_trajectory_df( max_t_sel = float(np.nanmax(t_sel_tensor[good_mask])) t_sel_grid = np.linspace(0.0, max_t_sel, t_fine).tolist() - q_spec = {"q05": 0.05, "median": 0.5, "q95": 0.95} - all_dfs = predict( orchestrator, param_posteriors, predict_sites=["growth_pred", "ln_cfu0"], - q_to_get=q_spec, + q_to_get=[0.05, 0.5, 0.95], num_samples=None, num_marginal_samples=num_marginal_samples, t_sel=t_sel_grid, @@ -278,7 +276,7 @@ def predict_geno_trajectory_df( .drop_duplicates(subset=["replicate", "condition_pre"]) ) ln_cfu0_vals = ( - ln_cfu0_raw[["replicate", "condition_pre", "genotype", "q05", "median", "q95"]] + ln_cfu0_raw[["replicate", "condition_pre", "genotype", "q0.05", "q0.5", "q0.95"]] .drop_duplicates(subset=["replicate", "condition_pre", "genotype"]) ) valid_combos = orchestrator.growth_df[_CONDITION_COLS].drop_duplicates() @@ -313,7 +311,7 @@ def predict_geno_trajectory_df( obs_df = orchestrator.growth_df[obs_cols].copy() if genotypes is not None: obs_df = obs_df[obs_df["genotype"].isin(list(genotypes))] - for col in ("q05", "median", "q95"): + for col in ("q0.05", "q0.5", "q0.95"): obs_df[col] = np.nan # Titrant-name filter (predict() has no titrant_name argument) diff --git a/src/tfscreen/plot/plot_theta_fits.py b/src/tfscreen/plot/plot_theta_fits.py index ad5bf9de..73e7d93a 100644 --- a/src/tfscreen/plot/plot_theta_fits.py +++ b/src/tfscreen/plot/plot_theta_fits.py @@ -115,26 +115,26 @@ def plot_theta_fits(df, color=colors[counter],zorder=0) # Central estimate (posterior median or MAP point estimate) - center_col = "median" if "median" in g_df.columns else "point_est" + center_col = "q0.5" if "q0.5" in g_df.columns else "point_est" ax.plot(g_df["titrant_conc"], g_df[center_col], lw=2,color=colors[counter]) - # Standard error - if set(["lower_std","upper_std"]) <= set(g_df.columns): - + # ±1 SD credible band (q0.159 / q0.841) + if set(["q0.159","q0.841"]) <= set(g_df.columns): + ax.fill_between(x=g_df["titrant_conc"], - y1=g_df["lower_std"], - y2=g_df["upper_std"], + y1=g_df["q0.159"], + y2=g_df["q0.841"], color=colors[counter], alpha=0.7,zorder=-10) - # 95% CI - if set(["lower_95","upper_95"]) <= set(g_df.columns): - + # 95% CI (q0.025 / q0.975) + if set(["q0.025","q0.975"]) <= set(g_df.columns): + ax.fill_between(x=g_df["titrant_conc"], - y1=g_df["lower_95"], - y2=g_df["upper_95"], + y1=g_df["q0.025"], + y2=g_df["q0.975"], color=colors[counter], alpha=0.4,zorder=-20) counter += 1 diff --git a/src/tfscreen/tfmodel/analysis/error_calibration.py b/src/tfscreen/tfmodel/analysis/error_calibration.py new file mode 100644 index 00000000..5d786464 --- /dev/null +++ b/src/tfscreen/tfmodel/analysis/error_calibration.py @@ -0,0 +1,642 @@ +""" +Error calibration diagnostics for posterior distributions. + +Theory +------ +A well-calibrated posterior assigns the correct probability to every interval: +if the model says the 90 % credible interval covers the true value 90 % of the +time, it should. The central diagnostic tool is the **Probability Integral +Transform (PIT)**: for a single parameter with posterior CDF F and true value +θ*, the PIT is F(θ*). For a perfectly calibrated model, PIT values drawn +across many parameters or many independent datasets are Uniform(0, 1). + +This module supports two calibration workflows that share the same core: + +1. **Single-run calibration** (``calibration_summary``): one inference run on + simulated data with known ground truth. True values come from the fixed + simulation; posterior uncertainty comes from the marginal quantile CSVs + written by ``tfs-param-quantiles``. The PIT for each genotype/parameter + is interpolated from the stored quantiles. + +2. **Multi-run SBC** (``summarize_sbc``, Simulation-Based Calibration per + Talts et al. 2018): many independent inference runs, each with a dataset + simulated from a fresh prior draw. The PIT for each parameter element is + computed from the full posterior sample array. Pooling PIT values across + runs and testing uniformity validates that the inference algorithm correctly + implements the generative model's prior. + +Functional layers +----------------- +- **Core** : ``pit_from_samples``, ``pit_from_quantiles`` +- **Stats** : ``calibration_curve``, ``pit_uniformity_test`` +- **Plots** : ``plot_pit_histogram``, ``plot_calibration_curve`` +- **Single-run** : ``calibration_summary`` +- **SBC** : ``compute_sbc_ranks``, ``summarize_sbc`` + +References +---------- +Talts et al. (2018) "Validating Bayesian Inference Algorithms with +Simulation-Based Calibration". arXiv:1804.06788. +""" + +import glob +import os +import warnings +from typing import Dict, List, Optional, Tuple + +import h5py +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt +from scipy.stats import ks_1samp, uniform + + +# ============================================================ +# Part 1 — Core PIT computation +# ============================================================ + +def pit_from_samples(true_vals, posterior_samples): + """Compute PIT values from raw posterior samples. + + For each element the PIT is the fraction of posterior samples strictly + less than the true value — equivalently, the posterior rank of the true + value. For a perfectly calibrated model these values are Uniform(0, 1). + + Parameters + ---------- + true_vals : array-like, shape (*D) + True (ground-truth) parameter values. NaN entries propagate as NaN + in the output. + posterior_samples : array-like, shape (S, *D) + S posterior samples for each element of *D*. + + Returns + ------- + ndarray, shape (*D) + PIT values in [0, 1], NaN where ``true_vals`` is NaN. + """ + true_vals = np.asarray(true_vals, dtype=float) + posterior_samples = np.asarray(posterior_samples, dtype=float) + + pit = np.mean(posterior_samples < true_vals, axis=0).astype(float) + + nan_mask = np.isnan(true_vals) + if np.any(nan_mask): + pit = np.where(nan_mask, np.nan, pit) + + return pit + + +def pit_from_quantiles(true_vals, quantile_matrix, quantile_levels): + """Interpolate PIT values from stored posterior quantiles. + + For each observation i the PIT is estimated by linear interpolation of the + posterior CDF at ``true_vals[i]``. Values below the lowest stored + quantile receive PIT = 0; values above the highest receive PIT = 1. + + Parameters + ---------- + true_vals : array-like, shape (N,) + True (ground-truth) values, one per observation. + quantile_matrix : array-like, shape (N, Q) + Posterior quantile values; row i contains the Q quantile estimates for + observation i. Columns must be in ascending order of ``quantile_levels``. + quantile_levels : array-like, shape (Q,) + Probability levels corresponding to the columns of ``quantile_matrix``, + e.g. ``[0.025, 0.25, 0.50, 0.75, 0.975]``. Must be sorted ascending. + + Returns + ------- + ndarray, shape (N,) + Interpolated PIT values in [0, 1]. NaN where ``true_vals[i]`` is NaN + or any quantile for row i is NaN. + + Notes + ----- + Accuracy depends on the density of stored quantile levels; coarse grids + (e.g. only 2.5 % and 97.5 %) produce step-function PIT estimates. + """ + true_vals = np.asarray(true_vals, dtype=float) + quantile_matrix = np.asarray(quantile_matrix, dtype=float) + quantile_levels = np.asarray(quantile_levels, dtype=float) + + N = len(true_vals) + pit = np.full(N, np.nan) + + for i in range(N): + tv = true_vals[i] + qv = quantile_matrix[i] + if np.isnan(tv) or np.any(np.isnan(qv)): + continue + # np.interp: xp must be increasing; quantile values are non-decreasing + pit[i] = np.interp(tv, qv, quantile_levels, left=0.0, right=1.0) + + return pit + + +# ============================================================ +# Part 2 — Calibration statistics +# ============================================================ + +_DEFAULT_CALIBRATION_LEVELS = np.array([0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99]) + + +def calibration_curve(pit_vals, levels=None): + """Compute empirical coverage at multiple nominal credible-interval levels. + + For each nominal level α the empirical coverage is the fraction of PIT + values that fall in the symmetric interval [(1−α)/2, (1+α)/2]. For a + perfectly calibrated model the empirical coverage equals α at every level, + so plotting empirical vs. nominal should lie on the diagonal. + + Parameters + ---------- + pit_vals : array-like + PIT values in [0, 1]. Non-finite values are ignored. + levels : array-like, optional + Nominal coverage levels to evaluate, e.g. ``[0.5, 0.9, 0.95]``. + Defaults to ``[0.50, 0.60, 0.70, 0.80, 0.90, 0.95, 0.99]``. + + Returns + ------- + dict + Mapping ``{nominal_level (float): empirical_coverage (float)}``. + """ + if levels is None: + levels = _DEFAULT_CALIBRATION_LEVELS + pit = np.asarray(pit_vals, dtype=float) + pit = pit[np.isfinite(pit)] + + result = {} + for alpha in levels: + lower = (1.0 - float(alpha)) / 2.0 + upper = 1.0 - lower + empirical = float(np.mean((pit >= lower) & (pit <= upper))) + result[float(alpha)] = empirical + return result + + +def pit_uniformity_test(pit_vals): + """One-sample KS test of PIT values against Uniform(0, 1). + + Parameters + ---------- + pit_vals : array-like + PIT values. Non-finite values are ignored. + + Returns + ------- + dict + ``ks_stat`` (float), ``ks_pval`` (float), ``mean_pit`` (float), + ``n_vals`` (int). All numeric fields are NaN when no finite values + are present. + """ + pit = np.asarray(pit_vals, dtype=float) + pit = pit[np.isfinite(pit)] + n = int(len(pit)) + if n == 0: + return {"ks_stat": np.nan, "ks_pval": np.nan, "mean_pit": np.nan, "n_vals": 0} + mean_pit = float(np.mean(pit)) + ks_stat, ks_pval = ks_1samp(pit, uniform(0, 1).cdf) + return { + "ks_stat": float(ks_stat), + "ks_pval": float(ks_pval), + "mean_pit": mean_pit, + "n_vals": n, + } + + +# ============================================================ +# Part 3 — Plotting +# ============================================================ + +def plot_pit_histogram(pit_vals, ax=None, title=None, n_bins=10): + """Plot a PIT histogram with a uniform-density reference line. + + Parameters + ---------- + pit_vals : array-like + PIT values. Non-finite values are silently dropped. + ax : matplotlib.axes.Axes, optional + Axes to draw on. A new figure is created when *None*. + title : str, optional + Axes title. + n_bins : int + Number of histogram bins over [0, 1] (default 10). + + Returns + ------- + matplotlib.axes.Axes + """ + pit = np.asarray(pit_vals, dtype=float) + pit = pit[np.isfinite(pit)] + + if ax is None: + _, ax = plt.subplots(1, 1, figsize=(5, 4)) + + ax.hist(pit, bins=n_bins, range=(0, 1), density=True, + color="steelblue", alpha=0.75, edgecolor="white") + ax.axhline(1.0, color="firebrick", lw=1.5, ls="--", label="Uniform") + ax.set_xlim(0, 1) + ax.set_xlabel("PIT value") + ax.set_ylabel("Density") + if title is not None: + ax.set_title(title) + ax.legend(fontsize=8) + return ax + + +def plot_calibration_curve(calibration_dict, ax=None, label=None): + """Plot empirical vs. nominal coverage (calibration curve). + + A perfectly calibrated model lies on the diagonal. Points above the + diagonal indicate underconfidence (CIs too wide); points below indicate + overconfidence (CIs too narrow). + + Parameters + ---------- + calibration_dict : dict + ``{nominal_level: empirical_coverage}`` as returned by + ``calibration_curve``. + ax : matplotlib.axes.Axes, optional + Axes to draw on. A new figure is created when *None*. + label : str, optional + Legend label for the model curve. + + Returns + ------- + matplotlib.axes.Axes + """ + if ax is None: + _, ax = plt.subplots(1, 1, figsize=(5, 5)) + + if not calibration_dict: + ax.set_xlabel("Nominal coverage") + ax.set_ylabel("Empirical coverage") + return ax + + nominals = np.array(sorted(calibration_dict.keys())) + empiricals = np.array([calibration_dict[k] for k in nominals]) + + ax.plot([0, 1], [0, 1], "k--", lw=1, label="Ideal") + ax.plot(nominals, empiricals, "o-", color="steelblue", + label=label or "Model") + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.set_xlabel("Nominal coverage") + ax.set_ylabel("Empirical coverage") + ax.legend(fontsize=8) + return ax + + +# ============================================================ +# Part 4 — Single-run calibration summary +# ============================================================ + +def calibration_summary(true_vals, quantile_matrix, quantile_levels, + out_prefix, label=""): + """Compute and write calibration diagnostics for a single inference run. + + Computes PIT values via interpolation from stored quantiles, then writes + a PIT CSV, a calibration-curve CSV, and a two-panel PDF. + + **CSV files are the authoritative outputs.** The PDF is a human-readable + summary; the CSVs are the record of truth for downstream analysis. + + Output files + ------------ + ``{out_prefix}_pit.csv`` + Columns: ``true_val``, ``pit``. + ``{out_prefix}_calibration_curve.csv`` + Columns: ``nominal``, ``empirical``. + ``{out_prefix}_calibration.pdf`` + Two panels: PIT histogram (left) and calibration curve (right). + + Parameters + ---------- + true_vals : array-like, shape (N,) + Known ground-truth values from simulation. + quantile_matrix : array-like, shape (N, Q) + Posterior quantile values; row i is quantile estimates for observation + i at levels ``quantile_levels``. + quantile_levels : array-like, shape (Q,) + Probability levels (sorted ascending) corresponding to columns of + ``quantile_matrix``. + out_prefix : str + File-system prefix for all output files. + label : str, optional + Human-readable label used in plot titles. + + Returns + ------- + dict + ``{"n_vals": int, "ks_stat": float, "ks_pval": float, + "mean_pit": float, "calibration_curve": {nominal: empirical, ...}}`` + """ + os.makedirs(os.path.dirname(os.path.abspath(out_prefix)), exist_ok=True) + + true_vals = np.asarray(true_vals, dtype=float) + pit_vals = pit_from_quantiles(true_vals, quantile_matrix, quantile_levels) + cal_curve = calibration_curve(pit_vals) + uniformity = pit_uniformity_test(pit_vals) + + # --- PIT CSV --- + pit_csv = f"{out_prefix}_pit.csv" + try: + pd.DataFrame({"true_val": true_vals, "pit": pit_vals}).to_csv(pit_csv, index=False) + print(f"Wrote PIT values to {pit_csv}") + except Exception as exc: + warnings.warn(f"Could not write PIT CSV to {pit_csv}: {exc}") + + # --- Calibration-curve CSV --- + cal_csv = f"{out_prefix}_calibration_curve.csv" + try: + pd.DataFrame({ + "nominal": list(cal_curve.keys()), + "empirical": list(cal_curve.values()), + }).to_csv(cal_csv, index=False) + print(f"Wrote calibration curve to {cal_csv}") + except Exception as exc: + warnings.warn(f"Could not write calibration curve CSV to {cal_csv}: {exc}") + + # --- Two-panel PDF --- + pdf_path = f"{out_prefix}_calibration.pdf" + try: + fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + hist_title = f"PIT histogram — {label}" if label else "PIT histogram" + curve_title = f"Calibration curve — {label}" if label else "Calibration curve" + plot_pit_histogram(pit_vals, ax=axes[0], title=hist_title) + plot_calibration_curve(cal_curve, ax=axes[1]) + axes[1].set_title(curve_title) + fig.tight_layout() + fig.savefig(pdf_path, format="pdf", bbox_inches="tight") + plt.close(fig) + print(f"Wrote calibration plot to {pdf_path}") + except Exception as exc: + warnings.warn(f"Could not write calibration PDF to {pdf_path}: {exc}") + + return { + "n_vals": uniformity["n_vals"], + "ks_stat": uniformity["ks_stat"], + "ks_pval": uniformity["ks_pval"], + "mean_pit": uniformity["mean_pit"], + "calibration_curve": cal_curve, + } + + +# ============================================================ +# Part 5 — Multi-run SBC (Simulation-Based Calibration) +# ============================================================ + +def _load_h5_params(path: str) -> Dict[str, np.ndarray]: + """Load all dataset arrays from an HDF5 file.""" + out = {} + with h5py.File(path, "r") as hf: + for key in hf.keys(): + out[key] = hf[key][:] + return out + + +def compute_sbc_ranks(ground_truth_path: str, + posterior_path: str) -> Dict[str, np.ndarray]: + """Compute the posterior rank (PIT) of each ground-truth parameter value. + + For each parameter present in both files the rank is the fraction of + posterior samples strictly less than the ground-truth value:: + + rank[i] = mean(posterior_samples[:, i] < gt_value[i]) ∈ [0, 1] + + Parameters + ---------- + ground_truth_path : str + HDF5 file produced by ``tfs-sample-prior``. Each dataset has shape + ``(1, *param_shape)`` (num_draws=1). + posterior_path : str + HDF5 file produced by ``tfs-sample-posterior``. Each dataset has + shape ``(S, *param_shape)`` with S posterior samples. + + Returns + ------- + dict + Mapping from parameter name to a 1-D rank array (one value per scalar + element of the parameter). Only parameters present in both files are + included. + """ + gt_params = _load_h5_params(ground_truth_path) + post_params = _load_h5_params(posterior_path) + + ranks = {} + for name, gt_val in gt_params.items(): + if name not in post_params: + continue + + gt = gt_val[0] if gt_val.ndim >= 1 else gt_val + post = post_params[name] + + if post.ndim < 1 or post.shape[0] == 0: + continue + + try: + gt_f = np.asarray(gt, dtype=float) + post_f = np.asarray(post, dtype=float) + except (TypeError, ValueError): + continue + + ranks[name] = pit_from_samples(gt_f, post_f).ravel() + + return ranks + + +def _find_pairs(sbc_dir: str) -> List[Tuple[str, str, Optional[str]]]: + """Find (run_id, ground_truth_path, posterior_path_or_None) triples. + + Scans *sbc_dir* for files matching ``*_ground_truth.h5``. For each, + looks for a corresponding ``*_posterior.h5`` with the same prefix + (replacing ``_ground_truth`` with ``_posterior``). + """ + gt_files = sorted(glob.glob(os.path.join(sbc_dir, "*_ground_truth.h5"))) + pairs = [] + for gt_path in gt_files: + basename = os.path.basename(gt_path) + run_id = basename[: -len("_ground_truth.h5")] + post_path = os.path.join(sbc_dir, f"{run_id}_posterior.h5") + pairs.append((run_id, gt_path, + post_path if os.path.exists(post_path) else None)) + return pairs + + +_SBC_HIST_BINS = 10 + + +def _plot_sbc_rank_histograms(all_ranks: Dict[str, List[np.ndarray]], + out_prefix: str) -> None: + """Write a PDF with one PIT-histogram panel per parameter.""" + params = sorted(all_ranks) + n = len(params) + if n == 0: + return + + ncols = min(4, n) + nrows = (n + ncols - 1) // ncols + fig, axes = plt.subplots(nrows, ncols, figsize=(4 * ncols, 3 * nrows), + squeeze=False) + + for ax_idx, param in enumerate(params): + ax = axes[ax_idx // ncols][ax_idx % ncols] + pooled = np.concatenate(all_ranks[param]) + pooled = pooled[np.isfinite(pooled)] + + plot_pit_histogram(pooled, ax=ax, n_bins=_SBC_HIST_BINS) + + title = param if len(param) <= 30 else f"…{param[-27:]}" + _, ks_pval = ks_1samp(pooled, uniform(0, 1).cdf) if len(pooled) > 0 else (np.nan, np.nan) + ax.set_title(f"{title}\n(n={len(pooled)}, p={ks_pval:.2g})", fontsize=9) + + for ax_idx in range(n, nrows * ncols): + axes[ax_idx // ncols][ax_idx % ncols].set_visible(False) + + fig.tight_layout() + pdf_path = f"{out_prefix}_rank_hist.pdf" + try: + fig.savefig(pdf_path) + print(f"Wrote rank histograms → {pdf_path}", flush=True) + except Exception as exc: + warnings.warn(f"Could not write rank histogram PDF: {exc}") + finally: + plt.close(fig) + + +def summarize_sbc(sbc_dir: str, out_prefix: Optional[str] = None) -> pd.DataFrame: + """Scan *sbc_dir* for SBC run pairs and compute rank-uniformity statistics. + + For each ``*_ground_truth.h5`` / ``*_posterior.h5`` pair found, compute + the posterior rank (PIT) of every ground-truth parameter value. Pool + ranks across runs and write three output files: + + ``{out_prefix}_sbc_summary.csv`` + One row per parameter with mean rank, KS statistic, and p-value. + Mean rank near 0.5 and high p-value indicate good calibration. + + ``{out_prefix}_sbc_ranks.csv`` + Long-form table of every individual rank value. + Columns: ``run_id``, ``param``, ``element_idx``, ``rank``. + + ``{out_prefix}_rank_hist.pdf`` + One-panel-per-parameter PIT histogram. A flat histogram indicates + uniform ranks (good calibration). + + Parameters + ---------- + sbc_dir : str + Directory containing ``*_ground_truth.h5`` and ``*_posterior.h5`` + files produced by ``tfs-sample-prior`` and ``tfs-sample-posterior``. + out_prefix : str, optional + Prefix for all output files. Defaults to ``{sbc_dir}/sbc``. + + Returns + ------- + pd.DataFrame + Per-parameter summary table (same content as the CSV). + """ + sbc_dir = os.path.abspath(sbc_dir) + if not os.path.isdir(sbc_dir): + raise FileNotFoundError(f"SBC directory not found: {sbc_dir}") + + if out_prefix is None: + out_prefix = os.path.join(sbc_dir, "sbc") + + pairs = _find_pairs(sbc_dir) + if not pairs: + print(f"No *_ground_truth.h5 files found in {sbc_dir}/", flush=True) + return pd.DataFrame() + + n_total = len(pairs) + n_posterior = sum(1 for _, _, p in pairs if p is not None) + print( + f"Found {n_total} ground-truth file(s), " + f"{n_posterior} with matching posterior.", + flush=True, + ) + if n_posterior == 0: + warnings.warn( + "No posterior files found. Run tfs-sample-posterior for each " + "synthetic dataset before calling tfs-summarize-sbc." + ) + return pd.DataFrame() + + all_ranks: Dict[str, List[np.ndarray]] = {} + rank_rows = [] + + for run_id, gt_path, post_path in pairs: + if post_path is None: + continue + try: + run_ranks = compute_sbc_ranks(gt_path, post_path) + except Exception as exc: + warnings.warn(f"Skipping run '{run_id}': {exc}") + continue + + for param, rank_arr in run_ranks.items(): + all_ranks.setdefault(param, []).append(rank_arr) + for idx, r in enumerate(rank_arr): + rank_rows.append({ + "run_id": run_id, + "param": param, + "element_idx": idx, + "rank": float(r), + }) + + if not all_ranks: + warnings.warn("No matching parameters found across ground-truth and posterior files.") + return pd.DataFrame() + + summary_rows = [] + for param in sorted(all_ranks): + pooled = np.concatenate(all_ranks[param]) + uniformity = pit_uniformity_test(pooled) + n_runs = len(all_ranks[param]) + summary_rows.append({ + "param": param, + "n_runs": n_runs, + "n_ranks": uniformity["n_vals"], + "mean_rank": uniformity["mean_pit"], + "expected_rank": 0.5, + "ks_stat": uniformity["ks_stat"], + "ks_pval": uniformity["ks_pval"], + }) + + summary_df = pd.DataFrame(summary_rows) + + summary_csv = f"{out_prefix}_sbc_summary.csv" + summary_df.to_csv(summary_csv, index=False) + print(f"Wrote per-parameter summary → {summary_csv}", flush=True) + + if rank_rows: + ranks_csv = f"{out_prefix}_sbc_ranks.csv" + ranks_df = pd.DataFrame(rank_rows) + ranks_df.to_csv(ranks_csv, index=False) + print(f"Wrote all rank values → {ranks_csv}", flush=True) + + _plot_sbc_rank_histograms(all_ranks, out_prefix) + + print( + f"\nSummary: {len(summary_df)} parameter(s) across " + f"{n_posterior} run(s).", + flush=True, + ) + low_pval = summary_df[summary_df["ks_pval"] < 0.05] if len(summary_df) else pd.DataFrame() + if len(low_pval): + print( + f" {len(low_pval)} parameter(s) with KS p-value < 0.05 " + f"(possible miscalibration):", + flush=True, + ) + for _, row in low_pval.iterrows(): + print( + f" {row['param']:40s} KS={row['ks_stat']:.3f} p={row['ks_pval']:.3g}", + flush=True, + ) + else: + print(" All parameters pass KS uniformity test (p ≥ 0.05).", flush=True) + + return summary_df diff --git a/src/tfscreen/tfmodel/analysis/prior_predictive.py b/src/tfscreen/tfmodel/analysis/prior_predictive.py index 34f960d2..2e46c66f 100644 --- a/src/tfscreen/tfmodel/analysis/prior_predictive.py +++ b/src/tfscreen/tfmodel/analysis/prior_predictive.py @@ -112,17 +112,17 @@ def growth_df_from_prior(orchestrator, latent_params, draw_idx=0, noise_rng=None orchestrator, single_draw, predict_sites=["growth_pred"], - q_to_get={"_prior_ln_cfu": 0.5}, + q_to_get=[0.5], num_samples=None, ) # pred_df may contain extra rows (expanded grid); merge to keep original rows. out_df = orchestrator.growth_df.drop(columns=["ln_cfu"]).merge( - pred_df[merge_keys + ["_prior_ln_cfu"]], + pred_df[merge_keys + ["q0.5"]], on=merge_keys, how="left", ) - out_df = out_df.rename(columns={"_prior_ln_cfu": "ln_cfu"}) + out_df = out_df.rename(columns={"q0.5": "ln_cfu"}) if noise_rng is not None: out_df["ln_cfu"] = ( diff --git a/src/tfscreen/tfmodel/analysis/sbc.py b/src/tfscreen/tfmodel/analysis/sbc.py deleted file mode 100644 index 1a64bb2e..00000000 --- a/src/tfscreen/tfmodel/analysis/sbc.py +++ /dev/null @@ -1,331 +0,0 @@ -""" -Simulation-Based Calibration (SBC) rank statistics. - -Theory ------- -For a correctly specified model, the posterior rank of the true parameter -value should be uniformly distributed on [0, 1]. Given N ground-truth / -posterior pairs (each drawn from the same generative model), we: - - 1. For each parameter array θ of shape (*D), compute the rank of the - ground-truth value θ_true among the S posterior samples:: - - rank[...] = mean(posterior_samples < θ_true, axis=0) ∈ [0, 1] - - 2. Pool all ranks across runs and parameter elements. - 3. Test uniformity with a KS test and summarise with a rank histogram. - -References ----------- -Talts et al. (2018) "Validating Bayesian Inference Algorithms with -Simulation-Based Calibration". arXiv:1804.06788. -""" - -import glob -import os -import warnings -from typing import Dict, List, Optional, Tuple - -import h5py -import numpy as np -import pandas as pd -from matplotlib import pyplot as plt -from scipy.stats import ks_1samp, uniform - - -# --------------------------------------------------------------------------- -# Low-level helpers -# --------------------------------------------------------------------------- - -def _load_h5_params(path: str) -> Dict[str, np.ndarray]: - """Load all dataset arrays from an HDF5 file.""" - out = {} - with h5py.File(path, "r") as hf: - for key in hf.keys(): - out[key] = hf[key][:] - return out - - -def compute_sbc_ranks(ground_truth_path: str, - posterior_path: str) -> Dict[str, np.ndarray]: - """ - Compute the posterior rank of each ground-truth parameter value. - - For each parameter present in *both* files the rank is the fraction of - posterior samples that are strictly less than the ground-truth value:: - - rank[i] = mean(posterior_samples[:, i] < gt_value[i]) ∈ [0, 1] - - Parameters - ---------- - ground_truth_path : str - HDF5 file produced by ``tfs-sample-prior``. Each dataset has shape - ``(1, *param_shape)`` (num_draws=1). - posterior_path : str - HDF5 file produced by ``tfs-sample-posterior``. Each dataset has - shape ``(S, *param_shape)`` with S posterior samples. - - Returns - ------- - dict - Mapping from parameter name to a 1-D rank array (one value per - scalar element of the parameter). Only parameters present in both - files are included. - """ - gt_params = _load_h5_params(ground_truth_path) - post_params = _load_h5_params(posterior_path) - - ranks = {} - for name, gt_val in gt_params.items(): - if name not in post_params: - continue - - # Ground truth: shape (1, *param_shape) → squeeze to (*param_shape) - gt = gt_val[0] if gt_val.ndim >= 1 else gt_val - post = post_params[name] # (S, *param_shape) - - if post.ndim < 1 or post.shape[0] == 0: - continue - - try: - gt_f = np.asarray(gt, dtype=float) - post_f = np.asarray(post, dtype=float) - except (TypeError, ValueError): - continue # skip non-numeric parameters - - # Broadcast: (S, *param_shape) vs (*param_shape) - rank = np.mean(post_f < gt_f, axis=0).ravel() # shape: (n_elements,) - ranks[name] = rank - - return ranks - - -# --------------------------------------------------------------------------- -# Directory scanner -# --------------------------------------------------------------------------- - -def _find_pairs(sbc_dir: str) -> List[Tuple[str, str, Optional[str]]]: - """ - Find (run_id, ground_truth_path, posterior_path_or_None) triples. - - Scans *sbc_dir* for files matching ``*_ground_truth.h5``. For each, - looks for a corresponding ``*_posterior.h5`` with the same prefix - (replacing ``_ground_truth`` with ``_posterior``). - """ - gt_files = sorted(glob.glob(os.path.join(sbc_dir, "*_ground_truth.h5"))) - pairs = [] - for gt_path in gt_files: - basename = os.path.basename(gt_path) - run_id = basename[: -len("_ground_truth.h5")] - post_path = os.path.join(sbc_dir, f"{run_id}_posterior.h5") - pairs.append((run_id, gt_path, - post_path if os.path.exists(post_path) else None)) - return pairs - - -# --------------------------------------------------------------------------- -# Main summary function -# --------------------------------------------------------------------------- - -def summarize_sbc(sbc_dir: str, out_prefix: Optional[str] = None) -> pd.DataFrame: - """ - Scan *sbc_dir* for SBC run pairs and compute rank-uniformity statistics. - - For each ``*_ground_truth.h5`` / ``*_posterior.h5`` pair found, compute - the posterior rank of every ground-truth parameter value. Pool ranks - across runs and write three output files: - - ``{out_prefix}_sbc_summary.csv`` - One row per parameter with mean rank, KS statistic, and p-value. - Mean rank near 0.5 and high p-value indicate good calibration. - - ``{out_prefix}_sbc_ranks.csv`` - Long-form table of every individual rank value, for custom analysis. - Columns: ``run_id``, ``param``, ``element_idx``, ``rank``. - - ``{out_prefix}_rank_hist.pdf`` - One-panel-per-parameter rank histogram. A flat histogram indicates - uniform ranks (good calibration). - - Parameters - ---------- - sbc_dir : str - Directory containing ``*_ground_truth.h5`` and ``*_posterior.h5`` - files produced by ``tfs-sample-prior`` and ``tfs-sample-posterior``. - out_prefix : str, optional - Prefix for all output files. Defaults to ``{sbc_dir}/sbc``. - - Returns - ------- - pd.DataFrame - Per-parameter summary table (same content as the CSV). - """ - sbc_dir = os.path.abspath(sbc_dir) - if not os.path.isdir(sbc_dir): - raise FileNotFoundError(f"SBC directory not found: {sbc_dir}") - - if out_prefix is None: - out_prefix = os.path.join(sbc_dir, "sbc") - - pairs = _find_pairs(sbc_dir) - if not pairs: - print(f"No *_ground_truth.h5 files found in {sbc_dir}/", flush=True) - return pd.DataFrame() - - n_total = len(pairs) - n_posterior = sum(1 for _, _, p in pairs if p is not None) - print( - f"Found {n_total} ground-truth file(s), " - f"{n_posterior} with matching posterior.", - flush=True, - ) - if n_posterior == 0: - warnings.warn( - "No posterior files found. Run tfs-sample-posterior for each " - "synthetic dataset before calling tfs-summarize-sbc." - ) - return pd.DataFrame() - - # Accumulate ranks: param_name → list of rank arrays (one per run) - all_ranks: Dict[str, List[np.ndarray]] = {} - rank_rows = [] - - for run_id, gt_path, post_path in pairs: - if post_path is None: - continue - try: - run_ranks = compute_sbc_ranks(gt_path, post_path) - except Exception as exc: - warnings.warn(f"Skipping run '{run_id}': {exc}") - continue - - for param, rank_arr in run_ranks.items(): - all_ranks.setdefault(param, []).append(rank_arr) - for idx, r in enumerate(rank_arr): - rank_rows.append({ - "run_id": run_id, - "param": param, - "element_idx": idx, - "rank": float(r), - }) - - if not all_ranks: - warnings.warn("No matching parameters found across ground-truth and posterior files.") - return pd.DataFrame() - - # ----------------------------------------------------------------- - # Per-parameter summary: mean rank, KS test against Uniform(0,1) - # ----------------------------------------------------------------- - summary_rows = [] - for param in sorted(all_ranks): - pooled = np.concatenate(all_ranks[param]) - pooled = pooled[np.isfinite(pooled)] - n_runs = len(all_ranks[param]) - n_vals = len(pooled) - if n_vals == 0: - continue - mean_rank = float(np.mean(pooled)) - ks_stat, ks_pval = ks_1samp(pooled, uniform(0, 1).cdf) - summary_rows.append({ - "param": param, - "n_runs": n_runs, - "n_ranks": n_vals, - "mean_rank": mean_rank, - "expected_rank": 0.5, - "ks_stat": float(ks_stat), - "ks_pval": float(ks_pval), - }) - - summary_df = pd.DataFrame(summary_rows) - - # ----------------------------------------------------------------- - # Write outputs - # ----------------------------------------------------------------- - summary_csv = f"{out_prefix}_sbc_summary.csv" - summary_df.to_csv(summary_csv, index=False) - print(f"Wrote per-parameter summary → {summary_csv}", flush=True) - - if rank_rows: - ranks_csv = f"{out_prefix}_sbc_ranks.csv" - ranks_df = pd.DataFrame(rank_rows) - ranks_df.to_csv(ranks_csv, index=False) - print(f"Wrote all rank values → {ranks_csv}", flush=True) - else: - ranks_df = pd.DataFrame() - - _plot_rank_histograms(all_ranks, out_prefix) - - print( - f"\nSummary: {len(summary_df)} parameter(s) across " - f"{n_posterior} run(s).", - flush=True, - ) - low_pval = summary_df[summary_df["ks_pval"] < 0.05] if len(summary_df) else pd.DataFrame() - if len(low_pval): - print( - f" {len(low_pval)} parameter(s) with KS p-value < 0.05 " - f"(possible miscalibration):", - flush=True, - ) - for _, row in low_pval.iterrows(): - print( - f" {row['param']:40s} KS={row['ks_stat']:.3f} p={row['ks_pval']:.3g}", - flush=True, - ) - else: - print(" All parameters pass KS uniformity test (p ≥ 0.05).", flush=True) - - return summary_df - - -# --------------------------------------------------------------------------- -# Plotting -# --------------------------------------------------------------------------- - -_HIST_BINS = 10 - - -def _plot_rank_histograms(all_ranks: Dict[str, List[np.ndarray]], - out_prefix: str) -> None: - """Write a PDF with one rank-histogram panel per parameter.""" - params = sorted(all_ranks) - n = len(params) - if n == 0: - return - - ncols = min(4, n) - nrows = (n + ncols - 1) // ncols - fig, axes = plt.subplots(nrows, ncols, figsize=(4 * ncols, 3 * nrows), - squeeze=False) - - for ax_idx, param in enumerate(params): - ax = axes[ax_idx // ncols][ax_idx % ncols] - pooled = np.concatenate(all_ranks[param]) - pooled = pooled[np.isfinite(pooled)] - n_vals = len(pooled) - - ax.hist(pooled, bins=_HIST_BINS, range=(0, 1), - density=True, color="steelblue", alpha=0.75, edgecolor="white") - ax.axhline(1.0, color="firebrick", lw=1.5, ls="--", label="uniform") - ax.set_xlim(0, 1) - ax.set_xlabel("Posterior rank") - ax.set_ylabel("Density") - - # Truncate long param names for the title - title = param if len(param) <= 30 else f"…{param[-27:]}" - _, ks_pval = ks_1samp(pooled, uniform(0, 1).cdf) - ax.set_title(f"{title}\n(n={n_vals}, p={ks_pval:.2g})", fontsize=9) - - # Hide unused panels - for ax_idx in range(n, nrows * ncols): - axes[ax_idx // ncols][ax_idx % ncols].set_visible(False) - - fig.tight_layout() - pdf_path = f"{out_prefix}_rank_hist.pdf" - try: - fig.savefig(pdf_path) - print(f"Wrote rank histograms → {pdf_path}", flush=True) - except Exception as exc: - warnings.warn(f"Could not write rank histogram PDF: {exc}") - finally: - plt.close(fig) diff --git a/src/tfscreen/tfmodel/inference/posteriors.py b/src/tfscreen/tfmodel/inference/posteriors.py index d5c2d468..d28c7c58 100644 --- a/src/tfscreen/tfmodel/inference/posteriors.py +++ b/src/tfscreen/tfmodel/inference/posteriors.py @@ -1,6 +1,28 @@ import numpy as np import h5py +# --------------------------------------------------------------------------- +# Default quantile levels and column-name helper +# --------------------------------------------------------------------------- + +_DEFAULT_QUANTILE_LEVELS = np.array([ + 0.001, 0.005, 0.01, 0.025, 0.05, + 0.10, 0.159, 0.25, 0.50, 0.75, 0.841, 0.90, + 0.95, 0.975, 0.99, 0.995, 0.999, +]) + + +def quantile_col(level): + """Return the standard column name for quantile *level*. + + Uses ``f"q{level:g}"`` so trailing zeros are stripped: + ``quantile_col(0.5)`` → ``"q0.5"``, + ``quantile_col(0.025)`` → ``"q0.025"``, + ``quantile_col(0.159)`` → ``"q0.159"``. + """ + return f"q{level:g}" + + def get_posterior_samples(param_posteriors, param_name): """ Get posterior samples for a parameter, handling name fallbacks and HDF5. @@ -39,7 +61,7 @@ def get_posterior_samples(param_posteriors, param_name): keys_str = ", ".join(available_keys[:5]) + " ... " + ", ".join(available_keys[-5:]) else: keys_str = ", ".join(available_keys) - + error_msg = f"Parameter '{param_name}' not found in posteriors. Available keys: {keys_str}" raise KeyError(error_msg) @@ -49,25 +71,36 @@ def get_posterior_samples(param_posteriors, param_name): def load_posteriors(posteriors, q_to_get=None): """ - Consolidates the reading logic for posterior samples and sets default quantiles. + Consolidate the reading logic for posterior samples and build quantile mapping. Parameters ---------- posteriors : dict or str - Assumes this is a dictionary of posteriors keying parameters to - numpy arrays, a numpy.lib.npyio.NpzFile object, or a path to a - .npz or .h5/.hdf5 file containing posterior samples for model + A dictionary of posteriors keying parameters to numpy arrays, a + ``numpy.lib.npyio.NpzFile`` object, or a path to a ``.npz`` or + ``.h5``/``.hdf5`` file containing posterior samples for model parameters. - q_to_get : dict, optional - Dictionary mapping output column names to quantile values (between 0 and 1) - to extract from the posterior samples. If None, a default set of quantiles - is used (min, lower_95, lower_std, lower_quartile, median, upper_std, - upper_quartile, upper_95, max). + q_to_get : array-like of float, optional + Quantile levels in [0, 1] to extract from posterior samples. Column + names are generated automatically as ``q{level:g}`` + (e.g. ``q0.5`` for the median, ``q0.025`` for the 2.5th percentile). + If ``None``, a dense default set is used:: + + [0.001, 0.005, 0.01, 0.025, 0.05, + 0.10, 0.159, 0.25, 0.50, 0.75, 0.841, 0.90, + 0.95, 0.975, 0.99, 0.995, 0.999] Returns ------- tuple - A tuple containing (q_to_get, param_posteriors). + ``(q_dict, param_posteriors)`` where ``q_dict`` maps column names to + quantile levels (e.g. ``{"q0.5": 0.5, "q0.025": 0.025, ...}``). + + Raises + ------ + ValueError + If ``q_to_get`` cannot be converted to a 1-D float array or any + value is outside [0, 1]. """ # Load the posterior file @@ -95,22 +128,24 @@ def load_posteriors(posteriors, q_to_get=None): "path), an .h5 file (loaded or path), or an h5py group." ) - # Named quantiles to pull from the posterior distribution + # Build the quantile level array if q_to_get is None: - q_to_get = {"min": 0.0, - "lower_95": 0.025, - "lower_std": 0.159, - "lower_quartile": 0.25, - "median": 0.5, - "upper_quartile": 0.75, - "upper_std": 0.841, - "upper_95": 0.975, - "max": 1.0} - - # make sure q_to_get is a dictionary - if not isinstance(q_to_get, dict): - raise ValueError( - "q_to_get should be a dictionary keying column names to quantiles" - ) - - return q_to_get, param_posteriors + q_arr = _DEFAULT_QUANTILE_LEVELS + else: + try: + q_arr = np.atleast_1d(np.asarray(q_to_get, dtype=float)).ravel() + except (TypeError, ValueError): + raise ValueError( + "q_to_get should be a 1-D array-like of quantile levels in [0, 1]; " + f"got {type(q_to_get).__name__}" + ) + if not np.all((q_arr >= 0) & (q_arr <= 1)): + raise ValueError( + "q_to_get values must all be in [0, 1]; " + f"got min={float(q_arr.min()):.4g}, max={float(q_arr.max()):.4g}" + ) + + # Build column-name → level dict used by all downstream extraction code + q_dict = {quantile_col(level): float(level) for level in q_arr} + + return q_dict, param_posteriors diff --git a/src/tfscreen/tfmodel/model_orchestrator.py b/src/tfscreen/tfmodel/model_orchestrator.py index 3dc2dbb5..731284e0 100644 --- a/src/tfscreen/tfmodel/model_orchestrator.py +++ b/src/tfscreen/tfmodel/model_orchestrator.py @@ -470,15 +470,9 @@ def _setup_batching(growth_genotypes, not_binding_batch_size = batch_size - num_binding # Build idx array. The first entries correspond to the binding data and are - # the same for all rounds. Non-binding positions are filled with - # not_binding_idx[:not_binding_batch_size] as a deterministic default. - # During training, get_random_idx() overrides these entries with a fresh - # random sample each step. During prediction (no get_random_idx call), - # the static batch_idx is used directly, so it must contain valid indices. - idx = np.zeros(batch_size, dtype=int) + # the same for all rounds + idx = np.zeros(batch_size,dtype=int) idx[:num_binding] = binding_idx - if not_binding_batch_size > 0: - idx[num_binding:] = not_binding_idx[:not_binding_batch_size] # Calculate scale vector, which will be the same for all rounds scale_vector = np.ones(batch_size,dtype=float) diff --git a/src/tfscreen/tfmodel/scripts/extract_params_cli.py b/src/tfscreen/tfmodel/scripts/extract_params_cli.py index e55c4d7d..241848d7 100644 --- a/src/tfscreen/tfmodel/scripts/extract_params_cli.py +++ b/src/tfscreen/tfmodel/scripts/extract_params_cli.py @@ -70,7 +70,7 @@ def _extract_from_checkpoint(orchestrator, ckpt_path, out_prefix): posteriors = {k: np.expand_dims(np.asarray(v), 0) for k, v in constrained.items()} print(f"Writing parameter CSVs to {out_prefix}_*.csv...", flush=True) - param_dfs = extract_parameters(orchestrator, posteriors, q_to_get={"point_est": 0.5}) + param_dfs = extract_parameters(orchestrator, posteriors, q_to_get=[0.5]) for p_name, df in param_dfs.items(): out_file = f"{out_prefix}_{p_name}.csv" df.to_csv(out_file, index=False) diff --git a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py index d38fef23..381e4524 100644 --- a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py @@ -100,7 +100,7 @@ def predict_growth(config_file, training_concs = list(orchestrator.growth_df["titrant_conc"].unique()) titrant_concs = sorted(set(training_concs) | set(file_concs)) if file_concs else None - q_to_get = {"point_est": 0.5} if is_map else None + q_to_get = [0.5] if is_map else None print("Running growth predictions...", flush=True) result_df = predict(orchestrator=orchestrator, param_posteriors=param_file, diff --git a/src/tfscreen/tfmodel/scripts/predict_theta_cli.py b/src/tfscreen/tfmodel/scripts/predict_theta_cli.py index 5b808bb1..7d1caa6b 100644 --- a/src/tfscreen/tfmodel/scripts/predict_theta_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_theta_cli.py @@ -174,7 +174,7 @@ def predict_theta(config_file, .drop_duplicates() .reset_index(drop=True) ) - q_to_get = {"point_est": 0.5} if is_map else None + q_to_get = [0.5] if is_map else None result_df = extract_theta_unmeasured( orchestrator=orchestrator, posteriors=param_file, @@ -186,7 +186,7 @@ def predict_theta(config_file, else: print(f"Predicting theta for {len(requested_genotypes)} training genotype(s)...", flush=True) - q_to_get = {"point_est": 0.5} if is_map else None + q_to_get = [0.5] if is_map else None result_df = extract_theta_curves( orchestrator=orchestrator, posteriors=param_file, diff --git a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py index 06880ac0..21bfc00f 100644 --- a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py @@ -706,7 +706,7 @@ def _run_calibration_map(ri, _DIAG_PLOT_COLS = [ "replicate", "condition_pre", "condition_sel", "titrant_name", "titrant_conc", "genotype", - "t_sel", "ln_cfu", "ln_cfu_std", "q05", "median", "q95", + "t_sel", "ln_cfu", "ln_cfu_std", "q0.05", "q0.5", "q0.95", ] _DIAG_CONDITION_COLS = [ "condition_pre", "condition_sel", "titrant_name", "titrant_conc" @@ -750,7 +750,7 @@ def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pr orchestrator_cal, params, predict_sites=["growth_pred", "ln_cfu0"], - q_to_get={"median": 0.5, "q05": 0.05, "q95": 0.95}, + q_to_get=[0.05, 0.5, 0.95], num_samples=None, num_marginal_samples=1, ) @@ -758,11 +758,11 @@ def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pr ln_cfu0_raw = all_dfs["ln_cfu0"] # ------------------------------------------------------------------ - # 2. Add ln_cfu_pred (alias for median) and optional Laplace std for + # 2. Add ln_cfu_pred (alias for q0.5) and optional Laplace std for # the stats / correlation helpers. # ------------------------------------------------------------------ pred_df = pred_df.copy() - pred_df["ln_cfu_pred"] = pred_df["median"] + pred_df["ln_cfu_pred"] = pred_df["q0.5"] if growth_pred_std is not None: tm = orchestrator_cal.growth_tm @@ -795,7 +795,7 @@ def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pr ) ln_cfu0_vals = ( ln_cfu0_raw[["replicate", "condition_pre", "genotype", - "q05", "median", "q95"]] + "q0.05", "q0.5", "q0.95"]] .drop_duplicates(subset=["replicate", "condition_pre", "genotype"]) ) valid_combos = ( diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index e117a28e..a5232e0e 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -2,6 +2,7 @@ import glob import json import os +import re import warnings import numpy as np @@ -231,6 +232,248 @@ def _blank_panel(ax, message): ha="center", va="center", fontsize=12, color="gray") +def _build_transform_registry(sim_df): + """Return a dict mapping parameter name → obs Series indexed by genotype. + + Supports four resolution strategies (tried in this order at registration): + + 1. **Compound**: ``logit_delta`` = logit(theta_high) – logit(theta_low), + registered before the column loop so it is never overwritten. + 2. **Direct**: column name is the parameter name. + 3. **log_ prefix**: ``log_{col}`` → ``np.log(sim[col])``. + 4. **logit_ prefix**: ``logit_{col}`` → logit(sim[col]). For columns + that start with ``theta_`` the short form is also registered, e.g. + ``theta_low`` registers both ``logit_theta_low`` and ``logit_low``. + + ``setdefault`` is used for entries 2–4 so compound entries win. + """ + sim_lookup = sim_df.set_index("genotype") + sim_cols = [c for c in sim_df.columns if c != "genotype"] + registry = {} + + # Compound: logit_delta = logit(theta_high) - logit(theta_low) + if "theta_high" in sim_cols and "theta_low" in sim_cols: + high = sim_lookup["theta_high"].clip(1e-9, 1 - 1e-9) + low = sim_lookup["theta_low"].clip(1e-9, 1 - 1e-9) + registry["logit_delta"] = np.log(high / (1 - high)) - np.log(low / (1 - low)) + + for col in sim_cols: + col_s = sim_lookup[col] + clipped = col_s.clip(1e-9, 1 - 1e-9) + logit_s = np.log(clipped / (1 - clipped)) + + registry.setdefault(col, col_s) + registry.setdefault(f"log_{col}", np.log(col_s)) + registry.setdefault(f"logit_{col}", logit_s) + + # For theta_* columns also register the short logit/log/direct forms, + # e.g. theta_low → logit_low, log_low, low. + if col.startswith("theta_"): + base = col[6:] + registry.setdefault(f"logit_{base}", logit_s) + registry.setdefault(f"log_{base}", np.log(col_s)) + registry.setdefault(base, col_s) + + return registry + + +def _compute_direct_obs(params_df, obs_series): + """Return obs array for a direct params file keyed on genotype. + + Returns ``None`` when params_df has no ``genotype`` column (e.g. files + keyed on condition/replicate that cannot be matched to sim_parameters). + """ + if "genotype" not in params_df.columns: + return None + return obs_series.reindex(params_df["genotype"]).values + + +def _compute_diff_obs(params_df, obs_series): + """Return obs array for a diff params file keyed on mutation. + + obs = sim[mutation] − sim[wt]. Returns ``None`` when params_df has no + ``mutation`` column. Sets obs to NaN when ``wt`` is absent from + obs_series. + """ + if "mutation" not in params_df.columns: + return None + if "wt" not in obs_series.index: + warnings.warn("'wt' not found in sim_parameters; setting diff obs to NaN") + return np.full(len(params_df), np.nan) + wt_val = obs_series["wt"] + return obs_series.reindex(params_df["mutation"]).values - wt_val + + +def _compute_epi_obs(params_df, obs_series, param_name): + """Return obs array for an epi params file keyed on pair. + + Uses ``extract_epistasis`` (additive scale) to compute per-double-mutant + epistasis from the ground-truth obs_series. Genotypes not present in the + result (triple+, unrecognised pairs) receive NaN. Returns ``None`` when + params_df has no ``pair`` column. + """ + from tfscreen.analysis.extract_epistasis import extract_epistasis + + if "pair" not in params_df.columns: + return None + + work_df = pd.DataFrame({ + "genotype": obs_series.index, + "_obs": obs_series.values, + }) + try: + ep_result = extract_epistasis(work_df, y_obs="_obs", scale="add") + except Exception as exc: + warnings.warn(f"extract_epistasis failed for {param_name}: {exc}") + return np.full(len(params_df), np.nan) + + if len(ep_result) == 0: + return np.full(len(params_df), np.nan) + + from tfscreen.genetics import standardize_genotypes + + ep_map = ep_result.set_index("genotype")["ep_obs"] + std_pairs = pd.Series(standardize_genotypes(params_df["pair"]), + index=params_df.index) + return ep_map.reindex(std_pairs).values + + +def _try_plot_params_corr(out_df, label, pdf_path): + """Save a scatter correlation plot of obs vs median for a params CSV. + + Silently skips when fewer than two finite (obs, median) pairs exist. + Warns and returns on any other failure so the rest of the output is + unaffected. + """ + try: + valid = out_df[["obs", "q0.5"]].dropna() + if len(valid) < 2: + return + fig, ax = plt.subplots(1, 1, figsize=(6, 6)) + xy_corr( + x_values=valid["obs"].values, + y_values=valid["q0.5"].values, + as_hexbin=False, + ax=ax, + ) + ax.set_xlabel(f"Simulated {label}") + ax.set_ylabel(f"Predicted {label}") + ax.set_title(label) + fig.tight_layout() + fig.savefig(pdf_path, format="pdf", bbox_inches="tight") + plt.close(fig) + print(f"Wrote parameter correlation plot to {pdf_path}") + except Exception as exc: + warnings.warn(f"Could not generate parameter correlation plot {pdf_path}: {exc}") + + +def _summarize_params(run_dir, out_prefix): + """Annotate *_params_*.csv files with ground-truth obs from tfs_sim_parameters.csv. + + Scans run_dir for a ``*_sim_parameters.csv`` file. If absent the function + returns silently (real-data runs have no ground truth). + + For each non-genotype column in sim_parameters the function checks that a + matching ``*_params_{col}.csv`` exists and warns if it does not. + + All ``*_params_*.csv`` files in run_dir are then classified by their + suffix as *direct* (genotype key), *diff* (``_d_`` infix, mutation key), + or *epi* (``_epi_`` infix, pair key). An ``obs`` column is appended to + each file whose parameter name resolves via the transform registry and + written to the summary directory under ``{out_prefix}_params_*.csv``. + """ + sim_path = _find_unique(run_dir, "_sim_parameters.csv", "sim_parameters", + warn_missing=False) + if sim_path is None: + return + + try: + sim_df = pd.read_csv(sim_path) + except Exception as exc: + warnings.warn(f"Could not load sim parameters from {sim_path}: {exc}") + return + + if "genotype" not in sim_df.columns: + warnings.warn( + f"sim_parameters {sim_path} has no 'genotype' column; " + "skipping param summaries" + ) + return + + sim_cols = [c for c in sim_df.columns if c != "genotype"] + + for col in sim_cols: + if not glob.glob(os.path.join(run_dir, f"*_params_{col}.csv")): + warnings.warn(f"Expected *_params_{col}.csv in {run_dir} but none found") + + transform_registry = _build_transform_registry(sim_df) + + for param_file in sorted(glob.glob(os.path.join(run_dir, "*_params_*.csv"))): + basename = os.path.basename(param_file) + + m_epi = re.search(r"_params_epi_(.+)\.csv$", basename) + m_diff = re.search(r"_params_d_(.+)\.csv$", basename) + m_direct = re.search(r"_params_(.+)\.csv$", basename) + + if m_epi: + kind, param_name = "epi", m_epi.group(1) + elif m_diff: + kind, param_name = "diff", m_diff.group(1) + elif m_direct: + kind, param_name = "direct", m_direct.group(1) + else: + continue + + if param_name not in transform_registry: + continue + + obs_series = transform_registry[param_name] + + try: + params_df = pd.read_csv(param_file) + except Exception as exc: + warnings.warn(f"Could not read {param_file}: {exc}") + continue + + obs = None + try: + if kind == "direct": + obs = _compute_direct_obs(params_df, obs_series) + elif kind == "diff": + obs = _compute_diff_obs(params_df, obs_series) + else: + obs = _compute_epi_obs(params_df, obs_series, param_name) + except Exception as exc: + warnings.warn(f"Could not compute obs for {basename}: {exc}") + continue + + if obs is None: + continue + + out_df = params_df.copy() + out_df["obs"] = obs + + if kind == "epi": + out_name = f"{out_prefix}_params_epi_{param_name}.csv" + plot_label = f"epi_{param_name}" + elif kind == "diff": + out_name = f"{out_prefix}_params_d_{param_name}.csv" + plot_label = f"d_{param_name}" + else: + out_name = f"{out_prefix}_params_{param_name}.csv" + plot_label = param_name + + try: + out_df.to_csv(out_name, index=False) + print(f"Wrote parameter summary to {out_name}") + except Exception as exc: + warnings.warn(f"Could not write {out_name}: {exc}") + continue + + pdf_name = out_name.replace(".csv", ".pdf") + _try_plot_params_corr(out_df, plot_label, pdf_name) + + def summarize_fit(run_dir, ground_truth_file=None, out_prefix=None): @@ -382,7 +625,7 @@ def summarize_fit(run_dir, metadata["n_theta_training_points"] = len(train_merged) if len(train_merged) > 0: theta_training_stats = _run_stats( - train_merged["median"].values, + train_merged["q0.5"].values, train_merged["theta_obs"].values, ) except Exception as exc: @@ -414,7 +657,7 @@ def summarize_fit(run_dir, metadata["n_theta_test_points"] = len(test_merged) if len(test_merged) > 0: theta_test_stats = _run_stats( - test_merged["median"].values, + test_merged["q0.5"].values, test_merged["theta_obs"].values, ) except Exception as exc: @@ -425,11 +668,11 @@ def summarize_fit(run_dir, try: growth_pred_df = pd.read_csv(growth_pred_file) # Drop rows where observed ln_cfu is missing (no observation at that timepoint) - growth_valid = growth_pred_df.dropna(subset=["ln_cfu", "median"]) + growth_valid = growth_pred_df.dropna(subset=["ln_cfu", "q0.5"]) metadata["n_growth_training_points"] = len(growth_valid) if len(growth_valid) > 0: growth_training_stats = _run_stats( - growth_valid["median"].values, + growth_valid["q0.5"].values, growth_valid["ln_cfu"].values, ) except Exception as exc: @@ -485,7 +728,7 @@ def summarize_fit(run_dir, if train_merged is not None and len(train_merged) > 0: xy_corr( x_values=train_merged["theta_obs"].values, - y_values=train_merged["median"].values, + y_values=train_merged["q0.5"].values, as_hexbin=False, ax=axes[0], ) @@ -498,7 +741,7 @@ def summarize_fit(run_dir, if test_merged is not None and len(test_merged) > 0: xy_corr( x_values=test_merged["theta_obs"].values, - y_values=test_merged["median"].values, + y_values=test_merged["q0.5"].values, as_hexbin=False, ax=axes[1], ) @@ -537,7 +780,7 @@ def summarize_fit(run_dir, fig, ax = plt.subplots(1, 1, figsize=(6, 6)) xy_corr( x_values=growth_pred_df["ln_cfu"].values, - y_values=growth_pred_df["median"].values, + y_values=growth_pred_df["q0.5"].values, as_hexbin=False, ax=ax, ) @@ -567,6 +810,9 @@ def summarize_fit(run_dir, except Exception as exc: warnings.warn(f"Could not generate loss curve plot: {exc}") + # --- Sim-parameter correlation CSVs --- + _summarize_params(run_dir, out_prefix) + return results diff --git a/src/tfscreen/tfmodel/scripts/summarize_sbc_cli.py b/src/tfscreen/tfmodel/scripts/summarize_sbc_cli.py index 230c32fe..d105d97a 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_sbc_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_sbc_cli.py @@ -6,7 +6,7 @@ statistics, and writes summary outputs. """ -from tfscreen.tfmodel.analysis.sbc import summarize_sbc +from tfscreen.tfmodel.analysis.error_calibration import summarize_sbc from tfscreen.util.cli import generalized_main diff --git a/tests/smoke-tests/test_prediction_smoke.py b/tests/smoke-tests/test_prediction_smoke.py index b3235a12..2000d69c 100644 --- a/tests/smoke-tests/test_prediction_smoke.py +++ b/tests/smoke-tests/test_prediction_smoke.py @@ -68,9 +68,9 @@ def test_prediction_smoke(growth_smoke_csv, assert set(pred_df["titrant_conc"]) == set(new_titrant_conc) # Check for quantile columns (defaults from load_posteriors) - assert "median" in pred_df.columns - assert "lower_95" in pred_df.columns - assert "upper_95" in pred_df.columns + assert "q0.5" in pred_df.columns + assert "q0.025" in pred_df.columns + assert "q0.975" in pred_df.columns # 3. Test expansion restriction for plated dimensions # 'logit_norm' plates on titrant_conc, so expanding it should fail. diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response_cli.py b/tests/tfscreen/analysis/cat_response/test_cat_response_cli.py index 2081f87a..8bcc9e78 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response_cli.py @@ -36,15 +36,15 @@ def _make_theta_df(center_col): "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [0.0, 1.0], center_col: [0.3, 0.7], - "upper_std": [0.4, 0.8], - "lower_std": [0.2, 0.6], + "q0.841": [0.4, 0.8], + "q0.159": [0.2, 0.6], }) class TestThetaColAutoDetect: - def test_autodetects_median(self, tmp_path): - """Values from median column are passed to the fitter.""" + def test_autodetects_q0_5(self, tmp_path): + """Values from q0.5 column are passed to the fitter.""" f = str(tmp_path / "theta.csv") captured = {} @@ -52,7 +52,7 @@ def fake_fit(x, y, y_std, models_to_run): captured["y"] = list(y) return (_FLAT_RESULT, None) - df = _make_theta_df("median") + df = _make_theta_df("q0.5") with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", return_value=df), \ patch("tfscreen.analysis.cat_response.cat_response_cli.cat_fit", @@ -90,7 +90,7 @@ def fake_fit(x, y, y_std, models_to_run): captured["y"] = list(y) return (_FLAT_RESULT, None) - df = _make_theta_df("median").copy() + df = _make_theta_df("q0.5").copy() df["my_col"] = [0.11, 0.22] with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", return_value=df), \ @@ -101,8 +101,8 @@ def fake_fit(x, y, y_std, models_to_run): assert captured["y"] == pytest.approx([0.11, 0.22]) - def test_median_preferred_over_point_est(self, tmp_path): - """When both median and point_est are present, median wins.""" + def test_q0_5_preferred_over_point_est(self, tmp_path): + """When both q0.5 and point_est are present, q0.5 wins.""" f = str(tmp_path / "theta.csv") captured = {} @@ -110,7 +110,7 @@ def fake_fit(x, y, y_std, models_to_run): captured["y"] = list(y) return (_FLAT_RESULT, None) - df = _make_theta_df("median").copy() + df = _make_theta_df("q0.5").copy() df["point_est"] = [0.9, 0.8] with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", return_value=df), \ @@ -122,14 +122,14 @@ def fake_fit(x, y, y_std, models_to_run): assert captured["y"] == pytest.approx([0.3, 0.7]) def test_raises_when_no_theta_col(self, tmp_path): - """ValueError when neither median nor point_est is present.""" + """ValueError when neither q0.5 nor point_est is present.""" f = str(tmp_path / "theta.csv") df = pd.DataFrame({ "genotype": ["wt"], "titrant_name": ["IPTG"], "titrant_conc": [0.0], - "upper_std": [0.5], - "lower_std": [0.3], + "q0.841": [0.5], + "q0.159": [0.3], }) with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", return_value=df): diff --git a/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py b/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py index 6fc0fc26..253c8ecd 100644 --- a/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py +++ b/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py @@ -36,15 +36,15 @@ def _make_df(center_col): "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [0.0, 1.0], center_col: [0.3, 0.7], - "upper_std": [0.4, 0.8], - "lower_std": [0.2, 0.6], + "q0.841": [0.4, 0.8], + "q0.159": [0.2, 0.6], }) class TestFitResponseThetaColAutoDetect: - def test_autodetects_median(self): - """Values from median column are passed to the fitter.""" + def test_autodetects_q0_5(self): + """Values from q0.5 column are passed to the fitter.""" captured = {} def fake_fit(x, y, y_std, models_to_run): @@ -53,12 +53,12 @@ def fake_fit(x, y, y_std, models_to_run): with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", side_effect=fake_fit), _SYNC_EXECUTOR: - fit_response(_make_df("median"), models_to_run=["flat"]) + fit_response(_make_df("q0.5"), models_to_run=["flat"]) assert captured["y"] == pytest.approx([0.3, 0.7]) def test_autodetects_point_est(self): - """Values from point_est column are passed to the fitter when median is absent.""" + """Values from point_est column are passed to the fitter when q0.5 is absent.""" captured = {} def fake_fit(x, y, y_std, models_to_run): @@ -72,14 +72,14 @@ def fake_fit(x, y, y_std, models_to_run): assert captured["y"] == pytest.approx([0.3, 0.7]) def test_explicit_theta_col_overrides(self): - """Explicit theta_col is used even when median is also present.""" + """Explicit theta_col is used even when q0.5 is also present.""" captured = {} def fake_fit(x, y, y_std, models_to_run): captured["y"] = list(y) return (_FLAT_RESULT, None) - df = _make_df("median").copy() + df = _make_df("q0.5").copy() df["my_col"] = [0.11, 0.22] with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", side_effect=fake_fit), _SYNC_EXECUTOR: @@ -87,15 +87,15 @@ def fake_fit(x, y, y_std, models_to_run): assert captured["y"] == pytest.approx([0.11, 0.22]) - def test_median_preferred_over_point_est(self): - """When both median and point_est are present, median wins.""" + def test_q0_5_preferred_over_point_est(self): + """When both q0.5 and point_est are present, q0.5 wins.""" captured = {} def fake_fit(x, y, y_std, models_to_run): captured["y"] = list(y) return (_FLAT_RESULT, None) - df = _make_df("median").copy() + df = _make_df("q0.5").copy() df["point_est"] = [0.9, 0.8] with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", side_effect=fake_fit), _SYNC_EXECUTOR: @@ -104,13 +104,13 @@ def fake_fit(x, y, y_std, models_to_run): assert captured["y"] == pytest.approx([0.3, 0.7]) def test_raises_when_no_theta_col(self): - """ValueError when neither median nor point_est is present.""" + """ValueError when neither q0.5 nor point_est is present.""" df = pd.DataFrame({ "genotype": ["wt"], "titrant_name": ["IPTG"], "titrant_conc": [0.0], - "upper_std": [0.5], - "lower_std": [0.3], + "q0.841": [0.5], + "q0.159": [0.3], }) with pytest.raises(ValueError, match="No theta column found"): fit_response(df, models_to_run=["flat"]) diff --git a/tests/tfscreen/plot/test_geno_trajectory.py b/tests/tfscreen/plot/test_geno_trajectory.py index 238427b4..eae62d0b 100644 --- a/tests/tfscreen/plot/test_geno_trajectory.py +++ b/tests/tfscreen/plot/test_geno_trajectory.py @@ -58,11 +58,11 @@ def _make_pred_df( "t_sel": t, "ln_cfu": (base + slope * t) if (include_obs and t >= 0) else np.nan, "ln_cfu_std": 0.2 if (include_obs and t >= 0) else np.nan, - "median": med, + "q0.5": med, } if include_ci: - row["q05"] = med - 0.5 - row["q95"] = med + 0.5 + row["q0.05"] = med - 0.5 + row["q0.95"] = med + 0.5 rows.append(row) return pd.DataFrame(rows) @@ -145,7 +145,7 @@ def test_no_crash_all_nan_ln_cfu(self): def test_no_crash_all_nan_median(self): df = _make_pred_df() - df["median"] = np.nan + df["q0.5"] = np.nan fig = plot_geno_trajectory(df) assert isinstance(fig, plt.Figure) plt.close(fig) @@ -361,9 +361,9 @@ def test_npz_path_converted_to_dict(self, tmp_path): "t_sel": np.linspace(0, 10, 5), "ln_cfu": [np.nan] * 5, "ln_cfu_std": [np.nan] * 5, - "q05": np.linspace(8, 12, 5) - 0.5, - "median": np.linspace(8, 12, 5), - "q95": np.linspace(8, 12, 5) + 0.5, + "q0.05": np.linspace(8, 12, 5) - 0.5, + "q0.5": np.linspace(8, 12, 5), + "q0.95": np.linspace(8, 12, 5) + 0.5, }) orchestrator.growth_df = pd.DataFrame({ "replicate": ["r0"], @@ -387,9 +387,9 @@ def _mock_predict(orch, params, predict_sites, **kwargs): "replicate": ["r0"], "condition_pre": ["pre"], "genotype": ["g0"], - "q05": [9.5], - "median": [10.0], - "q95": [10.5], + "q0.05": [9.5], + "q0.5": [10.0], + "q0.95": [10.5], }) return {"growth_pred": fine_df, "ln_cfu0": ln0} return fine_df @@ -454,17 +454,17 @@ def _mock_predict_two_sites(orch, params, predict_sites, **kwargs): "t_sel": np.linspace(0, 10, 5), "ln_cfu": [np.nan] * 5, "ln_cfu_std": [np.nan] * 5, - "q05": np.linspace(8, 12, 5) - 0.5, - "median": np.linspace(8, 12, 5), - "q95": np.linspace(8, 12, 5) + 0.5, + "q0.05": np.linspace(8, 12, 5) - 0.5, + "q0.5": np.linspace(8, 12, 5), + "q0.95": np.linspace(8, 12, 5) + 0.5, }) ln0 = pd.DataFrame({ "replicate": ["r0"], "condition_pre": ["pre"], "genotype": ["g0"], - "q05": [9.5], - "median": [10.0], - "q95": [10.5], + "q0.05": [9.5], + "q0.5": [10.0], + "q0.95": [10.5], }) return {"growth_pred": fine_df, "ln_cfu0": ln0} diff --git a/tests/tfscreen/plot/test_plot_theta_fits.py b/tests/tfscreen/plot/test_plot_theta_fits.py index 9ec37547..89fbd214 100644 --- a/tests/tfscreen/plot/test_plot_theta_fits.py +++ b/tests/tfscreen/plot/test_plot_theta_fits.py @@ -27,14 +27,14 @@ def _make_df(genotypes=("wt",), titrant_names=("IPTG",), "titrant_conc": c, "theta_obs": rng.uniform(0.1, 0.9), "theta_std": 0.05, - "median": rng.uniform(0.1, 0.9), + "q0.5": rng.uniform(0.1, 0.9), } if with_std_band: - row["lower_std"] = row["median"] - 0.1 - row["upper_std"] = row["median"] + 0.1 + row["q0.159"] = row["q0.5"] - 0.1 + row["q0.841"] = row["q0.5"] + 0.1 if with_95_band: - row["lower_95"] = row["median"] - 0.2 - row["upper_95"] = row["median"] + 0.2 + row["q0.025"] = row["q0.5"] - 0.2 + row["q0.975"] = row["q0.5"] + 0.2 rows.append(row) return pd.DataFrame(rows) @@ -265,8 +265,8 @@ def test_custom_markers_accepted(): plt.close("all") -def test_point_est_used_when_median_absent(): - """point_est column is used as centre line when median is not present.""" +def test_point_est_used_when_q0_5_absent(): + """point_est column is used as centre line when q0.5 is not present.""" rng = np.random.default_rng(42) df = pd.DataFrame({ "genotype": ["wt"] * 4, @@ -282,8 +282,8 @@ def test_point_est_used_when_median_absent(): plt.close("all") -def test_median_takes_precedence_over_point_est(): - """median is used when both median and point_est columns are present.""" +def test_q0_5_takes_precedence_over_point_est(): + """q0.5 is used when both q0.5 and point_est columns are present.""" rng = np.random.default_rng(0) df = pd.DataFrame({ "genotype": ["wt"] * 4, @@ -291,7 +291,7 @@ def test_median_takes_precedence_over_point_est(): "titrant_conc": [0.1, 1.0, 10.0, 100.0], "theta_obs": rng.uniform(0.1, 0.9, 4), "theta_std": [0.05] * 4, - "median": np.array([0.1, 0.2, 0.3, 0.4]), + "q0.5": np.array([0.1, 0.2, 0.3, 0.4]), "point_est": np.array([0.9, 0.8, 0.7, 0.6]), }) plt.close("all") diff --git a/tests/tfscreen/tfmodel/analysis/__init__.py b/tests/tfscreen/tfmodel/analysis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tfscreen/tfmodel/analysis/test_error_calibration.py b/tests/tfscreen/tfmodel/analysis/test_error_calibration.py new file mode 100644 index 00000000..0d1d039f --- /dev/null +++ b/tests/tfscreen/tfmodel/analysis/test_error_calibration.py @@ -0,0 +1,722 @@ +""" +Tests for tfscreen.tfmodel.analysis.error_calibration. + +Covers all public functions and the private SBC helpers that are tested by +the now-deleted test_sbc.py. +""" + +import os + +import h5py +import numpy as np +import pandas as pd +import pytest + +from tfscreen.tfmodel.analysis.error_calibration import ( + # Core + pit_from_samples, + pit_from_quantiles, + # Stats + calibration_curve, + pit_uniformity_test, + # Plots + plot_pit_histogram, + plot_calibration_curve, + # Single-run + calibration_summary, + # SBC + _find_pairs, + _load_h5_params, + compute_sbc_ranks, + summarize_sbc, +) + + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + +def _write_h5(path, arrays): + """Write a dict of numpy arrays to an HDF5 file.""" + with h5py.File(path, "w") as hf: + for key, val in arrays.items(): + hf.create_dataset(key, data=val) + + +# ============================================================ +# Part 1 — pit_from_samples +# ============================================================ + +class TestPitFromSamples: + + def test_rank_zero_when_gt_below_all_posterior(self): + # true = -100; all posterior > -100 → PIT = 0 + pit = pit_from_samples(np.array([-100.0]), np.ones((50, 1))) + np.testing.assert_allclose(pit, [0.0]) + + def test_rank_one_when_gt_above_all_posterior(self): + # true = 100; all posterior < 100 → PIT = 1 + pit = pit_from_samples(np.array([100.0]), -np.ones((50, 1))) + np.testing.assert_allclose(pit, [1.0]) + + def test_rank_half_at_median(self): + # posterior is -10..10 (21 values), true = 0 → 10/21 + post = np.arange(-10, 11, dtype=float).reshape(21, 1) + pit = pit_from_samples(np.array([0.0]), post) + np.testing.assert_allclose(pit, [10 / 21]) + + def test_nan_true_value_gives_nan(self): + post = np.ones((20, 1)) + pit = pit_from_samples(np.array([np.nan]), post) + assert np.isnan(pit[0]) + + def test_mixed_nan_and_valid(self): + true_vals = np.array([0.0, np.nan, 1000.0]) + post = np.zeros((20, 3)) # all samples = 0 + pit = pit_from_samples(true_vals, post) + # true=0: mean(0 < 0) = 0 + np.testing.assert_allclose(pit[0], 0.0) + assert np.isnan(pit[1]) + # true=1000: mean(0 < 1000) = 1 + np.testing.assert_allclose(pit[2], 1.0) + + def test_multidimensional_true_vals(self): + rng = np.random.default_rng(0) + post = rng.normal(size=(100, 4)) + true_vals = np.zeros(4) + pit = pit_from_samples(true_vals, post) + assert pit.shape == (4,) + assert np.all((pit >= 0) & (pit <= 1)) + + def test_output_in_unit_interval(self): + rng = np.random.default_rng(42) + post = rng.normal(size=(200, 10)) + true_vals = rng.normal(size=10) + pit = pit_from_samples(true_vals, post) + assert np.all((pit >= 0) & (pit <= 1)) + + def test_scalar_true_val(self): + # true_vals can be a scalar if posterior is shape (S,) + post = np.linspace(-1, 1, 101) + pit = pit_from_samples(0.0, post) + # 50 values < 0 → 50/101 + np.testing.assert_allclose(pit, 50 / 101) + + +# ============================================================ +# Part 1 — pit_from_quantiles +# ============================================================ + +class TestPitFromQuantiles: + + def _make_gaussian_quantiles(self, n=5): + """Return quantile_matrix and levels for N(0,1) at 9 levels.""" + from scipy.stats import norm + levels = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]) + qmat = np.tile(norm.ppf(levels), (n, 1)) # same distribution for all n + return qmat, levels + + def test_true_at_stored_quantile_returns_that_level(self): + from scipy.stats import norm + levels = np.array([0.1, 0.5, 0.9]) + qmat = norm.ppf(levels).reshape(1, 3) + # True value exactly at the 50th percentile + pit = pit_from_quantiles(np.array([0.0]), qmat, levels) + np.testing.assert_allclose(pit, [0.5]) + + def test_true_below_all_quantiles_returns_zero(self): + levels = np.array([0.1, 0.5, 0.9]) + qmat = np.array([[1.0, 2.0, 3.0]]) + pit = pit_from_quantiles(np.array([-999.0]), qmat, levels) + np.testing.assert_allclose(pit, [0.0]) + + def test_true_above_all_quantiles_returns_one(self): + levels = np.array([0.1, 0.5, 0.9]) + qmat = np.array([[1.0, 2.0, 3.0]]) + pit = pit_from_quantiles(np.array([999.0]), qmat, levels) + np.testing.assert_allclose(pit, [1.0]) + + def test_linear_interpolation_between_levels(self): + # quantile values 0,1,2,3,4 at levels 0,0.25,0.50,0.75,1.0 + levels = np.array([0.0, 0.25, 0.50, 0.75, 1.0]) + qmat = np.array([[0.0, 1.0, 2.0, 3.0, 4.0]]) + # True = 1.5: halfway between q=1.0 (level 0.25) and q=2.0 (level 0.5) + pit = pit_from_quantiles(np.array([1.5]), qmat, levels) + np.testing.assert_allclose(pit, [0.375], atol=1e-10) + + def test_nan_true_value_gives_nan(self): + levels = np.array([0.1, 0.5, 0.9]) + qmat = np.array([[1.0, 2.0, 3.0]]) + pit = pit_from_quantiles(np.array([np.nan]), qmat, levels) + assert np.isnan(pit[0]) + + def test_nan_in_quantile_row_gives_nan(self): + levels = np.array([0.1, 0.5, 0.9]) + qmat = np.array([[1.0, np.nan, 3.0]]) + pit = pit_from_quantiles(np.array([2.0]), qmat, levels) + assert np.isnan(pit[0]) + + def test_shape_preserved(self): + qmat, levels = self._make_gaussian_quantiles(n=7) + true_vals = np.zeros(7) + pit = pit_from_quantiles(true_vals, qmat, levels) + assert pit.shape == (7,) + + def test_output_in_unit_interval_for_gaussian(self): + rng = np.random.default_rng(7) + qmat, levels = self._make_gaussian_quantiles(n=20) + true_vals = rng.normal(size=20) + pit = pit_from_quantiles(true_vals, qmat, levels) + finite = pit[np.isfinite(pit)] + assert np.all((finite >= 0) & (finite <= 1)) + + def test_multiple_rows_independent(self): + # Each row has different quantile values; all true values at median + levels = np.array([0.1, 0.5, 0.9]) + qmat = np.array([ + [0.0, 1.0, 2.0], + [10.0, 20.0, 30.0], + ]) + true_vals = np.array([1.0, 20.0]) # each at the median + pit = pit_from_quantiles(true_vals, qmat, levels) + np.testing.assert_allclose(pit, [0.5, 0.5]) + + +# ============================================================ +# Part 2 — calibration_curve +# ============================================================ + +class TestCalibrationCurve: + + def test_uniform_pit_gives_empirical_near_nominal(self): + rng = np.random.default_rng(0) + pit = rng.uniform(0, 1, size=20_000) + curve = calibration_curve(pit) + for nominal, empirical in curve.items(): + assert abs(empirical - nominal) < 0.03, ( + f"nominal={nominal:.2f}, empirical={empirical:.3f}" + ) + + def test_overconfident_model_below_diagonal(self): + # All PIT near 0 or 1 (U-shaped) → CIs too narrow → empirical < nominal + rng = np.random.default_rng(1) + pit = np.concatenate([ + rng.uniform(0.0, 0.05, 500), + rng.uniform(0.95, 1.0, 500), + ]) + curve = calibration_curve(pit, levels=[0.9]) + # For 90% CI we expect ~90% coverage but get ~0% (all PIT outside [0.05, 0.95]) + assert curve[0.9] < 0.5 + + def test_underconfident_model_above_diagonal(self): + # All PIT near 0.5 → posterior spread too wide → empirical > nominal for tight CIs + pit = np.full(1000, 0.5) + curve = calibration_curve(pit, levels=[0.5]) + # 50% CI spans [0.25, 0.75]; all PIT = 0.5 are inside → empirical = 1.0 > 0.5 + assert curve[0.5] > 0.9 + + def test_returns_dict_with_default_keys(self): + pit = np.linspace(0.01, 0.99, 100) + curve = calibration_curve(pit) + assert set(curve.keys()) == {0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99} + + def test_custom_levels(self): + pit = np.linspace(0, 1, 200) + curve = calibration_curve(pit, levels=[0.8, 0.9]) + assert set(curve.keys()) == {0.8, 0.9} + + def test_nan_in_pit_ignored(self): + rng = np.random.default_rng(2) + pit = rng.uniform(0, 1, size=1000) + pit_with_nan = np.concatenate([pit, np.full(50, np.nan)]) + curve_clean = calibration_curve(pit) + curve_nan = calibration_curve(pit_with_nan) + for k in curve_clean: + np.testing.assert_allclose(curve_clean[k], curve_nan[k]) + + def test_values_in_unit_interval(self): + pit = np.linspace(0.01, 0.99, 200) + curve = calibration_curve(pit) + for v in curve.values(): + assert 0.0 <= v <= 1.0 + + def test_empty_pit_after_nan_removal(self): + pit = np.full(10, np.nan) + curve = calibration_curve(pit) + # np.mean of an empty slice returns NaN + for v in curve.values(): + assert np.isnan(v) + + +# ============================================================ +# Part 2 — pit_uniformity_test +# ============================================================ + +class TestPitUniformityTest: + + def test_uniform_input_high_pvalue(self): + rng = np.random.default_rng(42) + pit = rng.uniform(0, 1, size=2000) + result = pit_uniformity_test(pit) + assert result["ks_pval"] > 0.05 + + def test_nonuniform_input_low_pvalue(self): + # Bimodal near 0 and 1 — clearly not uniform + pit = np.concatenate([np.full(200, 0.01), np.full(200, 0.99)]) + result = pit_uniformity_test(pit) + assert result["ks_pval"] < 0.001 + + def test_returns_expected_keys(self): + pit = np.linspace(0, 1, 50) + result = pit_uniformity_test(pit) + assert set(result.keys()) == {"ks_stat", "ks_pval", "mean_pit", "n_vals"} + + def test_empty_input_returns_nan(self): + result = pit_uniformity_test(np.array([])) + assert np.isnan(result["ks_stat"]) + assert np.isnan(result["ks_pval"]) + assert np.isnan(result["mean_pit"]) + assert result["n_vals"] == 0 + + def test_all_nan_returns_nan(self): + result = pit_uniformity_test(np.full(10, np.nan)) + assert np.isnan(result["ks_stat"]) + assert result["n_vals"] == 0 + + def test_mean_pit_near_half_for_uniform(self): + rng = np.random.default_rng(5) + pit = rng.uniform(0, 1, size=5000) + result = pit_uniformity_test(pit) + assert abs(result["mean_pit"] - 0.5) < 0.05 + + def test_n_vals_excludes_nan(self): + pit = np.array([0.1, 0.5, 0.9, np.nan, np.nan]) + result = pit_uniformity_test(pit) + assert result["n_vals"] == 3 + + +# ============================================================ +# Part 3 — plot_pit_histogram +# ============================================================ + +class TestPlotPitHistogram: + + def test_returns_axes(self): + from matplotlib.axes import Axes + pit = np.linspace(0.05, 0.95, 50) + ax = plot_pit_histogram(pit) + assert isinstance(ax, Axes) + + def test_accepts_existing_axes(self): + from matplotlib import pyplot as plt + from matplotlib.axes import Axes + fig, ax = plt.subplots() + pit = np.linspace(0.1, 0.9, 30) + returned = plot_pit_histogram(pit, ax=ax) + assert returned is ax + plt.close(fig) + + def test_title_set(self): + from matplotlib import pyplot as plt + pit = np.linspace(0.1, 0.9, 30) + ax = plot_pit_histogram(pit, title="my title") + assert ax.get_title() == "my title" + plt.close(ax.get_figure()) + + def test_nan_filtered_no_crash(self): + pit = np.array([0.1, np.nan, 0.5, np.nan, 0.9]) + ax = plot_pit_histogram(pit) + from matplotlib import pyplot as plt + plt.close(ax.get_figure()) + + def test_custom_bins(self): + from matplotlib import pyplot as plt + pit = np.linspace(0.01, 0.99, 100) + ax = plot_pit_histogram(pit, n_bins=5) + # Just checks it runs without error + plt.close(ax.get_figure()) + + +# ============================================================ +# Part 3 — plot_calibration_curve +# ============================================================ + +class TestPlotCalibrationCurve: + + def test_returns_axes(self): + from matplotlib.axes import Axes + curve = {0.5: 0.48, 0.9: 0.88, 0.95: 0.93} + ax = plot_calibration_curve(curve) + assert isinstance(ax, Axes) + + def test_accepts_existing_axes(self): + from matplotlib import pyplot as plt + from matplotlib.axes import Axes + fig, ax = plt.subplots() + curve = {0.5: 0.5, 0.9: 0.9} + returned = plot_calibration_curve(curve, ax=ax) + assert returned is ax + plt.close(fig) + + def test_empty_dict_no_crash(self): + from matplotlib import pyplot as plt + ax = plot_calibration_curve({}) + plt.close(ax.get_figure()) + + def test_label_appears_in_legend(self): + from matplotlib import pyplot as plt + curve = {0.5: 0.5, 0.9: 0.9} + ax = plot_calibration_curve(curve, label="TestModel") + legend_texts = [t.get_text() for t in ax.get_legend().get_texts()] + assert "TestModel" in legend_texts + plt.close(ax.get_figure()) + + +# ============================================================ +# Part 4 — calibration_summary +# ============================================================ + +def _make_calibration_inputs(n=40, seed=0): + """Return (true_vals, quantile_matrix, quantile_levels) for N(0,1). + + Stores 9 quantile levels; true values are drawn from N(0,1) so the PIT + values should be approximately uniform. + """ + from scipy.stats import norm + rng = np.random.default_rng(seed) + levels = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]) + true_vals = rng.normal(size=n) + # All observations share the same N(0,1) posterior + qmat = np.tile(norm.ppf(levels), (n, 1)) + return true_vals, qmat, levels + + +class TestCalibrationSummary: + + def test_writes_pit_csv(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + assert os.path.exists(f"{prefix}_pit.csv") + + def test_pit_csv_has_correct_columns(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + df = pd.read_csv(f"{prefix}_pit.csv") + assert "true_val" in df.columns + assert "pit" in df.columns + + def test_pit_csv_has_correct_length(self, tmp_path): + n = 25 + true_vals, qmat, levels = _make_calibration_inputs(n=n) + prefix = str(tmp_path / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + df = pd.read_csv(f"{prefix}_pit.csv") + assert len(df) == n + + def test_writes_calibration_curve_csv(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + assert os.path.exists(f"{prefix}_calibration_curve.csv") + + def test_calibration_curve_csv_columns(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + df = pd.read_csv(f"{prefix}_calibration_curve.csv") + assert "nominal" in df.columns + assert "empirical" in df.columns + + def test_writes_pdf(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + assert os.path.exists(f"{prefix}_calibration.pdf") + + def test_returns_dict_with_expected_keys(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + result = calibration_summary(true_vals, qmat, levels, prefix) + for key in ("n_vals", "ks_stat", "ks_pval", "mean_pit", "calibration_curve"): + assert key in result + + def test_calibration_curve_in_result(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs() + prefix = str(tmp_path / "cal") + result = calibration_summary(true_vals, qmat, levels, prefix) + assert isinstance(result["calibration_curve"], dict) + assert len(result["calibration_curve"]) > 0 + + def test_n_vals_matches_finite_pit(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs(n=30) + prefix = str(tmp_path / "cal") + result = calibration_summary(true_vals, qmat, levels, prefix) + assert result["n_vals"] == 30 + + def test_nan_true_vals_handled_gracefully(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs(n=20) + true_vals[::4] = np.nan # introduce NaNs + prefix = str(tmp_path / "cal") + result = calibration_summary(true_vals, qmat, levels, prefix) + assert result["n_vals"] < 20 + + def test_label_used_in_no_crash(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs(n=10) + prefix = str(tmp_path / "cal") + # Should not raise + calibration_summary(true_vals, qmat, levels, prefix, label="theta_high") + + def test_creates_output_directory(self, tmp_path): + true_vals, qmat, levels = _make_calibration_inputs(n=10) + prefix = str(tmp_path / "nested" / "deep" / "cal") + calibration_summary(true_vals, qmat, levels, prefix) + assert os.path.exists(str(tmp_path / "nested" / "deep")) + + def test_well_calibrated_model_passes_ks(self, tmp_path): + # N(0,1) true values with N(0,1) posterior → PIT ~ Uniform + true_vals, qmat, levels = _make_calibration_inputs(n=200, seed=99) + prefix = str(tmp_path / "cal") + result = calibration_summary(true_vals, qmat, levels, prefix) + # With 200 samples the KS test should not flag a calibrated model + assert result["ks_pval"] > 0.01 + + +# ============================================================ +# Part 5 — _load_h5_params (migrated from test_sbc.py) +# ============================================================ + +class TestLoadH5Params: + + def test_round_trips_arrays(self, tmp_path): + data = {"alpha": np.array([1.0, 2.0, 3.0]), "beta": np.array([[4.0, 5.0]])} + p = str(tmp_path / "test.h5") + _write_h5(p, data) + result = _load_h5_params(p) + np.testing.assert_allclose(result["alpha"], data["alpha"]) + np.testing.assert_allclose(result["beta"], data["beta"]) + + def test_returns_all_keys(self, tmp_path): + data = {"x": np.ones(3), "y": np.zeros(5)} + p = str(tmp_path / "test.h5") + _write_h5(p, data) + result = _load_h5_params(p) + assert set(result.keys()) == {"x", "y"} + + def test_empty_file(self, tmp_path): + p = str(tmp_path / "empty.h5") + _write_h5(p, {}) + result = _load_h5_params(p) + assert result == {} + + +# ============================================================ +# Part 5 — compute_sbc_ranks (migrated from test_sbc.py) +# ============================================================ + +class TestComputeSBCRanks: + + def _make_pair(self, tmp_path, gt_val, posterior_samples, name="param"): + gt_path = str(tmp_path / "run_ground_truth.h5") + post_path = str(tmp_path / "run_posterior.h5") + _write_h5(gt_path, {name: np.array(gt_val)[np.newaxis, :]}) + _write_h5(post_path, {name: np.array(posterior_samples)}) + return gt_path, post_path + + def test_rank_zero_when_gt_below_all_posterior(self, tmp_path): + gt_path, post_path = self._make_pair( + tmp_path, gt_val=[-100.0], posterior_samples=np.ones((20, 1)) + ) + ranks = compute_sbc_ranks(gt_path, post_path) + np.testing.assert_allclose(ranks["param"], [0.0]) + + def test_rank_one_when_gt_above_all_posterior(self, tmp_path): + gt_path, post_path = self._make_pair( + tmp_path, gt_val=[100.0], posterior_samples=-np.ones((20, 1)) + ) + ranks = compute_sbc_ranks(gt_path, post_path) + np.testing.assert_allclose(ranks["param"], [1.0]) + + def test_rank_half_when_gt_at_median(self, tmp_path): + post = np.arange(-10, 11, dtype=float).reshape(21, 1) + gt_path, post_path = self._make_pair(tmp_path, gt_val=[0.0], posterior_samples=post) + ranks = compute_sbc_ranks(gt_path, post_path) + np.testing.assert_allclose(ranks["param"], [10 / 21]) + + def test_multidimensional_param(self, tmp_path): + rng = np.random.default_rng(0) + post = rng.normal(size=(100, 3)) + gt = np.zeros((1, 3)) + gt_path = str(tmp_path / "run_ground_truth.h5") + post_path = str(tmp_path / "run_posterior.h5") + _write_h5(gt_path, {"param": gt}) + _write_h5(post_path, {"param": post}) + ranks = compute_sbc_ranks(gt_path, post_path) + assert ranks["param"].shape == (3,) + assert np.all((ranks["param"] >= 0) & (ranks["param"] <= 1)) + + def test_missing_posterior_param_skipped(self, tmp_path): + gt_path = str(tmp_path / "run_ground_truth.h5") + post_path = str(tmp_path / "run_posterior.h5") + _write_h5(gt_path, {"param_a": np.ones((1, 2)), "param_b": np.ones((1, 2))}) + _write_h5(post_path, {"param_a": np.ones((10, 2))}) + ranks = compute_sbc_ranks(gt_path, post_path) + assert "param_a" in ranks + assert "param_b" not in ranks + + def test_returns_empty_when_no_common_params(self, tmp_path): + gt_path = str(tmp_path / "run_ground_truth.h5") + post_path = str(tmp_path / "run_posterior.h5") + _write_h5(gt_path, {"a": np.ones((1, 2))}) + _write_h5(post_path, {"b": np.ones((10, 2))}) + ranks = compute_sbc_ranks(gt_path, post_path) + assert ranks == {} + + def test_rank_values_in_unit_interval(self, tmp_path): + rng = np.random.default_rng(42) + post = rng.normal(size=(50, 4)) + gt = rng.normal(size=(1, 4)) + gt_path = str(tmp_path / "run_ground_truth.h5") + post_path = str(tmp_path / "run_posterior.h5") + _write_h5(gt_path, {"theta": gt}) + _write_h5(post_path, {"theta": post}) + ranks = compute_sbc_ranks(gt_path, post_path) + assert np.all((ranks["theta"] >= 0) & (ranks["theta"] <= 1)) + + +# ============================================================ +# Part 5 — _find_pairs (migrated from test_sbc.py) +# ============================================================ + +class TestFindPairs: + + def test_finds_paired_files(self, tmp_path): + for prefix in ("run001", "run002"): + (tmp_path / f"{prefix}_ground_truth.h5").touch() + (tmp_path / f"{prefix}_posterior.h5").touch() + pairs = _find_pairs(str(tmp_path)) + assert len(pairs) == 2 + run_ids = {p[0] for p in pairs} + assert run_ids == {"run001", "run002"} + + def test_missing_posterior_gives_none(self, tmp_path): + (tmp_path / "run001_ground_truth.h5").touch() + pairs = _find_pairs(str(tmp_path)) + assert len(pairs) == 1 + run_id, gt_path, post_path = pairs[0] + assert run_id == "run001" + assert post_path is None + + def test_returns_empty_when_no_gt_files(self, tmp_path): + (tmp_path / "run001_posterior.h5").touch() + pairs = _find_pairs(str(tmp_path)) + assert pairs == [] + + def test_gt_path_is_absolute(self, tmp_path): + (tmp_path / "run001_ground_truth.h5").touch() + (tmp_path / "run001_posterior.h5").touch() + pairs = _find_pairs(str(tmp_path)) + _, gt_path, post_path = pairs[0] + assert os.path.isabs(gt_path) + assert os.path.isabs(post_path) + + +# ============================================================ +# Part 5 — summarize_sbc (migrated from test_sbc.py) +# ============================================================ + +def _make_calibrated_sbc_dir(tmp_path, n_runs=20, n_samples=200, seed=0): + """Create an SBC directory with approximately uniform ranks. + + For each run: gt ~ N(0,1), posterior ~ N(0,1) — ranks are Uniform(0,1). + """ + rng = np.random.default_rng(seed) + sbc_dir = tmp_path / "sbc" + sbc_dir.mkdir() + for i in range(n_runs): + gt = rng.normal(size=(1, 3)) + post = rng.normal(size=(n_samples, 3)) + run_id = f"run{i:04d}" + _write_h5(str(sbc_dir / f"{run_id}_ground_truth.h5"), {"alpha": gt}) + _write_h5(str(sbc_dir / f"{run_id}_posterior.h5"), {"alpha": post}) + return str(sbc_dir) + + +class TestSummarizeSBC: + + def test_returns_dataframe_with_expected_columns(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path) + df = summarize_sbc(sbc_dir) + assert not df.empty + for col in ("param", "n_runs", "n_ranks", "mean_rank", "ks_stat", "ks_pval"): + assert col in df.columns + + def test_one_row_per_parameter(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path) + df = summarize_sbc(sbc_dir) + assert len(df) == 1 + assert df.iloc[0]["param"] == "alpha" + + def test_n_runs_correct(self, tmp_path): + n_runs = 15 + sbc_dir = _make_calibrated_sbc_dir(tmp_path, n_runs=n_runs) + df = summarize_sbc(sbc_dir) + assert df.iloc[0]["n_runs"] == n_runs + + def test_n_ranks_correct(self, tmp_path): + # 10 runs × 3 elements each = 30 rank values + sbc_dir = _make_calibrated_sbc_dir(tmp_path, n_runs=10) + df = summarize_sbc(sbc_dir) + assert df.iloc[0]["n_ranks"] == 30 + + def test_writes_summary_csv(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path) + summarize_sbc(sbc_dir) + assert os.path.exists(os.path.join(sbc_dir, "sbc_sbc_summary.csv")) + + def test_writes_ranks_csv(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path) + summarize_sbc(sbc_dir) + assert os.path.exists(os.path.join(sbc_dir, "sbc_sbc_ranks.csv")) + + def test_writes_histogram_pdf(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path) + summarize_sbc(sbc_dir) + assert os.path.exists(os.path.join(sbc_dir, "sbc_rank_hist.pdf")) + + def test_custom_out_prefix(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path) + prefix = str(tmp_path / "myprefix") + summarize_sbc(sbc_dir, out_prefix=prefix) + assert os.path.exists(f"{prefix}_sbc_summary.csv") + assert os.path.exists(f"{prefix}_sbc_ranks.csv") + + def test_empty_dir_returns_empty_dataframe(self, tmp_path): + sbc_dir = str(tmp_path / "empty") + os.makedirs(sbc_dir) + df = summarize_sbc(sbc_dir) + assert df.empty + + def test_missing_posterior_skipped_without_crash(self, tmp_path): + sbc_dir = tmp_path / "sbc" + sbc_dir.mkdir() + rng = np.random.default_rng(1) + gt = rng.normal(size=(1, 2)) + post = rng.normal(size=(50, 2)) + _write_h5(str(sbc_dir / "run0000_ground_truth.h5"), {"alpha": gt}) + _write_h5(str(sbc_dir / "run0000_posterior.h5"), {"alpha": post}) + _write_h5(str(sbc_dir / "run0001_ground_truth.h5"), + {"alpha": rng.normal(size=(1, 2))}) + df = summarize_sbc(str(sbc_dir)) + assert not df.empty + assert df.iloc[0]["n_runs"] == 1 + + def test_nonexistent_dir_raises(self, tmp_path): + with pytest.raises(FileNotFoundError): + summarize_sbc(str(tmp_path / "does_not_exist")) + + def test_mean_rank_near_half_for_calibrated_model(self, tmp_path): + sbc_dir = _make_calibrated_sbc_dir(tmp_path, n_runs=100, n_samples=500) + df = summarize_sbc(sbc_dir) + assert abs(df.iloc[0]["mean_rank"] - 0.5) < 0.05 diff --git a/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py b/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py index f9bebfd8..99e0cae9 100644 --- a/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py @@ -112,6 +112,7 @@ def test_creates_csv_files(self, tmp_path): assert os.path.exists(f"{out_prefix}_activity.csv") def test_passes_point_est_q_to_get(self, tmp_path): + """q_to_get=[0.5] is passed to extract_parameters for MAP checkpoints.""" model = ExtractModel(num_genotype=4) ckpt_path = _make_map_checkpoint(tmp_path, model, step=100) out_prefix = str(tmp_path / "out") @@ -119,7 +120,7 @@ def test_passes_point_est_q_to_get(self, tmp_path): def fake_extract(orchestrator, posteriors, q_to_get=None): captured["q_to_get"] = q_to_get - return {"activity": pd.DataFrame({"genotype": ["wt"], "point_est": [0.5]})} + return {"activity": pd.DataFrame({"genotype": ["wt"], "q0.5": [0.5]})} fake_gm = FakeTFModel(model) with patch( @@ -133,7 +134,7 @@ def fake_extract(orchestrator, posteriors, q_to_get=None): ): extract_params("cfg.yaml", ckpt_path, out_prefix=out_prefix) - assert captured["q_to_get"] == {"point_est": 0.5} + assert captured["q_to_get"] == [0.5] def test_posteriors_have_leading_sample_dim(self, tmp_path): model = ExtractModel(num_genotype=4) diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py index 3f3596ca..8c34a719 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py @@ -46,7 +46,7 @@ def fake_predict(**kwargs): genotypes = kwargs.get("genotypes") or mock_orchestrator.growth_df["genotype"].unique().tolist() concs = kwargs.get("titrant_conc") or mock_orchestrator.growth_df["titrant_conc"].unique().tolist() rows = [{"genotype": g, "titrant_name": "IPTG", "titrant_conc": c, - "median": 10.0} + "q0.5": 10.0} for g in genotypes for c in concs] return pd.DataFrame(rows) @@ -209,7 +209,7 @@ def fake_predict(**kwargs): genotypes = kwargs.get("genotypes") or mock_orchestrator.growth_df["genotype"].unique().tolist() concs = kwargs.get("titrant_conc") or mock_orchestrator.growth_df["titrant_conc"].unique().tolist() rows = [{"genotype": g, "titrant_name": "IPTG", "titrant_conc": c, - "median": 10.0} + "q0.5": 10.0} for g in genotypes for c in concs] return pd.DataFrame(rows) @@ -271,7 +271,7 @@ def test_resolved_path_passed_to_predict(self, mock_orchestrator, tmp_path): def fake_predict(**kwargs): predict_calls["param_posteriors"] = kwargs.get("param_posteriors") return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [10.0]}) + "titrant_conc": [0.0], "q0.5": [10.0]}) patches = self._make_fixtures(mock_orchestrator) with patches[0], patch( @@ -289,13 +289,13 @@ def fake_predict(**kwargs): assert predict_calls["param_posteriors"] == "resolved_map.h5" def test_pkl_passes_point_est_q_to_get(self, mock_orchestrator, tmp_path): - """q_to_get={"point_est": 0.5} is passed to predict when param_file is .pkl.""" + """q_to_get=[0.5] is passed to predict when param_file is .pkl.""" predict_calls = {} def fake_predict(**kwargs): predict_calls["q_to_get"] = kwargs.get("q_to_get") return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "point_est": [10.0]}) + "titrant_conc": [0.0], "q0.5": [10.0]}) patches = self._make_fixtures(mock_orchestrator) with patches[0], patch( @@ -310,7 +310,7 @@ def fake_predict(**kwargs): predict_growth("cfg.yaml", "run_checkpoint.pkl", out_prefix=str(tmp_path / "out")) - assert predict_calls["q_to_get"] == {"point_est": 0.5} + assert predict_calls["q_to_get"] == [0.5] def test_h5_passes_none_q_to_get(self, mock_orchestrator, tmp_path): """q_to_get=None is passed to predict when param_file is .h5.""" @@ -319,7 +319,7 @@ def test_h5_passes_none_q_to_get(self, mock_orchestrator, tmp_path): def fake_predict(**kwargs): predict_calls["q_to_get"] = kwargs.get("q_to_get") return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [10.0]}) + "titrant_conc": [0.0], "q0.5": [10.0]}) patches = self._make_fixtures(mock_orchestrator) with patches[0], patch( diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py index 7e9c6dbc..e140e0a3 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py @@ -48,7 +48,7 @@ def _fake_extract(model, posteriors, **kwargs): else: pairs = [("IPTG", 0.0), ("IPTG", 1.0)] target = kwargs.get("target_genotypes") or ["wt", "A1B"] - rows = [{"genotype": g, "titrant_name": tn, "titrant_conc": tc, "median": 0.5} + rows = [{"genotype": g, "titrant_name": tn, "titrant_conc": tc, "q0.5": 0.5} for g in target for tn, tc in pairs] return pd.DataFrame(rows) @@ -102,7 +102,7 @@ def test_no_files_uses_all_training_genotypes(self, mock_extract, mock_orchestra ) as mock_curves: mock_curves.return_value = pd.DataFrame( {"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]} + "titrant_conc": [0.0], "q0.5": [0.5]} ) predict_theta("cfg.yaml", "post.h5", out_prefix=out) assert mock_curves.called @@ -126,7 +126,7 @@ def test_genotypes_file_unions_with_training(self, mock_extract, mock_orchestrat {"genotype": ["wt", "A1B", "C2D"], "titrant_name": ["IPTG"] * 3, "titrant_conc": [0.0] * 3, - "median": [0.5] * 3} + "q0.5": [0.5] * 3} ) predict_theta("cfg.yaml", "post.h5", genotypes_file=gf, out_prefix=out) @@ -147,7 +147,7 @@ def test_titrant_files_union_with_training_grid(self, mock_extract, mock_orchest ) as mock_curves: mock_curves.return_value = pd.DataFrame( {"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]} + "titrant_conc": [0.0], "q0.5": [0.5]} ) predict_theta("cfg.yaml", "post.h5", titrant_names_file=nf, @@ -170,7 +170,7 @@ def test_single_titrant_name_broadcast_across_concs(self, mock_extract, mock_orc ) as mock_curves: mock_curves.return_value = pd.DataFrame( {"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]} + "titrant_conc": [0.0], "q0.5": [0.5]} ) predict_theta("cfg.yaml", "post.h5", titrant_names_file=nf, @@ -203,7 +203,7 @@ def test_duplicate_titrant_pairs_not_repeated(self, mock_extract, mock_orchestra ) as mock_curves: mock_curves.return_value = pd.DataFrame( {"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]} + "titrant_conc": [0.0], "q0.5": [0.5]} ) predict_theta("cfg.yaml", "post.h5", titrant_names_file=nf, @@ -233,7 +233,7 @@ def test_only_files_restricts_genotypes_to_file(self, mock_extract, mock_orchest ) as mock_curves: mock_curves.return_value = pd.DataFrame( {"genotype": ["A1B"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]} + "titrant_conc": [0.0], "q0.5": [0.5]} ) predict_theta("cfg.yaml", "post.h5", genotypes_file=gf, @@ -256,7 +256,7 @@ def test_only_files_restricts_titrant_grid_to_file(self, mock_extract, mock_orch ) as mock_curves: mock_curves.return_value = pd.DataFrame( {"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [5.0], "median": [0.5]} + "titrant_conc": [5.0], "q0.5": [0.5]} ) predict_theta("cfg.yaml", "post.h5", titrant_names_file=nf, @@ -287,7 +287,7 @@ def _base_patches(self, mock_orchestrator): ".predict_theta_cli.extract_theta_curves", return_value=pd.DataFrame({ "genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5], + "titrant_conc": [0.0], "q0.5": [0.5], }), ), patch( @@ -295,7 +295,7 @@ def _base_patches(self, mock_orchestrator): ".predict_theta_cli.extract_theta_unmeasured", return_value=pd.DataFrame({ "genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5], + "titrant_conc": [0.0], "q0.5": [0.5], }), ), ] @@ -327,7 +327,7 @@ def fake_curves(**kwargs): extract_calls["posteriors"] = kwargs.get("posteriors") return pd.DataFrame({ "genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5], + "titrant_conc": [0.0], "q0.5": [0.5], }) p = self._base_patches(mock_orchestrator) @@ -365,7 +365,7 @@ def test_custom_batch_size_forwarded(self, mock_extract, mock_orchestrator, tmp_ "genotype": ["wt", "A1B", "C2D"], "titrant_name": ["IPTG"] * 3, "titrant_conc": [0.0] * 3, - "median": [0.5] * 3, + "q0.5": [0.5] * 3, }) predict_theta("cfg.yaml", "post.h5", genotypes_file=gf, @@ -385,7 +385,7 @@ def test_default_batch_size_is_2000(self, mock_extract, mock_orchestrator, tmp_p "genotype": ["wt", "A1B", "C2D"], "titrant_name": ["IPTG"] * 3, "titrant_conc": [0.0] * 3, - "median": [0.5] * 3, + "q0.5": [0.5] * 3, }) predict_theta("cfg.yaml", "post.h5", genotypes_file=gf, @@ -400,13 +400,13 @@ def test_default_batch_size_is_2000(self, mock_extract, mock_orchestrator, tmp_p class TestPredictThetaQToGet: def test_pkl_passes_point_est_q_to_get(self, mock_orchestrator, tmp_path): - """q_to_get={"point_est": 0.5} is passed to extract_theta_curves for .pkl input.""" + """q_to_get=[0.5] is passed to extract_theta_curves for .pkl input.""" extract_calls = {} def fake_curves(**kwargs): extract_calls["q_to_get"] = kwargs.get("q_to_get") return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "point_est": [0.5]}) + "titrant_conc": [0.0], "q0.5": [0.5]}) with patch( "tfscreen.tfmodel.scripts" @@ -424,12 +424,12 @@ def fake_curves(**kwargs): "tfscreen.tfmodel.scripts" ".predict_theta_cli.extract_theta_unmeasured", return_value=pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "point_est": [0.5]}), + "titrant_conc": [0.0], "q0.5": [0.5]}), ): predict_theta("cfg.yaml", "run_checkpoint.pkl", out_prefix=str(tmp_path / "out")) - assert extract_calls["q_to_get"] == {"point_est": 0.5} + assert extract_calls["q_to_get"] == [0.5] def test_h5_passes_none_q_to_get(self, mock_orchestrator, tmp_path): """q_to_get=None is passed to extract_theta_curves for .h5 input.""" @@ -438,7 +438,7 @@ def test_h5_passes_none_q_to_get(self, mock_orchestrator, tmp_path): def fake_curves(**kwargs): extract_calls["q_to_get"] = kwargs.get("q_to_get") return pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]}) + "titrant_conc": [0.0], "q0.5": [0.5]}) with patch( "tfscreen.tfmodel.scripts" @@ -456,7 +456,7 @@ def fake_curves(**kwargs): "tfscreen.tfmodel.scripts" ".predict_theta_cli.extract_theta_unmeasured", return_value=pd.DataFrame({"genotype": ["wt"], "titrant_name": ["IPTG"], - "titrant_conc": [0.0], "median": [0.5]}), + "titrant_conc": [0.0], "q0.5": [0.5]}), ): predict_theta("cfg.yaml", "post.h5", out_prefix=str(tmp_path / "out")) diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py index 9331351b..e41fcbd1 100644 --- a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py +++ b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py @@ -1453,9 +1453,9 @@ def _make_fake_predict_dfs(genotypes=("gA", "gB"), n_t=3): "t_sel": t, "ln_cfu": rng.uniform(8, 12), "ln_cfu_std": 0.2, - "q05": rng.uniform(7, 9), - "median": rng.uniform(9, 11), - "q95": rng.uniform(11, 13), + "q0.05": rng.uniform(7, 9), + "q0.5": rng.uniform(9, 11), + "q0.95": rng.uniform(11, 13), "replicate_idx": 0, "time_idx": t_i, "condition_pre_idx": 0, @@ -1473,9 +1473,9 @@ def _make_fake_predict_dfs(genotypes=("gA", "gB"), n_t=3): "replicate": "r1", "condition_pre": "cp1", "genotype": g, - "q05": 8.5, - "median": 9.0, - "q95": 9.5, + "q0.05": 8.5, + "q0.5": 9.0, + "q0.95": 9.5, }) ln_cfu0_df = pd.DataFrame(ln_cfu0_rows) diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index b460fc3d..1615d6da 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -75,9 +75,9 @@ def _make_pred_csv(path, n_training=3, extra_genotypes=None): "genotype": g, "titrant_name": TITRANT_NAME, "titrant_conc": tc, - "median": theta + tc * 0.0001, - "lower_95": theta - 0.05, - "upper_95": theta + 0.05, + "q0.5": theta + tc * 0.0001, + "q0.025": theta - 0.05, + "q0.975": theta + 0.05, "in_training_data": in_train, }) pd.DataFrame(rows).to_csv(path, index=False) @@ -394,7 +394,7 @@ def test_growth_training_stats_populated_when_file_present(self, run_dir): pd.DataFrame({ "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), - "median": np.linspace(8.1, 13.1, 6), + "q0.5": np.linspace(8.1, 13.1, 6), }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) summarize_fit(run_dir) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: @@ -409,7 +409,7 @@ def test_growth_training_drops_nan_ln_cfu(self, run_dir): df = pd.DataFrame({ "genotype": ["wt"] * 6, "ln_cfu": [8.0, np.nan, 9.0, np.nan, 10.0, 11.0], - "median": np.linspace(8.1, 13.1, 6), + "q0.5": np.linspace(8.1, 13.1, 6), }) df.to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) summarize_fit(run_dir) @@ -426,7 +426,7 @@ def test_growth_corr_pdf_written_when_file_present(self, run_dir): pd.DataFrame({ "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), - "median": np.linspace(8.1, 13.1, 6), + "q0.5": np.linspace(8.1, 13.1, 6), }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) summarize_fit(run_dir) assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.pdf")) @@ -445,7 +445,7 @@ def test_theta_corr_training_csv_written(self, run_dir): assert os.path.exists(csv_path) df = pd.read_csv(csv_path) assert "theta_obs" in df.columns - assert "median" in df.columns + assert "q0.5" in df.columns assert len(df) > 0 def test_theta_corr_training_csv_not_written_when_no_binding(self, run_dir): @@ -476,7 +476,7 @@ def test_theta_corr_test_csv_written_with_ground_truth(self, run_dir, ground_tru assert os.path.exists(csv_path) df = pd.read_csv(csv_path) assert "theta_obs" in df.columns - assert "median" in df.columns + assert "q0.5" in df.columns assert len(df) == len(TITRANT_CONCS) def test_growth_corr_csv_symlink_created(self, run_dir): @@ -484,7 +484,7 @@ def test_growth_corr_csv_symlink_created(self, run_dir): pd.DataFrame({ "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), - "median": np.linspace(8.1, 13.1, 6), + "q0.5": np.linspace(8.1, 13.1, 6), }).to_csv(growth_pred_path, index=False) summarize_fit(run_dir) csv_path = os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") @@ -503,7 +503,7 @@ def test_growth_corr_csv_symlink_overwritten_on_rerun(self, run_dir): pd.DataFrame({ "genotype": ["wt"] * 3, "ln_cfu": [8.0, 9.0, 10.0], - "median": [8.1, 9.1, 10.1], + "q0.5": [8.1, 9.1, 10.1], }).to_csv(growth_pred_path, index=False) summarize_fit(run_dir) summarize_fit(run_dir) # second call must not raise @@ -706,7 +706,7 @@ def test_warns_on_multiple_params_files_falls_back_to_first(self, tmp_path): "genotype": ["wt", "A1B", "C2D"], "titrant_name": ["IPTG"] * 3, "titrant_conc": [10.0] * 3, - "median": [0.12, 0.48, 0.88], + "q0.5": [0.12, 0.48, 0.88], "in_training_data": [1, 1, 1], }) @@ -835,7 +835,7 @@ def test_writes_csv_per_genotype(self, tmp_path): assert os.path.exists(csv_path), f"Missing CSV for {geno}" df = pd.read_csv(csv_path) assert "theta_obs" in df.columns - assert "median" in df.columns + assert "q0.5" in df.columns assert all(df["genotype"] == geno) def test_csv_written_before_pdf(self, tmp_path): @@ -1159,3 +1159,443 @@ def test_summarize_fit_skips_plot_when_no_pred_file(self, run_dir): summarize_fit(run_dir) mock_pdf.assert_not_called() + + +# --------------------------------------------------------------------------- +# _build_transform_registry / _compute_*_obs / _summarize_params +# --------------------------------------------------------------------------- + +from tfscreen.tfmodel.scripts.summarize_fit_cli import ( + _build_transform_registry, + _compute_direct_obs, + _compute_diff_obs, + _compute_epi_obs, + _summarize_params, + _try_plot_params_corr, +) + +# Shared sim_parameters DataFrame used across param-summary tests. +_SIM_DF = pd.DataFrame([ + {"genotype": "wt", "activity": 1.0, "theta_low": 0.90, "theta_high": 0.05, "log_hill_K": -4.0}, + {"genotype": "A1G", "activity": 0.5, "theta_low": 0.70, "theta_high": 0.08, "log_hill_K": -3.5}, + {"genotype": "B2H", "activity": 2.0, "theta_low": 0.80, "theta_high": 0.03, "log_hill_K": -4.5}, + {"genotype": "C3K", "activity": 1.5, "theta_low": 0.85, "theta_high": 0.04, "log_hill_K": -4.1}, + {"genotype": "A1G/B2H", "activity": 1.2, "theta_low": 0.75, "theta_high": 0.06, "log_hill_K": -4.2}, + {"genotype": "A1G/C3K", "activity": 0.8, "theta_low": 0.78, "theta_high": 0.07, "log_hill_K": -3.9}, +]) + + +def _logit(x): + x = np.clip(x, 1e-9, 1 - 1e-9) + return np.log(x / (1 - x)) + + +class TestBuildTransformRegistry: + + def test_direct_column_registered(self): + reg = _build_transform_registry(_SIM_DF) + assert "activity" in reg + assert reg["activity"]["wt"] == pytest.approx(1.0) + assert reg["activity"]["A1G"] == pytest.approx(0.5) + + def test_log_prefix_registered(self): + reg = _build_transform_registry(_SIM_DF) + assert "log_activity" in reg + assert reg["log_activity"]["A1G"] == pytest.approx(np.log(0.5)) + + def test_logit_theta_col_registered_under_long_name(self): + reg = _build_transform_registry(_SIM_DF) + assert "logit_theta_low" in reg + assert reg["logit_theta_low"]["wt"] == pytest.approx(_logit(0.90)) + + def test_logit_theta_col_also_registered_under_short_name(self): + # theta_low → logit_low (short form used by hill_mut component) + reg = _build_transform_registry(_SIM_DF) + assert "logit_low" in reg + assert reg["logit_low"]["A1G"] == pytest.approx(_logit(0.70)) + + def test_logit_delta_compound_registered(self): + reg = _build_transform_registry(_SIM_DF) + expected_wt = _logit(0.05) - _logit(0.90) + assert "logit_delta" in reg + assert reg["logit_delta"]["wt"] == pytest.approx(expected_wt) + + def test_logit_delta_absent_when_theta_high_missing(self): + df = _SIM_DF.drop(columns=["theta_high"]) + reg = _build_transform_registry(df) + assert "logit_delta" not in reg + + def test_direct_column_log_hill_K_not_double_logged(self): + # log_hill_K is already logged in sim; should appear as direct match. + reg = _build_transform_registry(_SIM_DF) + assert "log_hill_K" in reg + assert reg["log_hill_K"]["wt"] == pytest.approx(-4.0) + + +class TestComputeDirectObs: + + def test_returns_obs_for_genotype_key(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"genotype": ["wt", "A1G", "B2H"], "q0.5": [1.0, 0.4, 1.9]}) + obs = _compute_direct_obs(params_df, obs_s) + assert obs == pytest.approx([1.0, 0.5, 2.0]) + + def test_returns_none_when_no_genotype_column(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"condition": ["A"], "q0.5": [1.0]}) + assert _compute_direct_obs(params_df, obs_s) is None + + def test_nan_for_unknown_genotype(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"genotype": ["wt", "UNKNOWN"], "q0.5": [1.0, 0.5]}) + obs = _compute_direct_obs(params_df, obs_s) + assert obs[0] == pytest.approx(1.0) + assert np.isnan(obs[1]) + + def test_extra_columns_preserved(self): + # The function only computes obs; it does not modify params_df + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({ + "genotype": ["wt"], + "titrant_name": ["iptg"], + "q0.5": [0.9], + }) + obs = _compute_direct_obs(params_df, obs_s) + assert obs == pytest.approx([1.0]) + + +class TestComputeDiffObs: + + def test_computes_diff_from_wt(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + # activity: wt=1.0, A1G=0.5, B2H=2.0 + params_df = pd.DataFrame({ + "titrant_name": ["iptg", "iptg"], + "mutation": ["A1G", "B2H"], + "q0.5": [-0.55, 0.95], + }) + obs = _compute_diff_obs(params_df, obs_s) + assert obs == pytest.approx([-0.5, 1.0]) + + def test_returns_none_when_no_mutation_column(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"genotype": ["A1G"], "q0.5": [0.5]}) + assert _compute_diff_obs(params_df, obs_s) is None + + def test_returns_nan_when_wt_absent(self): + df_no_wt = _SIM_DF[_SIM_DF["genotype"] != "wt"].copy() + obs_s = df_no_wt.set_index("genotype")["activity"] + params_df = pd.DataFrame({"mutation": ["A1G"], "q0.5": [0.4]}) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + obs = _compute_diff_obs(params_df, obs_s) + assert np.all(np.isnan(obs)) + assert any("wt" in str(x.message).lower() for x in w) + + def test_nan_for_unknown_mutation(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"mutation": ["A1G", "UNKNOWN"], "q0.5": [0.0, 0.0]}) + obs = _compute_diff_obs(params_df, obs_s) + assert obs[0] == pytest.approx(-0.5) + assert np.isnan(obs[1]) + + def test_diff_with_logit_low_transform(self): + obs_s = _build_transform_registry(_SIM_DF)["logit_low"] + # logit(theta_low[A1G]) - logit(theta_low[wt]) = logit(0.7) - logit(0.9) + params_df = pd.DataFrame({"mutation": ["A1G"], "q0.5": [0.0]}) + obs = _compute_diff_obs(params_df, obs_s) + expected = _logit(0.70) - _logit(0.90) + assert obs == pytest.approx([expected]) + + +class TestComputeEpiObs: + + def test_computes_epistasis_for_double_mutant(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + # ep = (activity[A1G/B2H] - activity[B2H]) - (activity[A1G] - activity[wt]) + # = (1.2 - 2.0) - (0.5 - 1.0) = -0.8 - (-0.5) = -0.3 + params_df = pd.DataFrame({"pair": ["A1G/B2H"], "q0.5": [0.0]}) + obs = _compute_epi_obs(params_df, obs_s, "activity") + assert obs == pytest.approx([-0.3]) + + def test_returns_none_when_no_pair_column(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"genotype": ["A1G/B2H"], "q0.5": [0.0]}) + assert _compute_epi_obs(params_df, obs_s, "activity") is None + + def test_nan_for_unknown_pair(self): + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"pair": ["A1G/B2H", "X1Y/Z2W"], "q0.5": [0.0, 0.0]}) + obs = _compute_epi_obs(params_df, obs_s, "activity") + assert obs[0] == pytest.approx(-0.3) + assert np.isnan(obs[1]) + + def test_reversed_pair_order_resolved_via_standardize(self): + # extract_epistasis standardizes 'B2H/A1G' → 'A1G/B2H'; the pair + # column in the params file may use the opposite order. + obs_s = _build_transform_registry(_SIM_DF)["activity"] + params_df = pd.DataFrame({"pair": ["B2H/A1G"], "q0.5": [0.0]}) + obs = _compute_epi_obs(params_df, obs_s, "activity") + assert obs == pytest.approx([-0.3]) + + def test_logit_delta_compound_epi(self): + obs_s = _build_transform_registry(_SIM_DF)["logit_delta"] + params_df = pd.DataFrame({"pair": ["A1G/B2H"], "q0.5": [0.0]}) + obs = _compute_epi_obs(params_df, obs_s, "logit_delta") + # Manually compute expected epistasis + def ld(row): return _logit(row["theta_high"]) - _logit(row["theta_low"]) + sim_idx = _SIM_DF.set_index("genotype") + ld_wt = ld(sim_idx.loc["wt"]) + ld_a1g = ld(sim_idx.loc["A1G"]) + ld_b2h = ld(sim_idx.loc["B2H"]) + ld_dbl = ld(sim_idx.loc["A1G/B2H"]) + expected = (ld_dbl - ld_b2h) - (ld_a1g - ld_wt) + assert obs == pytest.approx([expected]) + + +class TestTryPlotParamsCorr: + + def _make_df(self, n=10, nan_obs=False): + obs = np.linspace(0.0, 1.0, n) + med = obs + np.random.default_rng(0).normal(0, 0.05, n) + df = pd.DataFrame({"obs": obs, "q0.5": med}) + if nan_obs: + df.loc[::3, "obs"] = np.nan + return df + + def test_pdf_written(self, tmp_path): + pdf = str(tmp_path / "out.pdf") + _try_plot_params_corr(self._make_df(), "activity", pdf) + assert os.path.exists(pdf) + + def test_skips_when_fewer_than_two_valid_rows(self, tmp_path): + pdf = str(tmp_path / "out.pdf") + df = pd.DataFrame({"obs": [np.nan, np.nan], "q0.5": [0.5, 0.6]}) + _try_plot_params_corr(df, "activity", pdf) + assert not os.path.exists(pdf) + + def test_skips_when_only_one_valid_row(self, tmp_path): + pdf = str(tmp_path / "out.pdf") + df = pd.DataFrame({"obs": [1.0, np.nan], "q0.5": [1.1, 0.5]}) + _try_plot_params_corr(df, "activity", pdf) + assert not os.path.exists(pdf) + + def test_nan_rows_dropped_before_plot(self, tmp_path): + pdf = str(tmp_path / "out.pdf") + _try_plot_params_corr(self._make_df(nan_obs=True), "activity", pdf) + assert os.path.exists(pdf) + + def test_warns_gracefully_on_failure(self, tmp_path): + pdf = str(tmp_path / "out.pdf") + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch("tfscreen.tfmodel.scripts.summarize_fit_cli.xy_corr", + side_effect=RuntimeError("render failed")): + _try_plot_params_corr(self._make_df(), "activity", pdf) + assert not os.path.exists(pdf) + assert any("correlation plot" in str(x.message).lower() for x in w) + + def test_label_used_in_axis_titles(self, tmp_path): + """Axis labels include the param name; just check no crash with unusual label.""" + pdf = str(tmp_path / "out.pdf") + _try_plot_params_corr(self._make_df(), "logit_low", pdf) + assert os.path.exists(pdf) + + +class TestSummarizeParams: + """Integration tests for _summarize_params using real file I/O.""" + + def _write_sim_params(self, path): + _SIM_DF.to_csv(path, index=False) + + def _write_direct_params(self, path, col="activity"): + # genotype + titrant_name + median columns (as tfs_params_activity.csv looks) + rows = [] + for g in _SIM_DF["genotype"]: + rows.append({"genotype": g, "titrant_name": "iptg", "q0.5": 0.0}) + pd.DataFrame(rows).to_csv(path, index=False) + + def _write_diff_params(self, path, col="activity"): + rows = [] + for g in ["A1G", "B2H"]: + rows.append({"titrant_name": "iptg", "mutation": g, "q0.5": 0.0}) + pd.DataFrame(rows).to_csv(path, index=False) + + def _write_epi_params(self, path, col="activity"): + rows = [ + {"titrant_name": "iptg", "pair": "A1G/B2H", "q0.5": 0.0}, + {"titrant_name": "iptg", "pair": "A1G/C3K", "q0.5": 0.0}, + ] + pd.DataFrame(rows).to_csv(path, index=False) + + def test_no_output_when_sim_params_absent(self, tmp_path): + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + _summarize_params(str(tmp_path), out_prefix) + assert not any(f.name.startswith("tfs_summarize_params") + for f in (tmp_path / "summary").iterdir()) + + def test_direct_csv_written_with_obs(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_direct_params(str(tmp_path / "run_params_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + _summarize_params(str(tmp_path), out_prefix) + out_path = out_prefix + "_params_activity.csv" + assert os.path.exists(out_path) + df = pd.read_csv(out_path) + assert "obs" in df.columns + expected = dict(zip(_SIM_DF["genotype"], _SIM_DF["activity"])) + for _, row in df.iterrows(): + assert row["obs"] == pytest.approx(expected[row["genotype"]]) + + def test_diff_csv_written_with_obs(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_diff_params(str(tmp_path / "run_params_d_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + out_path = out_prefix + "_params_d_activity.csv" + assert os.path.exists(out_path) + df = pd.read_csv(out_path) + assert "obs" in df.columns + wt_act = 1.0 + for _, row in df.iterrows(): + act = {"A1G": 0.5, "B2H": 2.0}[row["mutation"]] + assert row["obs"] == pytest.approx(act - wt_act) + + def test_epi_csv_written_with_obs(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_epi_params(str(tmp_path / "run_params_epi_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + out_path = out_prefix + "_params_epi_activity.csv" + assert os.path.exists(out_path) + df = pd.read_csv(out_path) + assert "obs" in df.columns + # ep = (1.2 - 2.0) - (0.5 - 1.0) = -0.3 + assert df.loc[df["pair"] == "A1G/B2H", "obs"].values[0] == pytest.approx(-0.3) + + def test_logit_low_diff_obs(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + pd.DataFrame([ + {"titrant_name": "iptg", "mutation": "A1G", "q0.5": 0.0}, + ]).to_csv(str(tmp_path / "run_params_d_logit_low.csv"), index=False) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + out_path = out_prefix + "_params_d_logit_low.csv" + assert os.path.exists(out_path) + df = pd.read_csv(out_path) + expected = _logit(0.70) - _logit(0.90) + assert df["obs"].values[0] == pytest.approx(expected) + + def test_logit_delta_epi_obs(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + pd.DataFrame([ + {"titrant_name": "iptg", "pair": "A1G/B2H", "q0.5": 0.0}, + ]).to_csv(str(tmp_path / "run_params_epi_logit_delta.csv"), index=False) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + out_path = out_prefix + "_params_epi_logit_delta.csv" + assert os.path.exists(out_path) + df = pd.read_csv(out_path) + sim_idx = _SIM_DF.set_index("genotype") + def ld(g): return _logit(sim_idx.loc[g, "theta_high"]) - _logit(sim_idx.loc[g, "theta_low"]) + expected = (ld("A1G/B2H") - ld("B2H")) - (ld("A1G") - ld("wt")) + assert df["obs"].values[0] == pytest.approx(expected) + + def test_warns_when_expected_direct_file_missing(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + # Deliberately omit *_params_activity.csv + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + assert any("activity" in str(x.message) for x in w) + + def test_output_written_to_summary_dir(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_direct_params(str(tmp_path / "run_params_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + summary_dir = tmp_path / "summary" + written = [f.name for f in summary_dir.iterdir()] + assert any("params_activity" in n for n in written) + + def test_direct_pdf_written_alongside_csv(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_direct_params(str(tmp_path / "run_params_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + assert os.path.exists(out_prefix + "_params_activity.pdf") + + def test_diff_pdf_written_alongside_csv(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_diff_params(str(tmp_path / "run_params_d_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + assert os.path.exists(out_prefix + "_params_d_activity.pdf") + + def test_epi_pdf_written_alongside_csv(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + self._write_epi_params(str(tmp_path / "run_params_epi_activity.csv")) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + assert os.path.exists(out_prefix + "_params_epi_activity.pdf") + + def test_file_without_genotype_key_skipped_silently(self, tmp_path): + self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) + # ln_cfu0-style file: no genotype column + pd.DataFrame([ + {"condition_rep": "kanR+kan", "q0.5": 30.5}, + ]).to_csv(str(tmp_path / "run_params_ln_cfu0.csv"), index=False) + out_prefix = str(tmp_path / "summary" / "tfs_summarize") + os.makedirs(os.path.dirname(out_prefix), exist_ok=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) + # No crash and no output file for ln_cfu0 (not in sim_params columns) + summary_dir = tmp_path / "summary" + assert not any("ln_cfu0" in f.name for f in summary_dir.iterdir()) + + def test_summarize_fit_calls_summarize_params(self, run_dir): + """_summarize_params is invoked from summarize_fit end-to-end.""" + sim_path = os.path.join(run_dir, "tfs_sim_parameters.csv") + _SIM_DF.to_csv(sim_path, index=False) + params_path = os.path.join(run_dir, "tfs_params_activity.csv") + rows = [{"genotype": g, "titrant_name": "iptg", "q0.5": 0.0} + for g in _SIM_DF["genotype"]] + pd.DataFrame(rows).to_csv(params_path, index=False) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + summarize_fit(run_dir) + out_csv = os.path.join(run_dir, "summary", "tfs_summarize_params_activity.csv") + out_pdf = os.path.join(run_dir, "summary", "tfs_summarize_params_activity.pdf") + assert os.path.exists(out_csv) + assert os.path.exists(out_pdf) + df = pd.read_csv(out_csv) + assert "obs" in df.columns diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_sbc_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_sbc_cli.py index 874a2a71..7d974d4c 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_sbc_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_sbc_cli.py @@ -15,5 +15,5 @@ def test_main_calls_generalized_main_with_summarize_sbc(self): def test_summarize_sbc_imported_from_analysis(self): from tfscreen.tfmodel.scripts.summarize_sbc_cli import summarize_sbc - from tfscreen.tfmodel.analysis.sbc import summarize_sbc as canonical + from tfscreen.tfmodel.analysis.error_calibration import summarize_sbc as canonical assert summarize_sbc is canonical diff --git a/tests/tfscreen/tfmodel/test_extract_growth_predictions.py b/tests/tfscreen/tfmodel/test_extract_growth_predictions.py index 80ed5162..9fa473d4 100644 --- a/tests/tfscreen/tfmodel/test_extract_growth_predictions.py +++ b/tests/tfscreen/tfmodel/test_extract_growth_predictions.py @@ -56,30 +56,27 @@ def mock_posteriors(): def test_extract_growth_predictions_basic(mock_model, mock_posteriors): """Test default behavior for extracting growth predictions.""" results = extract_growth_predictions(mock_model, mock_posteriors) - + # Check output structure assert isinstance(results, pd.DataFrame) - assert "median" in results.columns + assert "q0.5" in results.columns assert len(results) == len(mock_model.growth_df) - + # Verify values - # Row 0 -> median should be 10.5 - assert results.iloc[0]["median"] == 10.5 - # Row 1 -> median should be 11.5 - assert results.iloc[1]["median"] == 11.5 + # Row 0 -> q0.5 should be 10.5 + assert results.iloc[0]["q0.5"] == 10.5 + # Row 1 -> q0.5 should be 11.5 + assert results.iloc[1]["q0.5"] == 11.5 def test_extract_growth_predictions_custom_quantiles(mock_model, mock_posteriors): """Test extracting custom quantiles.""" - q_to_get = {"mean": 0.5, "low": 0.1, "high": 0.9} - # Add columns to mock_model.growth_df for this test - for q in q_to_get: - mock_model.growth_df[q] = np.nan - results = extract_growth_predictions(mock_model, mock_posteriors, q_to_get=q_to_get) - - assert "mean" in results.columns - assert "low" in results.columns - assert "high" in results.columns - assert results.iloc[0]["mean"] == 10.5 + results = extract_growth_predictions(mock_model, mock_posteriors, + q_to_get=[0.1, 0.5, 0.9]) + + assert "q0.5" in results.columns + assert "q0.1" in results.columns + assert "q0.9" in results.columns + assert results.iloc[0]["q0.5"] == 10.5 def test_extract_growth_predictions_missing_field(mock_model): """Test error handling when growth_pred is missing from posteriors.""" @@ -88,8 +85,8 @@ def test_extract_growth_predictions_missing_field(mock_model): def test_extract_growth_predictions_invalid_quantiles(mock_model, mock_posteriors): """Test error handling for bad quantile input.""" - with pytest.raises(ValueError, match="q_to_get should be a dictionary"): - extract_growth_predictions(mock_model, mock_posteriors, q_to_get=[0.5]) + with pytest.raises(ValueError, match="should be a 1-D array-like"): + extract_growth_predictions(mock_model, mock_posteriors, q_to_get={"x": 0.5}) def test_extract_growth_predictions_file_loading(mock_model): """Test loading posteriors from file.""" diff --git a/tests/tfscreen/tfmodel/test_extract_parameters_congression.py b/tests/tfscreen/tfmodel/test_extract_parameters_congression.py index 04db7fd8..4e6522a5 100644 --- a/tests/tfscreen/tfmodel/test_extract_parameters_congression.py +++ b/tests/tfscreen/tfmodel/test_extract_parameters_congression.py @@ -59,18 +59,18 @@ def test_extract_parameters_congression(mock_model_congression, mock_posteriors_ lam_df = params["lam"] assert len(lam_df) == 1 assert lam_df.iloc[0]["parameter"] == "lam" - assert lam_df.iloc[0]["median"] == 1.2 + assert lam_df.iloc[0]["q0.5"] == 1.2 # Check mu mu_df = params["mu"] assert len(mu_df) == 2 assert set(mu_df["titrant_conc"]) == {0.0, 1.0} - assert np.allclose(mu_df["median"], 0.5) + assert np.allclose(mu_df["q0.5"], 0.5) # Check sigma sigma_df = params["sigma"] assert len(sigma_df) == 2 - assert np.allclose(sigma_df["median"], 0.1) + assert np.allclose(sigma_df["q0.5"], 0.1) def test_extract_parameters_no_congression(mock_model_congression): """Test that congression parameters are NOT extracted when transformation is none.""" diff --git a/tests/tfscreen/tfmodel/test_extract_samples.py b/tests/tfscreen/tfmodel/test_extract_samples.py index 49e9ab1b..661cf802 100644 --- a/tests/tfscreen/tfmodel/test_extract_samples.py +++ b/tests/tfscreen/tfmodel/test_extract_samples.py @@ -23,7 +23,7 @@ # Shared helpers # --------------------------------------------------------------------------- -_Q = {"median": 0.5} # single quantile keeps assertions simple +_Q = [0.5] # single quantile keeps assertions simple def _hill_model(): @@ -158,7 +158,7 @@ def test_num_samples_adds_correct_number_of_columns(self): def test_quantile_columns_still_present_alongside_samples(self): model = _hill_model() df = extract_theta_curves(model, _hill_posteriors(), q_to_get=_Q, num_samples=3) - assert "median" in df.columns + assert "q0.5" in df.columns def test_sample_values_within_posterior_range(self): """Sample columns must be actual draws, not summaries outside [min, max].""" @@ -185,7 +185,7 @@ def test_constant_posteriors_samples_equal_median(self): df = extract_theta_curves(model, _hill_posteriors(S=10), q_to_get=_Q, num_samples=4) for i in range(4): np.testing.assert_allclose(df[f"sample_{i}"].values, - df["median"].values, + df["q0.5"].values, rtol=1e-6) def test_num_samples_exceeds_posterior_size_uses_replacement(self): @@ -233,7 +233,7 @@ def test_sample_columns_are_distinct_rows_of_theta_samples(self): sample_cols = [c for c in df.columns if c.startswith("sample_")] assert len(sample_cols) == 5 # At least one sample column should differ from the median somewhere - diffs = [not np.allclose(df[c].values, df["median"].values) for c in sample_cols] + diffs = [not np.allclose(df[c].values, df["q0.5"].values) for c in sample_cols] assert any(diffs), "All sample columns equal the median — draws are not varying" @@ -254,7 +254,7 @@ def test_constant_posteriors_samples_equal_median(self): df = extract_theta_curves(model, _hill_mut_posteriors(S=8), q_to_get=_Q, num_samples=3) for i in range(3): np.testing.assert_allclose(df[f"sample_{i}"].values, - df["median"].values, + df["q0.5"].values, rtol=1e-6) def test_index_columns_dropped(self): @@ -289,7 +289,7 @@ def test_constant_posteriors_samples_equal_median(self): num_samples=3) for i in range(3): np.testing.assert_allclose(df[f"sample_{i}"].values, - df["median"].values, + df["q0.5"].values, rtol=1e-5) def test_index_columns_dropped(self): @@ -339,7 +339,7 @@ def test_quantile_columns_still_present(self): model = _tf_model() df = extract_growth_predictions(model, _growth_posteriors(), q_to_get=_Q, num_samples=3) - assert "median" in df.columns + assert "q0.5" in df.columns def test_constant_posteriors_samples_equal_median(self): """When all posterior draws are the same value, samples == median.""" @@ -349,7 +349,7 @@ def test_constant_posteriors_samples_equal_median(self): df = extract_growth_predictions(model, posteriors, q_to_get=_Q, num_samples=4) for i in range(4): np.testing.assert_allclose(df[f"sample_{i}"].values, - df["median"].values, + df["q0.5"].values, rtol=1e-6) def test_sample_values_within_posterior_range(self): diff --git a/tests/tfscreen/tfmodel/test_extract_theta_curves.py b/tests/tfscreen/tfmodel/test_extract_theta_curves.py index 6d46448f..3183e657 100644 --- a/tests/tfscreen/tfmodel/test_extract_theta_curves.py +++ b/tests/tfscreen/tfmodel/test_extract_theta_curves.py @@ -49,18 +49,18 @@ def test_extract_theta_curves_basic(mock_model, mock_posteriors): assert "genotype" in results.columns assert "titrant_name" in results.columns assert "titrant_conc" in results.columns - assert "median" in results.columns - + assert "q0.5" in results.columns + # Should have unique (genotype, titrant_name, titrant_conc) # wt: 0.0, 1.0; mut: 0.0, 1.0 -> 4 rows assert len(results) == 4 - + # Verify values for wt at conc=1.0 # hill_n=2, log_K=-1.0 (K=0.367), high=0.9, low=0.1 # occupancy = 1 / (1 + exp(-2 * (log(1.0) - (-1.0)))) = 1 / (1 + exp(-2)) = 1 / (1 + 0.135) = 0.88 # theta = 0.1 + (0.9 - 0.1) * 0.88 = 0.1 + 0.8 * 0.88 = 0.804 wt_1 = results[(results["genotype"] == "wt") & (results["titrant_conc"] == 1.0)] - assert np.allclose(wt_1["median"], 0.804, atol=1e-3) + assert np.allclose(wt_1["q0.5"], 0.804, atol=1e-3) def test_extract_theta_curves_manual_df(mock_model, mock_posteriors): """Test providing a manual titrant DataFrame.""" @@ -155,14 +155,14 @@ def _make_unmeasured_model(theta_name="hill_mut"): def _fake_predict_unmeasured(target_genotypes, titrant_names, manual_titrant_df, mut_labels, pair_labels, param_posteriors, q_to_get): - """Returns one row per (genotype, titrant_conc) with median=0.5.""" + """Returns one row per (genotype, titrant_conc) with q0.5=0.5.""" rows = [] for g in target_genotypes: for _, row in manual_titrant_df.iterrows(): rows.append({"genotype": g, "titrant_name": row["titrant_name"], "titrant_conc": row["titrant_conc"], - "median": 0.5}) + "q0.5": 0.5}) return pd.DataFrame(rows) @@ -177,7 +177,7 @@ def patched_unmeasured_module(): {"theta": registry_patch}, ), patch( "tfscreen.tfmodel.analysis.extraction.load_posteriors", - return_value=({"median": 0.5}, {}), + return_value=({"q0.5": 0.5}, {}), ): yield fake_module @@ -257,7 +257,7 @@ def test_missing_predict_unmeasured_raises(self): {"theta": {}}, ), patch( "tfscreen.tfmodel.analysis.extraction.load_posteriors", - return_value=({"median": 0.5}, {}), + return_value=({"q0.5": 0.5}, {}), ): with pytest.raises(ValueError, match="predict_unmeasured"): extract_theta_unmeasured( diff --git a/tests/tfscreen/tfmodel/test_extraction_new_models.py b/tests/tfscreen/tfmodel/test_extraction_new_models.py index 5cae8d8d..b25c70bb 100644 --- a/tests/tfscreen/tfmodel/test_extraction_new_models.py +++ b/tests/tfscreen/tfmodel/test_extraction_new_models.py @@ -97,7 +97,7 @@ def _make_model(theta="none", dk_geno="none", activity="fixed", return model -_Q = {"median": 0.5} # single quantile to keep assertions simple +_Q = [0.5] # single quantile to keep assertions simple # --------------------------------------------------------------------------- @@ -135,7 +135,7 @@ def test_assembled_output_shape_genotype_x_titrant(self): # 3 genotypes × 2 titrant names = 6 rows df = params["theta_low"] assert len(df) == 6 - assert set(df.columns) >= {"genotype", "titrant_name", "median"} + assert set(df.columns) >= {"genotype", "titrant_name", "q0.5"} def test_mutation_output_shape_mutation_x_titrant(self): model = _make_model(theta="hill_mut") @@ -143,7 +143,7 @@ def test_mutation_output_shape_mutation_x_titrant(self): # 2 mutations × 2 titrant names = 4 rows df = params["d_logit_low"] assert len(df) == 4 - assert set(df.columns) >= {"mutation", "titrant_name", "median"} + assert set(df.columns) >= {"mutation", "titrant_name", "q0.5"} def test_compound_index_selects_correct_posterior(self): """Each row should use the posterior slice for its (titrant_name_idx, genotype_idx).""" @@ -165,21 +165,21 @@ def test_compound_index_selects_correct_posterior(self): # --- assembled (T, G) indexing --- df = params["theta_low"].set_index(["titrant_name", "genotype"]) - assert df.loc[("iptg", "wt"), "median"] == pytest.approx(0.0) - assert df.loc[("iptg", "B"), "median"] == pytest.approx(2.0) - assert df.loc[("atc", "A"), "median"] == pytest.approx(4.0) - assert df.loc[("atc", "B"), "median"] == pytest.approx(5.0) + assert df.loc[("iptg", "wt"), "q0.5"] == pytest.approx(0.0) + assert df.loc[("iptg", "B"), "q0.5"] == pytest.approx(2.0) + assert df.loc[("atc", "A"), "q0.5"] == pytest.approx(4.0) + assert df.loc[("atc", "B"), "q0.5"] == pytest.approx(5.0) # --- per-mutation (T, M) indexing: flat index = titrant_idx*M + mut_idx --- dd = params["d_logit_low"].set_index(["titrant_name", "mutation"]) # (iptg=0, A=0) → flat 0*2+0=0 → value 0.0 - assert dd.loc[("iptg", "A"), "median"] == pytest.approx(0.0) + assert dd.loc[("iptg", "A"), "q0.5"] == pytest.approx(0.0) # (iptg=0, B=1) → flat 0*2+1=1 → value 1.0 - assert dd.loc[("iptg", "B"), "median"] == pytest.approx(1.0) + assert dd.loc[("iptg", "B"), "q0.5"] == pytest.approx(1.0) # (atc=1, A=0) → flat 1*2+0=2 → value 2.0 - assert dd.loc[("atc", "A"), "median"] == pytest.approx(2.0) + assert dd.loc[("atc", "A"), "q0.5"] == pytest.approx(2.0) # (atc=1, B=1) → flat 1*2+1=3 → value 3.0 - assert dd.loc[("atc", "B"), "median"] == pytest.approx(3.0) + assert dd.loc[("atc", "B"), "q0.5"] == pytest.approx(3.0) # --------------------------------------------------------------------------- @@ -342,7 +342,7 @@ def test_default_no_manual_df_output_shape(self, model): assert "genotype" in df.columns assert "titrant_name" in df.columns assert "titrant_conc" in df.columns - assert "median" in df.columns + assert "q0.5" in df.columns def test_index_columns_dropped_from_output(self, model): df = extract_theta_curves(model, self._flat_posteriors(), @@ -373,7 +373,7 @@ def test_hill_equation_math(self, model): q_to_get=_Q, manual_titrant_df=manual_df, num_samples=None) expected_occ = 1.0 / (1.0 + np.exp(-2.0 * (0.0 - (-1.0)))) expected_theta = 0.1 + 0.8 * expected_occ - assert df["median"].iloc[0] == pytest.approx(expected_theta, rel=1e-5) + assert df["q0.5"].iloc[0] == pytest.approx(expected_theta, rel=1e-5) def test_different_indices_use_different_posteriors(self, model): """ @@ -400,11 +400,11 @@ def test_different_indices_use_different_posteriors(self, model): # iptg(0)/A(1): theta_low=1.0, conc=1.0→log=0, log_K=0, n=1 occ_iptg = 1.0 / (1.0 + np.exp(-1.0 * (np.log(1.0) - 0.0))) - assert row_iptg_A["median"].iloc[0] == pytest.approx(1.0 + 1.0 * occ_iptg, rel=1e-5) + assert row_iptg_A["q0.5"].iloc[0] == pytest.approx(1.0 + 1.0 * occ_iptg, rel=1e-5) # atc(1)/B(2): theta_low=5.0, conc=2.0→log=log(2), log_K=0, n=1 occ_atc = 1.0 / (1.0 + np.exp(-1.0 * (np.log(2.0) - 0.0))) - assert row_atc_B["median"].iloc[0] == pytest.approx(5.0 + 1.0 * occ_atc, rel=1e-5) + assert row_atc_B["q0.5"].iloc[0] == pytest.approx(5.0 + 1.0 * occ_atc, rel=1e-5) def test_zero_concentration_handled(self, model): """Zero concentrations should not produce NaN (uses ZERO_CONC_VALUE).""" @@ -415,7 +415,7 @@ def test_zero_concentration_handled(self, model): }) df = extract_theta_curves(model, self._flat_posteriors(), q_to_get=_Q, manual_titrant_df=manual_df, num_samples=None) - assert not df["median"].isna().any() + assert not df["q0.5"].isna().any() def test_broadcast_without_genotype(self, model): manual_df = pd.DataFrame({ @@ -444,7 +444,7 @@ def test_dispatches_to_hill(self): } df = extract_theta_curves(model, posteriors, q_to_get=_Q) assert "map_theta_group" not in df.columns - assert "median" in df.columns + assert "q0.5" in df.columns def test_dispatches_to_hill_mut(self): model = _make_model(theta="hill_mut") @@ -456,7 +456,7 @@ def test_dispatches_to_hill_mut(self): } df = extract_theta_curves(model, posteriors, q_to_get=_Q) assert "genotype_idx" not in df.columns - assert "median" in df.columns + assert "q0.5" in df.columns def test_raises_for_categorical(self): # categorical now supports the interface; use 'simple' to test the error path @@ -519,5 +519,5 @@ def test_hill_and_hill_mut_agree_on_math(self): } df_mut = extract_theta_curves(model_mut, posteriors_mut, q_to_get=_Q) - assert df_hill["median"].iloc[0] == pytest.approx( - df_mut["median"].iloc[0], rel=1e-5) + assert df_hill["q0.5"].iloc[0] == pytest.approx( + df_mut["q0.5"].iloc[0], rel=1e-5) diff --git a/tests/tfscreen/tfmodel/test_extraction_robustness.py b/tests/tfscreen/tfmodel/test_extraction_robustness.py index 799fa65a..6d5eebb9 100644 --- a/tests/tfscreen/tfmodel/test_extraction_robustness.py +++ b/tests/tfscreen/tfmodel/test_extraction_robustness.py @@ -176,7 +176,7 @@ def test_extract_theta_curves_manual_genotype(mock_model): "theta_theta_low": np.random.rand(10, 1) } df = extract_theta_curves(mock_model, posteriors, manual_titrant_df=manual_df) - assert "median" in df.columns + assert "q0.5" in df.columns assert len(df) == 1 def test_extract_theta_curves_broadcast(mock_model): @@ -274,9 +274,10 @@ def test_extract_parameters_growth_transition_shares_replicates( def test_extract_parameters_errors(mock_model): - """Test error handling in extract_parameters.""" - with pytest.raises(ValueError, match="q_to_get should be a dictionary"): - extract_parameters(mock_model, {}, q_to_get=[0.5]) + """Test error handling in extract_parameters: dict is now invalid, list is valid.""" + # A plain list is now the correct API; a dict should raise + with pytest.raises(ValueError): + extract_parameters(mock_model, {}, q_to_get={"point_est": 0.5}) def test_extract_parameters_h5_file(mock_model, tmp_path): """Test extract_parameters loading from an HDF5 file path.""" @@ -310,7 +311,7 @@ def test_extract_theta_curves_hdf5_path(mock_model, tmp_path): f.create_dataset(k, data=v) df = extract_theta_curves(mock_model, h5_path) - assert "median" in df.columns + assert "q0.5" in df.columns def test_extract_growth_predictions_hdf5_basic(mock_model, tmp_path): """Test extract_growth_predictions reads correctly from HDF5.""" @@ -325,11 +326,11 @@ def test_extract_growth_predictions_hdf5_basic(mock_model, tmp_path): # Test reading from an open h5py.File with h5py.File(h5_path, "r") as f: df = extract_growth_predictions(mock_model, f) - assert "median" in df.columns + assert "q0.5" in df.columns # Test load from file path (.h5) to hit the File loading branch df_path = extract_growth_predictions(mock_model, h5_path) - assert "median" in df_path.columns + assert "q0.5" in df_path.columns def test_extract_growth_predictions_hdf5_multiple_groups(mock_model, tmp_path): """Test extract_growth_predictions with multiple (rep, time, condition) groups. @@ -368,14 +369,14 @@ def test_extract_growth_predictions_hdf5_multiple_groups(mock_model, tmp_path): with h5py.File(h5_path, "r") as f: df = extract_growth_predictions(mock_model, f) - assert "median" in df.columns + assert "q0.5" in df.columns assert len(df) == 2 def test_extract_theta_curves_q_to_get_error(mock_model): - """Test extract_theta_curves with invalid q_to_get.""" + """Test extract_theta_curves with invalid q_to_get: dict is now invalid.""" mock_model._theta = "hill_geno" - with pytest.raises(ValueError, match="q_to_get should be a dictionary"): - extract_theta_curves(mock_model, {}, q_to_get=[0.5]) + with pytest.raises(ValueError): + extract_theta_curves(mock_model, {}, q_to_get={"median": 0.5}) def test_extract_growth_predictions_hdf5_is_h5_check(mock_model, tmp_path): """Explicitly check that we are hitting the is_h5 branch.""" @@ -390,4 +391,4 @@ def test_extract_growth_predictions_hdf5_is_h5_check(mock_model, tmp_path): ds = f["growth_pred"] # extract_growth_predictions takes param_posteriors which is the file/group df = extract_growth_predictions(mock_model, f) - assert "median" in df.columns + assert "q0.5" in df.columns diff --git a/tests/tfscreen/tfmodel/test_model_orchestrator.py b/tests/tfscreen/tfmodel/test_model_orchestrator.py index 6560f877..6a78a6c8 100644 --- a/tests/tfscreen/tfmodel/test_model_orchestrator.py +++ b/tests/tfscreen/tfmodel/test_model_orchestrator.py @@ -486,8 +486,8 @@ def test_extract_parameters_full(initialized_model_class): def test_extract_parameters_errors(initialized_model_class): model = initialized_model_class - with pytest.raises(ValueError, match="should be a dictionary"): - extract_parameters(model, {}, q_to_get="not_a_dict") + with pytest.raises(ValueError, match="should be a 1-D array-like"): + extract_parameters(model, {}, q_to_get={"not": "valid"}) def test_extract_parameters_npz(initialized_model_class, tmpdir): model = initialized_model_class @@ -563,8 +563,8 @@ def test_extract_theta_curves_errors(initialized_model_class): with pytest.raises(ValueError, match="does not support this interface"): extract_theta_curves(model, {}) model._theta = "hill_geno" - with pytest.raises(ValueError, match="should be a dictionary"): - extract_theta_curves(model, {}, q_to_get="not_a_dict") + with pytest.raises(ValueError, match="should be a 1-D array-like"): + extract_theta_curves(model, {}, q_to_get={"not": "valid"}) def test_extract_growth_predictions_full(initialized_model_class, tmpdir): model = initialized_model_class @@ -590,12 +590,12 @@ def test_extract_growth_predictions_full(initialized_model_class, tmpdir): }) post = {"growth_pred": np.zeros((1, 1, 1, 1, 1, 1, 1, 1))} res = extract_growth_predictions(model, post) - assert "median" in res.columns - + assert "q0.5" in res.columns + with pytest.raises(ValueError, match="'growth_pred' not found"): extract_growth_predictions(model, {}) - with pytest.raises(ValueError, match="should be a dictionary"): - extract_growth_predictions(model, {"growth_pred": None}, q_to_get="bad") + with pytest.raises(ValueError, match="should be a 1-D array-like"): + extract_growth_predictions(model, {"growth_pred": None}, q_to_get={"not": "valid"}) path = os.path.join(tmpdir, "gp.npz") np.savez(path, growth_pred=np.zeros((1,1,1,1,1,1,1,1))) diff --git a/tests/tfscreen/tfmodel/test_posteriors.py b/tests/tfscreen/tfmodel/test_posteriors.py index 6ac90a49..127e87f2 100644 --- a/tests/tfscreen/tfmodel/test_posteriors.py +++ b/tests/tfscreen/tfmodel/test_posteriors.py @@ -7,11 +7,11 @@ def test_load_posteriors_dict(): """Test loading posteriors from a dictionary.""" posteriors = {"param1": np.array([1, 2, 3]), "param2": np.array([4, 5, 6])} q, p = load_posteriors(posteriors) - + assert p == posteriors - assert "median" in q - assert q["median"] == 0.5 - assert len(q) == 9 + assert "q0.5" in q + assert q["q0.5"] == 0.5 + assert len(q) == 17 def test_load_posteriors_npz(tmp_path): """Test loading posteriors from an .npz file.""" @@ -22,7 +22,7 @@ def test_load_posteriors_npz(tmp_path): assert "param1" in p assert np.array_equal(p["param1"], [1, 2, 3]) - assert "median" in q + assert "q0.5" in q def test_load_posteriors_h5(tmp_path): """Test loading posteriors from an .h5 file.""" @@ -45,19 +45,19 @@ def test_load_posteriors_h5(tmp_path): def test_load_posteriors_custom_q(): """Test loading with custom quantiles.""" posteriors = {"param1": np.array([1, 2, 3])} - custom_q = {"test": 0.123} - q, p = load_posteriors(posteriors, q_to_get=custom_q) - - assert q == custom_q + q, p = load_posteriors(posteriors, q_to_get=[0.123]) + + assert "q0.123" in q + assert q["q0.123"] == 0.123 assert p == posteriors def test_load_posteriors_errors(): """Test validation errors.""" posteriors = {"param1": np.array([1, 2, 3])} - - with pytest.raises(ValueError, match="q_to_get should be a dictionary"): - load_posteriors(posteriors, q_to_get=[0.5]) - + + with pytest.raises(ValueError, match="q_to_get should be a 1-D array-like"): + load_posteriors(posteriors, q_to_get={"test": 0.5}) + # Test invalid file path (np.load will raise error) with pytest.raises(Exception): load_posteriors("non_existent_file.npz") diff --git a/tests/tfscreen/tfmodel/test_prior_predictive.py b/tests/tfscreen/tfmodel/test_prior_predictive.py index 89f05531..2d23e24e 100644 --- a/tests/tfscreen/tfmodel/test_prior_predictive.py +++ b/tests/tfscreen/tfmodel/test_prior_predictive.py @@ -142,5 +142,5 @@ def test_latent_params_compatible_with_predict(dummy_orchestrator): _, latent_params = draw_prior(dummy_orchestrator, rng_key=0, num_draws=2) result = predict(dummy_orchestrator, latent_params, predict_sites=["growth_pred"], num_samples=None, num_marginal_samples=2) - assert "median" in result.columns or any(c in result.columns - for c in ["lower_95", "upper_95"]) + assert "q0.5" in result.columns or any(c in result.columns + for c in ["q0.025", "q0.975"]) diff --git a/tests/tfscreen/tfmodel/test_sbc.py b/tests/tfscreen/tfmodel/test_sbc.py deleted file mode 100644 index a69bd718..00000000 --- a/tests/tfscreen/tfmodel/test_sbc.py +++ /dev/null @@ -1,284 +0,0 @@ -""" -Tests for tfscreen.tfmodel.analysis.sbc. -""" - -import os -import tempfile - -import h5py -import numpy as np -import pytest - -from tfscreen.tfmodel.analysis.sbc import ( - _find_pairs, - _load_h5_params, - compute_sbc_ranks, - summarize_sbc, -) - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -def _write_h5(path, arrays): - """Write a dict of numpy arrays to an HDF5 file.""" - with h5py.File(path, "w") as hf: - for key, val in arrays.items(): - hf.create_dataset(key, data=val) - - -# --------------------------------------------------------------------------- -# _load_h5_params -# --------------------------------------------------------------------------- - -class TestLoadH5Params: - - def test_round_trips_arrays(self, tmp_path): - data = {"alpha": np.array([1.0, 2.0, 3.0]), "beta": np.array([[4.0, 5.0]])} - p = str(tmp_path / "test.h5") - _write_h5(p, data) - result = _load_h5_params(p) - np.testing.assert_allclose(result["alpha"], data["alpha"]) - np.testing.assert_allclose(result["beta"], data["beta"]) - - def test_returns_all_keys(self, tmp_path): - data = {"x": np.ones(3), "y": np.zeros(5)} - p = str(tmp_path / "test.h5") - _write_h5(p, data) - result = _load_h5_params(p) - assert set(result.keys()) == {"x", "y"} - - def test_empty_file(self, tmp_path): - p = str(tmp_path / "empty.h5") - _write_h5(p, {}) - result = _load_h5_params(p) - assert result == {} - - -# --------------------------------------------------------------------------- -# compute_sbc_ranks -# --------------------------------------------------------------------------- - -class TestComputeSBCRanks: - - def _make_pair(self, tmp_path, gt_val, posterior_samples, name="param"): - """Write ground-truth (shape (1, *)) and posterior (shape (S, *)) files.""" - gt_path = str(tmp_path / "run_ground_truth.h5") - post_path = str(tmp_path / "run_posterior.h5") - _write_h5(gt_path, {name: np.array(gt_val)[np.newaxis, :]}) - _write_h5(post_path, {name: np.array(posterior_samples)}) - return gt_path, post_path - - def test_rank_zero_when_gt_below_all_posterior(self, tmp_path): - # gt = -100; all posterior > -100 → rank = 0 - gt_path, post_path = self._make_pair( - tmp_path, - gt_val=[-100.0], - posterior_samples=np.ones((20, 1)), - ) - ranks = compute_sbc_ranks(gt_path, post_path) - assert "param" in ranks - np.testing.assert_allclose(ranks["param"], [0.0]) - - def test_rank_one_when_gt_above_all_posterior(self, tmp_path): - # gt = 100; all posterior < 100 → rank = 1 - gt_path, post_path = self._make_pair( - tmp_path, - gt_val=[100.0], - posterior_samples=-np.ones((20, 1)), - ) - ranks = compute_sbc_ranks(gt_path, post_path) - np.testing.assert_allclose(ranks["param"], [1.0]) - - def test_rank_half_when_gt_at_median(self, tmp_path): - # posterior is [-10, -9, ..., -1, 0, 1, ..., 10] (21 values), gt = 0 - # 10 values < 0, 11 >= 0 → rank = 10/21 - post = np.arange(-10, 11, dtype=float).reshape(21, 1) - gt_path, post_path = self._make_pair( - tmp_path, gt_val=[0.0], posterior_samples=post - ) - ranks = compute_sbc_ranks(gt_path, post_path) - np.testing.assert_allclose(ranks["param"], [10 / 21]) - - def test_multidimensional_param(self, tmp_path): - # param shape (3,): three independent elements - rng = np.random.default_rng(0) - post = rng.normal(size=(100, 3)) - gt = np.zeros((1, 3)) - gt_path = str(tmp_path / "run_ground_truth.h5") - post_path = str(tmp_path / "run_posterior.h5") - _write_h5(gt_path, {"param": gt}) - _write_h5(post_path, {"param": post}) - ranks = compute_sbc_ranks(gt_path, post_path) - assert ranks["param"].shape == (3,) - assert np.all((ranks["param"] >= 0) & (ranks["param"] <= 1)) - - def test_missing_posterior_param_skipped(self, tmp_path): - gt_path = str(tmp_path / "run_ground_truth.h5") - post_path = str(tmp_path / "run_posterior.h5") - _write_h5(gt_path, {"param_a": np.ones((1, 2)), "param_b": np.ones((1, 2))}) - _write_h5(post_path, {"param_a": np.ones((10, 2))}) - ranks = compute_sbc_ranks(gt_path, post_path) - assert "param_a" in ranks - assert "param_b" not in ranks - - def test_returns_empty_when_no_common_params(self, tmp_path): - gt_path = str(tmp_path / "run_ground_truth.h5") - post_path = str(tmp_path / "run_posterior.h5") - _write_h5(gt_path, {"a": np.ones((1, 2))}) - _write_h5(post_path, {"b": np.ones((10, 2))}) - ranks = compute_sbc_ranks(gt_path, post_path) - assert ranks == {} - - def test_rank_values_in_unit_interval(self, tmp_path): - rng = np.random.default_rng(42) - post = rng.normal(size=(50, 4)) - gt = rng.normal(size=(1, 4)) - gt_path = str(tmp_path / "run_ground_truth.h5") - post_path = str(tmp_path / "run_posterior.h5") - _write_h5(gt_path, {"theta": gt}) - _write_h5(post_path, {"theta": post}) - ranks = compute_sbc_ranks(gt_path, post_path) - assert np.all((ranks["theta"] >= 0) & (ranks["theta"] <= 1)) - - -# --------------------------------------------------------------------------- -# _find_pairs -# --------------------------------------------------------------------------- - -class TestFindPairs: - - def test_finds_paired_files(self, tmp_path): - for prefix in ("run001", "run002"): - (tmp_path / f"{prefix}_ground_truth.h5").touch() - (tmp_path / f"{prefix}_posterior.h5").touch() - pairs = _find_pairs(str(tmp_path)) - assert len(pairs) == 2 - run_ids = {p[0] for p in pairs} - assert run_ids == {"run001", "run002"} - - def test_missing_posterior_gives_none(self, tmp_path): - (tmp_path / "run001_ground_truth.h5").touch() - pairs = _find_pairs(str(tmp_path)) - assert len(pairs) == 1 - run_id, gt_path, post_path = pairs[0] - assert run_id == "run001" - assert post_path is None - - def test_returns_empty_when_no_gt_files(self, tmp_path): - (tmp_path / "run001_posterior.h5").touch() - pairs = _find_pairs(str(tmp_path)) - assert pairs == [] - - def test_gt_path_is_absolute(self, tmp_path): - (tmp_path / "run001_ground_truth.h5").touch() - (tmp_path / "run001_posterior.h5").touch() - pairs = _find_pairs(str(tmp_path)) - _, gt_path, post_path = pairs[0] - assert os.path.isabs(gt_path) - assert os.path.isabs(post_path) - - -# --------------------------------------------------------------------------- -# summarize_sbc (integration) -# --------------------------------------------------------------------------- - -def _make_calibrated_sbc_dir(tmp_path, n_runs=20, n_samples=200, seed=0): - """ - Create a synthetic SBC directory where ranks are genuinely uniform. - - For each run: draw gt ~ N(0,1), draw posterior ~ N(0,1) (S samples). - The ranks will be approximately uniform under this prior-predictive setup. - """ - rng = np.random.default_rng(seed) - sbc_dir = tmp_path / "sbc" - sbc_dir.mkdir() - for i in range(n_runs): - gt = rng.normal(size=(1, 3)) - post = rng.normal(size=(n_samples, 3)) - run_id = f"run{i:04d}" - _write_h5(str(sbc_dir / f"{run_id}_ground_truth.h5"), {"alpha": gt}) - _write_h5(str(sbc_dir / f"{run_id}_posterior.h5"), {"alpha": post}) - return str(sbc_dir) - - -class TestSummarizeSBC: - - def test_returns_dataframe_with_expected_columns(self, tmp_path): - sbc_dir = _make_calibrated_sbc_dir(tmp_path) - df = summarize_sbc(sbc_dir) - assert not df.empty - for col in ("param", "n_runs", "n_ranks", "mean_rank", "ks_stat", "ks_pval"): - assert col in df.columns - - def test_one_row_per_parameter(self, tmp_path): - sbc_dir = _make_calibrated_sbc_dir(tmp_path) - df = summarize_sbc(sbc_dir) - assert len(df) == 1 # only "alpha" - assert df.iloc[0]["param"] == "alpha" - - def test_n_runs_correct(self, tmp_path): - n_runs = 15 - sbc_dir = _make_calibrated_sbc_dir(tmp_path, n_runs=n_runs) - df = summarize_sbc(sbc_dir) - assert df.iloc[0]["n_runs"] == n_runs - - def test_n_ranks_correct(self, tmp_path): - # 10 runs × 3 elements each = 30 rank values - sbc_dir = _make_calibrated_sbc_dir(tmp_path, n_runs=10) - df = summarize_sbc(sbc_dir) - assert df.iloc[0]["n_ranks"] == 30 - - def test_writes_summary_csv(self, tmp_path): - sbc_dir = _make_calibrated_sbc_dir(tmp_path) - summarize_sbc(sbc_dir) - assert os.path.exists(os.path.join(sbc_dir, "sbc_sbc_summary.csv")) - - def test_writes_ranks_csv(self, tmp_path): - sbc_dir = _make_calibrated_sbc_dir(tmp_path) - summarize_sbc(sbc_dir) - assert os.path.exists(os.path.join(sbc_dir, "sbc_sbc_ranks.csv")) - - def test_writes_histogram_pdf(self, tmp_path): - sbc_dir = _make_calibrated_sbc_dir(tmp_path) - summarize_sbc(sbc_dir) - assert os.path.exists(os.path.join(sbc_dir, "sbc_rank_hist.pdf")) - - def test_custom_out_prefix(self, tmp_path): - sbc_dir = _make_calibrated_sbc_dir(tmp_path) - prefix = str(tmp_path / "myprefix") - summarize_sbc(sbc_dir, out_prefix=prefix) - assert os.path.exists(f"{prefix}_sbc_summary.csv") - assert os.path.exists(f"{prefix}_sbc_ranks.csv") - - def test_empty_dir_returns_empty_dataframe(self, tmp_path): - sbc_dir = str(tmp_path / "empty") - os.makedirs(sbc_dir) - df = summarize_sbc(sbc_dir) - assert df.empty - - def test_missing_posterior_skipped_without_crash(self, tmp_path): - sbc_dir = tmp_path / "sbc" - sbc_dir.mkdir() - rng = np.random.default_rng(1) - # Run 0: has both files - gt = rng.normal(size=(1, 2)) - post = rng.normal(size=(50, 2)) - _write_h5(str(sbc_dir / "run0000_ground_truth.h5"), {"alpha": gt}) - _write_h5(str(sbc_dir / "run0000_posterior.h5"), {"alpha": post}) - # Run 1: only ground truth - _write_h5(str(sbc_dir / "run0001_ground_truth.h5"), {"alpha": rng.normal(size=(1, 2))}) - df = summarize_sbc(str(sbc_dir)) - assert not df.empty - assert df.iloc[0]["n_runs"] == 1 - - def test_nonexistent_dir_raises(self, tmp_path): - with pytest.raises(FileNotFoundError): - summarize_sbc(str(tmp_path / "does_not_exist")) - - def test_mean_rank_near_half_for_calibrated_model(self, tmp_path): - # With many runs, mean rank should be close to 0.5 - sbc_dir = _make_calibrated_sbc_dir(tmp_path, n_runs=100, n_samples=500) - df = summarize_sbc(sbc_dir) - assert abs(df.iloc[0]["mean_rank"] - 0.5) < 0.05 From 58ebeb8a16a47cd4a9b00544f6ffcd43dc44ef76 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Thu, 11 Jun 2026 22:48:50 -0400 Subject: [PATCH 12/24] cleaning up summarize_fit_cli --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 8 +- reports/junit/junit.xml | 2 +- .../tfmodel/scripts/summarize_fit_cli.py | 135 ++++++++-------- .../tfmodel/scripts/test_summarize_fit_cli.py | 151 ++++++++++++------ 6 files changed, 185 insertions(+), 115 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index fca926e5..1e07082a 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.97%coverage95.97% \ No newline at end of file +coverage: 95.98%coverage95.98% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index e40398f3..6b9bd963 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3347tests3347 \ No newline at end of file +tests: 3351tests3351 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index ff12b2a4..c4443730 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -6537,10 +6537,10 @@ ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:19:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:21:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:23:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:900:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:903:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1168:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1348:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:961:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:964:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1229:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1409:14: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 330d456c..51e3f0a8 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index a5232e0e..53dd75ca 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -233,7 +233,7 @@ def _blank_panel(ax, message): def _build_transform_registry(sim_df): - """Return a dict mapping parameter name → obs Series indexed by genotype. + """Return a dict mapping parameter name → ref Series indexed by genotype. Supports four resolution strategies (tried in this order at registration): @@ -277,38 +277,38 @@ def _build_transform_registry(sim_df): return registry -def _compute_direct_obs(params_df, obs_series): - """Return obs array for a direct params file keyed on genotype. +def _compute_direct_obs(params_df, ref_series): + """Return ref array for a direct params file keyed on genotype. Returns ``None`` when params_df has no ``genotype`` column (e.g. files keyed on condition/replicate that cannot be matched to sim_parameters). """ if "genotype" not in params_df.columns: return None - return obs_series.reindex(params_df["genotype"]).values + return ref_series.reindex(params_df["genotype"]).values -def _compute_diff_obs(params_df, obs_series): - """Return obs array for a diff params file keyed on mutation. +def _compute_diff_obs(params_df, ref_series): + """Return ref array for a diff params file keyed on mutation. - obs = sim[mutation] − sim[wt]. Returns ``None`` when params_df has no - ``mutation`` column. Sets obs to NaN when ``wt`` is absent from - obs_series. + ref = sim[mutation] − sim[wt]. Returns ``None`` when params_df has no + ``mutation`` column. Sets ref to NaN when ``wt`` is absent from + ref_series. """ if "mutation" not in params_df.columns: return None - if "wt" not in obs_series.index: - warnings.warn("'wt' not found in sim_parameters; setting diff obs to NaN") + if "wt" not in ref_series.index: + warnings.warn("'wt' not found in sim_parameters; setting diff ref to NaN") return np.full(len(params_df), np.nan) - wt_val = obs_series["wt"] - return obs_series.reindex(params_df["mutation"]).values - wt_val + wt_val = ref_series["wt"] + return ref_series.reindex(params_df["mutation"]).values - wt_val -def _compute_epi_obs(params_df, obs_series, param_name): - """Return obs array for an epi params file keyed on pair. +def _compute_epi_obs(params_df, ref_series, param_name): + """Return ref array for an epi params file keyed on pair. Uses ``extract_epistasis`` (additive scale) to compute per-double-mutant - epistasis from the ground-truth obs_series. Genotypes not present in the + epistasis from the ground-truth ref_series. Genotypes not present in the result (triple+, unrecognised pairs) receive NaN. Returns ``None`` when params_df has no ``pair`` column. """ @@ -318,8 +318,8 @@ def _compute_epi_obs(params_df, obs_series, param_name): return None work_df = pd.DataFrame({ - "genotype": obs_series.index, - "_obs": obs_series.values, + "genotype": ref_series.index, + "_obs": ref_series.values, }) try: ep_result = extract_epistasis(work_df, y_obs="_obs", scale="add") @@ -339,19 +339,19 @@ def _compute_epi_obs(params_df, obs_series, param_name): def _try_plot_params_corr(out_df, label, pdf_path): - """Save a scatter correlation plot of obs vs median for a params CSV. + """Save a scatter correlation plot of ref vs median for a params CSV. - Silently skips when fewer than two finite (obs, median) pairs exist. + Silently skips when fewer than two finite (ref, median) pairs exist. Warns and returns on any other failure so the rest of the output is unaffected. """ try: - valid = out_df[["obs", "q0.5"]].dropna() + valid = out_df[["ref", "q0.5"]].dropna() if len(valid) < 2: return fig, ax = plt.subplots(1, 1, figsize=(6, 6)) xy_corr( - x_values=valid["obs"].values, + x_values=valid["ref"].values, y_values=valid["q0.5"].values, as_hexbin=False, ax=ax, @@ -368,7 +368,7 @@ def _try_plot_params_corr(out_df, label, pdf_path): def _summarize_params(run_dir, out_prefix): - """Annotate *_params_*.csv files with ground-truth obs from tfs_sim_parameters.csv. + """Annotate *_params_*.csv files with ground-truth ref from tfs_sim_parameters.csv. Scans run_dir for a ``*_sim_parameters.csv`` file. If absent the function returns silently (real-data runs have no ground truth). @@ -378,7 +378,7 @@ def _summarize_params(run_dir, out_prefix): All ``*_params_*.csv`` files in run_dir are then classified by their suffix as *direct* (genotype key), *diff* (``_d_`` infix, mutation key), - or *epi* (``_epi_`` infix, pair key). An ``obs`` column is appended to + or *epi* (``_epi_`` infix, pair key). A ``ref`` column is appended to each file whose parameter name resolves via the transform registry and written to the summary directory under ``{out_prefix}_params_*.csv``. """ @@ -427,7 +427,7 @@ def _summarize_params(run_dir, out_prefix): if param_name not in transform_registry: continue - obs_series = transform_registry[param_name] + ref_series = transform_registry[param_name] try: params_df = pd.read_csv(param_file) @@ -435,23 +435,23 @@ def _summarize_params(run_dir, out_prefix): warnings.warn(f"Could not read {param_file}: {exc}") continue - obs = None + ref = None try: if kind == "direct": - obs = _compute_direct_obs(params_df, obs_series) + ref = _compute_direct_obs(params_df, ref_series) elif kind == "diff": - obs = _compute_diff_obs(params_df, obs_series) + ref = _compute_diff_obs(params_df, ref_series) else: - obs = _compute_epi_obs(params_df, obs_series, param_name) + ref = _compute_epi_obs(params_df, ref_series, param_name) except Exception as exc: - warnings.warn(f"Could not compute obs for {basename}: {exc}") + warnings.warn(f"Could not compute ref for {basename}: {exc}") continue - if obs is None: + if ref is None: continue out_df = params_df.copy() - out_df["obs"] = obs + out_df["ref"] = ref if kind == "epi": out_name = f"{out_prefix}_params_epi_{param_name}.csv" @@ -475,7 +475,7 @@ def _summarize_params(run_dir, out_prefix): def summarize_fit(run_dir, - ground_truth_file=None, + ref_theta_file=None, out_prefix=None): """ Evaluate theta prediction quality for a tfs-fit-model run directory. @@ -494,10 +494,11 @@ def summarize_fit(run_dir, - ``{out_prefix}_fit_summary.json`` — nested statistics with top-level keys ``metadata``, ``theta`` (training / test sub-keys), and ``growth`` (training sub-key). - - ``{out_prefix}_theta_corr_training.csv`` — joined (observed, predicted) - theta pairs for training genotypes. + - ``{out_prefix}_theta_corr_training.csv`` — joined (ref, predicted) + theta pairs for training genotypes; ``ref`` column holds the training + observation used as ground truth. - ``{out_prefix}_theta_corr_test.csv`` — same for test genotypes (only - written when *ground_truth_file* is provided and has matching rows). + written when a ref theta file is resolved and has matching rows). - ``{out_prefix}_theta_corr.pdf`` — two-panel correlation plot for theta (training left, test right). - ``{out_prefix}_growth_corr.csv`` — relative symlink to the @@ -517,13 +518,17 @@ def summarize_fit(run_dir, ---------- run_dir : str Directory containing model fit outputs. - ground_truth_file : str, optional + ref_theta_file : str, optional CSV with columns genotype, titrant_name, titrant_conc, and a theta column containing known theta values for evaluating out-of-sample - prediction. The theta column is resolved in order: ``theta_obs`` - (preferred) → ``theta`` (fallback). If neither is present a warning - is issued and test statistics are skipped. When the argument is - omitted entirely, only training statistics are computed. + prediction. When omitted the function looks for a + ``*_sim_genotype_theta.csv`` file in *run_dir* automatically (the + file produced by ``tfs-simulate``). The theta column is resolved + in order: ``theta_obs`` (preferred) → ``theta`` (fallback); it is + renamed to ``ref`` in the output CSV. If neither column is present + a warning is issued and test statistics are skipped. Pass this + argument explicitly to use a theta reference from outside *run_dir* + or with a non-standard name. out_prefix : str, optional Prefix for output files. Defaults to {run_dir}/tfs_summarize. """ @@ -534,7 +539,7 @@ def summarize_fit(run_dir, metadata = { "run_dir": run_dir, - "ground_truth_file": ground_truth_file, + "ref_theta_file": None, "timestamp": datetime.datetime.now().isoformat(), "n_parameters": None, "n_theta_training_points": None, @@ -621,20 +626,26 @@ def summarize_fit(run_dir, binding_df[join_cols + ["theta_obs"]], on=join_cols, how="inner", - ) + ).rename(columns={"theta_obs": "ref"}) metadata["n_theta_training_points"] = len(train_merged) if len(train_merged) > 0: theta_training_stats = _run_stats( train_merged["q0.5"].values, - train_merged["theta_obs"].values, + train_merged["ref"].values, ) except Exception as exc: warnings.warn(f"Could not compute theta training statistics: {exc}") + # --- Resolve ref_theta_file: explicit arg wins, else auto-discover --- + if ref_theta_file is None: + ref_theta_file = _find_unique(run_dir, "_sim_genotype_theta.csv", + "ref theta", warn_missing=False) + metadata["ref_theta_file"] = ref_theta_file + # --- Theta test statistics --- - if ground_truth_file is not None and pred_df is not None: + if ref_theta_file is not None and pred_df is not None: try: - gt_df = pd.read_csv(ground_truth_file) + gt_df = pd.read_csv(ref_theta_file) join_cols = ["genotype", "titrant_name", "titrant_conc"] if "theta_obs" in gt_df.columns: theta_col = "theta_obs" @@ -642,14 +653,14 @@ def summarize_fit(run_dir, theta_col = "theta" else: warnings.warn( - f"Ground truth file {ground_truth_file} has neither " + f"Ref theta file {ref_theta_file} has neither " f"'theta_obs' nor 'theta' column; skipping test statistics" ) theta_col = None if theta_col is not None: - gt_df = gt_df.rename(columns={theta_col: "theta_obs"}) + gt_df = gt_df.rename(columns={theta_col: "ref"}) test_merged = pred_df.merge( - gt_df[join_cols + ["theta_obs"]], + gt_df[join_cols + ["ref"]], on=join_cols, how="inner", ) if theta_col is not None else None @@ -658,10 +669,10 @@ def summarize_fit(run_dir, if len(test_merged) > 0: theta_test_stats = _run_stats( test_merged["q0.5"].values, - test_merged["theta_obs"].values, + test_merged["ref"].values, ) except Exception as exc: - warnings.warn(f"Could not compute theta test statistics: {exc}") + warnings.warn(f"Could not compute theta test statistics from {ref_theta_file}: {exc}") # --- Growth training statistics --- if growth_pred_file is not None: @@ -727,7 +738,7 @@ def summarize_fit(run_dir, if train_merged is not None and len(train_merged) > 0: xy_corr( - x_values=train_merged["theta_obs"].values, + x_values=train_merged["ref"].values, y_values=train_merged["q0.5"].values, as_hexbin=False, ax=axes[0], @@ -740,7 +751,7 @@ def summarize_fit(run_dir, if test_merged is not None and len(test_merged) > 0: xy_corr( - x_values=test_merged["theta_obs"].values, + x_values=test_merged["ref"].values, y_values=test_merged["q0.5"].values, as_hexbin=False, ax=axes[1], @@ -758,20 +769,18 @@ def summarize_fit(run_dir, except Exception as exc: warnings.warn(f"Could not generate correlation plot: {exc}") - # --- Growth correlation CSV symlink --- + # --- Growth correlation CSV --- if growth_pred_file is not None: growth_csv_file = f"{out_prefix}_growth_corr.csv" try: - rel_target = os.path.relpath( - os.path.abspath(growth_pred_file), - os.path.dirname(os.path.abspath(growth_csv_file)), + growth_copy_df = pd.read_csv(growth_pred_file) + growth_copy_df = growth_copy_df.rename( + columns={"ln_cfu": "ref", "ln_cfu_std": "ref_std"} ) - if os.path.islink(growth_csv_file): - os.remove(growth_csv_file) - os.symlink(rel_target, growth_csv_file) - print(f"Linked growth data to {growth_csv_file}") + growth_copy_df.to_csv(growth_csv_file, index=False) + print(f"Wrote growth data to {growth_csv_file}") except Exception as exc: - warnings.warn(f"Could not create growth CSV symlink at {growth_csv_file}: {exc}") + warnings.warn(f"Could not write growth CSV at {growth_csv_file}: {exc}") # --- Growth correlation plot --- growth_pdf_file = f"{out_prefix}_growth_corr.pdf" @@ -819,7 +828,7 @@ def summarize_fit(run_dir, def main(): return generalized_main( summarize_fit, - manual_arg_types={"ground_truth_file": str}, + manual_arg_types={"ref_theta_file": str}, ) diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index 1615d6da..79181f99 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -133,8 +133,8 @@ def run_dir(tmp_path): return str(tmp_path) -def _make_ground_truth_csv(path, theta_col="theta_obs"): - """Write a ground-truth CSV for the 4th genotype using the given column name.""" +def _make_ref_theta_csv(path, theta_col="theta_obs"): + """Write a ref theta CSV for the 4th genotype using the given column name.""" rows = [] for tc in TITRANT_CONCS: rows.append({ @@ -148,10 +148,10 @@ def _make_ground_truth_csv(path, theta_col="theta_obs"): @pytest.fixture -def ground_truth_file(tmp_path): - """Ground-truth file containing the 4th genotype (not in training data).""" - path = str(tmp_path / "ground_truth.csv") - _make_ground_truth_csv(path, theta_col="theta_obs") +def ref_theta_file(tmp_path): + """Ref theta file containing the 4th genotype (not in training data).""" + path = str(tmp_path / "ref_theta.csv") + _make_ref_theta_csv(path, theta_col="theta_obs") return path @@ -376,7 +376,7 @@ def test_theta_training_stats_populated(self, run_dir): for key in ("rmse", "pearson_r", "spearman_r", "r_squared", "mean_error"): assert key in stats, f"Missing key: {key}" - def test_theta_test_null_when_no_ground_truth(self, run_dir): + def test_theta_test_null_when_no_ref_theta(self, run_dir): summarize_fit(run_dir) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) @@ -444,7 +444,7 @@ def test_theta_corr_training_csv_written(self, run_dir): csv_path = os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_training.csv") assert os.path.exists(csv_path) df = pd.read_csv(csv_path) - assert "theta_obs" in df.columns + assert "ref" in df.columns assert "q0.5" in df.columns assert len(df) > 0 @@ -462,43 +462,48 @@ def test_theta_corr_training_csv_not_written_when_no_binding(self, run_dir): os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_training.csv") ) - def test_theta_corr_test_csv_not_written_without_ground_truth(self, run_dir): + def test_theta_corr_test_csv_not_written_without_ref_theta(self, run_dir): summarize_fit(run_dir) assert not os.path.exists( os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_test.csv") ) - def test_theta_corr_test_csv_written_with_ground_truth(self, run_dir, ground_truth_file): + def test_theta_corr_test_csv_written_with_ref_theta_file(self, run_dir, ref_theta_file): pred_path = os.path.join(run_dir, "run_theta_pred.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) - summarize_fit(run_dir, ground_truth_file=ground_truth_file) + summarize_fit(run_dir, ref_theta_file=ref_theta_file) csv_path = os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_test.csv") assert os.path.exists(csv_path) df = pd.read_csv(csv_path) - assert "theta_obs" in df.columns + assert "ref" in df.columns assert "q0.5" in df.columns assert len(df) == len(TITRANT_CONCS) - def test_growth_corr_csv_symlink_created(self, run_dir): + def test_growth_corr_csv_written(self, run_dir): growth_pred_path = os.path.join(run_dir, "tfs_growth_pred.csv") pd.DataFrame({ "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), + "ln_cfu_std": np.ones(6) * 0.1, "q0.5": np.linspace(8.1, 13.1, 6), }).to_csv(growth_pred_path, index=False) summarize_fit(run_dir) csv_path = os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") - assert os.path.islink(csv_path) - # symlink must resolve to the actual growth pred file - assert os.path.realpath(csv_path) == os.path.realpath(growth_pred_path) + assert os.path.exists(csv_path) + assert not os.path.islink(csv_path) + df = pd.read_csv(csv_path) + assert "ref" in df.columns + assert "ref_std" in df.columns + assert "ln_cfu" not in df.columns + assert "ln_cfu_std" not in df.columns - def test_growth_corr_csv_symlink_not_created_when_file_absent(self, run_dir): + def test_growth_corr_csv_not_written_when_file_absent(self, run_dir): summarize_fit(run_dir) assert not os.path.exists( os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") ) - def test_growth_corr_csv_symlink_overwritten_on_rerun(self, run_dir): + def test_growth_corr_csv_overwritten_on_rerun(self, run_dir): growth_pred_path = os.path.join(run_dir, "tfs_growth_pred.csv") pd.DataFrame({ "genotype": ["wt"] * 3, @@ -508,7 +513,8 @@ def test_growth_corr_csv_symlink_overwritten_on_rerun(self, run_dir): summarize_fit(run_dir) summarize_fit(run_dir) # second call must not raise csv_path = os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.csv") - assert os.path.islink(csv_path) + assert os.path.exists(csv_path) + assert not os.path.islink(csv_path) def test_custom_out_prefix(self, run_dir, tmp_path): custom_prefix = str(tmp_path / "custom" / "myrun") @@ -518,27 +524,27 @@ def test_custom_out_prefix(self, run_dir, tmp_path): # --------------------------------------------------------------------------- -# summarize_fit — with ground truth (test statistics) +# summarize_fit — with ref theta file (test statistics) # --------------------------------------------------------------------------- -class TestSummarizeFitWithGroundTruth: +class TestSummarizeFitWithRefTheta: - def test_theta_test_stats_populated(self, run_dir, ground_truth_file): + def test_theta_test_stats_populated(self, run_dir, ref_theta_file): # Add out-of-training genotype to pred CSV pred_path = os.path.join(run_dir, "run_theta_pred.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) - summarize_fit(run_dir, ground_truth_file=ground_truth_file) + summarize_fit(run_dir, ref_theta_file=ref_theta_file) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["test"] is not None assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) - def test_theta_test_pearson_r_reasonable(self, run_dir, ground_truth_file): + def test_theta_test_pearson_r_reasonable(self, run_dir, ref_theta_file): pred_path = os.path.join(run_dir, "run_theta_pred.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) - summarize_fit(run_dir, ground_truth_file=ground_truth_file) + summarize_fit(run_dir, ref_theta_file=ref_theta_file) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) r = data["theta"]["test"]["pearson_r"] @@ -546,25 +552,25 @@ def test_theta_test_pearson_r_reasonable(self, run_dir, ground_truth_file): assert r is not None and r > 0.9 def test_theta_col_fallback_to_theta(self, run_dir, tmp_path): - """Ground-truth file with 'theta' column (no 'theta_obs') still works.""" + """Ref theta file with 'theta' column (no 'theta_obs') still works.""" pred_path = os.path.join(run_dir, "run_theta_pred.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) - gt_path = str(tmp_path / "gt_theta_col.csv") - _make_ground_truth_csv(gt_path, theta_col="theta") + gt_path = str(tmp_path / "ref_theta_col.csv") + _make_ref_theta_csv(gt_path, theta_col="theta") - summarize_fit(run_dir, ground_truth_file=gt_path) + summarize_fit(run_dir, ref_theta_file=gt_path) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) assert data["theta"]["test"] is not None assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): - """Ground-truth file with no theta column issues a warning and skips test stats.""" + """Ref theta file with no theta column issues a warning and skips test stats.""" pred_path = os.path.join(run_dir, "run_theta_pred.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) - gt_path = str(tmp_path / "gt_no_theta.csv") + gt_path = str(tmp_path / "ref_no_theta.csv") pd.DataFrame({ "genotype": ["E4F"], "titrant_name": [TITRANT_NAME], @@ -574,7 +580,7 @@ def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - summarize_fit(run_dir, ground_truth_file=gt_path) + summarize_fit(run_dir, ref_theta_file=gt_path) assert any("neither" in str(x.message).lower() for x in w) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: @@ -582,6 +588,61 @@ def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): assert data["theta"]["test"] is None assert data["metadata"]["n_theta_test_points"] is None + def test_auto_discovers_sim_genotype_theta_in_run_dir(self, run_dir): + """*_sim_genotype_theta.csv in run_dir is used automatically when no arg given.""" + pred_path = os.path.join(run_dir, "run_theta_pred.csv") + _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) + _make_ref_theta_csv( + os.path.join(run_dir, "tfs_sim_genotype_theta.csv"), theta_col="theta_obs" + ) + + summarize_fit(run_dir) + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: + data = json.load(fh) + assert data["theta"]["test"] is not None + assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) + assert data["metadata"]["ref_theta_file"] is not None + + def test_ref_theta_file_arg_overrides_auto_discovery(self, run_dir, tmp_path): + """Explicit ref_theta_file takes precedence over auto-discovered file.""" + pred_path = os.path.join(run_dir, "run_theta_pred.csv") + _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) + + # Auto-discoverable file: has no matching genotypes → would give 0 test points + pd.DataFrame({ + "genotype": ["NOMATCH"], + "titrant_name": [TITRANT_NAME], + "titrant_conc": [0.0], + "theta_obs": [0.5], + }).to_csv(os.path.join(run_dir, "tfs_sim_genotype_theta.csv"), index=False) + + # Explicit file: has the real test genotype → should win + explicit_path = str(tmp_path / "explicit_ref.csv") + _make_ref_theta_csv(explicit_path, theta_col="theta_obs") + + summarize_fit(run_dir, ref_theta_file=explicit_path) + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: + data = json.load(fh) + assert data["metadata"]["ref_theta_file"] == explicit_path + assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) + + def test_metadata_ref_theta_file_recorded(self, run_dir, ref_theta_file): + """ref_theta_file path is recorded in JSON metadata.""" + pred_path = os.path.join(run_dir, "run_theta_pred.csv") + _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) + + summarize_fit(run_dir, ref_theta_file=ref_theta_file) + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: + data = json.load(fh) + assert data["metadata"]["ref_theta_file"] == ref_theta_file + + def test_metadata_ref_theta_file_null_when_nothing_found(self, run_dir): + """ref_theta_file metadata key is None when neither arg nor auto-discovery finds a file.""" + summarize_fit(run_dir) + with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: + data = json.load(fh) + assert data["metadata"]["ref_theta_file"] is None + # --------------------------------------------------------------------------- # summarize_fit — graceful failure cases @@ -1358,9 +1419,9 @@ class TestTryPlotParamsCorr: def _make_df(self, n=10, nan_obs=False): obs = np.linspace(0.0, 1.0, n) med = obs + np.random.default_rng(0).normal(0, 0.05, n) - df = pd.DataFrame({"obs": obs, "q0.5": med}) + df = pd.DataFrame({"ref": obs, "q0.5": med}) if nan_obs: - df.loc[::3, "obs"] = np.nan + df.loc[::3, "ref"] = np.nan return df def test_pdf_written(self, tmp_path): @@ -1370,13 +1431,13 @@ def test_pdf_written(self, tmp_path): def test_skips_when_fewer_than_two_valid_rows(self, tmp_path): pdf = str(tmp_path / "out.pdf") - df = pd.DataFrame({"obs": [np.nan, np.nan], "q0.5": [0.5, 0.6]}) + df = pd.DataFrame({"ref": [np.nan, np.nan], "q0.5": [0.5, 0.6]}) _try_plot_params_corr(df, "activity", pdf) assert not os.path.exists(pdf) def test_skips_when_only_one_valid_row(self, tmp_path): pdf = str(tmp_path / "out.pdf") - df = pd.DataFrame({"obs": [1.0, np.nan], "q0.5": [1.1, 0.5]}) + df = pd.DataFrame({"ref": [1.0, np.nan], "q0.5": [1.1, 0.5]}) _try_plot_params_corr(df, "activity", pdf) assert not os.path.exists(pdf) @@ -1444,10 +1505,10 @@ def test_direct_csv_written_with_obs(self, tmp_path): out_path = out_prefix + "_params_activity.csv" assert os.path.exists(out_path) df = pd.read_csv(out_path) - assert "obs" in df.columns + assert "ref" in df.columns expected = dict(zip(_SIM_DF["genotype"], _SIM_DF["activity"])) for _, row in df.iterrows(): - assert row["obs"] == pytest.approx(expected[row["genotype"]]) + assert row["ref"] == pytest.approx(expected[row["genotype"]]) def test_diff_csv_written_with_obs(self, tmp_path): self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) @@ -1460,11 +1521,11 @@ def test_diff_csv_written_with_obs(self, tmp_path): out_path = out_prefix + "_params_d_activity.csv" assert os.path.exists(out_path) df = pd.read_csv(out_path) - assert "obs" in df.columns + assert "ref" in df.columns wt_act = 1.0 for _, row in df.iterrows(): act = {"A1G": 0.5, "B2H": 2.0}[row["mutation"]] - assert row["obs"] == pytest.approx(act - wt_act) + assert row["ref"] == pytest.approx(act - wt_act) def test_epi_csv_written_with_obs(self, tmp_path): self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) @@ -1477,9 +1538,9 @@ def test_epi_csv_written_with_obs(self, tmp_path): out_path = out_prefix + "_params_epi_activity.csv" assert os.path.exists(out_path) df = pd.read_csv(out_path) - assert "obs" in df.columns + assert "ref" in df.columns # ep = (1.2 - 2.0) - (0.5 - 1.0) = -0.3 - assert df.loc[df["pair"] == "A1G/B2H", "obs"].values[0] == pytest.approx(-0.3) + assert df.loc[df["pair"] == "A1G/B2H", "ref"].values[0] == pytest.approx(-0.3) def test_logit_low_diff_obs(self, tmp_path): self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) @@ -1495,7 +1556,7 @@ def test_logit_low_diff_obs(self, tmp_path): assert os.path.exists(out_path) df = pd.read_csv(out_path) expected = _logit(0.70) - _logit(0.90) - assert df["obs"].values[0] == pytest.approx(expected) + assert df["ref"].values[0] == pytest.approx(expected) def test_logit_delta_epi_obs(self, tmp_path): self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) @@ -1513,7 +1574,7 @@ def test_logit_delta_epi_obs(self, tmp_path): sim_idx = _SIM_DF.set_index("genotype") def ld(g): return _logit(sim_idx.loc[g, "theta_high"]) - _logit(sim_idx.loc[g, "theta_low"]) expected = (ld("A1G/B2H") - ld("B2H")) - (ld("A1G") - ld("wt")) - assert df["obs"].values[0] == pytest.approx(expected) + assert df["ref"].values[0] == pytest.approx(expected) def test_warns_when_expected_direct_file_missing(self, tmp_path): self._write_sim_params(str(tmp_path / "run_sim_parameters.csv")) @@ -1598,4 +1659,4 @@ def test_summarize_fit_calls_summarize_params(self, run_dir): assert os.path.exists(out_csv) assert os.path.exists(out_pdf) df = pd.read_csv(out_csv) - assert "obs" in df.columns + assert "ref" in df.columns From bf892157677314791c5ebc3e50107ad005ae8e10 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 15 Jun 2026 15:33:24 -0700 Subject: [PATCH 13/24] clean up summarize output --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 634 +++++++++--------- reports/junit/junit.xml | 2 +- .../simulate/scripts/run_simulation_cli.py | 10 +- src/tfscreen/simulate/selection_experiment.py | 11 +- .../tfmodel/scripts/summarize_fit_cli.py | 60 +- .../simulate/test_generate_presplit_data.py | 4 +- .../simulate/test_selection_experiment.py | 1 - .../tfmodel/scripts/test_summarize_fit_cli.py | 334 +++++++++ 10 files changed, 716 insertions(+), 344 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 1e07082a..9bd8285b 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.98%coverage95.98% \ No newline at end of file +coverage: 96.00%coverage96.00% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 6b9bd963..f8fe7989 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3351tests3351 \ No newline at end of file +tests: 3375tests3375 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index c4443730..498c3db0 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1313,26 +1313,25 @@ ./src/tfscreen/simulate/run_simulation.py:101:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:106:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:107:1: W391 blank line at end of file -./src/tfscreen/simulate/scripts/run_simulation_cli.py:141:15: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:143:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:144:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:145:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:149:20: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:150:22: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:151:21: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:170:43: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/scripts/run_simulation_cli.py:171:14: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:172:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:177:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:190:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:194:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:195:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:196:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:199:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:201:25: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:203:20: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:204:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:218:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:140:15: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:141:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:142:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:143:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:147:20: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:148:22: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:149:21: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:168:43: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/scripts/run_simulation_cli.py:169:14: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:170:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:175:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:188:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:191:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:192:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:193:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:196:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:198:25: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:200:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/run_simulation_cli.py:214:19: E221 multiple spaces before operator ./src/tfscreen/simulate/scripts/setup_sim_grid_cli.py:101:1: C901 'setup_sim_grid' is too complex (17) ./src/tfscreen/simulate/selection_experiment.py:42:38: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:43:37: E231 missing whitespace after ':' @@ -1343,140 +1342,134 @@ ./src/tfscreen/simulate/selection_experiment.py:72:1: C901 '_check_dict_number' is too complex (13) ./src/tfscreen/simulate/selection_experiment.py:72:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/simulate/selection_experiment.py:115:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:158:1: C901 '_check_cf' is too complex (26) +./src/tfscreen/simulate/selection_experiment.py:158:1: C901 '_check_cf' is too complex (25) ./src/tfscreen/simulate/selection_experiment.py:180:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:202:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:204:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:204:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:204:65: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:204:85: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:227:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:228:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:294:1: C901 '_check_lib_spec' is too complex (12) -./src/tfscreen/simulate/selection_experiment.py:294:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:346:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:351:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:373:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:426:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:442:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:450:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:450:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:460:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:470:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:513:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:517:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:518:75: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:528:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:531:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:577:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:588:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:589:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:592:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:596:55: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:602:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:607:30: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:608:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:612:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:654:51: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:660:35: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:665:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:682:50: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:682:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:684:75: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:686:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:687:46: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:688:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:689:67: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:739:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:756:79: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:762:29: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:804:43: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:810:74: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:811:18: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:817:79: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:819:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:824:80: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:829:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:835:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:846:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:889:73: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:894:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:911:25: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1012:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1020:26: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1020:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1020:44: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1020:52: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1020:60: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1028:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1029:32: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1037:42: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1046:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1047:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1049:63: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1055:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1057:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1064:28: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1066:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1067:24: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1074:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:199:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:223:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:224:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:290:1: C901 '_check_lib_spec' is too complex (12) +./src/tfscreen/simulate/selection_experiment.py:290:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:342:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:347:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:369:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:422:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:438:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:446:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:446:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:456:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:466:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:509:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:513:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:514:75: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:524:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:527:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:573:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:584:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:585:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:588:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:592:55: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:598:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:603:30: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:604:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:608:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:650:51: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:656:35: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:661:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:678:50: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:678:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:680:75: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:682:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:683:46: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:684:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:685:67: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:735:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:752:79: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:758:29: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:800:43: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:806:74: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:807:18: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:813:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:815:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:820:80: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:825:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:831:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:842:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:885:73: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:890:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:907:25: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1015:26: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1015:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1015:44: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1015:52: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1015:60: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1023:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1024:32: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1032:42: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1041:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1042:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1044:63: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1050:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1052:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1059:28: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1061:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1062:24: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1069:79: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1074:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1075:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1077:36: W291 trailing whitespace ./src/tfscreen/simulate/selection_experiment.py:1079:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1080:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1082:36: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1084:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1088:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1094:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1095:128: E501 line too long (145 > 127 characters) -./src/tfscreen/simulate/selection_experiment.py:1099:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1100:72: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1104:34: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1111:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1113:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1113:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1114:20: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1114:56: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1116:32: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1117:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1129:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1137:41: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1144:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1146:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1151:36: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1152:37: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1152:45: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1152:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1152:61: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1157:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/selection_experiment.py:1192:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1213:57: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1214:53: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1217:28: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1217:39: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1222:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1228:78: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1231:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1239:33: E261 at least two spaces before inline comment -./src/tfscreen/simulate/selection_experiment.py:1240:49: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1241:37: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:1083:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1089:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1090:128: E501 line too long (145 > 127 characters) +./src/tfscreen/simulate/selection_experiment.py:1094:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1095:72: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1099:34: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1106:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1108:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1108:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1109:20: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1111:32: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1112:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1124:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1132:41: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1139:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1141:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1146:36: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1147:37: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1147:45: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1147:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1147:61: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1152:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/selection_experiment.py:1187:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1208:57: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1209:53: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1212:28: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1212:39: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1217:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1223:78: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1226:19: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1234:33: E261 at least two spaces before inline comment +./src/tfscreen/simulate/selection_experiment.py:1235:49: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1236:37: E231 missing whitespace after ':' +./src/tfscreen/simulate/selection_experiment.py:1238:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1239:76: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1241:19: W291 trailing whitespace ./src/tfscreen/simulate/selection_experiment.py:1243:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1244:76: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1246:19: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1248:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1256:77: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1257:43: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1261:41: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/selection_experiment.py:1264:49: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1288:71: W291 trailing whitespace -./src/tfscreen/simulate/selection_experiment.py:1296:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1297:76: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1302:11: F541 f-string is missing placeholders -./src/tfscreen/simulate/selection_experiment.py:1307:38: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1324:6: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/simulate/selection_experiment.py:1331:51: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1332:48: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1333:1: W293 blank line contains whitespace -./src/tfscreen/simulate/selection_experiment.py:1334:33: E231 missing whitespace after ',' -./src/tfscreen/simulate/selection_experiment.py:1337:1: W391 blank line at end of file +./src/tfscreen/simulate/selection_experiment.py:1251:77: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1252:43: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1256:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/selection_experiment.py:1259:49: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1283:71: W291 trailing whitespace +./src/tfscreen/simulate/selection_experiment.py:1291:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1292:76: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1297:11: F541 f-string is missing placeholders +./src/tfscreen/simulate/selection_experiment.py:1302:38: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1319:6: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/simulate/selection_experiment.py:1326:51: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1327:48: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1328:1: W293 blank line contains whitespace +./src/tfscreen/simulate/selection_experiment.py:1329:33: E231 missing whitespace after ',' +./src/tfscreen/simulate/selection_experiment.py:1332:1: W391 blank line at end of file ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent @@ -3676,9 +3669,9 @@ ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:153:1: C901 '_read_all_losses' is too complex (11) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:370:1: C901 '_summarize_params' is too complex (23) -./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:477:1: C901 'summarize_fit' is too complex (59) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:154:1: C901 '_read_all_losses' is too complex (11) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:413:1: C901 '_summarize_params' is too complex (23) +./src/tfscreen/tfmodel/scripts/summarize_fit_cli.py:521:1: C901 'summarize_fit' is too complex (59) ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:47:30: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/scripts/summarize_grid_cli.py:76:1: C901 'summarize_grid' is too complex (14) ./src/tfscreen/tfmodel/tensors/batch.py:5:1: E302 expected 2 blank lines, found 1 @@ -5175,28 +5168,28 @@ ./tests/tfscreen/simulate/test_build_sample_dataframes.py:128:55: W292 no newline at end of file ./tests/tfscreen/simulate/test_generate_presplit_data.py:11:1: F401 'pytest' imported but unused ./tests/tfscreen/simulate/test_generate_presplit_data.py:14:1: F401 'unittest.mock.MagicMock' imported but unused -./tests/tfscreen/simulate/test_generate_presplit_data.py:75:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:74:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:86:42: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_generate_presplit_data.py:87:42: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:88:42: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:90:39: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:98:39: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:106:39: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:124:39: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:125:14: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_generate_presplit_data.py:141:20: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_generate_presplit_data.py:142:51: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:143:22: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_generate_presplit_data.py:144:51: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:146:22: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_generate_presplit_data.py:159:35: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:161:35: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:163:36: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:174:11: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_generate_presplit_data.py:178:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:222:11: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_generate_presplit_data.py:227:33: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:229:31: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_generate_presplit_data.py:231:33: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:89:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:97:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:105:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:123:39: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:124:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:140:20: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:141:51: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:142:22: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:143:51: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:145:22: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:158:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:160:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:162:36: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:173:11: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:177:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:220:11: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_generate_presplit_data.py:225:33: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:227:31: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_generate_presplit_data.py:229:33: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_library_prediction.py:47:41: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_run_simulation.py:3:1: F401 'os' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:5:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5223,152 +5216,152 @@ ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:34:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_selection_experiment.py:39:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:69:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:78:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:86:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:117:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:142:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:176:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:179:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:186:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:195:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:204:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:222:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:228:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:237:51: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:238:63: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:250:16: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:252:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:252:57: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:253:69: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:273:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:277:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:281:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:284:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:287:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:290:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:294:52: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:295:64: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:304:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:307:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:315:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:321:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:323:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:327:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:333:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:336:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:338:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:339:61: E231 missing whitespace after ',' -./tests/tfscreen/simulate/test_selection_experiment.py:344:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:346:31: E231 missing whitespace after ',' -./tests/tfscreen/simulate/test_selection_experiment.py:346:32: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:369:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:374:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:381:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:384:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:386:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:391:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:401:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:407:1: E303 too many blank lines (3) -./tests/tfscreen/simulate/test_selection_experiment.py:411:1: E302 expected 2 blank lines, found 3 -./tests/tfscreen/simulate/test_selection_experiment.py:417:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:419:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:422:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:429:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:437:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:445:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:449:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:453:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:457:31: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:461:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:465:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:471:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:486:22: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:487:24: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:489:70: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:498:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:501:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:515:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:518:25: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:529:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:534:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:551:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:77:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:85:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:116:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:141:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:175:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:178:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:185:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:194:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:203:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:221:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:227:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:236:51: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:237:63: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:249:16: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:251:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:251:57: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:252:69: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:272:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:276:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:280:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:283:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:286:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:289:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:293:52: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:294:64: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:303:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:306:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:314:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:320:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:322:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:326:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:332:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:335:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:337:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:338:61: E231 missing whitespace after ',' +./tests/tfscreen/simulate/test_selection_experiment.py:343:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:345:31: E231 missing whitespace after ',' +./tests/tfscreen/simulate/test_selection_experiment.py:345:32: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:368:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:373:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:380:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:383:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:385:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:390:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:400:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:406:1: E303 too many blank lines (3) +./tests/tfscreen/simulate/test_selection_experiment.py:410:1: E302 expected 2 blank lines, found 3 +./tests/tfscreen/simulate/test_selection_experiment.py:416:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:418:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:421:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:428:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:436:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:444:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:448:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:452:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:456:31: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:460:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:464:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:470:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:485:22: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:486:24: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:488:70: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:497:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:500:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:514:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:517:25: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:528:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:533:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:550:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:553:20: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:554:20: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:555:20: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:556:20: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:570:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:577:47: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:569:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:576:47: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:582:20: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:583:20: E261 at least two spaces before inline comment ./tests/tfscreen/simulate/test_selection_experiment.py:584:20: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:585:20: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:593:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:596:9: F811 redefinition of unused 'gmean' from line 6 -./tests/tfscreen/simulate/test_selection_experiment.py:604:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:608:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:615:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:622:59: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:624:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:641:41: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:655:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:663:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:679:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:687:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:694:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:702:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:713:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:592:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:595:9: F811 redefinition of unused 'gmean' from line 6 +./tests/tfscreen/simulate/test_selection_experiment.py:603:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:607:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:614:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:621:59: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:623:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:640:41: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:654:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:662:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:678:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:686:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:693:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:701:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:712:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:713:48: W291 trailing whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:714:48: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:715:48: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:721:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:724:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:732:43: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:747:31: E201 whitespace after '[' -./tests/tfscreen/simulate/test_selection_experiment.py:748:54: E202 whitespace before ']' -./tests/tfscreen/simulate/test_selection_experiment.py:749:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:753:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:755:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:756:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:765:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:767:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:771:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:779:63: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:780:75: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:781:77: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:790:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:795:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:798:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:804:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:809:25: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:815:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:819:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:821:45: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:822:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:823:48: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:826:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:834:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:839:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:843:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:849:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:853:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:856:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:859:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:863:37: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:884:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:889:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:892:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:897:41: E261 at least two spaces before inline comment -./tests/tfscreen/simulate/test_selection_experiment.py:917:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/simulate/test_selection_experiment.py:1148:12: F841 local variable 'k_sel' is assigned to but never used -./tests/tfscreen/simulate/test_selection_experiment.py:1163:29: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_selection_experiment.py:1165:29: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_selection_experiment.py:1187:20: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_selection_experiment.py:1202:14: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_selection_experiment.py:1205:14: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_selection_experiment.py:1243:69: W291 trailing whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1255:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1266:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1270:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1273:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_selection_experiment.py:1314:27: W292 no newline at end of file +./tests/tfscreen/simulate/test_selection_experiment.py:720:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:723:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:731:43: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:746:31: E201 whitespace after '[' +./tests/tfscreen/simulate/test_selection_experiment.py:747:54: E202 whitespace before ']' +./tests/tfscreen/simulate/test_selection_experiment.py:748:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:752:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:754:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:755:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:764:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:766:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:770:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:778:63: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:779:75: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:780:77: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:789:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:794:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:797:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:803:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:808:25: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:814:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:818:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:820:45: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:821:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:822:48: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:825:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:833:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:838:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:842:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:848:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:852:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:855:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:858:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:862:37: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:883:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:888:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:891:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:896:41: E261 at least two spaces before inline comment +./tests/tfscreen/simulate/test_selection_experiment.py:916:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/simulate/test_selection_experiment.py:1147:12: F841 local variable 'k_sel' is assigned to but never used +./tests/tfscreen/simulate/test_selection_experiment.py:1162:29: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_selection_experiment.py:1164:29: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_selection_experiment.py:1186:20: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_selection_experiment.py:1201:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_selection_experiment.py:1204:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_selection_experiment.py:1242:69: W291 trailing whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1254:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1265:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1269:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1272:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_selection_experiment.py:1313:27: W292 no newline at end of file ./tests/tfscreen/simulate/test_thermo_to_growth.py:28:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:32:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:41:1: E302 expected 2 blank lines, found 1 @@ -6540,7 +6533,8 @@ ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:961:18: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:964:17: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1229:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1409:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1411:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1741:21: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused @@ -7679,7 +7673,7 @@ 1768 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1120 E231 missing whitespace after ',' +1115 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 228 E261 at least two spaces before inline comment @@ -7707,6 +7701,6 @@ 40 F841 local variable 'e' is assigned to but never used 669 W291 trailing whitespace 86 W292 no newline at end of file -1675 W293 blank line contains whitespace +1674 W293 blank line contains whitespace 29 W391 blank line at end of file -7663 +7657 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 51e3f0a8..69b128ff 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/scripts/run_simulation_cli.py b/src/tfscreen/simulate/scripts/run_simulation_cli.py index 3f9a9954..01f817f1 100644 --- a/src/tfscreen/simulate/scripts/run_simulation_cli.py +++ b/src/tfscreen/simulate/scripts/run_simulation_cli.py @@ -122,8 +122,7 @@ def _generate_presplit_data(combined_sample_df, combined_counts_df, cf, rng): initial log-CFU per genotype computed by the simulation). cf : dict Full run configuration (already read from YAML). Used keys: - ``cfu0``, ``total_num_reads``, ``final_cfu_pct_err``, - ``prob_index_hop``. The optional ``presplit_data`` sub-dict may + ``cfu0``, ``total_num_reads``, ``prob_index_hop``. The optional ``presplit_data`` sub-dict may contain a scalar ``noise`` key (default 0) to add extra Gaussian noise to ``ln_cfu`` on the log scale. rng : numpy.random.Generator @@ -139,7 +138,6 @@ def _generate_presplit_data(combined_sample_df, combined_counts_df, cf, rng): transformation pool) receive ``ln_cfu = NaN``. """ total_cfu0 = float(cf["cfu0"]) - final_cfu_pct_err = float(cf.get("final_cfu_pct_err", 0.05)) presplit_cfg = cf.get("presplit_data", {}) extra_noise = float(presplit_cfg.get("noise", 0.0)) if presplit_cfg else 0.0 pseudocount = 1 @@ -188,20 +186,18 @@ def _generate_presplit_data(combined_sample_df, combined_counts_df, cf, rng): # ---------- counts → ln_cfu (mirrors counts_to_lncfu logic) ---------- sample_cfu = total_cfu0 - sample_cfu_std = total_cfu0 * final_cfu_pct_err total_adjusted = counts.sum() + len(counts) * pseudocount adj_counts = counts + pseudocount freq_est = adj_counts / total_adjusted cfu_est = freq_est * sample_cfu - # Variance from binomial frequency uncertainty + sample-CFU uncertainty + # Variance from binomial frequency uncertainty only var_freq = freq_est * (1.0 - freq_est) / total_adjusted with np.errstate(divide="ignore", invalid="ignore"): rel_var_freq = np.where(freq_est > 0, var_freq / (freq_est ** 2), 0.0) - rel_var_cfu = (sample_cfu_std / sample_cfu) ** 2 - ln_cfu_var = rel_var_freq + rel_var_cfu + ln_cfu_var = rel_var_freq # Optional additional noise on the log scale if extra_noise > 0.0: diff --git a/src/tfscreen/simulate/selection_experiment.py b/src/tfscreen/simulate/selection_experiment.py index 12bae72f..2fda6fcc 100644 --- a/src/tfscreen/simulate/selection_experiment.py +++ b/src/tfscreen/simulate/selection_experiment.py @@ -62,7 +62,7 @@ "transformation_poisson_lambda", "multi_plasmid_combine_fcn", "cfu0", "tube_noise_sigma", "growth_transition", # Data collection - "total_num_reads", "prob_index_hop", "random_seed", "final_cfu_pct_err", + "total_num_reads", "prob_index_hop", "random_seed", # Column selectors (rarely overridden) "condition_selector", "library_selector", # Optional output blocks @@ -190,9 +190,6 @@ def _check_cf( check_unknown_keys(cf, SIMULATE_KNOWN_KEYS, label="simulate config") - if "final_cfu_pct_err" not in cf: - cf["final_cfu_pct_err"] = 0.05 - # --- Validate single numerical values --- cf = _check_dict_number("prob_index_hop", cf, min_allowed=0, max_allowed=1, allow_none=True) cf = _check_dict_number("lib_assembly_skew_sigma", cf, min_allowed=0, allow_none=True) @@ -201,7 +198,6 @@ def _check_cf( cf = _check_dict_number("random_seed", cf, cast_type=int, min_allowed=0, allow_none=True) cf = _check_dict_number("cfu0", cf, allow_none=False,min_allowed=0) cf = _check_dict_number("total_num_reads", cf, cast_type=int, min_allowed=0, inclusive_min=False) - cf = _check_dict_number("final_cfu_pct_err",cf,min_allowed=0,inclusive_min=False,allow_none=False) # --- Validate nested dictionaries --- for key in ["transform_sizes", "library_mixture"]: @@ -1008,8 +1004,7 @@ def _simulate_library_group( multi_plasmid_combine_fcn = cf["multi_plasmid_combine_fcn"] prob_index_hop = cf["prob_index_hop"] total_cfu0 = cf["cfu0"] - final_cfu_pct_err = cf["final_cfu_pct_err"] - + num_genotypes = len(ordered_genotypes) # -- create output dataframes -- @@ -1111,7 +1106,7 @@ def _simulate_library_group( # Record cfu/mL over all conditions sample_df.loc[:,"sample_cfu"] = np.sum(trans_cfu,axis=0) - sample_df.loc[:,"sample_cfu_std"] = sample_df.loc[:,"sample_cfu"]*final_cfu_pct_err + sample_df.loc[:,"sample_cfu_std"] = 0.0 # -- simulate sequencing -- print("--> simulating sequencing",flush=True) diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index 53dd75ca..94da2545 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -14,6 +14,7 @@ from tfscreen.plot import default_styles # noqa: F401 — applies global rcParams on import from tfscreen.plot.default_styles import DEFAULT_COLORS from tfscreen.plot.xy_corr import xy_corr +from tfscreen.tfmodel.analysis.error_calibration import calibration_summary from tfscreen.util.cli.generalized_main import generalized_main @@ -232,6 +233,48 @@ def _blank_panel(ax, message): ha="center", va="center", fontsize=12, color="gray") +def _extract_quantile_data(df): + """Return (quantile_matrix, levels) from a DataFrame with q{level} columns. + + Columns must match ``q{float}`` exactly (e.g. ``q0.025``, ``q0.5``). + Returns ``(None, None)`` when fewer than two such columns exist. + """ + q_cols = sorted( + [c for c in df.columns if re.match(r"^q\d*\.?\d+$", c)], + key=lambda c: float(c[1:]), + ) + if len(q_cols) < 2: + return None, None + levels = np.array([float(c[1:]) for c in q_cols]) + return df[q_cols].values, levels + + +def _try_calibration(df, ref_col, out_prefix, label): + """Run calibration_summary on df[ref_col] vs quantile columns. + + Silently skips when df is None, ref_col is absent, or fewer than two + quantile columns exist. Warns and returns on any other failure so the + rest of the output is unaffected. + """ + if df is None: + return + if ref_col not in df.columns: + return + quantile_matrix, levels = _extract_quantile_data(df) + if quantile_matrix is None: + return + try: + calibration_summary( + true_vals=df[ref_col].values, + quantile_matrix=quantile_matrix, + quantile_levels=levels, + out_prefix=out_prefix, + label=label, + ) + except Exception as exc: + warnings.warn(f"Could not run calibration summary for {label}: {exc}") + + def _build_transform_registry(sim_df): """Return a dict mapping parameter name → ref Series indexed by genotype. @@ -472,6 +515,7 @@ def _summarize_params(run_dir, out_prefix): pdf_name = out_name.replace(".csv", ".pdf") _try_plot_params_corr(out_df, plot_label, pdf_name) + _try_calibration(out_df, "ref", out_name[:-4], plot_label) def summarize_fit(run_dir, @@ -633,6 +677,10 @@ def summarize_fit(run_dir, train_merged["q0.5"].values, train_merged["ref"].values, ) + _try_calibration( + train_merged, "ref", + f"{out_prefix}_theta_training", "theta training", + ) except Exception as exc: warnings.warn(f"Could not compute theta training statistics: {exc}") @@ -671,6 +719,10 @@ def summarize_fit(run_dir, test_merged["q0.5"].values, test_merged["ref"].values, ) + _try_calibration( + test_merged, "ref", + f"{out_prefix}_theta_test", "theta test", + ) except Exception as exc: warnings.warn(f"Could not compute theta test statistics from {ref_theta_file}: {exc}") @@ -770,17 +822,21 @@ def summarize_fit(run_dir, warnings.warn(f"Could not generate correlation plot: {exc}") # --- Growth correlation CSV --- + growth_copy_df = None if growth_pred_file is not None: growth_csv_file = f"{out_prefix}_growth_corr.csv" try: - growth_copy_df = pd.read_csv(growth_pred_file) - growth_copy_df = growth_copy_df.rename( + growth_copy_df = pd.read_csv(growth_pred_file).rename( columns={"ln_cfu": "ref", "ln_cfu_std": "ref_std"} ) growth_copy_df.to_csv(growth_csv_file, index=False) print(f"Wrote growth data to {growth_csv_file}") except Exception as exc: warnings.warn(f"Could not write growth CSV at {growth_csv_file}: {exc}") + _try_calibration( + growth_copy_df, "ref", + f"{out_prefix}_growth", "growth", + ) # --- Growth correlation plot --- growth_pdf_file = f"{out_prefix}_growth_corr.pdf" diff --git a/tests/tfscreen/simulate/test_generate_presplit_data.py b/tests/tfscreen/simulate/test_generate_presplit_data.py index 8d3693f3..5b1b7b56 100644 --- a/tests/tfscreen/simulate/test_generate_presplit_data.py +++ b/tests/tfscreen/simulate/test_generate_presplit_data.py @@ -56,7 +56,6 @@ def _minimal_cf(noise=0.0, extra_keys=None): cf = { "cfu0": 1e8, "total_num_reads": 30_000_000, - "final_cfu_pct_err": 0.05, "prob_index_hop": None, "presplit_data": {"noise": noise}, } @@ -194,7 +193,6 @@ def test_run_simulation_writes_presplit_csv(tmp_path): "random_seed": 1, "cfu0": 1e8, "total_num_reads": 10_000_000, - "final_cfu_pct_err": 0.05, "prob_index_hop": None, "presplit_data": {"noise": 0.0}, } @@ -232,7 +230,7 @@ def test_run_simulation_no_presplit_without_config(tmp_path): growth_df = pd.DataFrame({"genotype": ["wt"], "ln_cfu": [10.0]}) cf = {"random_seed": 1, "cfu0": 1e8, "total_num_reads": 1_000_000, - "final_cfu_pct_err": 0.05, "prob_index_hop": None} + "prob_index_hop": None} with patch("tfscreen.util.read_yaml", return_value=cf), \ patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", diff --git a/tests/tfscreen/simulate/test_selection_experiment.py b/tests/tfscreen/simulate/test_selection_experiment.py index d50b07ad..202d45a4 100644 --- a/tests/tfscreen/simulate/test_selection_experiment.py +++ b/tests/tfscreen/simulate/test_selection_experiment.py @@ -43,7 +43,6 @@ def base_config() -> dict: "lib_assembly_skew_sigma": 0.5, "transformation_poisson_lambda": 0.8, "tube_noise_sigma": 0.002, - "final_cfu_pct_err": 0.03, "random_seed": 42, "cfu0": 1.0e7, "total_num_reads": 5_000_000, diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index 79181f99..a1fb2d49 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -1231,7 +1231,9 @@ def test_summarize_fit_skips_plot_when_no_pred_file(self, run_dir): _compute_direct_obs, _compute_diff_obs, _compute_epi_obs, + _extract_quantile_data, _summarize_params, + _try_calibration, _try_plot_params_corr, ) @@ -1660,3 +1662,335 @@ def test_summarize_fit_calls_summarize_params(self, run_dir): assert os.path.exists(out_pdf) df = pd.read_csv(out_csv) assert "ref" in df.columns + + +# --------------------------------------------------------------------------- +# _extract_quantile_data +# --------------------------------------------------------------------------- + +_PATCH_CALIBRATION_SUMMARY = ( + "tfscreen.tfmodel.scripts.summarize_fit_cli.calibration_summary" +) + + +class TestExtractQuantileData: + + def test_standard_q_cols_returns_matrix_and_levels(self): + df = pd.DataFrame({ + "q0.025": [0.1, 0.2], + "q0.5": [0.5, 0.6], + "q0.975": [0.9, 1.0], + "ref": [0.4, 0.5], + }) + mat, levels = _extract_quantile_data(df) + assert levels == pytest.approx([0.025, 0.5, 0.975]) + assert mat.shape == (2, 3) + assert mat[0, 1] == pytest.approx(0.5) # q0.5 for first row + + def test_levels_sorted_numerically_not_lexicographically(self): + # q0.1 < q0.25 numerically; lexicographic would put q0.25 < q0.1 + df = pd.DataFrame({"q0.25": [0.3], "q0.1": [0.1], "q0.9": [0.8]}) + _, levels = _extract_quantile_data(df) + assert list(levels) == pytest.approx([0.1, 0.25, 0.9]) + + def test_returns_none_none_when_no_q_cols(self): + df = pd.DataFrame({"ref": [0.1], "genotype": ["wt"]}) + mat, levels = _extract_quantile_data(df) + assert mat is None + assert levels is None + + def test_returns_none_none_with_exactly_one_q_col(self): + df = pd.DataFrame({"q0.5": [0.5], "ref": [0.4]}) + mat, levels = _extract_quantile_data(df) + assert mat is None + assert levels is None + + def test_non_q_columns_excluded(self): + df = pd.DataFrame({ + "q0.5": [0.5], + "q0.975": [0.9], + "quality": [1.0], # must not match + }) + mat, levels = _extract_quantile_data(df) + assert mat is not None + assert mat.shape == (1, 2) + assert list(levels) == pytest.approx([0.5, 0.975]) + + def test_returns_values_in_same_row_order_as_df(self): + df = pd.DataFrame({ + "q0.025": [0.10, 0.20, 0.30], + "q0.975": [0.80, 0.85, 0.90], + }) + mat, _ = _extract_quantile_data(df) + assert mat[1, 0] == pytest.approx(0.20) + assert mat[2, 1] == pytest.approx(0.90) + + +# --------------------------------------------------------------------------- +# _try_calibration +# --------------------------------------------------------------------------- + +def _make_calibration_df(n=20, q_cols=True, ref_col="ref"): + """Build a minimal DataFrame for calibration tests.""" + rng = np.random.default_rng(42) + data = {} + if ref_col: + data[ref_col] = rng.uniform(0, 1, n) + if q_cols: + data["q0.025"] = rng.uniform(0.0, 0.3, n) + data["q0.5"] = rng.uniform(0.3, 0.7, n) + data["q0.975"] = rng.uniform(0.7, 1.0, n) + else: + data["q0.5"] = rng.uniform(0.3, 0.7, n) + return pd.DataFrame(data) + + +class TestTryCalibration: + + def test_calls_calibration_summary_with_correct_shapes(self, tmp_path): + df = _make_calibration_df(n=30) + out_prefix = str(tmp_path / "cal") + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + _try_calibration(df, "ref", out_prefix, "test label") + mock_cal.assert_called_once() + _, kwargs = mock_cal.call_args + assert kwargs["true_vals"].shape == (30,) + assert kwargs["quantile_matrix"].shape == (30, 3) + assert kwargs["quantile_levels"].tolist() == pytest.approx([0.025, 0.5, 0.975]) + assert kwargs["out_prefix"] == out_prefix + assert kwargs["label"] == "test label" + + def test_skips_silently_when_df_is_none(self, tmp_path): + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + _try_calibration(None, "ref", str(tmp_path / "cal"), "label") + mock_cal.assert_not_called() + + def test_skips_silently_when_ref_col_absent(self, tmp_path): + df = _make_calibration_df() + df = df.drop(columns=["ref"]) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + _try_calibration(df, "ref", str(tmp_path / "cal"), "label") + mock_cal.assert_not_called() + + def test_skips_silently_when_fewer_than_two_q_cols(self, tmp_path): + df = _make_calibration_df(q_cols=False) # only q0.5 + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + _try_calibration(df, "ref", str(tmp_path / "cal"), "label") + mock_cal.assert_not_called() + + def test_warns_gracefully_when_calibration_summary_raises(self, tmp_path): + df = _make_calibration_df() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with patch(_PATCH_CALIBRATION_SUMMARY, side_effect=RuntimeError("boom")): + _try_calibration(df, "ref", str(tmp_path / "cal"), "my label") + assert any("my label" in str(x.message) for x in w) + + def test_passes_ref_col_values_as_true_vals(self, tmp_path): + rng = np.random.default_rng(7) + ref_vals = rng.uniform(0, 1, 10) + df = pd.DataFrame({ + "ref": ref_vals, + "q0.1": rng.uniform(0, 0.4, 10), + "q0.9": rng.uniform(0.6, 1.0, 10), + }) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + _try_calibration(df, "ref", str(tmp_path / "cal"), "label") + _, kwargs = mock_cal.call_args + np.testing.assert_array_equal(kwargs["true_vals"], ref_vals) + + def test_works_with_non_default_ref_col_name(self, tmp_path): + df = _make_calibration_df(ref_col="ln_cfu") + df = df.rename(columns={"ref": "other"}) # ensure "ref" is absent + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + _try_calibration(df, "ln_cfu", str(tmp_path / "cal"), "growth") + mock_cal.assert_called_once() + + +# --------------------------------------------------------------------------- +# Integration: calibration called from each summarize_fit call site +# --------------------------------------------------------------------------- + +def _make_pred_csv_with_quantiles(path, n_training=3, extra_genotypes=None): + """Like _make_pred_csv but includes q0.025 / q0.5 / q0.975.""" + rows = [] + genotypes = list(GENOTYPES[:n_training]) + if extra_genotypes: + genotypes += extra_genotypes + theta_map = dict(zip(GENOTYPES, THETA_PRED)) + for g in genotypes: + in_train = 1 if g in GENOTYPES[:n_training] else 0 + theta = theta_map.get(g, 0.5) + for tc in TITRANT_CONCS: + rows.append({ + "genotype": g, + "titrant_name": TITRANT_NAME, + "titrant_conc": tc, + "q0.025": max(0.0, theta - 0.10), + "q0.5": theta + tc * 0.0001, + "q0.975": min(1.0, theta + 0.10), + "in_training_data": in_train, + }) + pd.DataFrame(rows).to_csv(path, index=False) + + +class TestCalibrationIntegration: + """Verify calibration_summary is (or isn't) called from each wiring point.""" + + # ------------------------------------------------------------------ + # Theta training + # ------------------------------------------------------------------ + + def test_calibration_called_for_theta_training_with_quantile_cols(self, run_dir): + _make_pred_csv_with_quantiles(os.path.join(run_dir, "run_theta_pred.csv")) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert any("theta_training" in p for p in prefixes) + + def test_calibration_skipped_for_theta_training_without_quantile_cols(self, run_dir): + # Overwrite with a pred CSV that has only q0.5 (no interval columns) + rows = [] + for g in GENOTYPES[:3]: + for tc in TITRANT_CONCS: + rows.append({ + "genotype": g, "titrant_name": TITRANT_NAME, + "titrant_conc": tc, "q0.5": 0.5, "in_training_data": 1, + }) + pd.DataFrame(rows).to_csv( + os.path.join(run_dir, "run_theta_pred.csv"), index=False + ) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert not any("theta_training" in p for p in prefixes) + + # ------------------------------------------------------------------ + # Theta test + # ------------------------------------------------------------------ + + def test_calibration_called_for_theta_test_with_quantile_cols( + self, run_dir, ref_theta_file + ): + _make_pred_csv_with_quantiles( + os.path.join(run_dir, "run_theta_pred.csv"), + n_training=3, extra_genotypes=["E4F"], + ) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir, ref_theta_file=ref_theta_file) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert any("theta_test" in p for p in prefixes) + + def test_calibration_skipped_for_theta_test_when_no_ref_theta_file(self, run_dir): + _make_pred_csv_with_quantiles(os.path.join(run_dir, "run_theta_pred.csv")) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert not any("theta_test" in p for p in prefixes) + + # ------------------------------------------------------------------ + # Growth + # ------------------------------------------------------------------ + + def test_calibration_called_for_growth_with_quantile_cols(self, run_dir): + pd.DataFrame({ + "genotype": ["wt"] * 6, + "ln_cfu": np.linspace(8.0, 13.0, 6), + "q0.025": np.linspace(7.5, 12.5, 6), + "q0.5": np.linspace(8.0, 13.0, 6), + "q0.975": np.linspace(8.5, 13.5, 6), + }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert any("growth" in p for p in prefixes) + + def test_calibration_uses_ref_col_for_growth(self, run_dir): + pd.DataFrame({ + "genotype": ["wt"] * 6, + "ln_cfu": np.linspace(8.0, 13.0, 6), + "q0.025": np.linspace(7.5, 12.5, 6), + "q0.5": np.linspace(8.0, 13.0, 6), + "q0.975": np.linspace(8.5, 13.5, 6), + }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + growth_calls = [c for c in mock_cal.call_args_list + if "growth" in c.kwargs["out_prefix"]] + assert len(growth_calls) == 1 + # true_vals must come from the renamed "ref" column (was ln_cfu) + np.testing.assert_array_almost_equal( + growth_calls[0].kwargs["true_vals"], + np.linspace(8.0, 13.0, 6), + ) + + def test_calibration_skipped_for_growth_without_quantile_cols(self, run_dir): + pd.DataFrame({ + "genotype": ["wt"] * 6, + "ln_cfu": np.linspace(8.0, 13.0, 6), + "q0.5": np.linspace(8.0, 13.0, 6), + }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert not any("growth" in p for p in prefixes) + + def test_calibration_skipped_for_growth_when_no_growth_pred_file(self, run_dir): + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert not any("growth" in p for p in prefixes) + + # ------------------------------------------------------------------ + # Params + # ------------------------------------------------------------------ + + def test_calibration_called_for_params_with_quantile_cols(self, run_dir): + _SIM_DF.to_csv(os.path.join(run_dir, "tfs_sim_parameters.csv"), index=False) + rows = [ + {"genotype": g, "titrant_name": "iptg", + "q0.025": 0.0, "q0.5": 0.0, "q0.975": 0.0} + for g in _SIM_DF["genotype"] + ] + pd.DataFrame(rows).to_csv( + os.path.join(run_dir, "tfs_params_activity.csv"), index=False + ) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert any("params_activity" in p for p in prefixes) + + def test_calibration_skipped_for_params_without_quantile_cols(self, run_dir): + _SIM_DF.to_csv(os.path.join(run_dir, "tfs_sim_parameters.csv"), index=False) + rows = [{"genotype": g, "titrant_name": "iptg", "q0.5": 0.0} + for g in _SIM_DF["genotype"]] + pd.DataFrame(rows).to_csv( + os.path.join(run_dir, "tfs_params_activity.csv"), index=False + ) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] + assert not any("params_activity" in p for p in prefixes) + + def test_params_calibration_out_prefix_strips_csv_extension(self, run_dir): + _SIM_DF.to_csv(os.path.join(run_dir, "tfs_sim_parameters.csv"), index=False) + rows = [ + {"genotype": g, "titrant_name": "iptg", + "q0.025": 0.0, "q0.5": 0.0, "q0.975": 0.0} + for g in _SIM_DF["genotype"] + ] + pd.DataFrame(rows).to_csv( + os.path.join(run_dir, "tfs_params_activity.csv"), index=False + ) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: + summarize_fit(run_dir) + params_calls = [c for c in mock_cal.call_args_list + if "params_activity" in c.kwargs["out_prefix"]] + assert len(params_calls) == 1 + assert not params_calls[0].kwargs["out_prefix"].endswith(".csv") From ee8a1d5a63c399ab949d8cb3c68556432b502019 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 15 Jun 2026 17:44:44 -0700 Subject: [PATCH 14/24] various ui/ux/test cleanups --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 823 +++++++++--------- reports/junit/junit.xml | 2 +- src/tfscreen/analysis/__init__.py | 4 + .../analysis/cat_response/fit_response_cli.py | 186 ---- .../analysis/cat_response/scripts/__init__.py | 0 .../{ => scripts}/cat_response_cli.py | 0 .../{mle => analysis}/stats_test_suite.py | 0 src/tfscreen/mle/__init__.py | 3 - src/tfscreen/simulate/library_prediction.py | 11 +- ...{run_simulation_cli.py => simulate_cli.py} | 27 +- src/tfscreen/simulate/selection_experiment.py | 8 +- src/tfscreen/tfmodel/model_orchestrator.py | 33 +- ...ation_cli.py => prefit_calibration_cli.py} | 421 +-------- ..._predictive_cli.py => sample_prior_cli.py} | 0 .../tfmodel/scripts/summarize_fit_cli.py | 2 +- .../analysis/cat_response/scripts/__init__.py | 0 .../{ => scripts}/test_cat_response_cli.py | 22 +- .../cat_response/test_fit_response_cli.py | 116 --- .../test_stats_test_suite.py | 4 +- tests/tfscreen/plot/test_geno_trajectory.py | 3 +- .../simulate/test_generate_presplit_data.py | 24 +- .../simulate/test_library_prediction.py | 149 +++- .../simulate/test_run_simulation_cli.py | 145 ++- .../simulate/test_selection_experiment.py | 4 +- .../simulate/test_setup_sim_grid_cli.py | 6 +- .../analysis/test_error_calibration.py | 3 +- .../tfmodel/components/theta/test_hill_mut.py | 9 +- .../scripts/test_prior_predictive_cli.py | 70 +- .../scripts/test_run_prefit_calibration.py | 738 ++-------------- .../tfmodel/scripts/test_summarize_fit_cli.py | 12 +- .../tfmodel/test_model_orchestrator.py | 8 +- tests/tfscreen/tfmodel/test_presplit.py | 5 +- 34 files changed, 907 insertions(+), 1935 deletions(-) delete mode 100644 src/tfscreen/analysis/cat_response/fit_response_cli.py create mode 100644 src/tfscreen/analysis/cat_response/scripts/__init__.py rename src/tfscreen/analysis/cat_response/{ => scripts}/cat_response_cli.py (100%) rename src/tfscreen/{mle => analysis}/stats_test_suite.py (100%) rename src/tfscreen/simulate/scripts/{run_simulation_cli.py => simulate_cli.py} (94%) rename src/tfscreen/tfmodel/scripts/{run_prefit_calibration_cli.py => prefit_calibration_cli.py} (66%) rename src/tfscreen/tfmodel/scripts/{prior_predictive_cli.py => sample_prior_cli.py} (100%) create mode 100644 tests/tfscreen/analysis/cat_response/scripts/__init__.py rename tests/tfscreen/analysis/cat_response/{ => scripts}/test_cat_response_cli.py (80%) delete mode 100644 tests/tfscreen/analysis/cat_response/test_fit_response_cli.py rename tests/tfscreen/{mle => analysis}/test_stats_test_suite.py (95%) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 9bd8285b..c24babeb 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.00%coverage96.00% \ No newline at end of file +coverage: 96.12%coverage96.12% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index f8fe7989..d34a1886 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3375tests3375 \ No newline at end of file +tests: 3353tests3353 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 498c3db0..a9e5ef62 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -3,7 +3,7 @@ ./scripts/generate_struct_ensemble.py:433:18: E221 multiple spaces before operator ./src/tfscreen/__init__.py:19:1: W391 blank line at end of file ./src/tfscreen/__version__.py:5:16: W291 trailing whitespace -./src/tfscreen/analysis/__init__.py:9:1: W391 blank line at end of file +./src/tfscreen/analysis/__init__.py:13:1: W391 blank line at end of file ./src/tfscreen/analysis/cat_response/__init__.py:2:77: W291 trailing whitespace ./src/tfscreen/analysis/cat_response/__init__.py:3:55: W291 trailing whitespace ./src/tfscreen/analysis/cat_response/cat_fit.py:14:1: C901 'cat_fit' is too complex (19) @@ -70,11 +70,9 @@ ./src/tfscreen/analysis/cat_response/cat_response.py:134:34: E231 missing whitespace after ',' ./src/tfscreen/analysis/cat_response/cat_response.py:138:42: E231 missing whitespace after ',' ./src/tfscreen/analysis/cat_response/cat_response.py:141:56: W292 no newline at end of file -./src/tfscreen/analysis/cat_response/cat_response_cli.py:12:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/analysis/cat_response/cat_response_cli.py:21:1: C901 'cat_response' is too complex (11) -./src/tfscreen/analysis/cat_response/cat_response_cli.py:91:56: E127 continuation line over-indented for visual indent -./src/tfscreen/analysis/cat_response/fit_response_cli.py:25:1: C901 'fit_response' is too complex (11) -./src/tfscreen/analysis/cat_response/fit_response_cli.py:87:56: E127 continuation line over-indented for visual indent +./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:12:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:21:1: C901 'cat_response' is too complex (11) +./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:91:56: E127 continuation line over-indented for visual indent ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/extract_epistasis.py:33:77: W291 trailing whitespace @@ -103,6 +101,28 @@ ./src/tfscreen/analysis/extract_epistasis.py:229:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:231:10: W291 trailing whitespace ./src/tfscreen/analysis/extract_epistasis.py:233:30: W291 trailing whitespace +./src/tfscreen/analysis/stats_test_suite.py:8:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/analysis/stats_test_suite.py:8:31: E231 missing whitespace after ',' +./src/tfscreen/analysis/stats_test_suite.py:8:42: E231 missing whitespace after ',' +./src/tfscreen/analysis/stats_test_suite.py:55:75: W291 trailing whitespace +./src/tfscreen/analysis/stats_test_suite.py:66:52: W291 trailing whitespace +./src/tfscreen/analysis/stats_test_suite.py:81:1: W293 blank line contains whitespace +./src/tfscreen/analysis/stats_test_suite.py:91:1: W293 blank line contains whitespace +./src/tfscreen/analysis/stats_test_suite.py:104:33: W291 trailing whitespace +./src/tfscreen/analysis/stats_test_suite.py:134:1: W293 blank line contains whitespace +./src/tfscreen/analysis/stats_test_suite.py:143:26: E231 missing whitespace after ',' +./src/tfscreen/analysis/stats_test_suite.py:144:1: W293 blank line contains whitespace +./src/tfscreen/analysis/stats_test_suite.py:148:5: E303 too many blank lines (2) +./src/tfscreen/analysis/stats_test_suite.py:149:22: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:150:15: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:151:26: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:155:21: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:156:24: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:157:24: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:158:32: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:159:21: E231 missing whitespace after ':' +./src/tfscreen/analysis/stats_test_suite.py:161:1: W293 blank line contains whitespace +./src/tfscreen/analysis/stats_test_suite.py:163:1: W391 blank line at end of file ./src/tfscreen/genetics/build_cycles.py:6:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/genetics/build_cycles.py:26:1: W293 blank line contains whitespace ./src/tfscreen/genetics/build_cycles.py:69:26: W291 trailing whitespace @@ -597,28 +617,6 @@ ./src/tfscreen/mle/predict_with_error.py:57:43: E231 missing whitespace after ',' ./src/tfscreen/mle/predict_with_error.py:61:45: E231 missing whitespace after ',' ./src/tfscreen/mle/predict_with_error.py:74:32: W292 no newline at end of file -./src/tfscreen/mle/stats_test_suite.py:8:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/stats_test_suite.py:8:31: E231 missing whitespace after ',' -./src/tfscreen/mle/stats_test_suite.py:8:42: E231 missing whitespace after ',' -./src/tfscreen/mle/stats_test_suite.py:55:75: W291 trailing whitespace -./src/tfscreen/mle/stats_test_suite.py:66:52: W291 trailing whitespace -./src/tfscreen/mle/stats_test_suite.py:81:1: W293 blank line contains whitespace -./src/tfscreen/mle/stats_test_suite.py:91:1: W293 blank line contains whitespace -./src/tfscreen/mle/stats_test_suite.py:104:33: W291 trailing whitespace -./src/tfscreen/mle/stats_test_suite.py:134:1: W293 blank line contains whitespace -./src/tfscreen/mle/stats_test_suite.py:143:26: E231 missing whitespace after ',' -./src/tfscreen/mle/stats_test_suite.py:144:1: W293 blank line contains whitespace -./src/tfscreen/mle/stats_test_suite.py:148:5: E303 too many blank lines (2) -./src/tfscreen/mle/stats_test_suite.py:149:22: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:150:15: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:151:26: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:155:21: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:156:24: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:157:24: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:158:32: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:159:21: E231 missing whitespace after ':' -./src/tfscreen/mle/stats_test_suite.py:161:1: W293 blank line contains whitespace -./src/tfscreen/mle/stats_test_suite.py:163:1: W391 blank line at end of file ./src/tfscreen/plot/__init__.py:36:63: W291 trailing whitespace ./src/tfscreen/plot/cat_fits.py:13:1: C901 'cat_fits' is too complex (11) ./src/tfscreen/plot/cat_fits.py:13:1: E302 expected 2 blank lines, found 1 @@ -1272,9 +1270,9 @@ ./src/tfscreen/simulate/build_sample_dataframes.py:83:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:87:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:95:21: W292 no newline at end of file -./src/tfscreen/simulate/library_prediction.py:16:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/library_prediction.py:17:43: E252 missing whitespace around parameter equals -./src/tfscreen/simulate/library_prediction.py:17:44: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:17:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/library_prediction.py:18:43: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:18:44: E252 missing whitespace around parameter equals ./src/tfscreen/simulate/run_simulation.py:12:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/simulate/run_simulation.py:17:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:18:36: E231 missing whitespace after ',' @@ -1313,26 +1311,26 @@ ./src/tfscreen/simulate/run_simulation.py:101:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:106:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:107:1: W391 blank line at end of file -./src/tfscreen/simulate/scripts/run_simulation_cli.py:140:15: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:141:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:142:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:143:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:147:20: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:148:22: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:149:21: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:168:43: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/scripts/run_simulation_cli.py:169:14: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:170:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:175:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:188:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:191:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:192:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:193:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:196:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:198:25: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:200:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/run_simulation_cli.py:214:19: E221 multiple spaces before operator ./src/tfscreen/simulate/scripts/setup_sim_grid_cli.py:101:1: C901 'setup_sim_grid' is too complex (17) +./src/tfscreen/simulate/scripts/simulate_cli.py:143:15: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:144:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:145:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:146:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:150:20: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:151:22: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:152:21: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:171:43: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/scripts/simulate_cli.py:172:14: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:173:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:178:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:191:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:194:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:195:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:196:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:199:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:201:25: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:203:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:217:19: E221 multiple spaces before operator ./src/tfscreen/simulate/selection_experiment.py:42:38: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:43:37: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:44:36: E231 missing whitespace after ':' @@ -3411,203 +3409,203 @@ ./src/tfscreen/tfmodel/inference/run_inference.py:1282:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/inference/run_inference.py:1285:5: C901 'RunInference.get_laplace_posteriors' is too complex (16) ./src/tfscreen/tfmodel/inference/run_inference.py:1495:5: C901 'RunInference.get_nuts_posteriors' is too complex (12) -./src/tfscreen/tfmodel/model_orchestrator.py:38:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:72:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:75:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:79:25: E222 multiple spaces after operator -./src/tfscreen/tfmodel/model_orchestrator.py:79:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:80:42: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:84:65: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:84:88: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:89:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:93:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:93:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:93:55: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:93:63: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:94:52: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:104:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:108:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:112:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:113:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:115:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/model_orchestrator.py:150:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:37:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:71:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:74:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:78:25: E222 multiple spaces after operator +./src/tfscreen/tfmodel/model_orchestrator.py:78:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:79:42: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:83:65: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:83:88: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:88:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:92:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:92:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:92:55: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:92:63: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:93:52: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:103:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:107:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:111:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:112:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:114:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/model_orchestrator.py:149:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:152:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:152:73: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:153:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:153:73: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:154:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:155:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:155:61: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:156:61: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:157:61: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:158:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:159:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:161:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:162:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:157:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:158:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:160:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:161:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:162:38: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:163:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:164:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:167:63: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:166:63: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:167:73: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:168:73: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:169:73: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:171:59: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:172:70: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:173:72: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:177:63: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:180:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:181:15: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:182:42: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:182:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:183:29: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:184:40: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:185:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:185:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:185:72: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:226:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:235:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:235:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:236:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:239:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:245:27: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:276:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/model_orchestrator.py:308:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:309:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:310:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:312:23: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:313:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:314:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:318:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:398:12: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:458:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:461:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:466:52: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:467:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:474:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:478:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:494:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:495:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:515:82: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:520:87: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:523:84: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:528:79: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:610:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) -./src/tfscreen/tfmodel/model_orchestrator.py:610:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:635:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:636:28: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:638:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:639:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:639:48: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:639:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:640:46: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:640:66: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:641:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:645:39: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:646:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:651:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:658:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:660:10: E116 unexpected indentation (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:662:76: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:665:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:667:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:669:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:681:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:689:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:711:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:712:41: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:712:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:713:44: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:713:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:714:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:714:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:715:46: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:715:71: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:717:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:724:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:729:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:729:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:729:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:736:40: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:782:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:783:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:784:26: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:785:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:825:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:828:78: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:829:32: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:830:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:834:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:835:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:836:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:837:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:839:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:850:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:850:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:853:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:854:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:170:59: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:171:70: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:172:72: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:176:63: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:179:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:180:15: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:181:42: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:181:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:182:29: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:183:40: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:184:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:184:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:184:72: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:225:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:234:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:234:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:235:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:238:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:244:27: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:281:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/model_orchestrator.py:313:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:314:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:315:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:317:23: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:318:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:319:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:323:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:403:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:463:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:466:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:471:52: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:472:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:479:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:483:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:499:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:500:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:520:82: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:525:87: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:528:84: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:533:79: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:615:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) +./src/tfscreen/tfmodel/model_orchestrator.py:615:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:640:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:641:28: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:643:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:644:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:644:48: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:644:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:645:46: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:645:66: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:646:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:650:39: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:651:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:656:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:663:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:665:10: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:665:10: E116 unexpected indentation (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:667:76: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:670:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:672:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:674:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:686:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:694:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:716:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:717:41: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:717:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:718:44: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:718:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:719:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:719:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:720:46: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:720:71: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:722:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:729:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:734:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:734:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:734:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:741:40: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:787:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:788:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:789:26: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:790:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:830:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:833:78: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:834:32: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:835:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:839:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:840:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:841:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:842:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:844:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:855:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:855:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:858:74: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:859:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:860:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:861:58: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:863:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:864:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:865:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:866:58: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:868:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:872:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:876:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:879:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:885:86: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:887:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:894:77: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:950:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:951:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:952:35: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:953:39: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:960:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:961:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:962:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1038:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:1039:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1041:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1114:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) -./src/tfscreen/tfmodel/model_orchestrator.py:1134:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1136:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1137:71: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1139:68: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1140:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1141:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1142:40: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1159:42: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1160:43: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1161:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1209:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1212:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1216:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1218:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1228:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1230:9: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1283:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1288:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1302:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1312:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1322:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1336:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1339:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1368:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1380:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1392:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1398:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1399:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1400:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1401:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1402:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1403:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1404:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1405:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1406:29: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1407:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1408:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1409:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1410:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1411:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1412:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:873:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:877:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:881:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:884:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:890:86: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:892:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:899:77: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:955:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:956:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:957:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:958:39: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:965:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:966:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:967:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1043:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:1044:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1046:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1119:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) +./src/tfscreen/tfmodel/model_orchestrator.py:1139:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1141:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1142:71: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1144:68: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1145:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1146:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1147:40: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1164:42: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1165:43: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1166:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1166:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1166:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1166:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1166:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1214:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1217:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1221:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1223:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1233:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1235:9: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1288:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1293:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1307:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1317:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1327:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1341:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1344:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1373:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1385:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1397:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1403:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1404:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1405:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1406:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1407:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1408:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1409:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1410:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1411:29: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1412:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1413:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1414:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1415:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1416:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1417:31: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:192:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:193:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:194:22: E128 continuation line under-indented for visual indent @@ -3660,12 +3658,14 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:253:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:255:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:258:1: C901 '_inject_calibration_priors' is too complex (21) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:449:1: C901 '_build_csv_updates' is too complex (13) -./src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py:551:1: C901 '_apply_guesses_updates' is too complex (12) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:245:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:246:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:247:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:250:1: C901 '_inject_calibration_priors' is too complex (21) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:441:1: C901 '_build_csv_updates' is too complex (11) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:535:1: C901 '_apply_guesses_updates' is too complex (12) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:687:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:691:1: E302 expected 2 blank lines, found 3 ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used @@ -4093,6 +4093,9 @@ ./tests/smoke-tests/test_run_growth_analysis_smoke.py:37:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:45:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:48:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py:14:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py:19:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py:24:1: E305 expected 2 blank lines after class or function definition, found 1 ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:4:1: F401 'pandas as pd' imported but unused ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:10:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:17:1: E302 expected 2 blank lines, found 1 @@ -4191,12 +4194,6 @@ ./tests/tfscreen/analysis/cat_response/test_cat_response.py:113:59: E261 at least two spaces before inline comment ./tests/tfscreen/analysis/cat_response/test_cat_response.py:114:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/cat_response/test_cat_response.py:119:1: W391 blank line at end of file -./tests/tfscreen/analysis/cat_response/test_cat_response_cli.py:14:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_response_cli.py:19:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_response_cli.py:24:1: E305 expected 2 blank lines after class or function definition, found 1 -./tests/tfscreen/analysis/cat_response/test_fit_response_cli.py:14:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_fit_response_cli.py:19:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_fit_response_cli.py:24:1: E305 expected 2 blank lines after class or function definition, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:22:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:34:35: W291 trailing whitespace @@ -4248,6 +4245,33 @@ ./tests/tfscreen/analysis/test_extract_epistasis.py:226:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:228:74: E231 missing whitespace after ',' ./tests/tfscreen/analysis/test_extract_epistasis.py:229:55: W292 no newline at end of file +./tests/tfscreen/analysis/test_stats_test_suite.py:6:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/test_stats_test_suite.py:9:52: E261 at least two spaces before inline comment +./tests/tfscreen/analysis/test_stats_test_suite.py:10:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:13:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:20:41: W291 trailing whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:23:39: W291 trailing whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:24:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:31:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:32:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/test_stats_test_suite.py:38:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:45:49: E261 at least two spaces before inline comment +./tests/tfscreen/analysis/test_stats_test_suite.py:46:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:53:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/test_stats_test_suite.py:57:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:64:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/test_stats_test_suite.py:68:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:74:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/analysis/test_stats_test_suite.py:86:1: F401 'warnings' imported but unused +./tests/tfscreen/analysis/test_stats_test_suite.py:86:1: E305 expected 2 blank lines after class or function definition, found 1 +./tests/tfscreen/analysis/test_stats_test_suite.py:86:1: E402 module level import not at top of file +./tests/tfscreen/analysis/test_stats_test_suite.py:87:1: E302 expected 2 blank lines, found 0 +./tests/tfscreen/analysis/test_stats_test_suite.py:90:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:94:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:96:10: E111 indentation is not a multiple of 4 +./tests/tfscreen/analysis/test_stats_test_suite.py:96:10: E117 over-indented +./tests/tfscreen/analysis/test_stats_test_suite.py:97:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/test_stats_test_suite.py:99:1: W391 blank line at end of file ./tests/tfscreen/genetics/test_build_cycles.py:7:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/genetics/test_build_cycles.py:21:38: E231 missing whitespace after ',' ./tests/tfscreen/genetics/test_build_cycles.py:23:28: E124 closing bracket does not match visual indentation @@ -4700,33 +4724,6 @@ ./tests/tfscreen/mle/test_predict_with_error.py:59:1: W293 blank line contains whitespace ./tests/tfscreen/mle/test_predict_with_error.py:64:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/test_predict_with_error.py:67:61: W291 trailing whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:6:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/test_stats_test_suite.py:9:52: E261 at least two spaces before inline comment -./tests/tfscreen/mle/test_stats_test_suite.py:10:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:13:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:20:41: W291 trailing whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:23:39: W291 trailing whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:24:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:31:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:32:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/test_stats_test_suite.py:38:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:45:49: E261 at least two spaces before inline comment -./tests/tfscreen/mle/test_stats_test_suite.py:46:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:53:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/test_stats_test_suite.py:57:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:64:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/test_stats_test_suite.py:68:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:74:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/test_stats_test_suite.py:86:1: F401 'warnings' imported but unused -./tests/tfscreen/mle/test_stats_test_suite.py:86:1: E305 expected 2 blank lines after class or function definition, found 1 -./tests/tfscreen/mle/test_stats_test_suite.py:86:1: E402 module level import not at top of file -./tests/tfscreen/mle/test_stats_test_suite.py:87:1: E302 expected 2 blank lines, found 0 -./tests/tfscreen/mle/test_stats_test_suite.py:90:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:94:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:96:10: E111 indentation is not a multiple of 4 -./tests/tfscreen/mle/test_stats_test_suite.py:96:10: E117 over-indented -./tests/tfscreen/mle/test_stats_test_suite.py:97:1: W293 blank line contains whitespace -./tests/tfscreen/mle/test_stats_test_suite.py:99:1: W391 blank line at end of file ./tests/tfscreen/plot/heatmap/test_heatmap_core.py:25:1: F401 'tfscreen.plot.default_styles.DEFAULT_HMAP_PATCH_KWARGS' imported but unused ./tests/tfscreen/plot/heatmap/test_heatmap_core.py:31:1: F401 'tfscreen.plot.helper.get_ax_limits' imported but unused ./tests/tfscreen/plot/heatmap/test_heatmap_core.py:31:47: W291 trailing whitespace @@ -4904,14 +4901,14 @@ ./tests/tfscreen/plot/test_est_v_real_summary.py:30:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/plot/test_geno_trajectory.py:18:1: E402 module level import not at top of file ./tests/tfscreen/plot/test_geno_trajectory.py:20:1: E402 module level import not at top of file -./tests/tfscreen/plot/test_geno_trajectory.py:158:25: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:303:31: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:304:42: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:319:31: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:320:42: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:487:19: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:515:19: E741 ambiguous variable name 'l' -./tests/tfscreen/plot/test_geno_trajectory.py:543:19: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:159:25: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:304:31: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:305:42: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:320:31: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:321:42: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:488:19: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:516:19: E741 ambiguous variable name 'l' +./tests/tfscreen/plot/test_geno_trajectory.py:544:19: E741 ambiguous variable name 'l' ./tests/tfscreen/plot/test_helper.py:3:1: F401 'pytest' imported but unused ./tests/tfscreen/plot/test_helper.py:6:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/plot/test_helper.py:9:1: W293 blank line contains whitespace @@ -5191,6 +5188,7 @@ ./tests/tfscreen/simulate/test_generate_presplit_data.py:227:31: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_generate_presplit_data.py:229:33: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_library_prediction.py:47:41: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_library_prediction.py:289:5: F401 'numpy as np' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:3:1: F401 'os' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:5:1: F401 'unittest.mock.MagicMock' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:7:1: F401 'tfscreen.util' imported but unused @@ -5211,6 +5209,9 @@ ./tests/tfscreen/simulate/test_run_simulation.py:113:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_run_simulation.py:116:5: F841 local variable 'results' is assigned to but never used ./tests/tfscreen/simulate/test_run_simulation.py:117:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_run_simulation_cli.py:3:1: F401 'os' imported but unused +./tests/tfscreen/simulate/test_run_simulation_cli.py:165:5: F401 'jax' imported but unused +./tests/tfscreen/simulate/test_run_simulation_cli.py:230:5: F401 'tfscreen.simulate.scripts.simulate_cli as cli_mod' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:4:1: F401 'numpy.ma' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:6:1: F401 'scipy.stats.gmean' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5374,8 +5375,8 @@ ./tests/tfscreen/simulate/test_thermo_to_growth.py:229:35: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:1163:1: E402 module level import not at top of file ./tests/tfscreen/simulate/test_thermo_to_growth.py:1251:9: F841 local variable 'G_out' is assigned to but never used -./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:312:9: F401 'matplotlib.axes.Axes' imported but unused -./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:354:9: F401 'matplotlib.axes.Axes' imported but unused +./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:313:9: F401 'matplotlib.axes.Axes' imported but unused +./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:355:9: F401 'matplotlib.axes.Axes' imported but unused ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 @@ -5789,27 +5790,27 @@ ./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:537:39: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:540:36: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:541:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:10:1: F401 'tfscreen.tfmodel.generative.components.theta.hill_mut._population_moments' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:53:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:59:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:66:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:123:15: F841 local variable 'G' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:134:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:138:15: F841 local variable 'G' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:138:18: F841 local variable 'P' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:244:9: F841 local variable 'M' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:252:62: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:451:56: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:478:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:480:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:490:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:492:26: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:507:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:510:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:511:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:526:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:528:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:528:52: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:12:1: F401 'tfscreen.tfmodel.generative.components.theta.hill_mut._population_moments' imported but unused +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:55:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:61:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:125:15: F841 local variable 'G' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:136:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:140:15: F841 local variable 'G' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:140:18: F841 local variable 'P' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:246:9: F841 local variable 'M' is assigned to but never used +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:254:62: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:453:56: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:480:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:482:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:492:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:494:26: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:509:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:512:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:513:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:528:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:530:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:530:52: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:25:11: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:30:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:155:31: E221 multiple spaces before operator @@ -6503,24 +6504,20 @@ ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:11:1: F401 'dataclasses' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:13:1: F401 'shutil' imported but unused ./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:14:1: F401 'sys' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.run_prefit_calibration_cli._identify_field_mapping' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:299:1: E303 too many blank lines (4) -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:303:1: E302 expected 2 blank lines, found 4 -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:671:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:672:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:673:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:674:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:675:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:676:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:677:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:678:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:679:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1084:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1103:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1118:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1386:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1412:63: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1534:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:21:1: F401 'jax' imported but unused +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.prefit_calibration_cli._identify_field_mapping' imported but unused +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:649:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:650:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:651:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:652:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:653:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:654:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:655:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:656:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:657:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1009:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1028:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1043:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:84:31: E741 ambiguous variable name 'l' ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:598:28: E127 continuation line over-indented for visual indent @@ -6529,12 +6526,12 @@ ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:18:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:19:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:21:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:23:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:961:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:964:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1229:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1411:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1741:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:31:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:969:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:972:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1237:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1419:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:1751:21: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:79:9: F841 local variable 'data' is assigned to but never used ./tests/tfscreen/tfmodel/scripts/test_summarize_grid_cli.py:183:91: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./tests/tfscreen/tfmodel/test_batch.py:3:1: F401 'numpy as np' imported but unused @@ -7023,72 +7020,72 @@ ./tests/tfscreen/tfmodel/test_model_orchestrator.py:120:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:129:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_model_orchestrator.py:146:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:274:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:297:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:303:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:305:76: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:328:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:336:128: E501 line too long (184 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:337:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:347:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:48: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:50: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:52: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:54: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:56: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:352:58: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:353:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:361:112: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:361:128: E501 line too long (191 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:366:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:369:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:380:128: E501 line too long (184 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:399:128: E501 line too long (198 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:404:128: E501 line too long (167 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:452:79: E712 comparison to False should be 'if cond is False:' or 'if not cond:' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:453:64: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:456:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:464:51: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:77: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:98: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:125: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:470:128: E501 line too long (131 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:471:128: E501 line too long (138 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:473:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:475:86: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:487:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:492:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:500:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:508:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:519:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:522:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:529:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:536:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:544:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:549:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:554:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:560:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:569:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:573:26: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:583:30: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:584:25: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:586:34: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:587:33: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:599:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:43: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:45: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:47: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:49: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:51: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:53: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:55: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:605:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:614:26: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_model_orchestrator.py:615:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:618:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:621:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:798:67: W292 no newline at end of file +./tests/tfscreen/tfmodel/test_model_orchestrator.py:276:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:299:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:305:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:307:76: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:330:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:338:128: E501 line too long (184 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:339:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:349:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:48: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:50: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:52: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:54: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:56: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:58: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:355:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:363:112: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:363:128: E501 line too long (191 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:368:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:371:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:375:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:382:128: E501 line too long (184 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:401:128: E501 line too long (198 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:406:128: E501 line too long (167 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:454:79: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:455:64: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:458:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:466:51: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:77: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:98: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:125: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:128: E501 line too long (131 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:473:128: E501 line too long (138 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:475:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:477:86: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:489:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:494:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:502:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:510:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:521:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:524:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:531:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:538:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:546:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:551:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:556:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:562:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:571:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:575:26: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:585:30: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:586:25: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:588:34: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:589:33: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:43: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:45: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:47: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:49: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:51: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:53: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:55: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:607:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:616:26: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_model_orchestrator.py:617:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:620:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:623:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:800:67: W292 no newline at end of file ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:16: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:31: E272 multiple spaces before keyword ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:31:16: E221 multiple spaces before operator @@ -7147,12 +7144,12 @@ ./tests/tfscreen/tfmodel/test_presplit.py:25:1: F401 'tfscreen.tfmodel.tensors.populate_dataclass.populate_dataclass' imported but unused ./tests/tfscreen/tfmodel/test_presplit.py:93:52: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/test_presplit.py:96:33: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_presplit.py:123:35: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_presplit.py:136:48: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_presplit.py:170:47: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_presplit.py:196:28: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/test_presplit.py:214:47: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/test_presplit.py:226:28: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:126:35: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:139:48: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:173:47: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:199:28: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:217:47: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/test_presplit.py:229:28: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/test_reference_math.py:29:11: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_reference_math.py:32:9: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_reference_math.py:75:14: E221 multiple spaces before operator @@ -7655,7 +7652,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -48 C901 'cat_fit' is too complex (19) +47 C901 'cat_fit' is too complex (19) 15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7663,14 +7660,14 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -166 E127 continuation line over-indented for visual indent +164 E127 continuation line over-indented for visual indent 79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1768 E221 multiple spaces before operator +1766 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1115 E231 missing whitespace after ',' @@ -7682,9 +7679,9 @@ 2 E266 too many leading '#' for block comment 148 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -801 E302 expected 2 blank lines, found 1 +799 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) -17 E305 expected 2 blank lines after class or function definition, found 1 +16 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line 37 E402 module level import not at top of file @@ -7695,7 +7692,7 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 20 E741 ambiguous variable name 'l' -223 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +228 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used @@ -7703,4 +7700,4 @@ 86 W292 no newline at end of file 1674 W293 blank line contains whitespace 29 W391 blank line at end of file -7657 +7654 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 69b128ff..c0fe10a4 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/__init__.py b/src/tfscreen/analysis/__init__.py index f63cb8a2..8175731d 100644 --- a/src/tfscreen/analysis/__init__.py +++ b/src/tfscreen/analysis/__init__.py @@ -7,3 +7,7 @@ extract_epistasis ) +from .stats_test_suite import ( # noqa: F401 + stats_test_suite +) + diff --git a/src/tfscreen/analysis/cat_response/fit_response_cli.py b/src/tfscreen/analysis/cat_response/fit_response_cli.py deleted file mode 100644 index edfa2f30..00000000 --- a/src/tfscreen/analysis/cat_response/fit_response_cli.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -CLI for fitting cat_response models to theta-vs-titrant data per genotype. -""" - -import argparse -from concurrent.futures import ProcessPoolExecutor, as_completed - -import pandas as pd - -from tfscreen.analysis.cat_response.cat_fit import cat_fit -from tfscreen.mle.curve_models import MODEL_LIBRARY - -DEFAULT_MODELS = ["flat", "repressor", "inducer", "hill_repressor", "hill_inducer"] - - -def _fit_one(args): - """Worker: run cat_fit for one (genotype, titrant_name) pair.""" - genotype, titrant_name, x, y, y_std, models_to_run = args - flat_out, _ = cat_fit(x, y, y_std, models_to_run=models_to_run) - flat_out["genotype"] = genotype - flat_out["titrant_name"] = titrant_name - return flat_out - - -def fit_response(df, - theta_col=None, - sigma_col=None, - models_to_run=None, - workers=1): - """ - Run cat_fit for every (genotype, titrant_name) group in df. - - Parameters - ---------- - df : pd.DataFrame - Must contain: genotype, titrant_name, titrant_conc, theta_col, and - either sigma_col or both upper_std and lower_std. - theta_col : str or None - Column holding theta values passed to the fitter as y. If ``None``, - auto-detected: ``median`` if present, then ``point_est``. - sigma_col : str or None - Column holding per-observation sigma. If None and df has - upper_std / lower_std, sigma = (upper_std - lower_std) / 2. - models_to_run : list of str or None - Subset of MODEL_LIBRARY keys to fit. Defaults to DEFAULT_MODELS. - workers : int - Parallel worker processes. - - Returns - ------- - pd.DataFrame - One row per (genotype, titrant_name). Columns include best_model, - status, AIC weights, and all parameter estimates/stds for every model, - with '|' replaced by '_' in column names. - """ - if models_to_run is None: - models_to_run = DEFAULT_MODELS - - bad = [m for m in models_to_run if m not in MODEL_LIBRARY] - if bad: - raise ValueError(f"Unknown model(s): {bad}. Valid: {list(MODEL_LIBRARY)}") - - if theta_col is None: - if "q0.5" in df.columns: - theta_col = "q0.5" - elif "point_est" in df.columns: - theta_col = "point_est" - else: - raise ValueError( - "No theta column found. Expected 'q0.5' (posterior median) or " - "'point_est' (MAP). Pass theta_col explicitly to override." - ) - - if sigma_col is None: - if "q0.841" in df.columns and "q0.159" in df.columns: - df = df.copy() - df["_sigma"] = (df["q0.841"] - df["q0.159"]) / 2 - sigma_col = "_sigma" - else: - raise ValueError( - "No sigma_col specified and df lacks q0.841/q0.159 columns. " - "Provide --sigma_col or ensure q0.841 and q0.159 are present." - ) - - work_items = [] - for (genotype, titrant_name), group in df.groupby(["genotype", "titrant_name"], - sort=False): - x = group["titrant_conc"].to_numpy(dtype=float) - y = group[theta_col].to_numpy(dtype=float) - y_std = group[sigma_col].to_numpy(dtype=float) - work_items.append((genotype, titrant_name, x, y, y_std, models_to_run)) - - n_total = len(work_items) - results = [None] * n_total - idx_map = {id(item): i for i, item in enumerate(work_items)} - - print(f" Fitting {n_total} (genotype, titrant_name) pairs " - f"with {workers} worker(s)...", flush=True) - - with ProcessPoolExecutor(max_workers=workers) as executor: - futures = {executor.submit(_fit_one, item): idx_map[id(item)] - for item in work_items} - n_done = 0 - for future in as_completed(futures): - results[futures[future]] = future.result() - n_done += 1 - if n_done % 5000 == 0 or n_done == n_total: - print(f" {n_done}/{n_total} fits complete", flush=True) - - out_df = pd.DataFrame(results) - id_cols = ["genotype", "titrant_name"] - other_cols = [c for c in out_df.columns if c not in id_cols] - out_df = out_df[id_cols + other_cols].copy() - out_df.columns = [c.replace("|", "_") for c in out_df.columns] - return out_df - - -def main(): - """CLI entry point for fitting cat_response models to theta data.""" - - parser = argparse.ArgumentParser( - prog="tfs-fit-response", - description=fit_response.__doc__, - formatter_class=argparse.RawTextHelpFormatter, - ) - parser.add_argument( - "input", - type=str, - help="Input CSV with columns: genotype, titrant_name, titrant_conc, " - "and the theta / sigma columns.", - ) - parser.add_argument( - "--theta_col", - type=str, - default=None, - help="Column name for theta values. Auto-detected if omitted " - "('median' if present, else 'point_est').", - ) - parser.add_argument( - "--sigma_col", - type=str, - default=None, - help="Column name for per-observation sigma. If omitted, computed " - "as (upper_std - lower_std) / 2.", - ) - parser.add_argument( - "--models", - type=str, - nargs="+", - default=None, - metavar="MODEL", - help="Models to fit (default: flat repressor inducer hill_repressor " - "hill_inducer). Valid choices: " + " ".join(MODEL_LIBRARY), - ) - parser.add_argument( - "--workers", - type=int, - default=1, - help="Number of parallel worker processes (default: 1).", - ) - parser.add_argument( - "--out", - type=str, - default="fit_response.csv", - help="Output CSV path (default: fit_response.csv).", - ) - - args = parser.parse_args() - - print(f"Reading {args.input}...", flush=True) - df = pd.read_csv(args.input) - - result_df = fit_response( - df, - theta_col=args.theta_col, - sigma_col=args.sigma_col, - models_to_run=args.models, - workers=args.workers, - ) - - result_df.to_csv(args.out, index=False) - print(f"Wrote {len(result_df)} rows to {args.out}", flush=True) - - -if __name__ == "__main__": - main() diff --git a/src/tfscreen/analysis/cat_response/scripts/__init__.py b/src/tfscreen/analysis/cat_response/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tfscreen/analysis/cat_response/cat_response_cli.py b/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py similarity index 100% rename from src/tfscreen/analysis/cat_response/cat_response_cli.py rename to src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py diff --git a/src/tfscreen/mle/stats_test_suite.py b/src/tfscreen/analysis/stats_test_suite.py similarity index 100% rename from src/tfscreen/mle/stats_test_suite.py rename to src/tfscreen/analysis/stats_test_suite.py diff --git a/src/tfscreen/mle/__init__.py b/src/tfscreen/mle/__init__.py index 47e0cc43..ede64f9f 100644 --- a/src/tfscreen/mle/__init__.py +++ b/src/tfscreen/mle/__init__.py @@ -9,9 +9,6 @@ predict_with_error ) -from .stats_test_suite import ( # noqa: F401 - stats_test_suite -) from .fit_manager import ( # noqa: F401 FitManager diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index 82f07e4d..07a83de3 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -9,6 +9,7 @@ from tfscreen.genetics import library_manager import jax +import numpy as np from typing import Any, Dict, Union from pathlib import Path @@ -73,9 +74,12 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], thermo_data=cf.get('thermo_data'), ) - # RNG key for prior-predictive theta sampling - theta_rng_seed = cf.get('theta_rng_seed', 0) - theta_rng_key = jax.random.PRNGKey(theta_rng_seed) + # Derive both RNG objects from the single seed. They are independent + # objects with independent state; using the same integer for both introduces + # no correlation between the two streams. + seed = cf.get('seed', None) + theta_rng_key = jax.random.PRNGKey(seed if seed is not None else 0) + rng = np.random.default_rng(seed) dk_geno_zero = cf.get('dk_geno_zero', False) if dk_geno_zero: @@ -103,6 +107,7 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], dk_geno_zero=dk_geno_zero, activity_wt=cf.get('activity_wt', 1.0), activity_mut_scale=cf.get('activity_mut_scale', 0.0), + rng=rng, activity_component=cf.get('activity_component', 'fixed'), activity_priors_overrides=cf.get('activity_priors'), theta_rescale=cf.get('theta_rescale', 'passthrough'), diff --git a/src/tfscreen/simulate/scripts/run_simulation_cli.py b/src/tfscreen/simulate/scripts/simulate_cli.py similarity index 94% rename from src/tfscreen/simulate/scripts/run_simulation_cli.py rename to src/tfscreen/simulate/scripts/simulate_cli.py index 01f817f1..7a83b220 100644 --- a/src/tfscreen/simulate/scripts/run_simulation_cli.py +++ b/src/tfscreen/simulate/scripts/simulate_cli.py @@ -1,4 +1,5 @@ import os +import yaml import numpy as np import pandas as pd import jax @@ -57,8 +58,10 @@ def _generate_binding_data(cf, library_df, binding_cfg, rng): thermo_data=cf.get("thermo_data"), ) - # Sample theta from the prior using the same seed as the main run - theta_rng_key = jax.random.PRNGKey(cf.get("theta_rng_seed", 0)) + # Sample theta using the same seed as the main library_prediction run so + # binding theta values are consistent with the ground-truth phenotypes. + seed = cf.get("seed", None) + theta_rng_key = jax.random.PRNGKey(seed if seed is not None else 0) theta_gc, _ = sample_theta_prior( component_name=cf["theta_component"], sim_data=sim_data, @@ -255,33 +258,37 @@ def run_simulation_from_config( num_replicates : int Number of independent experimental replicates to simulate. Default 2. seed : int, optional - Random seed. Overrides random_seed in the config file when provided. + Random seed. Overrides seed in the config file when provided. """ cf = tfscreen.util.read_yaml(config_file) if seed is not None: - cf["random_seed"] = seed + cf["seed"] = seed os.makedirs(output_dir, exist_ok=True) def out_path(name): return os.path.join(output_dir, f"{output_prefix}{name}.csv") + config_out = os.path.join(output_dir, f"{output_prefix}input-config.yaml") + output_names = ["library", "parameters", "genotype_theta", "growth"] if "binding_data" in cf: output_names.append("binding") if "presplit_data" in cf: output_names.append("presplit") - existing = [n for n in output_names if os.path.exists(out_path(n))] + existing = [out_path(n) for n in output_names if os.path.exists(out_path(n))] + if os.path.exists(config_out): + existing.append(config_out) if existing: - paths = ", ".join(out_path(n) for n in existing) + paths = ", ".join(existing) raise FileExistsError( f"Output files already exist: {paths}\n" f"Delete them or choose a different output_dir / output_prefix " f"before re-running." ) - base_seed = cf.get("random_seed", None) + base_seed = cf.get("seed", None) rng = np.random.default_rng(base_seed) # ------------------------------------------------------------------------- @@ -302,7 +309,7 @@ def out_path(name): # Give each replicate a distinct (but reproducible) random seed so # that replicates differ even when a base seed is set. rep_cf = dict(cf) - rep_cf["random_seed"] = ( + rep_cf["seed"] = ( base_seed * num_replicates + rep if base_seed is not None else None ) @@ -352,6 +359,10 @@ def out_path(name): presplit_df.to_csv(out_path("presplit"), index=False) print(f"Wrote: {out_path('presplit')}") + with open(config_out, "w") as fh: + yaml.dump(cf, fh, default_flow_style=False, sort_keys=False) + print(f"Wrote: {config_out}") + def main(): return generalized_main(run_simulation_from_config, diff --git a/src/tfscreen/simulate/selection_experiment.py b/src/tfscreen/simulate/selection_experiment.py index 2fda6fcc..29eb51a3 100644 --- a/src/tfscreen/simulate/selection_experiment.py +++ b/src/tfscreen/simulate/selection_experiment.py @@ -51,7 +51,7 @@ "reading_frame", "first_amplicon_residue", "wt_seq", "degen_sites", "sub_libraries", "library_combos", "spiked_seqs", "expected_5p", "expected_3p", # Phenotype / theta calculation - "theta_component", "theta_rng_seed", "thermo_data", "theta_priors", "theta_rescale", + "theta_component", "thermo_data", "theta_priors", "theta_rescale", "theta_sim_priors", # Conditions and growth "condition_blocks", "growth", @@ -62,7 +62,7 @@ "transformation_poisson_lambda", "multi_plasmid_combine_fcn", "cfu0", "tube_noise_sigma", "growth_transition", # Data collection - "total_num_reads", "prob_index_hop", "random_seed", + "total_num_reads", "prob_index_hop", "seed", # Column selectors (rarely overridden) "condition_selector", "library_selector", # Optional output blocks @@ -195,7 +195,7 @@ def _check_cf( cf = _check_dict_number("lib_assembly_skew_sigma", cf, min_allowed=0, allow_none=True) cf = _check_dict_number("transformation_poisson_lambda", cf, min_allowed=0, allow_none=True) cf = _check_dict_number("tube_noise_sigma", cf, min_allowed=0, allow_none=True) - cf = _check_dict_number("random_seed", cf, cast_type=int, min_allowed=0, allow_none=True) + cf = _check_dict_number("seed", cf, cast_type=int, min_allowed=0, allow_none=True) cf = _check_dict_number("cfu0", cf, allow_none=False,min_allowed=0) cf = _check_dict_number("total_num_reads", cf, cast_type=int, min_allowed=0, inclusive_min=False) @@ -1218,7 +1218,7 @@ def selection_experiment( print("Setting up calculation.", flush=True) # Initialize random number generator - rng = np.random.default_rng(cf["random_seed"]) + rng = np.random.default_rng(cf["seed"]) # Expand the library_df so every genotype is seen in every library_origin # to keep indexing consistent when we mix libraries. The .fillna(0) in this diff --git a/src/tfscreen/tfmodel/model_orchestrator.py b/src/tfscreen/tfmodel/model_orchestrator.py index 731284e0..6b3101ec 100644 --- a/src/tfscreen/tfmodel/model_orchestrator.py +++ b/src/tfscreen/tfmodel/model_orchestrator.py @@ -25,7 +25,6 @@ from functools import partial import inspect -import warnings # Declare float datatype FLOAT_DTYPE = jnp.float64 if jax.config.read("jax_enable_x64") else jnp.float32 @@ -247,12 +246,18 @@ def _read_binding_df(binding_df, growth_seen = growth_df[cols].drop_duplicates().set_index(cols) extra = binding_seen.index[~binding_seen.index.isin(growth_seen.index)] if len(extra) > 0: - warnings.warn( - f"binding_df contains {len(extra)} genotype/titrant_name pair(s) " - f"not present in growth_df; those rows will be dropped. " - f"Extra pairs: {list(extra[:5])}{'...' if len(extra) > 5 else ''}", - UserWarning, - stacklevel=2, + by_titrant = {} + for geno, tname in extra: + by_titrant.setdefault(tname, []).append(geno) + detail = "\n".join( + f" {tname}: {', '.join(sorted(genos))}" + for tname, genos in sorted(by_titrant.items()) + ) + print( + f"Syncing binding_df to growth_df: {len(extra)} " + f"genotype/titrant_name pair(s) not in growth_df will be dropped.\n" + + detail, + flush=True, ) # Inherit map_theta_group indices from growth_df so parameter indexing # stays consistent across the two datasets. Rows in binding_df with no @@ -354,15 +359,15 @@ def _read_presplit_df(presplit_df, growth_df): ) growth_genotypes = set(growth_df["genotype"]) - n_before = len(presplit_df) - presplit_df = presplit_df[ - presplit_df["genotype"].isin(growth_genotypes) - ].copy() - n_dropped = n_before - len(presplit_df) + mask = presplit_df["genotype"].isin(growth_genotypes) + dropped_genos = sorted(set(presplit_df.loc[~mask, "genotype"].astype(str))) + n_dropped = int((~mask).sum()) + presplit_df = presplit_df[mask].copy() if n_dropped > 0: print( - f"presplit_df: dropped {n_dropped} rows whose genotype was not " - f"found in growth_df.", + f"Syncing presplit_df to growth_df: {n_dropped} row(s) for " + f"{len(dropped_genos)} genotype(s) not found in growth_df will be dropped.\n" + f" Dropped genotypes: {', '.join(dropped_genos)}", flush=True, ) diff --git a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py similarity index 66% rename from src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py rename to src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py index 21bfc00f..90606237 100644 --- a/src/tfscreen/tfmodel/scripts/run_prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py @@ -24,23 +24,17 @@ After MAP convergence, the script: -1. Estimates a per-site 1-sigma uncertainty from the Hessian of the - negative log-joint at the MAP point (delta-method propagated through - any constrained-support bijections). -2. Identifies which fields of the production ``ModelPriors`` provide +1. Identifies which fields of the production ``ModelPriors`` provide ``dist.loc`` / ``dist.scale`` for each calibrated sample site, using a sentinel-trace introspection so we don't have to hard-code the field-naming conventions of every component. -3. Updates the production ``{out_prefix}_guesses.csv`` *in place*, writing - a ``.bak`` backup before overwriting. For simple-prior +2. Updates the production priors and guesses CSVs *in place*, writing + ``.bak`` backups before overwriting. For simple-prior ``condition_growth`` and ``growth_transition`` components, per-condition MAP estimates are written directly as per-condition guess values (``{site}_locs`` rows in the guesses CSV), giving the production SVI a warm start from the calibration fit. Only rows belonging to ``condition_growth`` or ``growth_transition`` are touched. - - For legacy hierarchical components (if any), scalar hyper-site MAP - estimates also update the corresponding rows in the priors CSV. """ import dataclasses @@ -64,8 +58,6 @@ from tfscreen.tfmodel.configuration_io import ( read_configuration, ) -from tfscreen.plot.geno_trajectory import plot_geno_trajectory -from tfscreen.tfmodel.analysis.prediction import predict # --------------------------------------------------------------------------- @@ -446,31 +438,30 @@ def _csv_row_name(component, field_name): return f"growth.{component}.{field_name}" -def _build_csv_updates(field_mapping, hessian_results): +def _build_csv_updates(field_mapping, params): """ - Translate the sentinel-trace mapping plus per-site MAP / sigma - estimates into two flat dicts of in-place updates: + Translate the sentinel-trace field mapping and MAP params into two flat + dicts of in-place updates. Returns ------- prior_updates : dict[str, float] ``{csv_row_name → new_value}`` for the priors CSV. - Normal sites contribute two rows (loc field ← MAP, scale field ← - Hessian sigma); HalfNormal contributes one (scale field ← MAP, - recentering the prior on the MAP point). - guess_updates : dict[str, float] - ``{site_name → MAP value}`` for the guesses CSV. Only scalar - sites are included. + Scalar Normal/HalfNormal sites write the MAP value to their + ``loc_field`` / ``scale_field`` respectively. + guess_updates : dict[str, np.ndarray | float] + ``{site_name → MAP value}`` for the guesses CSV. + Array sites write per-condition MAP arrays (keyed as + ``{site_name}_locs``); scalar sites write a single float. """ prior_updates = {} guess_updates = {} for site_name, info in field_mapping.items(): - if site_name not in hessian_results: + auto_loc_key = f"{site_name}_auto_loc" + if auto_loc_key not in params: continue - result = hessian_results[site_name] - map_val_arr = np.asarray(result["map"]) - sigma_arr = np.asarray(result["sigma"]) + map_val_arr = np.asarray(params[auto_loc_key]) component = info["component"] dist_class = info["dist_class"] @@ -479,9 +470,8 @@ def _build_csv_updates(field_mapping, hessian_results): is_array = info.get("is_array", False) if is_array: - # Simple-prior per-condition array site. - # Write per-condition MAP estimates to guesses so the production - # SVI starts from the calibration fit. Priors are left unchanged. + # Simple-prior per-condition array site: write MAP array to guesses + # so the production SVI starts from the calibration fit. if loc_field is not None: guess_updates[f"{site_name}_locs"] = map_val_arr else: @@ -489,22 +479,16 @@ def _build_csv_updates(field_mapping, hessian_results): if map_val_arr.shape != (): continue map_val = float(map_val_arr) - sigma_val = float(sigma_arr) if sigma_arr.shape == () else None if dist_class == "Normal": if loc_field is not None: prior_updates[_csv_row_name(component, loc_field)] = map_val - if scale_field is not None and sigma_val is not None: - prior_updates[_csv_row_name(component, scale_field)] = sigma_val elif dist_class == "HalfNormal": if scale_field is not None: - # Recenter the HalfNormal on the MAP point. prior_updates[_csv_row_name(component, scale_field)] = map_val else: if loc_field is not None: prior_updates[_csv_row_name(component, loc_field)] = map_val - if scale_field is not None and sigma_val is not None: - prior_updates[_csv_row_name(component, scale_field)] = sigma_val guess_updates[site_name] = map_val @@ -699,365 +683,6 @@ def _run_calibration_map(ri, return svi_state, params, converged -# --------------------------------------------------------------------------- -# Diagnostic plots -# --------------------------------------------------------------------------- - -_DIAG_PLOT_COLS = [ - "replicate", "condition_pre", "condition_sel", - "titrant_name", "titrant_conc", "genotype", - "t_sel", "ln_cfu", "ln_cfu_std", "q0.05", "q0.5", "q0.95", -] -_DIAG_CONDITION_COLS = [ - "condition_pre", "condition_sel", "titrant_name", "titrant_conc" -] - - -def _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, growth_pred_std=None): - """ - Generate per-genotype trajectory PDFs and write correlation / stats outputs. - - Calls :func:`~tfscreen.tfmodel.analysis.prediction.predict` at the - observed timepoints to obtain MAP point estimates, then iterates over - genotypes to save one trajectory PDF per genotype via - :func:`~tfscreen.plot.geno_trajectory.plot_geno_trajectory`. Also - writes the goodness-of-fit stats JSON and the observed-vs-predicted - correlation PDF. - - Parameters - ---------- - orchestrator_cal : ModelOrchestrator - Calibration ModelOrchestrator. - params : dict - MAP parameter dict (``{site}_auto_loc`` keys). - out_prefix : str - File-name prefix for all outputs. - growth_pred_std : np.ndarray or None - Optional Laplace-based per-cell prediction standard deviations, - shape ``(R, T, CP, CS, TN, TC, G)``. - """ - try: - import matplotlib.pyplot as plt - except ImportError: - return - - # ------------------------------------------------------------------ - # 1. MAP predictions at the observed timepoints. - # q_to_get uses "median" so the returned columns match - # plot_geno_trajectory's expected column names directly. - # ------------------------------------------------------------------ - all_dfs = predict( - orchestrator_cal, - params, - predict_sites=["growth_pred", "ln_cfu0"], - q_to_get=[0.05, 0.5, 0.95], - num_samples=None, - num_marginal_samples=1, - ) - pred_df = all_dfs["growth_pred"] - ln_cfu0_raw = all_dfs["ln_cfu0"] - - # ------------------------------------------------------------------ - # 2. Add ln_cfu_pred (alias for q0.5) and optional Laplace std for - # the stats / correlation helpers. - # ------------------------------------------------------------------ - pred_df = pred_df.copy() - pred_df["ln_cfu_pred"] = pred_df["q0.5"] - - if growth_pred_std is not None: - tm = orchestrator_cal.growth_tm - try: - indices = tuple( - pred_df[f"{dim}_idx"].values for dim in tm.tensor_dim_names - ) - pred_df["ln_cfu_pred_std"] = growth_pred_std[indices] - except (KeyError, IndexError): - pass - - # ------------------------------------------------------------------ - # 3. Calibration stats + observed-vs-predicted correlation. - # ------------------------------------------------------------------ - n_params = int(sum(np.asarray(v).size - for k, v in params.items() - if k.endswith("_auto_loc"))) - n_obs = int(pred_df["ln_cfu_pred"].notna().sum()) - _write_calibration_stats(pred_df, out_prefix, - n_params=n_params, n_obs=n_obs) - _make_correlation_plot(pred_df, out_prefix) - - # ------------------------------------------------------------------ - # 4. Build anchor rows (t_sel = -t_pre) from ln_cfu0 predictions. - # Mirror the logic in predict_and_plot_geno_trajectory. - # ------------------------------------------------------------------ - t_pre_df = ( - orchestrator_cal.growth_df[["replicate", "condition_pre", "t_pre"]] - .drop_duplicates(subset=["replicate", "condition_pre"]) - ) - ln_cfu0_vals = ( - ln_cfu0_raw[["replicate", "condition_pre", "genotype", - "q0.05", "q0.5", "q0.95"]] - .drop_duplicates(subset=["replicate", "condition_pre", "genotype"]) - ) - valid_combos = ( - orchestrator_cal.growth_df[_DIAG_CONDITION_COLS].drop_duplicates() - ) - anchor_df = ( - ln_cfu0_vals - .merge(valid_combos, on="condition_pre", how="inner") - .merge(t_pre_df, on=["replicate", "condition_pre"], how="left") - ) - anchor_df["t_sel"] = -anchor_df["t_pre"] - anchor_df["ln_cfu"] = np.nan - anchor_df["ln_cfu_std"] = np.nan - - presplit_df = getattr(orchestrator_cal, "presplit_df", None) - if presplit_df is not None: - ps = presplit_df[ - ["replicate", "condition_pre", "genotype", "ln_cfu", "ln_cfu_std"] - ].rename(columns={"ln_cfu": "_ps_ln", "ln_cfu_std": "_ps_ln_std"}) - anchor_df = anchor_df.merge( - ps, on=["replicate", "condition_pre", "genotype"], how="left" - ) - anchor_df["ln_cfu"] = anchor_df["_ps_ln"] - anchor_df["ln_cfu_std"] = anchor_df["_ps_ln_std"] - anchor_df = anchor_df.drop(columns=["_ps_ln", "_ps_ln_std"]) - - # ------------------------------------------------------------------ - # 5. One trajectory PDF per genotype. - # ------------------------------------------------------------------ - genotypes = sorted(pred_df["genotype"].unique().tolist(), key=str) - for geno in genotypes: - geno_pred = pred_df[pred_df["genotype"] == geno] - geno_anchor = anchor_df[anchor_df["genotype"] == geno] - geno_df = pd.concat( - [geno_pred[_DIAG_PLOT_COLS], geno_anchor[_DIAG_PLOT_COLS]], - ignore_index=True, - ) - safe_name = str(geno).replace("/", "_").replace(" ", "_") - geno_df.to_csv(f"{out_prefix}_{safe_name}_trajectory.csv") - try: - fig = plot_geno_trajectory(geno_df) - safe_name = str(geno).replace("/", "_").replace(" ", "_") - pdf_path = f"{out_prefix}_{safe_name}_trajectory.pdf" - fig.savefig(pdf_path, format="pdf", bbox_inches="tight") - plt.close(fig) - print(f" Saved {pdf_path}", flush=True) - except Exception as exc: - print( - f" Warning: trajectory plot failed for {geno!r}: {exc}", - flush=True, - ) - - -# --------------------------------------------------------------------------- -# Calibration stats JSON and correlation plot -# --------------------------------------------------------------------------- - -def _write_calibration_stats(df, out_prefix, n_params=None, n_obs=None): - """ - Compute goodness-of-fit statistics on the calibration predictions and - write ``{out_prefix}_calib_stats.json``. - - Requires ``ln_cfu``, ``ln_cfu_pred``, and ``ln_cfu_pred_std`` columns in - ``df``; returns silently if any are absent or if no valid rows remain - after dropping NaNs. - - Parameters - ---------- - df : pd.DataFrame - Merged predictions DataFrame. - out_prefix : str - File-name prefix for the JSON output. - n_params : int or None - Number of free model parameters (from ``_auto_loc`` keys in the MAP - params dict). Written to JSON as an integer or null. - n_obs : int or None - Number of valid observations used for the calibration fit (rows with - a non-NaN ``ln_cfu_pred``). Written to JSON as an integer or null. - """ - import json - from tfscreen.mle import stats_test_suite - - needed = ["ln_cfu", "ln_cfu_pred", "ln_cfu_pred_std"] - if not all(c in df.columns for c in needed): - return - - valid = df.dropna(subset=needed) - if len(valid) == 0: - return - - stats = stats_test_suite( - valid["ln_cfu_pred"].to_numpy(dtype=float), - valid["ln_cfu"].to_numpy(dtype=float), - valid["ln_cfu_pred_std"].to_numpy(dtype=float), - ) - - # json.dump cannot serialise numpy scalars or NaN; normalise to Python - # float / None so the output is always valid JSON. - serialisable = { - k: (None if (v is None or not np.isfinite(float(v))) else float(v)) - for k, v in stats.items() - } - serialisable["n_params"] = int(n_params) if n_params is not None else None - serialisable["n_obs"] = int(n_obs) if n_obs is not None else None - - json_path = f"{out_prefix}_calib_stats.json" - with open(json_path, "w") as fh: - json.dump(serialisable, fh, indent=2) - print(f" Saved {json_path}", flush=True) - - -def _make_correlation_plot(df, out_prefix): - """ - Write ``{out_prefix}_calib_correlation.pdf``: observed vs. predicted - ln_cfu, coloured by genotype. - - Series colours are taken from the active matplotlib prop-cycle, cycling - for any number of genotypes. Requires ``ln_cfu``, ``ln_cfu_pred``, and - ``genotype`` columns; returns silently if any are absent. - """ - try: - import matplotlib.pyplot as plt - except ImportError: - return - - from tfscreen.plot.helper import get_ax_limits - - needed = ["ln_cfu", "ln_cfu_pred", "genotype"] - if not all(c in df.columns for c in needed): - return - - valid = df.dropna(subset=["ln_cfu", "ln_cfu_pred"]) - if len(valid) == 0: - return - - genotypes = pd.unique(valid["genotype"]) - prop_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] - colors = {g: prop_colors[i % len(prop_colors)] for i, g in enumerate(genotypes)} - - fig, ax = plt.subplots(1, figsize=(6, 6)) - ax.set_aspect("equal") - - for g, g_df in valid.groupby("genotype", observed=True): - ax.scatter(g_df["ln_cfu"], g_df["ln_cfu_pred"], - s=50, edgecolor=colors[g], facecolor="none", label=g) - - lims = get_ax_limits(x_values=valid["ln_cfu"], - y_values=valid["ln_cfu_pred"], - pad_by=0.1) - ax.set_xlim(lims) - ax.set_ylim(lims) - ax.plot(lims, lims, "--", color="gray", zorder=-20) - ax.legend(fontsize=8) - - ticks = np.arange(np.ceil(lims[0]), np.floor(lims[1]) + 1, 2) - ax.set_xticks(ticks) - ax.set_yticks(ticks) - - ax.set_xlabel("ln_cfu obs") - ax.set_ylabel("ln_cfu pred") - - fig.tight_layout() - pdf_path = f"{out_prefix}_calib_correlation.pdf" - fig.savefig(pdf_path, format="pdf", bbox_inches="tight") - plt.close(fig) - print(f" Saved {pdf_path}", flush=True) - - -# --------------------------------------------------------------------------- -# Laplace-based prediction uncertainty -# --------------------------------------------------------------------------- - -def _compute_growth_pred_std(ri, params, orchestrator_cal, n_samples=100): - """ - Estimate per-cell growth_pred standard deviation via Laplace samples. - - Recomputes the Hessian of the negative log-joint at the MAP point, - projects it to the PD cone, draws ``n_samples`` samples from the - resulting Gaussian, runs the forward model on each, and returns the - elementwise standard deviation of ``growth_pred`` across samples. - - The Hessian computation duplicates work already done in - ``compute_hessian_sigmas``; this is intentional so that the full - covariance (not just the diagonal) is available for sampling. - - Returns ``None`` if ``growth_pred`` is absent from the model trace or - if the parameter dict is empty. - """ - from numpyro.infer.util import potential_energy - from numpyro.distributions.transforms import biject_to - from numpyro.infer import Predictive - import jax.flatten_util - - unconstrained = { - k[: -len("_auto_loc")]: jnp.array(v) - for k, v in params.items() - if k.endswith("_auto_loc") - } - if not unconstrained: - return None - - data_on_gpu = jax.device_put(orchestrator_cal.data) - all_indices = jnp.arange(orchestrator_cal.data.num_genotype) - full_data = orchestrator_cal.get_batch(data_on_gpu, all_indices) - model_kwargs = {"priors": orchestrator_cal.priors, "data": full_data} - - flat_map, unravel = jax.flatten_util.ravel_pytree(unconstrained) - D = int(flat_map.shape[0]) - - def pe_fn(flat_p): - return potential_energy( - orchestrator_cal.jax_model, [], model_kwargs, unravel(flat_p) - ) - - hessian = jax.hessian(pe_fn)(flat_map) - H_np = np.array(hessian, dtype=np.float64) - eigenvalues_np, eigenvectors_np = np.linalg.eigh(H_np) - eigenvalues_pd = np.maximum(eigenvalues_np, 1e-3) - cov_np = eigenvectors_np @ np.diag(1.0 / eigenvalues_pd) @ eigenvectors_np.T - L_np = np.linalg.cholesky(cov_np) - L = jnp.array(L_np, dtype=jnp.float32) - - # Per-site unconstrained → constrained bijections. - model_trace = trace(seed(orchestrator_cal.jax_model, rng_seed=0)).get_trace(**model_kwargs) - site_transforms = { - name: biject_to(site["fn"].support) - for name, site in model_trace.items() - if site["type"] == "sample" and not site.get("is_observed", False) - } - - # Draw n_samples at once: (n_samples, D). - sample_key = ri.get_key() - z = jax.random.normal(sample_key, shape=(n_samples, D)) - flat_samples = flat_map[None] + z @ L.T - - unc_samples = jax.vmap(unravel)(flat_samples) - constrained_samples = { - k: jax.vmap(site_transforms[k])(v) if k in site_transforms else v - for k, v in unc_samples.items() - } - - pred_fn = Predictive(orchestrator_cal.jax_model, posterior_samples=constrained_samples) - pred = pred_fn(ri.get_key(), data=full_data, priors=orchestrator_cal.priors) - - if "growth_pred" not in pred: - return None - - # pred["growth_pred"]: (n_samples, R, T, CP, CS, TN, TC, G) - # - # Some Laplace samples land far from the MAP (badly-constrained directions - # have large variance after Hessian eigenvalue clamping). The resulting - # forward-model predictions can be enormous in float32, and squaring them - # inside np.std overflows to inf. Two guards: - # 1. Cast to float64 — extends the safe range from ~3e38 to ~1e308. - # 2. Clip to ±_GROWTH_PRED_CLIP — any prediction outside this window is - # physically nonsensical (ln_cfu values live in ~[0, 30]) and would - # produce a misleadingly large std even in float64. - _GROWTH_PRED_CLIP = 1e4 - gp = np.asarray(pred["growth_pred"], dtype=np.float64) - np.clip(gp, -_GROWTH_PRED_CLIP, _GROWTH_PRED_CLIP, out=gp) - return gp.std(axis=0) - # --------------------------------------------------------------------------- # Public entry point @@ -1200,22 +825,12 @@ def run_prefit_calibration(config_file, epoch_checkpoint_interval=epoch_checkpoint_interval ) - print("Computing Hessian-based per-site uncertainties ...", flush=True) - hessian_results = ri.compute_hessian_sigmas(params) - # 5. Map sample sites → CSV fields and apply in-place updates. field_mapping = _identify_field_mapping(orchestrator_cal) - prior_updates, guess_updates = _build_csv_updates(field_mapping, - hessian_results) + prior_updates, guess_updates = _build_csv_updates(field_mapping, params) _apply_priors_updates(priors_path, prior_updates) _apply_guesses_updates(guesses_path, guess_updates) - # 6. Write per-genotype diagnostic plots with Laplace-based prediction uncertainty. - print("Computing growth_pred uncertainty via Laplace samples ...", flush=True) - growth_pred_std = _compute_growth_pred_std(ri, params, orchestrator_cal) - _run_calibration_diagnostics(orchestrator_cal, params, out_prefix, - growth_pred_std=growth_pred_std) - return svi_state, params, converged diff --git a/src/tfscreen/tfmodel/scripts/prior_predictive_cli.py b/src/tfscreen/tfmodel/scripts/sample_prior_cli.py similarity index 100% rename from src/tfscreen/tfmodel/scripts/prior_predictive_cli.py rename to src/tfscreen/tfmodel/scripts/sample_prior_cli.py diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index 94da2545..e3f8f944 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -10,7 +10,7 @@ import yaml from matplotlib import pyplot as plt -from tfscreen.mle.stats_test_suite import stats_test_suite +from tfscreen.analysis.stats_test_suite import stats_test_suite from tfscreen.plot import default_styles # noqa: F401 — applies global rcParams on import from tfscreen.plot.default_styles import DEFAULT_COLORS from tfscreen.plot.xy_corr import xy_corr diff --git a/tests/tfscreen/analysis/cat_response/scripts/__init__.py b/tests/tfscreen/analysis/cat_response/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response_cli.py b/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py similarity index 80% rename from tests/tfscreen/analysis/cat_response/test_cat_response_cli.py rename to tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py index 8bcc9e78..d47051a8 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py @@ -5,7 +5,7 @@ import pandas as pd from unittest.mock import patch -from tfscreen.analysis.cat_response.cat_response_cli import cat_response +from tfscreen.analysis.cat_response.scripts.cat_response_cli import cat_response # Run ProcessPoolExecutor synchronously so cat_fit mocks work in-process. @@ -22,7 +22,7 @@ def __exit__(self, *a): pass def submit(self, fn, *args): return _SyncFuture(fn(*args)) _SYNC_EXECUTOR = patch( - "tfscreen.analysis.cat_response.cat_response_cli.ProcessPoolExecutor", + "tfscreen.analysis.cat_response.scripts.cat_response_cli.ProcessPoolExecutor", return_value=_SyncExecutor(), ) @@ -53,9 +53,9 @@ def fake_fit(x, y, y_std, models_to_run): return (_FLAT_RESULT, None) df = _make_theta_df("q0.5") - with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", return_value=df), \ - patch("tfscreen.analysis.cat_response.cat_response_cli.cat_fit", + patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", side_effect=fake_fit), \ _SYNC_EXECUTOR: cat_response(f, out_prefix=str(tmp_path / "out")) @@ -72,9 +72,9 @@ def fake_fit(x, y, y_std, models_to_run): return (_FLAT_RESULT, None) df = _make_theta_df("point_est") - with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", return_value=df), \ - patch("tfscreen.analysis.cat_response.cat_response_cli.cat_fit", + patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", side_effect=fake_fit), \ _SYNC_EXECUTOR: cat_response(f, out_prefix=str(tmp_path / "out")) @@ -92,9 +92,9 @@ def fake_fit(x, y, y_std, models_to_run): df = _make_theta_df("q0.5").copy() df["my_col"] = [0.11, 0.22] - with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", return_value=df), \ - patch("tfscreen.analysis.cat_response.cat_response_cli.cat_fit", + patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", side_effect=fake_fit), \ _SYNC_EXECUTOR: cat_response(f, theta_col="my_col", out_prefix=str(tmp_path / "out")) @@ -112,9 +112,9 @@ def fake_fit(x, y, y_std, models_to_run): df = _make_theta_df("q0.5").copy() df["point_est"] = [0.9, 0.8] - with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", return_value=df), \ - patch("tfscreen.analysis.cat_response.cat_response_cli.cat_fit", + patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", side_effect=fake_fit), \ _SYNC_EXECUTOR: cat_response(f, out_prefix=str(tmp_path / "out")) @@ -131,7 +131,7 @@ def test_raises_when_no_theta_col(self, tmp_path): "q0.841": [0.5], "q0.159": [0.3], }) - with patch("tfscreen.analysis.cat_response.cat_response_cli.pd.read_csv", + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", return_value=df): with pytest.raises(ValueError, match="No theta column found"): cat_response(f, out_prefix=str(tmp_path / "out")) diff --git a/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py b/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py deleted file mode 100644 index 253c8ecd..00000000 --- a/tests/tfscreen/analysis/cat_response/test_fit_response_cli.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Tests for fit_response_cli.py — theta_col auto-detection and error handling. -""" -import pytest -import pandas as pd -from unittest.mock import patch - -from tfscreen.analysis.cat_response.fit_response_cli import fit_response - - -# Run ProcessPoolExecutor synchronously so cat_fit mocks work in-process. -from concurrent.futures import Future as _Future - -class _SyncFuture(_Future): - def __init__(self, result_val): - super().__init__() - self.set_result(result_val) - -class _SyncExecutor: - def __enter__(self): return self - def __exit__(self, *a): pass - def submit(self, fn, *args): return _SyncFuture(fn(*args)) - -_SYNC_EXECUTOR = patch( - "tfscreen.analysis.cat_response.fit_response_cli.ProcessPoolExecutor", - return_value=_SyncExecutor(), -) - -_FLAT_RESULT = {"genotype": "wt", "titrant_name": "IPTG", - "best_model": "flat", "status": "success"} - - -def _make_df(center_col): - return pd.DataFrame({ - "genotype": ["wt", "wt"], - "titrant_name": ["IPTG", "IPTG"], - "titrant_conc": [0.0, 1.0], - center_col: [0.3, 0.7], - "q0.841": [0.4, 0.8], - "q0.159": [0.2, 0.6], - }) - - -class TestFitResponseThetaColAutoDetect: - - def test_autodetects_q0_5(self): - """Values from q0.5 column are passed to the fitter.""" - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - - with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", - side_effect=fake_fit), _SYNC_EXECUTOR: - fit_response(_make_df("q0.5"), models_to_run=["flat"]) - - assert captured["y"] == pytest.approx([0.3, 0.7]) - - def test_autodetects_point_est(self): - """Values from point_est column are passed to the fitter when q0.5 is absent.""" - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - - with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", - side_effect=fake_fit), _SYNC_EXECUTOR: - fit_response(_make_df("point_est"), models_to_run=["flat"]) - - assert captured["y"] == pytest.approx([0.3, 0.7]) - - def test_explicit_theta_col_overrides(self): - """Explicit theta_col is used even when q0.5 is also present.""" - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - - df = _make_df("q0.5").copy() - df["my_col"] = [0.11, 0.22] - with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", - side_effect=fake_fit), _SYNC_EXECUTOR: - fit_response(df, theta_col="my_col", models_to_run=["flat"]) - - assert captured["y"] == pytest.approx([0.11, 0.22]) - - def test_q0_5_preferred_over_point_est(self): - """When both q0.5 and point_est are present, q0.5 wins.""" - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - - df = _make_df("q0.5").copy() - df["point_est"] = [0.9, 0.8] - with patch("tfscreen.analysis.cat_response.fit_response_cli.cat_fit", - side_effect=fake_fit), _SYNC_EXECUTOR: - fit_response(df, models_to_run=["flat"]) - - assert captured["y"] == pytest.approx([0.3, 0.7]) - - def test_raises_when_no_theta_col(self): - """ValueError when neither q0.5 nor point_est is present.""" - df = pd.DataFrame({ - "genotype": ["wt"], - "titrant_name": ["IPTG"], - "titrant_conc": [0.0], - "q0.841": [0.5], - "q0.159": [0.3], - }) - with pytest.raises(ValueError, match="No theta column found"): - fit_response(df, models_to_run=["flat"]) diff --git a/tests/tfscreen/mle/test_stats_test_suite.py b/tests/tfscreen/analysis/test_stats_test_suite.py similarity index 95% rename from tests/tfscreen/mle/test_stats_test_suite.py rename to tests/tfscreen/analysis/test_stats_test_suite.py index 7bfae6df..1f294ebd 100644 --- a/tests/tfscreen/mle/test_stats_test_suite.py +++ b/tests/tfscreen/analysis/test_stats_test_suite.py @@ -1,7 +1,7 @@ import pytest import numpy as np -from tfscreen.mle.stats_test_suite import stats_test_suite +from tfscreen.analysis.stats_test_suite import stats_test_suite def test_stats_test_suite_perfect_fit(): param_real = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) @@ -86,7 +86,7 @@ def test_stats_test_suite_no_std(): import warnings def test_het_breuschpagan_linalg_error(mocker): # Mock het_breuschpagan to raise LinAlgError - mocker.patch("tfscreen.mle.stats_test_suite.het_breuschpagan", side_effect=np.linalg.LinAlgError) + mocker.patch("tfscreen.analysis.stats_test_suite.het_breuschpagan", side_effect=np.linalg.LinAlgError) param_real = np.array([1.0, 2.0, 3.0]) param_est = np.array([1.1, 2.2, 3.3]) diff --git a/tests/tfscreen/plot/test_geno_trajectory.py b/tests/tfscreen/plot/test_geno_trajectory.py index eae62d0b..1cee409e 100644 --- a/tests/tfscreen/plot/test_geno_trajectory.py +++ b/tests/tfscreen/plot/test_geno_trajectory.py @@ -146,7 +146,8 @@ def test_no_crash_all_nan_ln_cfu(self): def test_no_crash_all_nan_median(self): df = _make_pred_df() df["q0.5"] = np.nan - fig = plot_geno_trajectory(df) + with pytest.warns(UserWarning, match="No artists with labels"): + fig = plot_geno_trajectory(df) assert isinstance(fig, plt.Figure) plt.close(fig) diff --git a/tests/tfscreen/simulate/test_generate_presplit_data.py b/tests/tfscreen/simulate/test_generate_presplit_data.py index 5b1b7b56..eec39165 100644 --- a/tests/tfscreen/simulate/test_generate_presplit_data.py +++ b/tests/tfscreen/simulate/test_generate_presplit_data.py @@ -1,5 +1,5 @@ """ -Tests for _generate_presplit_data in run_simulation_cli. +Tests for _generate_presplit_data in simulate_cli. Coverage: - Output columns and shape for a simple two-genotype, two-condition case @@ -13,7 +13,7 @@ import pandas as pd from unittest.mock import patch, MagicMock -from tfscreen.simulate.scripts.run_simulation_cli import _generate_presplit_data +from tfscreen.simulate.scripts.simulate_cli import _generate_presplit_data # --------------------------------------------------------------------------- @@ -168,7 +168,7 @@ def test_reproducibility(): def test_run_simulation_writes_presplit_csv(tmp_path): """presplit CSV is written when presplit_data block is in the config.""" - from tfscreen.simulate.scripts.run_simulation_cli import run_simulation_from_config + from tfscreen.simulate.scripts.simulate_cli import run_simulation_from_config lib_df = pd.DataFrame({"genotype": ["wt", "A1V"]}) pheno_df = pd.DataFrame({"genotype": ["wt", "A1V"]}) @@ -190,7 +190,7 @@ def test_run_simulation_writes_presplit_csv(tmp_path): growth_df = pd.DataFrame({"genotype": ["wt", "A1V"], "ln_cfu": [10.0, 9.9]}) cf = { - "random_seed": 1, + "seed": 1, "cfu0": 1e8, "total_num_reads": 10_000_000, "prob_index_hop": None, @@ -198,11 +198,11 @@ def test_run_simulation_writes_presplit_csv(tmp_path): } with patch("tfscreen.util.read_yaml", return_value=cf), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", return_value=(lib_df, pheno_df, theta_df, params_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", return_value=growth_df): run_simulation_from_config("fake_config.yaml", str(tmp_path)) @@ -215,7 +215,7 @@ def test_run_simulation_writes_presplit_csv(tmp_path): def test_run_simulation_no_presplit_without_config(tmp_path): """presplit CSV is NOT written when presplit_data is absent from config.""" - from tfscreen.simulate.scripts.run_simulation_cli import run_simulation_from_config + from tfscreen.simulate.scripts.simulate_cli import run_simulation_from_config lib_df = pd.DataFrame({"genotype": ["wt"]}) pheno_df = pd.DataFrame({"genotype": ["wt"]}) @@ -229,15 +229,15 @@ def test_run_simulation_no_presplit_without_config(tmp_path): "counts": 1000, "ln_cfu_0": 10.0}]) growth_df = pd.DataFrame({"genotype": ["wt"], "ln_cfu": [10.0]}) - cf = {"random_seed": 1, "cfu0": 1e8, "total_num_reads": 1_000_000, + cf = {"seed": 1, "cfu0": 1e8, "total_num_reads": 1_000_000, "prob_index_hop": None} with patch("tfscreen.util.read_yaml", return_value=cf), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", return_value=(lib_df, pheno_df, theta_df, params_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", return_value=growth_df): run_simulation_from_config("fake_config.yaml", str(tmp_path)) diff --git a/tests/tfscreen/simulate/test_library_prediction.py b/tests/tfscreen/simulate/test_library_prediction.py index 31b8cdfe..a1d68c65 100644 --- a/tests/tfscreen/simulate/test_library_prediction.py +++ b/tests/tfscreen/simulate/test_library_prediction.py @@ -10,7 +10,7 @@ def mock_config(): return { "condition_blocks": [{"some": "block"}], "theta_component": "mock_theta", - "theta_rng_seed": 7, + "seed": 7, "thermo_data": None, "theta_priors": None, "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, @@ -64,7 +64,7 @@ def test_library_prediction_success(mocker, mock_config): sample_df=mock_sample_df, thermo_data=None, ) - mock_jax_key.assert_called_once_with(7) + mock_jax_key.assert_called_once_with(7) # seed value mock_thermo.assert_called_once() _, kwargs = mock_thermo.call_args @@ -76,6 +76,7 @@ def test_library_prediction_success(mocker, mock_config): assert kwargs["dk_geno_hyper_shift"] == mock_config["dk_geno_hyper_shift"] assert kwargs["activity_component"] == "fixed" # default when key absent assert kwargs["activity_priors_overrides"] is None + assert kwargs["rng"] is not None # seeded RNG passed through assert lib_df.equals(mock_library_df) assert pheno_df.equals(mock_phenotype_df) @@ -122,7 +123,7 @@ def test_library_prediction_dk_geno_zero_makes_hyper_params_optional(mocker): config = { "condition_blocks": [{"some": "block"}], "theta_component": "mock_theta", - "theta_rng_seed": 0, + "seed": 0, "thermo_data": None, "theta_priors": None, "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, @@ -157,7 +158,7 @@ def test_library_prediction_missing_hyper_params_raises_without_dk_geno_zero(moc config = { "condition_blocks": [{"some": "block"}], "theta_component": "mock_theta", - "theta_rng_seed": 0, + "seed": 0, "thermo_data": None, "theta_priors": None, "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, @@ -176,3 +177,143 @@ def test_library_prediction_missing_hyper_params_raises_without_dk_geno_zero(moc with pytest.raises(KeyError): library_prediction(cf="config.yaml") + + +# --------------------------------------------------------------------------- +# Unified seed: seed drives both JAX key and numpy RNG +# --------------------------------------------------------------------------- + +def _make_mock_thermo_return(): + return ( + pd.DataFrame({"theta": [0.5]}), + pd.DataFrame({"genotype": ["wt"]}), + pd.DataFrame({"genotype": ["wt"], "dk_geno": [0.0], "activity": [1.0]}), + ) + + +def _patch_library_deps(mocker, config): + mocker.patch("tfscreen.util.read_yaml", return_value=config) + mocker.patch( + "tfscreen.simulate.library_prediction.library_manager.LibraryManager" + ).return_value.build_library_df.return_value = pd.DataFrame({"genotype": ["wt"]}) + mocker.patch( + "tfscreen.simulate.library_prediction.build_sample_dataframes", + return_value=pd.DataFrame({"titrant_conc": [0.0]}), + ) + mocker.patch("tfscreen.simulate.library_prediction.build_sim_data", return_value=MagicMock()) + + +def test_jax_key_derived_from_seed(mocker): + """JAX PRNGKey is called with the value of seed.""" + config = { + "condition_blocks": [{"some": "block"}], + "theta_component": "mock_theta", + "seed": 42, + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + "dk_geno_hyper_loc": -3.5, + "dk_geno_hyper_scale": 1.0, + "dk_geno_hyper_shift": 0.02, + } + _patch_library_deps(mocker, config) + mock_jax_key = mocker.patch( + "tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k" + ) + mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + return_value=_make_mock_thermo_return(), + ) + + library_prediction(cf="config.yaml") + + mock_jax_key.assert_called_once_with(42) + + +def test_jax_key_defaults_to_zero_when_no_seed(mocker): + """When seed is absent the JAX PRNGKey falls back to 0.""" + config = { + "condition_blocks": [{"some": "block"}], + "theta_component": "mock_theta", + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + "dk_geno_hyper_loc": -3.5, + "dk_geno_hyper_scale": 1.0, + "dk_geno_hyper_shift": 0.02, + } + _patch_library_deps(mocker, config) + mock_jax_key = mocker.patch( + "tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k" + ) + mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + return_value=_make_mock_thermo_return(), + ) + + library_prediction(cf="config.yaml") + + mock_jax_key.assert_called_once_with(0) + + +def test_numpy_rng_passed_to_thermo_to_growth(mocker): + """A seeded numpy RNG is always forwarded to thermo_to_growth.""" + import numpy as np + config = { + "condition_blocks": [{"some": "block"}], + "theta_component": "mock_theta", + "seed": 99, + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + "dk_geno_hyper_loc": -3.5, + "dk_geno_hyper_scale": 1.0, + "dk_geno_hyper_shift": 0.02, + } + _patch_library_deps(mocker, config) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + mock_thermo = mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + return_value=_make_mock_thermo_return(), + ) + + library_prediction(cf="config.yaml") + + _, kwargs = mock_thermo.call_args + assert isinstance(kwargs["rng"], np.random.Generator) + + +def test_numpy_rng_seeded_with_seed(mocker): + """The numpy RNG passed to thermo_to_growth is seeded from seed, + so two calls with the same seed produce the same first draw.""" + import numpy as np + captured_rngs = [] + + def capture_rng(*args, **kwargs): + captured_rngs.append(kwargs["rng"]) + return _make_mock_thermo_return() + + config = { + "condition_blocks": [{"some": "block"}], + "theta_component": "mock_theta", + "seed": 5, + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + "dk_geno_hyper_loc": -3.5, + "dk_geno_hyper_scale": 1.0, + "dk_geno_hyper_shift": 0.02, + } + + for _ in range(2): + _patch_library_deps(mocker, config) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + side_effect=capture_rng, + ) + library_prediction(cf="config.yaml") + + draw_a = captured_rngs[0].integers(0, 2**32) + draw_b = captured_rngs[1].integers(0, 2**32) + assert draw_a == draw_b, "Same seed must produce identical RNG state" diff --git a/tests/tfscreen/simulate/test_run_simulation_cli.py b/tests/tfscreen/simulate/test_run_simulation_cli.py index 17977a54..41bc962f 100644 --- a/tests/tfscreen/simulate/test_run_simulation_cli.py +++ b/tests/tfscreen/simulate/test_run_simulation_cli.py @@ -1,10 +1,11 @@ """Tests for tfs-simulate CLI (run_simulation_from_config).""" +import os import pytest from unittest.mock import patch, MagicMock import pandas as pd -from tfscreen.simulate.scripts.run_simulation_cli import run_simulation_from_config +from tfscreen.simulate.scripts.simulate_cli import run_simulation_from_config from tfscreen.util.cli.generalized_main import generalized_main @@ -69,22 +70,22 @@ def patched_simulation(tmp_path): """Patch all I/O so run_simulation_from_config can run without real data.""" lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df = _make_mock_dfs() - with patch("tfscreen.util.read_yaml", return_value={"random_seed": 99}) as mock_yaml, \ - patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + with patch("tfscreen.util.read_yaml", return_value={"seed": 99}) as mock_yaml, \ + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", return_value=(lib_df, pheno_df, theta_df, params_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", return_value=growth_df), \ patch.object(pd.DataFrame, "to_csv"): yield mock_yaml, tmp_path def test_seed_overrides_config(patched_simulation): - """When seed is given it replaces random_seed before seeding the RNG.""" + """When seed is given it replaces seed before seeding the RNG.""" mock_yaml, tmp_path = patched_simulation - with patch("tfscreen.simulate.scripts.run_simulation_cli.np.random.default_rng", + with patch("tfscreen.simulate.scripts.simulate_cli.np.random.default_rng", wraps=lambda s: MagicMock()) as mock_rng: run_simulation_from_config("config.yaml", str(tmp_path), seed=7) @@ -92,10 +93,10 @@ def test_seed_overrides_config(patched_simulation): def test_seed_none_preserves_config(patched_simulation): - """When seed=None the config's random_seed is left unchanged.""" + """When seed=None the config's seed is left unchanged.""" mock_yaml, tmp_path = patched_simulation - with patch("tfscreen.simulate.scripts.run_simulation_cli.np.random.default_rng", + with patch("tfscreen.simulate.scripts.simulate_cli.np.random.default_rng", wraps=lambda s: MagicMock()) as mock_rng: run_simulation_from_config("config.yaml", str(tmp_path), seed=None) @@ -115,12 +116,12 @@ def test_writes_parameters_not_phenotype(tmp_path): def capture_csv(self_df, path, **kwargs): written_paths.append(str(path)) - with patch("tfscreen.util.read_yaml", return_value={"random_seed": 0}), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + with patch("tfscreen.util.read_yaml", return_value={"seed": 0}), \ + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", return_value=(lib_df, pheno_df, theta_df, params_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", return_value=growth_df), \ patch.object(pd.DataFrame, "to_csv", capture_csv): run_simulation_from_config("config.yaml", str(tmp_path)) @@ -139,12 +140,12 @@ def test_output_file_names_include_expected_stems(tmp_path): def capture_csv(self_df, path, **kwargs): written_paths.append(str(path)) - with patch("tfscreen.util.read_yaml", return_value={"random_seed": 0}), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.library_prediction", + with patch("tfscreen.util.read_yaml", return_value={"seed": 0}), \ + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", return_value=(lib_df, pheno_df, theta_df, params_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.selection_experiment", + patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ - patch("tfscreen.simulate.scripts.run_simulation_cli.counts_to_lncfu", + patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", return_value=growth_df), \ patch.object(pd.DataFrame, "to_csv", capture_csv): run_simulation_from_config("config.yaml", str(tmp_path)) @@ -152,3 +153,113 @@ def capture_csv(self_df, path, **kwargs): written = "\n".join(written_paths) for stem in ("library", "parameters", "genotype_theta", "growth"): assert stem in written, f"Expected '{stem}' CSV to be written" + + +# --------------------------------------------------------------------------- +# _generate_binding_data: JAX key derived from seed +# --------------------------------------------------------------------------- + +def test_generate_binding_data_uses_seed_for_jax_key(tmp_path): + """_generate_binding_data must derive the JAX PRNGKey from seed, + not from a separate theta_rng_seed key.""" + import jax + import numpy as np + from unittest.mock import patch, MagicMock + from tfscreen.simulate.scripts.simulate_cli import _generate_binding_data + + library_df = pd.DataFrame({"genotype": ["wt"]}) + binding_cfg = { + "genotypes": ["wt"], + "titrant_name": "iptg", + "titrant_conc": [0.0, 1.0], + "noise": 0.0, + } + cf = {"seed": 55, "theta_component": "mock", "thermo_data": None} + rng = np.random.default_rng(0) + + with patch("tfscreen.simulate.scripts.simulate_cli.jax.random.PRNGKey", + return_value=MagicMock()) as mock_key, \ + patch("tfscreen.simulate.scripts.simulate_cli.build_sim_data", + return_value=MagicMock()), \ + patch("tfscreen.simulate.scripts.simulate_cli.sample_theta_prior", + return_value=(np.ones((1, 2)), MagicMock())), \ + patch("tfscreen.simulate.scripts.simulate_cli.standardize_genotypes", + return_value=["wt"]): + _generate_binding_data(cf, library_df, binding_cfg, rng) + + mock_key.assert_called_once_with(55) + + +# --------------------------------------------------------------------------- +# Rejecting the old theta_rng_seed key +# --------------------------------------------------------------------------- + +def test_theta_rng_seed_rejected_as_unknown_key(tmp_path): + """A config containing theta_rng_seed must be rejected with an error + (it is no longer a recognized key).""" + from tfscreen.simulate.selection_experiment import _check_cf + + cf = { + "theta_component": "hill_geno", + "theta_rng_seed": 0, # old key — must now be unknown + "condition_blocks": [], + "growth": {}, + "transform_sizes": {}, + "library_mixture": {}, + "lib_assembly_skew_sigma": 0.0, + "transformation_poisson_lambda": 1, + "multi_plasmid_combine_fcn": "gmean", + "cfu0": 1e7, + "tube_noise_sigma": 0.0, + "total_num_reads": 1000, + "prob_index_hop": 0.0, + "seed": 0, + } + with pytest.raises(Exception): # check_unknown_keys raises ValueError + _check_cf(cf) + + +# --------------------------------------------------------------------------- +# input-config.yaml output +# --------------------------------------------------------------------------- + +def test_writes_input_config_yaml(tmp_path): + """run_simulation_from_config must write an input-config.yaml using yaml.dump.""" + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df = _make_mock_dfs() + + import tfscreen.simulate.scripts.simulate_cli as cli_mod + + dumped = {} + + def capture_dump(data, fh, **kwargs): + dumped["data"] = data + + with patch("tfscreen.util.read_yaml", return_value={"seed": 5}), \ + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)), \ + patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", + return_value=(sample_df, counts_df)), \ + patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", + return_value=growth_df), \ + patch.object(pd.DataFrame, "to_csv"), \ + patch("tfscreen.simulate.scripts.simulate_cli.yaml.dump", capture_dump): + run_simulation_from_config("config.yaml", str(tmp_path), output_prefix="test_") + + assert "data" in dumped, "yaml.dump was not called" + assert dumped["data"]["seed"] == 5 + + +def test_input_config_yaml_existence_check(tmp_path): + """If input-config.yaml already exists, FileExistsError is raised before any work.""" + lib_df, pheno_df, theta_df, params_df, sample_df, counts_df, growth_df = _make_mock_dfs() + + existing_yaml = tmp_path / "tfs_sim_input-config.yaml" + existing_yaml.write_text("seed: 0\n") + + with patch("tfscreen.util.read_yaml", return_value={"seed": 0}), \ + patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", + return_value=(lib_df, pheno_df, theta_df, params_df)) as mock_lib: + with pytest.raises(FileExistsError, match="input-config.yaml"): + run_simulation_from_config("config.yaml", str(tmp_path)) + + mock_lib.assert_not_called() diff --git a/tests/tfscreen/simulate/test_selection_experiment.py b/tests/tfscreen/simulate/test_selection_experiment.py index 202d45a4..7915b92f 100644 --- a/tests/tfscreen/simulate/test_selection_experiment.py +++ b/tests/tfscreen/simulate/test_selection_experiment.py @@ -43,7 +43,7 @@ def base_config() -> dict: "lib_assembly_skew_sigma": 0.5, "transformation_poisson_lambda": 0.8, "tube_noise_sigma": 0.002, - "random_seed": 42, + "seed": 42, "cfu0": 1.0e7, "total_num_reads": 5_000_000, "transform_sizes": { @@ -198,7 +198,7 @@ def test_check_cf_loads_from_path(mocker, base_config: dict): # Assert that the loader was called correctly mock_loader.assert_called_once_with(dummy_path) # Assert that the loaded config was processed - assert validated_cf["random_seed"] == 42 + assert validated_cf["seed"] == 42 @pytest.mark.parametrize("key_to_pop, nested_key_to_pop, bad_value, match_error", [ # Test required numerical key missing diff --git a/tests/tfscreen/simulate/test_setup_sim_grid_cli.py b/tests/tfscreen/simulate/test_setup_sim_grid_cli.py index 4fe0fe5b..4c44b570 100644 --- a/tests/tfscreen/simulate/test_setup_sim_grid_cli.py +++ b/tests/tfscreen/simulate/test_setup_sim_grid_cli.py @@ -24,7 +24,7 @@ def base_config(tmp_path): "reading_frame": 0, "observable_calculator": "lac", "tube_noise_sigma": 0.01, - "random_seed": None, + "seed": None, } p = tmp_path / "simulate_config.yaml" p.write_text(yaml.dump(cfg)) @@ -151,8 +151,8 @@ def test_setup_sim_grid_cartesian_product(tmp_path, base_config): - name: seed variants: - - random_seed: 0 - - random_seed: 42 + - seed: 0 + - seed: 42 """ grid_path = tmp_path / "grid.yaml" grid_path.write_text(grid) diff --git a/tests/tfscreen/tfmodel/analysis/test_error_calibration.py b/tests/tfscreen/tfmodel/analysis/test_error_calibration.py index 0d1d039f..2da9b5bc 100644 --- a/tests/tfscreen/tfmodel/analysis/test_error_calibration.py +++ b/tests/tfscreen/tfmodel/analysis/test_error_calibration.py @@ -242,7 +242,8 @@ def test_values_in_unit_interval(self): def test_empty_pit_after_nan_removal(self): pit = np.full(10, np.nan) - curve = calibration_curve(pit) + with pytest.warns(RuntimeWarning): + curve = calibration_curve(pit) # np.mean of an empty slice returns NaN for v in curve.values(): assert np.isnan(v) diff --git a/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py b/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py index 5c88d720..c3ecf0be 100644 --- a/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py +++ b/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py @@ -1,3 +1,5 @@ +import warnings + import pytest import numpy as np import jax.numpy as jnp @@ -653,8 +655,11 @@ def median_abs_epi(tau_scale, n=200): effects.append(float(tp.log_hill_K[0, 3] - tp.log_hill_K[0, 0])) return float(np.median(np.abs(effects))) - small_epi = median_abs_epi(0.1) - large_epi = median_abs_epi(1.0) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="overflow encountered in exp", + category=RuntimeWarning) + small_epi = median_abs_epi(0.1) + large_epi = median_abs_epi(1.0) assert large_epi > small_epi, ( f"Expected larger tau to produce larger effects, but " f"small={small_epi:.4f}, large={large_epi:.4f}") diff --git a/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py b/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py index 3ec7e029..fd60c1fc 100644 --- a/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py @@ -1,4 +1,4 @@ -"""Tests for prior_predictive_cli.py — draws and writes synthetic prior datasets.""" +"""Tests for sample_prior_cli.py — draws and writes synthetic prior datasets.""" import os import pytest import numpy as np @@ -14,13 +14,13 @@ class TestWriteH5: def test_creates_file(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import _write_h5 + from tfscreen.tfmodel.scripts.sample_prior_cli import _write_h5 path = str(tmp_path / "out.h5") _write_h5(path, {"x": np.ones((5, 4))}, num_draws=5) assert os.path.isfile(path) def test_datasets_present(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import _write_h5 + from tfscreen.tfmodel.scripts.sample_prior_cli import _write_h5 path = str(tmp_path / "out.h5") data = {"alpha": np.zeros((3, 2)), "beta": np.ones(10)} _write_h5(path, data, num_draws=3) @@ -29,7 +29,7 @@ def test_datasets_present(self, tmp_path): assert "beta" in hf def test_data_values_correct(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import _write_h5 + from tfscreen.tfmodel.scripts.sample_prior_cli import _write_h5 path = str(tmp_path / "out.h5") arr = np.arange(12, dtype=float).reshape(3, 4) _write_h5(path, {"arr": arr}, num_draws=3) @@ -37,21 +37,21 @@ def test_data_values_correct(self, tmp_path): np.testing.assert_array_equal(hf["arr"][:], arr) def test_num_samples_attr(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import _write_h5 + from tfscreen.tfmodel.scripts.sample_prior_cli import _write_h5 path = str(tmp_path / "out.h5") _write_h5(path, {"x": np.ones(5)}, num_draws=7) with h5py.File(path, "r") as hf: assert hf.attrs["num_samples"] == 7 def test_gzip_compression(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import _write_h5 + from tfscreen.tfmodel.scripts.sample_prior_cli import _write_h5 path = str(tmp_path / "out.h5") _write_h5(path, {"x": np.ones((100, 50))}, num_draws=100) with h5py.File(path, "r") as hf: assert hf["x"].compression == "gzip" def test_1d_array_chunked(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import _write_h5 + from tfscreen.tfmodel.scripts.sample_prior_cli import _write_h5 path = str(tmp_path / "out.h5") arr = np.zeros(20) _write_h5(path, {"flat": arr}, num_draws=20) @@ -71,17 +71,17 @@ def _mock_draw_prior(self): return predictions, latent_params def test_writes_csv_and_h5_for_one_dataset(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior fake_gm = MagicMock() fake_growth_df = pd.DataFrame({"ln_cfu": [1.0, 2.0]}) latent_params = {"activity": np.array([[1.0]])} - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(fake_gm, {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", return_value=({}, latent_params)), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", return_value=fake_growth_df): out_prefix = str(tmp_path / "prior") @@ -91,17 +91,17 @@ def test_writes_csv_and_h5_for_one_dataset(self, tmp_path): assert os.path.isfile(f"{out_prefix}_000_ground_truth.h5") def test_writes_multiple_datasets(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior fake_gm = MagicMock() latent_params = {"activity": np.array([[1.0]])} fake_growth_df = pd.DataFrame({"ln_cfu": [1.0]}) - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(fake_gm, {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", return_value=({}, latent_params)), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", return_value=fake_growth_df): out_prefix = str(tmp_path / "prior") @@ -113,16 +113,16 @@ def test_writes_multiple_datasets(self, tmp_path): def test_zero_padded_index_width(self, tmp_path): """Indices must be zero-padded to at least 3 digits.""" - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior latent_params = {"activity": np.array([[1.0]])} fake_growth_df = pd.DataFrame({"ln_cfu": [1.0]}) - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(MagicMock(), {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", return_value=({}, latent_params)), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", return_value=fake_growth_df): out_prefix = str(tmp_path / "prior") @@ -132,7 +132,7 @@ def test_zero_padded_index_width(self, tmp_path): def test_each_dataset_uses_different_seed(self, tmp_path): """draw_prior must be called with seed+i for dataset i.""" - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior seeds_used = [] latent_params = {"activity": np.array([[1.0]])} @@ -142,11 +142,11 @@ def fake_draw_prior(orchestrator, rng_key, num_draws): seeds_used.append(rng_key) return {}, latent_params - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(MagicMock(), {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", side_effect=fake_draw_prior), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", return_value=fake_growth_df): out_prefix = str(tmp_path / "prior") @@ -156,7 +156,7 @@ def fake_draw_prior(orchestrator, rng_key, num_draws): def test_no_noise_passes_none_rng(self, tmp_path): """noise=False must pass noise_rng=None to growth_df_from_prior.""" - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior captured = {} latent_params = {"activity": np.array([[1.0]])} @@ -165,11 +165,11 @@ def fake_growth_df(orchestrator, params, draw_idx, noise_rng): captured["noise_rng"] = noise_rng return pd.DataFrame({"ln_cfu": [1.0]}) - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(MagicMock(), {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", return_value=({}, latent_params)), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", side_effect=fake_growth_df): out_prefix = str(tmp_path / "prior") @@ -180,7 +180,7 @@ def fake_growth_df(orchestrator, params, draw_idx, noise_rng): def test_with_noise_passes_rng(self, tmp_path): """noise=True must pass a non-None rng to growth_df_from_prior.""" - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior captured = {} latent_params = {"activity": np.array([[1.0]])} @@ -189,11 +189,11 @@ def fake_growth_df(orchestrator, params, draw_idx, noise_rng): captured["noise_rng"] = noise_rng return pd.DataFrame({"ln_cfu": [1.0]}) - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(MagicMock(), {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", return_value=({}, latent_params)), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", side_effect=fake_growth_df): out_prefix = str(tmp_path / "prior") @@ -203,16 +203,16 @@ def fake_growth_df(orchestrator, params, draw_idx, noise_rng): assert captured["noise_rng"] is not None def test_csv_content_written(self, tmp_path): - from tfscreen.tfmodel.scripts.prior_predictive_cli import sample_prior + from tfscreen.tfmodel.scripts.sample_prior_cli import sample_prior latent_params = {"activity": np.array([[1.0]])} fake_df = pd.DataFrame({"genotype": ["wt", "A1G"], "ln_cfu": [5.0, 4.5]}) - with patch("tfscreen.tfmodel.scripts.prior_predictive_cli.read_configuration", + with patch("tfscreen.tfmodel.scripts.sample_prior_cli.read_configuration", return_value=(MagicMock(), {})), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.draw_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.draw_prior", return_value=({}, latent_params)), \ - patch("tfscreen.tfmodel.scripts.prior_predictive_cli.growth_df_from_prior", + patch("tfscreen.tfmodel.scripts.sample_prior_cli.growth_df_from_prior", return_value=fake_df): out_prefix = str(tmp_path / "prior") diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py index e41fcbd1..6a76ce3c 100644 --- a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py +++ b/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py @@ -1,12 +1,12 @@ """ -Tests for run_prefit_calibration.py. +Tests for prefit_calibration_cli.py. The pre-fit script builds an in-process calibration ModelOrchestrator, -runs MAP, computes Hessian-based per-site sigmas, and then writes -in-place updates (with .bak backups) into the production priors and -guesses CSVs. These tests exercise the helper functions directly and -mock the heavy machinery (read_configuration, ModelOrchestrator, RunInference) -when testing the orchestration in run_prefit_calibration / main. +runs MAP, and then writes in-place updates (with .bak backups) into the +production priors and guesses CSVs. These tests exercise the helper +functions directly and mock the heavy machinery (read_configuration, +ModelOrchestrator, RunInference) when testing the orchestration in +run_prefit_calibration / main. """ import dataclasses import os @@ -23,21 +23,17 @@ import flax.struct as fstruct -from tfscreen.tfmodel.scripts.run_prefit_calibration_cli import ( +from tfscreen.tfmodel.scripts.prefit_calibration_cli import ( _apply_guesses_updates, _apply_priors_updates, _build_calibration_model, _build_csv_updates, - _compute_growth_pred_std, _compute_theta_values, _csv_row_name, _identify_field_mapping, _inject_calibration_priors, _intersect_data, - _run_calibration_diagnostics, - _make_correlation_plot, _resolve_csv_paths, - _write_calibration_stats, main, run_prefit_calibration, _CALIBRATION_OVERRIDES, @@ -294,8 +290,6 @@ def test_clip_to_open_interval(self): assert theta[0, 1] < 1.0 - - # --------------------------------------------------------------------------- # _csv_row_name / _build_csv_updates # --------------------------------------------------------------------------- @@ -309,9 +303,9 @@ def test_basic_row_name(self): class TestBuildCsvUpdates: - def test_normal_site_writes_loc_and_scale(self): + def test_normal_site_writes_loc_to_priors_and_guesses(self): field_mapping = { - "growth_k_hyper": { + "condition_growth_growth_k_hyper": { "component": "condition_growth", "dist_class": "Normal", "loc_field": "growth_k_hyper_loc", @@ -319,43 +313,33 @@ def test_normal_site_writes_loc_and_scale(self): "is_array": False, }, } - hessian_results = { - "growth_k_hyper": {"map": np.float32(2.5), - "sigma": np.float32(0.7)}, - } - prior_updates, guess_updates = _build_csv_updates(field_mapping, - hessian_results) - # MAP → loc_field; sigma → scale_field + params = {"condition_growth_growth_k_hyper_auto_loc": np.float32(2.5)} + prior_updates, guess_updates = _build_csv_updates(field_mapping, params) + # MAP → loc_field in priors assert prior_updates[ "growth.condition_growth.growth_k_hyper_loc"] == pytest.approx(2.5) - assert prior_updates[ - "growth.condition_growth.k_hyper_loc_scale"] == pytest.approx(0.7) - assert guess_updates["growth_k_hyper"] == pytest.approx(2.5) + # scale_field is NOT updated (no Hessian sigma) + assert "growth.condition_growth.k_hyper_loc_scale" not in prior_updates + # MAP → scalar guess + assert guess_updates["condition_growth_growth_k_hyper"] == pytest.approx(2.5) - def test_halfnormal_site_recenters_on_map(self): + def test_halfnormal_site_writes_map_to_scale_field(self): field_mapping = { - "k_hyper_scale_loc": { + "condition_growth_k_hyper_scale": { "component": "condition_growth", "dist_class": "HalfNormal", "scale_field": "growth_k_hyper_scale_loc", "is_array": False, }, } - hessian_results = { - "k_hyper_scale_loc": {"map": np.float32(0.42), - "sigma": np.float32(0.09)}, - } - prior_updates, guess_updates = _build_csv_updates(field_mapping, - hessian_results) - # HalfNormal has only one parameter (its scale). We recenter the - # prior on the MAP point — sigma is dropped because there is no - # second parameter to consume it. + params = {"condition_growth_k_hyper_scale_auto_loc": np.float32(0.42)} + prior_updates, guess_updates = _build_csv_updates(field_mapping, params) assert prior_updates == { "growth.condition_growth.growth_k_hyper_scale_loc": pytest.approx(0.42), } - assert guess_updates["k_hyper_scale_loc"] == pytest.approx(0.42) + assert guess_updates["condition_growth_k_hyper_scale"] == pytest.approx(0.42) - def test_skips_sites_without_hessian_result(self): + def test_skips_sites_without_auto_loc_in_params(self): field_mapping = { "missing_site": { "component": "condition_growth", @@ -380,14 +364,8 @@ def test_array_site_writes_locs_to_guesses(self): "is_array": True, }, } - hessian_results = { - "condition_growth_k": { - "map": np.array([1.0, 2.0, 3.0]), - "sigma": np.array([0.1, 0.2, 0.3]), - }, - } - prior_updates, guess_updates = _build_csv_updates(field_mapping, - hessian_results) + params = {"condition_growth_k_auto_loc": np.array([1.0, 2.0, 3.0])} + prior_updates, guess_updates = _build_csv_updates(field_mapping, params) # Array sites do NOT update priors assert prior_updates == {} # Guess locs get the per-condition MAP array @@ -612,7 +590,7 @@ def test_overrides_replace_production_components(self): with patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.ModelOrchestrator" + ".prefit_calibration_cli.ModelOrchestrator" ) as MockGM: MockGM.return_value = MagicMock() growth_df = pd.DataFrame() @@ -657,7 +635,7 @@ def test_binding_weight_reset_from_large_production_value(self): with patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.ModelOrchestrator" + ".prefit_calibration_cli.ModelOrchestrator" ) as MockGM: MockGM.return_value = MagicMock() _build_calibration_model(orchestrator_prod, pd.DataFrame(), pd.DataFrame()) @@ -681,7 +659,7 @@ def test_does_not_mutate_production_settings(self): with patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.ModelOrchestrator" + ".prefit_calibration_cli.ModelOrchestrator" ) as MockGM: MockGM.return_value = MagicMock() _build_calibration_model(orchestrator_prod, pd.DataFrame(), pd.DataFrame()) @@ -842,61 +820,51 @@ def _write_yaml_and_csvs(self, tmp_path): return str(cfg), str(priors), str(guesses) def _patch_pipeline(self, mocker, orchestrator_cal=None, params=None, - hessian_results=None, field_mapping=None): + field_mapping=None): """Stub out every heavy callsite of run_prefit_calibration.""" mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.read_configuration", + ".prefit_calibration_cli.read_configuration", return_value=(MagicMock(), {}), ) mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._intersect_data", + ".prefit_calibration_cli._intersect_data", return_value=(pd.DataFrame(), pd.DataFrame()), ) orchestrator_cal = orchestrator_cal or MagicMock() orchestrator_cal.init_params = {} mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._build_calibration_model", + ".prefit_calibration_cli._build_calibration_model", return_value=orchestrator_cal, ) mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._compute_theta_values", + ".prefit_calibration_cli._compute_theta_values", return_value=np.array([[0.5]]), ) mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._inject_calibration_priors", + ".prefit_calibration_cli._inject_calibration_priors", ) mock_ri = MagicMock() mock_ri._iterations_per_epoch = 1 - mock_ri.compute_hessian_sigmas.return_value = hessian_results or {} mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.RunInference", + ".prefit_calibration_cli.RunInference", return_value=mock_ri, ) mock_run_map = mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._run_calibration_map", + ".prefit_calibration_cli._run_calibration_map", return_value=("svi_state", params or {}, True), ) mocker.patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._identify_field_mapping", + ".prefit_calibration_cli._identify_field_mapping", return_value=field_mapping or {}, ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._compute_growth_pred_std", - return_value=None, - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._run_calibration_diagnostics", - ) return mock_ri, mock_run_map def test_raises_when_seed_and_checkpoint_both_none(self, tmp_path, mocker): @@ -912,32 +880,28 @@ def test_returns_run_calibration_map_result(self, tmp_path, mocker): def test_writes_bak_files_when_updates_present(self, tmp_path, mocker): cfg, priors, guesses = self._write_yaml_and_csvs(tmp_path) - # Seed the priors CSV with both the loc-field and scale-field rows - # that the Normal-site update should target, plus an unrelated - # row to verify we don't disturb it. + # Seed the priors CSV with a loc-field row that the Normal-site + # update should target, plus an unrelated row to verify we don't + # disturb it. pd.DataFrame({ "parameter": [ "growth.condition_growth.growth_k_hyper_loc", - "growth.condition_growth.k_hyper_loc_scale", "growth.activity.hyper_loc_loc", ], - "value": [1.0, 1.0, 0.0], + "value": [1.0, 0.0], }).to_csv(priors, index=False) pd.DataFrame({ - "parameter": ["growth_k_hyper"], + "parameter": ["condition_growth_growth_k_hyper"], "value": [1.0], "flat_index": [float("nan")], }).to_csv(guesses, index=False) + params = {"condition_growth_growth_k_hyper_auto_loc": np.float32(9.0)} self._patch_pipeline( mocker, - params={"growth_k_hyper_auto_loc": 9.0}, - hessian_results={ - "growth_k_hyper": {"map": np.float32(9.0), - "sigma": np.float32(0.5)}, - }, + params=params, field_mapping={ - "growth_k_hyper": { + "condition_growth_growth_k_hyper": { "component": "condition_growth", "dist_class": "Normal", "loc_field": "growth_k_hyper_loc", @@ -952,11 +916,8 @@ def test_writes_bak_files_when_updates_present(self, tmp_path, mocker): assert os.path.exists(priors + ".bak") assert os.path.exists(guesses + ".bak") new_priors = pd.read_csv(priors).set_index("parameter")["value"] - # MAP → loc-field row; Hessian sigma → scale-field row; - # unrelated row preserved. + # MAP → loc-field row; unrelated row preserved. assert new_priors["growth.condition_growth.growth_k_hyper_loc"] == 9.0 - assert new_priors[ - "growth.condition_growth.k_hyper_loc_scale"] == pytest.approx(0.5) assert new_priors["growth.activity.hyper_loc_loc"] == 0.0 new_guesses = pd.read_csv(guesses) assert new_guesses.iloc[0]["value"] == 9.0 @@ -1023,48 +984,12 @@ def test_default_init_param_jitter_is_zero(self, tmp_path, mocker): run_prefit_calibration(config_file=cfg, seed=1) assert mock_run_map.call_args.kwargs["init_param_jitter"] == 0.0 - def test_compute_growth_pred_std_called_with_ri_and_params(self, tmp_path, - mocker): - """_compute_growth_pred_std is called with the RunInference instance - and the exact params dict returned by _run_calibration_map.""" + def test_hessian_not_called(self, tmp_path, mocker): + """compute_hessian_sigmas must not be called after the cleanup.""" cfg, _, _ = self._write_yaml_and_csvs(tmp_path) - mock_params = {"alpha_auto_loc": np.float32(2.0)} - mock_ri, _ = self._patch_pipeline(mocker, params=mock_params) - - # Override the stub so we can inspect calls. - mock_std_fn = mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._compute_growth_pred_std", - return_value=None, - ) + mock_ri, _ = self._patch_pipeline(mocker) run_prefit_calibration(config_file=cfg, seed=1) - - assert mock_std_fn.call_count == 1 - call_args = mock_std_fn.call_args - assert call_args.args[0] is mock_ri # first positional: ri - assert call_args.args[1] is mock_params # second positional: params - - def test_growth_pred_std_forwarded_to_run_calibration_diagnostics( - self, tmp_path, mocker): - """The growth_pred_std returned by _compute_growth_pred_std is passed - as the growth_pred_std keyword argument to _run_calibration_diagnostics.""" - cfg, _, _ = self._write_yaml_and_csvs(tmp_path) - sentinel = np.ones((1, 2, 1, 1, 1, 1, 2)) - self._patch_pipeline(mocker) - - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._compute_growth_pred_std", - return_value=sentinel, - ) - mock_diag = mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._run_calibration_diagnostics", - ) - run_prefit_calibration(config_file=cfg, seed=1) - - diag_kwargs = mock_diag.call_args.kwargs - assert diag_kwargs.get("growth_pred_std") is sentinel + mock_ri.compute_hessian_sigmas.assert_not_called() # --------------------------------------------------------------------------- @@ -1078,7 +1003,7 @@ def test_main_invokes_run_prefit_with_required_args(self, tmp_path): argv = ["cal.yaml", "--seed", "13"] with patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.run_prefit_calibration", + ".prefit_calibration_cli.run_prefit_calibration", autospec=True, ) as mock_run, \ patch("sys.argv", ["tfs-prefit-calibration"] + argv): @@ -1097,7 +1022,7 @@ def test_main_forwards_custom_out_prefix(self, tmp_path): ] with patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.run_prefit_calibration", + ".prefit_calibration_cli.run_prefit_calibration", autospec=True, ) as mock_run, \ patch("sys.argv", ["tfs-prefit-calibration"] + argv): @@ -1112,568 +1037,9 @@ def test_main_forwards_checkpoint_file(self, tmp_path): ] with patch( "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.run_prefit_calibration", + ".prefit_calibration_cli.run_prefit_calibration", autospec=True, ) as mock_run, \ patch("sys.argv", ["tfs-prefit-calibration"] + argv): main() assert mock_run.call_args.kwargs["checkpoint_file"] == "/tmp/ck.pkl" - - -# --------------------------------------------------------------------------- -# _compute_growth_pred_std -# --------------------------------------------------------------------------- - -class TestComputeGrowthPredStd: - """Tests for _compute_growth_pred_std. - - The function wraps heavy JAX machinery (Hessian, Cholesky, vmap, Predictive). - Tests that require the Hessian/Predictive path stub those out via - ``_patch_heavy_machinery``; the empty-params case is exercised directly. - """ - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _patch_heavy_machinery(self, mocker, n_params, n_samples, pred_output): - """Stub JAX/NumPyro operations so the function reaches Predictive. - - * ``jax.device_put`` → identity passthrough - * ``jax.hessian`` → returns the n_params × n_params identity - * ``jax.random.normal`` → zeros, so all Laplace samples equal MAP - * ``trace`` / ``seed`` (in the module) → empty model trace, meaning - no constrained transforms and no per-site bijections applied - * ``numpyro.infer.Predictive`` → returns ``pred_output`` - """ - mocker.patch("jax.device_put", side_effect=lambda x: x) - hess = jnp.eye(n_params) - # jax.hessian(fn) returns a callable; that callable(flat_map) returns - # the matrix. Use a MagicMock so the two-call chain works correctly. - mock_hess_fn = MagicMock(return_value=hess) - mocker.patch("jax.hessian", return_value=mock_hess_fn) - mocker.patch( - "jax.random.normal", - return_value=jnp.zeros((n_samples, n_params)), - ) - mock_traced = MagicMock() - mock_traced.get_trace.return_value = {} - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.trace", - return_value=mock_traced, - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.seed", - return_value=MagicMock(), - ) - mock_pred_inst = MagicMock(return_value=pred_output) - mocker.patch("numpyro.infer.Predictive", return_value=mock_pred_inst) - - def _make_inputs(self, n_params=2): - """Build (ri, orchestrator_cal, params) mocks for _compute_growth_pred_std.""" - ri = MagicMock() - ri.get_key.return_value = jax.random.PRNGKey(0) - orchestrator_cal = MagicMock() - orchestrator_cal.data.num_genotype = 3 - orchestrator_cal.get_batch.return_value = MagicMock() - orchestrator_cal.priors = MagicMock() - params = {f"site{i}_auto_loc": np.float32(float(i)) - for i in range(n_params)} - return ri, orchestrator_cal, params - - # ------------------------------------------------------------------ - # Tests - # ------------------------------------------------------------------ - - def test_returns_none_when_params_empty(self): - """No _auto_loc params → early-exit None without touching JAX.""" - ri = MagicMock() - orchestrator_cal = MagicMock() - orchestrator_cal.data.num_genotype = 2 - orchestrator_cal.get_batch.return_value = MagicMock() - assert _compute_growth_pred_std(ri, {}, orchestrator_cal) is None - - def test_returns_none_when_growth_pred_absent(self, mocker): - """When Predictive returns no growth_pred site, result must be None.""" - n_params, n_samples = 2, 4 - ri, orchestrator_cal, params = self._make_inputs(n_params) - self._patch_heavy_machinery( - mocker, n_params, n_samples, - pred_output={"other_site": np.ones((n_samples, 3))}, - ) - assert _compute_growth_pred_std(ri, params, orchestrator_cal, - n_samples=n_samples) is None - - def test_returns_array_matching_growth_pred_shape(self, mocker): - """Output shape must equal the (R,T,CP,CS,TN,TC,G) tensor shape.""" - n_params, n_samples = 2, 4 - tensor_shape = (1, 3, 1, 1, 1, 1, 2) - ri, orchestrator_cal, params = self._make_inputs(n_params) - growth_pred = np.ones((n_samples,) + tensor_shape) * 5.0 - self._patch_heavy_machinery( - mocker, n_params, n_samples, - pred_output={"growth_pred": growth_pred}, - ) - result = _compute_growth_pred_std(ri, params, orchestrator_cal, - n_samples=n_samples) - assert result is not None - assert result.shape == tensor_shape - - def test_std_is_zero_for_identical_samples(self, mocker): - """All Laplace samples identical → elementwise std must be 0.""" - n_params, n_samples = 2, 6 - tensor_shape = (1, 2, 1, 1, 1, 1, 3) - ri, orchestrator_cal, params = self._make_inputs(n_params) - growth_pred = np.ones((n_samples,) + tensor_shape) * 7.0 - self._patch_heavy_machinery( - mocker, n_params, n_samples, - pred_output={"growth_pred": growth_pred}, - ) - result = _compute_growth_pred_std(ri, params, orchestrator_cal, - n_samples=n_samples) - assert np.allclose(result, 0.0) - - def test_std_matches_numpy_std_of_samples(self, mocker): - """Output must equal np.std(growth_pred_samples, axis=0) exactly.""" - n_params, n_samples = 2, 10 - tensor_shape = (1, 2, 1, 1, 1, 1, 3) - ri, orchestrator_cal, params = self._make_inputs(n_params) - rng = np.random.default_rng(42) - growth_pred = rng.normal(size=(n_samples,) + tensor_shape).astype(np.float32) - self._patch_heavy_machinery( - mocker, n_params, n_samples, - pred_output={"growth_pred": growth_pred}, - ) - result = _compute_growth_pred_std(ri, params, orchestrator_cal, - n_samples=n_samples) - # Compare in float64 (the function casts before computing std). - expected = growth_pred.astype(np.float64).std(axis=0) - assert np.allclose(result, expected, atol=1e-5) - - def test_no_overflow_for_large_growth_pred_values(self, mocker): - """float32 growth_pred values large enough to overflow when squared - must produce finite std (not inf/nan) after float64 cast + clipping.""" - n_params, n_samples = 2, 4 - tensor_shape = (1, 2, 1, 1, 1, 1, 2) - ri, orchestrator_cal, params = self._make_inputs(n_params) - # Values near the float32 overflow boundary (~1e38); squaring these - # in float32 would overflow to inf inside np.std. - growth_pred = np.full((n_samples,) + tensor_shape, 1e20, dtype=np.float32) - growth_pred[0] = -1e20 # introduce variance so std != 0 - self._patch_heavy_machinery( - mocker, n_params, n_samples, - pred_output={"growth_pred": growth_pred}, - ) - result = _compute_growth_pred_std(ri, params, orchestrator_cal, - n_samples=n_samples) - assert result is not None - assert np.all(np.isfinite(result)) - - -# --------------------------------------------------------------------------- -# _write_calibration_stats -# --------------------------------------------------------------------------- - -def _make_stats_df(n=20, include_std=True, seed=7): - """Return a minimal DataFrame suitable for _write_calibration_stats.""" - rng = np.random.default_rng(seed) - df = pd.DataFrame({ - "genotype": [f"g{i % 3}" for i in range(n)], - "ln_cfu": rng.normal(10.0, 1.0, n), - "ln_cfu_pred": rng.normal(10.0, 1.0, n), - }) - if include_std: - df["ln_cfu_pred_std"] = rng.uniform(0.1, 0.5, n) - return df - - -class TestWriteCalibrationStats: - - def test_writes_json_with_expected_keys(self, tmp_path): - df = _make_stats_df() - _write_calibration_stats(df, str(tmp_path / "run")) - json_path = tmp_path / "run_calib_stats.json" - assert json_path.exists() - import json - stats = json.loads(json_path.read_text()) - expected_keys = { - "pct_success", "rmse", "normalized_rmse", "pearson_r", - "spearman_r", "r_squared", "mean_error", "coverage_prob", - "residual_corr", "residual_corr_p_value", "bp_p_value", - "n_params", "n_obs", - } - assert expected_keys == set(stats.keys()) - - def test_all_values_are_python_float_int_or_null(self, tmp_path): - """Values must be Python float, int, or null (no numpy scalars or NaN).""" - df = _make_stats_df() - _write_calibration_stats(df, str(tmp_path / "run")) - import json - stats = json.loads((tmp_path / "run_calib_stats.json").read_text()) - int_keys = {"n_params", "n_obs"} - for k, v in stats.items(): - if k in int_keys: - assert v is None or isinstance(v, int), \ - f"Key '{k}' has non-int value {v!r}" - else: - assert v is None or isinstance(v, float), \ - f"Key '{k}' has non-float value {v!r}" - if isinstance(v, float): - assert np.isfinite(v), f"Key '{k}' is not finite: {v}" - - def test_n_params_and_n_obs_written_when_provided(self, tmp_path): - """n_params and n_obs appear as integers in the JSON when supplied.""" - import json - df = _make_stats_df(n=10) - _write_calibration_stats(df, str(tmp_path / "run"), n_params=7, n_obs=10) - stats = json.loads((tmp_path / "run_calib_stats.json").read_text()) - assert stats["n_params"] == 7 - assert stats["n_obs"] == 10 - assert isinstance(stats["n_params"], int) - assert isinstance(stats["n_obs"], int) - - def test_n_params_and_n_obs_null_when_not_provided(self, tmp_path): - """n_params and n_obs are null in the JSON when not supplied.""" - import json - df = _make_stats_df() - _write_calibration_stats(df, str(tmp_path / "run")) - stats = json.loads((tmp_path / "run_calib_stats.json").read_text()) - assert stats["n_params"] is None - assert stats["n_obs"] is None - - def test_returns_silently_when_std_column_missing(self, tmp_path): - """No JSON written when ln_cfu_pred_std is absent.""" - df = _make_stats_df(include_std=False) - _write_calibration_stats(df, str(tmp_path / "run")) - assert not (tmp_path / "run_calib_stats.json").exists() - - def test_returns_silently_when_pred_column_missing(self, tmp_path): - """No JSON written when ln_cfu_pred is absent.""" - df = _make_stats_df() - df = df.drop(columns=["ln_cfu_pred"]) - _write_calibration_stats(df, str(tmp_path / "run")) - assert not (tmp_path / "run_calib_stats.json").exists() - - def test_returns_silently_when_no_valid_rows(self, tmp_path): - """No JSON written when all rows have NaN predictions.""" - df = _make_stats_df() - df["ln_cfu_pred"] = np.nan - _write_calibration_stats(df, str(tmp_path / "run")) - assert not (tmp_path / "run_calib_stats.json").exists() - - def test_nan_stats_serialised_as_null(self, tmp_path): - """Stats entries that are NaN (e.g. constant inputs) must become null.""" - # Constant ln_cfu makes pearson_r undefined → NaN. - df = _make_stats_df() - df["ln_cfu"] = 10.0 - _write_calibration_stats(df, str(tmp_path / "run")) - import json - stats = json.loads((tmp_path / "run_calib_stats.json").read_text()) - assert stats["pearson_r"] is None - - -# --------------------------------------------------------------------------- -# _make_correlation_plot -# --------------------------------------------------------------------------- - -def _make_corr_df(n_genotypes=3, n_per=8, seed=11): - """Return a minimal DataFrame for _make_correlation_plot.""" - rng = np.random.default_rng(seed) - rows = [] - for i in range(n_genotypes): - obs = rng.normal(10.0 + i, 1.0, n_per) - pred = obs + rng.normal(0, 0.3, n_per) - for o, p in zip(obs, pred): - rows.append({"genotype": f"g{i}", "ln_cfu": o, "ln_cfu_pred": p}) - return pd.DataFrame(rows) - - -class TestMakeCorrelationPlot: - - def test_writes_pdf(self, tmp_path): - df = _make_corr_df() - _make_correlation_plot(df, str(tmp_path / "run")) - assert (tmp_path / "run_calib_correlation.pdf").exists() - - def test_returns_silently_when_genotype_column_missing(self, tmp_path): - df = _make_corr_df().drop(columns=["genotype"]) - _make_correlation_plot(df, str(tmp_path / "run")) - assert not (tmp_path / "run_calib_correlation.pdf").exists() - - def test_returns_silently_when_no_valid_rows(self, tmp_path): - df = _make_corr_df() - df["ln_cfu_pred"] = np.nan - _make_correlation_plot(df, str(tmp_path / "run")) - assert not (tmp_path / "run_calib_correlation.pdf").exists() - - def test_colors_cycle_for_more_genotypes_than_prop_cycle(self, tmp_path, - mocker): - """When n_genotypes > len(prop_cycle) the modulo wraps colours; no - KeyError or IndexError must be raised.""" - # Default prop_cycle has 10 colours; use 15 genotypes. - df = _make_corr_df(n_genotypes=15) - # Should complete without error and produce a PDF. - _make_correlation_plot(df, str(tmp_path / "run")) - assert (tmp_path / "run_calib_correlation.pdf").exists() - - def test_matplotlib_unavailable_returns_silently(self, tmp_path, mocker): - df = _make_corr_df() - real_import = __builtins__["__import__"] if isinstance(__builtins__, dict) \ - else __import__ - - def fake_import(name, *args, **kwargs): - if name == "matplotlib.pyplot": - raise ImportError("no matplotlib") - return real_import(name, *args, **kwargs) - - mocker.patch("builtins.__import__", side_effect=fake_import) - _make_correlation_plot(df, str(tmp_path / "run")) - assert not (tmp_path / "run_calib_correlation.pdf").exists() - - # ----------------------------------------------------------------- - # Helpers shared by the _run_calibration_diagnostics tests below. - # ----------------------------------------------------------------- - - @staticmethod - def _make_fake_predict_dfs(genotypes=("gA", "gB"), n_t=3): - """Return (all_dfs, orchestrator_mock) for diagnostics tests.""" - rng = np.random.default_rng(0) - rows = [] - for i, g in enumerate(genotypes): - for t_i, t in enumerate(np.linspace(0, 10, n_t)): - rows.append({ - "replicate": "r1", - "condition_pre": "cp1", - "condition_sel": "cs1", - "titrant_name": "tn1", - "titrant_conc": 0.0, - "genotype": g, - "t_sel": t, - "ln_cfu": rng.uniform(8, 12), - "ln_cfu_std": 0.2, - "q0.05": rng.uniform(7, 9), - "q0.5": rng.uniform(9, 11), - "q0.95": rng.uniform(11, 13), - "replicate_idx": 0, - "time_idx": t_i, - "condition_pre_idx": 0, - "condition_sel_idx": 0, - "titrant_name_idx": 0, - "titrant_conc_idx": 0, - "genotype_idx": i, - }) - - pred_df = pd.DataFrame(rows) - - ln_cfu0_rows = [] - for i, g in enumerate(genotypes): - ln_cfu0_rows.append({ - "replicate": "r1", - "condition_pre": "cp1", - "genotype": g, - "q0.05": 8.5, - "q0.5": 9.0, - "q0.95": 9.5, - }) - ln_cfu0_df = pd.DataFrame(ln_cfu0_rows) - - all_dfs = {"growth_pred": pred_df, "ln_cfu0": ln_cfu0_df} - - orch = MagicMock() - orch.growth_df = pd.DataFrame({ - "replicate": ["r1"] * len(genotypes), - "condition_pre": ["cp1"] * len(genotypes), - "condition_sel": ["cs1"] * len(genotypes), - "titrant_name": ["tn1"] * len(genotypes), - "titrant_conc": [0.0] * len(genotypes), - "genotype": list(genotypes), - "t_pre": [5.0] * len(genotypes), - "t_sel": [10.0] * len(genotypes), - "ln_cfu": [10.0] * len(genotypes), - "ln_cfu_std": [0.2] * len(genotypes), - }) - orch.presplit_df = None - orch.growth_tm.tensor_dim_names = [ - "replicate", "time", "condition_pre", "condition_sel", - "titrant_name", "titrant_conc", "genotype", - ] - - return all_dfs, orch - - def test_run_calibration_diagnostics_calls_both_helpers(self, tmp_path, mocker): - """_run_calibration_diagnostics calls _write_calibration_stats and - _make_correlation_plot with a DataFrame that has ln_cfu_pred.""" - mock_stats = mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._write_calibration_stats", - ) - mock_corr = mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_correlation_plot", - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.plot_geno_trajectory", - return_value=MagicMock(), - ) - all_dfs, orch = self._make_fake_predict_dfs() - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.predict", - return_value=all_dfs, - ) - - params = {"alpha_auto_loc": np.array([0.5, 0.6])} - _run_calibration_diagnostics(orch, params, out_prefix=str(tmp_path / "run")) - - assert mock_stats.call_count == 1 - assert mock_corr.call_count == 1 - df_arg_stats = mock_stats.call_args.args[0] - df_arg_corr = mock_corr.call_args.args[0] - assert isinstance(df_arg_stats, pd.DataFrame) - assert isinstance(df_arg_corr, pd.DataFrame) - assert "ln_cfu_pred" in df_arg_stats.columns - # n_params and n_obs forwarded as kwargs - stats_kwargs = mock_stats.call_args.kwargs - assert "n_params" in stats_kwargs and "n_obs" in stats_kwargs - assert isinstance(stats_kwargs["n_params"], int) - assert isinstance(stats_kwargs["n_obs"], int) - - def test_run_calibration_diagnostics_one_pdf_per_genotype(self, tmp_path, mocker): - """One trajectory PDF must be written for each genotype.""" - genotypes = ("genoX", "genoY", "genoZ") - all_dfs, orch = self._make_fake_predict_dfs(genotypes=genotypes) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.predict", - return_value=all_dfs, - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._write_calibration_stats", - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_correlation_plot", - ) - # plot_geno_trajectory returns a real Figure so savefig works. - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.plot_geno_trajectory", - side_effect=lambda df, **kw: plt.subplots(1)[0].get_figure(), - ) - - prefix = str(tmp_path / "run") - _run_calibration_diagnostics(orch, {}, out_prefix=prefix) - - pdfs = [f for f in tmp_path.iterdir() if f.suffix == ".pdf" - and "trajectory" in f.name] - assert len(pdfs) == len(genotypes), \ - f"expected {len(genotypes)} PDFs, got {len(pdfs)}: {sorted(pdfs)}" - - def test_run_calibration_diagnostics_pdf_named_by_genotype(self, tmp_path, mocker): - """Each PDF filename must contain the genotype name.""" - all_dfs, orch = self._make_fake_predict_dfs(genotypes=("WT", "V44A")) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.predict", - return_value=all_dfs, - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._write_calibration_stats", - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_correlation_plot", - ) - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.plot_geno_trajectory", - side_effect=lambda df, **kw: plt.subplots(1)[0].get_figure(), - ) - - prefix = str(tmp_path / "run") - _run_calibration_diagnostics(orch, {}, out_prefix=prefix) - - names = [f.name for f in tmp_path.iterdir() if f.suffix == ".pdf"] - assert any("WT" in n for n in names) - assert any("V44A" in n for n in names) - - def test_run_calibration_diagnostics_genotype_slash_sanitized(self, tmp_path, mocker): - """Slashes in genotype names are replaced with underscores in filenames.""" - all_dfs, orch = self._make_fake_predict_dfs(genotypes=("a/b",)) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.predict", - return_value=all_dfs, - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._write_calibration_stats", - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_correlation_plot", - ) - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.plot_geno_trajectory", - side_effect=lambda df, **kw: plt.subplots(1)[0].get_figure(), - ) - - prefix = str(tmp_path / "run") - _run_calibration_diagnostics(orch, {}, out_prefix=prefix) - - names = [f.name for f in tmp_path.iterdir() if f.suffix == ".pdf"] - assert any("a_b" in n for n in names), \ - f"Expected 'a_b' in a filename, got: {names}" - assert not any("a/b" in n for n in names) - - def test_run_calibration_diagnostics_growth_pred_std_attached(self, tmp_path, mocker): - """When growth_pred_std is supplied, ln_cfu_pred_std appears in the - DataFrame passed to _write_calibration_stats.""" - all_dfs, orch = self._make_fake_predict_dfs(genotypes=("g0",)) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.predict", - return_value=all_dfs, - ) - mock_stats = mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._write_calibration_stats", - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli._make_correlation_plot", - ) - mocker.patch( - "tfscreen.tfmodel.scripts" - ".run_prefit_calibration_cli.plot_geno_trajectory", - return_value=MagicMock(), - ) - - # Shape (1,3,1,1,1,1,1) matches 1 replicate × 3 times × ... × 1 geno - gp_std = np.full((1, 3, 1, 1, 1, 1, 1), 0.42) - _run_calibration_diagnostics( - orch, {}, out_prefix=str(tmp_path / "run"), - growth_pred_std=gp_std, - ) - - df_arg = mock_stats.call_args.args[0] - assert "ln_cfu_pred_std" in df_arg.columns - assert np.allclose(df_arg["ln_cfu_pred_std"].dropna(), 0.42) diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index a1fb2d49..86053c2b 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -20,6 +20,14 @@ from unittest.mock import MagicMock, patch +# _build_transform_registry registers log_{col} for every sim column, including +# log_hill_K which has negative values → np.log(negative) = NaN + RuntimeWarning. +# This is expected behaviour (the log_log_hill_K entry is just NaN), so suppress +# across this module rather than wrapping every individual test. +pytestmark = pytest.mark.filterwarnings( + "ignore:invalid value encountered in log:RuntimeWarning" +) + from tfscreen.tfmodel.scripts.summarize_fit_cli import ( _find_params_or_posterior, _find_unique, @@ -1503,7 +1511,9 @@ def test_direct_csv_written_with_obs(self, tmp_path): self._write_direct_params(str(tmp_path / "run_params_activity.csv")) out_prefix = str(tmp_path / "summary" / "tfs_summarize") os.makedirs(os.path.dirname(out_prefix), exist_ok=True) - _summarize_params(str(tmp_path), out_prefix) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _summarize_params(str(tmp_path), out_prefix) out_path = out_prefix + "_params_activity.csv" assert os.path.exists(out_path) df = pd.read_csv(out_path) diff --git a/tests/tfscreen/tfmodel/test_model_orchestrator.py b/tests/tfscreen/tfmodel/test_model_orchestrator.py index 6a78a6c8..bdb843c5 100644 --- a/tests/tfscreen/tfmodel/test_model_orchestrator.py +++ b/tests/tfscreen/tfmodel/test_model_orchestrator.py @@ -156,7 +156,7 @@ def test_read_binding_df_missing_col_error(mocker): _read_binding_df("path.csv", growth_df=growth_df) -def test_read_binding_df_extra_pairs_dropped_with_warning(): +def test_read_binding_df_extra_pairs_dropped_with_warning(capsys): """binding rows whose (genotype, titrant_name) is absent from growth_df are dropped.""" from tfscreen.genetics import set_categorical_genotype import tfscreen.util.dataframe @@ -187,8 +187,10 @@ def test_read_binding_df_extra_pairs_dropped_with_warning(): "theta_std": [0.02, 0.02], }) - with pytest.warns(UserWarning, match="will be dropped"): - result = _read_binding_df(binding_df, growth_df=growth_df) + result = _read_binding_df(binding_df, growth_df=growth_df) + captured = capsys.readouterr() + assert "will be dropped" in captured.out + assert "M42I" in captured.out # Only the "wt" row should survive assert set(result["genotype"]) == {"wt"} diff --git a/tests/tfscreen/tfmodel/test_presplit.py b/tests/tfscreen/tfmodel/test_presplit.py index 17f923dc..ac5f1093 100644 --- a/tests/tfscreen/tfmodel/test_presplit.py +++ b/tests/tfscreen/tfmodel/test_presplit.py @@ -90,7 +90,7 @@ def test_read_presplit_df_passes_through_valid(minimal_presplit_df, def test_read_presplit_df_drops_unknown_genotypes(minimal_presplit_df, - minimal_growth_df): + minimal_growth_df, capsys): df_extra = minimal_presplit_df.copy() extra_row = pd.DataFrame([{"replicate": 1, "condition_pre": "kanR", "genotype": "A99V", @@ -99,6 +99,9 @@ def test_read_presplit_df_drops_unknown_genotypes(minimal_presplit_df, gdf = _read_growth_df(minimal_growth_df.copy()) result = _read_presplit_df(df_extra, gdf) + captured = capsys.readouterr() + assert "will be dropped" in captured.out + assert "A99V" in captured.out assert "A99V" not in result["genotype"].values assert len(result) == len(minimal_presplit_df) From b6d9c527a29f88063bb7112ee57620f8cccd5c56 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 15 Jun 2026 18:04:19 -0700 Subject: [PATCH 15/24] cleaning up test and cli naming --- pyproject.toml | 8 +- reports/flake.txt | 2404 ++++++++--------- reports/junit/junit.xml | 2 +- ...s_counts.py => test_process_counts_cli.py} | 0 ...ess_fastq.py => test_process_fastq_cli.py} | 0 ...simulation_cli.py => test_simulate_cli.py} | 0 .../activity => generative}/__init__.py | 0 .../components/activity}/__init__.py | 0 .../components/activity/test_fixed.py | 0 .../activity/test_hierarchical_geno.py | 0 .../activity/test_hierarchical_mut.py | 0 .../activity/test_horseshoe_geno.py | 0 .../components/activity/test_horseshoe_mut.py | 0 .../components/dk_geno}/__init__.py | 0 .../components/dk_geno/test_fixed.py | 0 .../dk_geno/test_hierarchical_geno.py | 0 .../components/growth}/__init__.py | 0 .../components/growth/test_linear.py | 0 .../components/growth/test_power.py | 0 .../components/growth/test_saturation.py | 0 .../components/growth_noise}/__init__.py | 0 .../components/growth_noise/test_normal_kt.py | 0 .../components/growth_noise/test_zero.py | 0 .../components/growth_transition}/__init__.py | 0 .../growth_transition/test_baranyi.py | 0 .../growth_transition/test_baranyi_k.py | 0 .../growth_transition/test_baranyi_tau.py | 0 .../growth_transition/test_instant.py | 0 .../growth_transition/test_memory.py | 0 .../growth_transition/test_two_pop.py | 0 .../components/ln_cfu0}/__init__.py | 0 .../components/ln_cfu0/test_hierarchical.py | 0 .../ln_cfu0/test_hierarchical_factored.py | 0 .../components/noise}/__init__.py | 0 .../components/noise/test_beta.py | 0 .../components/noise/test_logit_normal.py | 0 .../components/noise/test_zero.py | 0 .../components/sample_offset}/__init__.py | 0 .../components/sample_offset/test_normal.py | 0 .../components/sample_offset/test_zero.py | 0 .../components/test_pinning.py | 0 .../components/theta}/__init__.py | 0 .../components/theta/test__simple.py | 0 .../components/theta/test_categorical_geno.py | 0 .../components/theta/test_hill_geno.py | 0 .../components/theta/test_hill_mut.py | 0 .../theta/test_predict_unmeasured.py | 0 .../theta/thermo/O2_C12_K5_U0_a}/__init__.py | 0 .../theta/thermo/O2_C12_K5_U0_a/test_PK.py | 0 .../theta/thermo/O2_C12_K5_U0_a/test_PddG.py | 0 .../theta/thermo/O2_C12_K5_U0_a/test_PnnC.py | 0 .../O2_C12_K5_U0_a/test_predict_unmeasured.py | 0 .../thermo/O2_C12_K5_U0_a/test_thermo.py | 0 .../theta/thermo/O2_C12_K5_U1_a}/__init__.py | 0 .../theta/thermo/O2_C12_K5_U1_a/test_PK.py | 0 .../theta/thermo/O2_C12_K5_U1_a/test_PddG.py | 0 .../theta/thermo/O2_C12_K5_U1_a/test_PnnC.py | 0 .../thermo/O2_C12_K5_U1_a/test_thermo.py | 0 .../theta/thermo/O2_C4_K3_U0_a}/__init__.py | 0 .../theta/thermo/O2_C4_K3_U0_a/test_PK.py | 0 .../theta/thermo/O2_C4_K3_U0_a/test_PddG.py | 0 .../theta/thermo/O2_C4_K3_U0_a/test_PnnC.py | 0 .../O2_C4_K3_U0_a/test_predict_unmeasured.py | 0 .../theta/thermo/O2_C4_K3_U0_a/test_thermo.py | 0 .../theta/thermo/O2_C4_K3_U1_a}/__init__.py | 0 .../theta/thermo/O2_C4_K3_U1_a/test_PK.py | 0 .../theta/thermo/O2_C4_K3_U1_a/test_PddG.py | 0 .../theta/thermo/O2_C4_K3_U1_a/test_PnnC.py | 0 .../theta/thermo/O2_C4_K3_U1_a/test_thermo.py | 0 .../components/theta/thermo}/__init__.py | 0 .../components/theta/thermo/test_features.py | 0 .../components/theta/thermo/test_horseshoe.py | 0 .../components/theta/thermo/test_io.py | 0 .../components/theta/thermo/test_nn.py | 0 .../components/theta/thermo/test_prior.py | 0 .../components/theta_rescale}/__init__.py | 0 .../components/theta_rescale/test_logit.py | 0 .../theta_rescale/test_passthrough.py | 0 .../components/transformation/__init__.py | 0 .../transformation/test__congression.py | 0 .../transformation/test_empirical.py | 0 .../transformation/test_logit_norm.py | 0 .../components/transformation/test_single.py | 0 .../observe/test_observe_binding.py | 0 .../observe/test_observe_growth.py | 0 ...tion.py => test_prefit_calibration_cli.py} | 0 ...ictive_cli.py => test_sample_prior_cli.py} | 0 ...otypes.py => test_subset_genotypes_cli.py} | 0 88 files changed, 1207 insertions(+), 1207 deletions(-) rename tests/tfscreen/process_raw/{test_process_counts.py => test_process_counts_cli.py} (100%) rename tests/tfscreen/process_raw/{test_process_fastq.py => test_process_fastq_cli.py} (100%) rename tests/tfscreen/simulate/{test_run_simulation_cli.py => test_simulate_cli.py} (100%) rename tests/tfscreen/tfmodel/{components/activity => generative}/__init__.py (100%) rename tests/tfscreen/tfmodel/{components/dk_geno => generative/components/activity}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/activity/test_fixed.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/activity/test_hierarchical_geno.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/activity/test_hierarchical_mut.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/activity/test_horseshoe_geno.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/activity/test_horseshoe_mut.py (100%) rename tests/tfscreen/tfmodel/{components/growth => generative/components/dk_geno}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/dk_geno/test_fixed.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/dk_geno/test_hierarchical_geno.py (100%) rename tests/tfscreen/tfmodel/{components/growth_noise => generative/components/growth}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth/test_linear.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth/test_power.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth/test_saturation.py (100%) rename tests/tfscreen/tfmodel/{components/growth_transition => generative/components/growth_noise}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_noise/test_normal_kt.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_noise/test_zero.py (100%) rename tests/tfscreen/tfmodel/{components/ln_cfu0 => generative/components/growth_transition}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_transition/test_baranyi.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_transition/test_baranyi_k.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_transition/test_baranyi_tau.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_transition/test_instant.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_transition/test_memory.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/growth_transition/test_two_pop.py (100%) rename tests/tfscreen/tfmodel/{components/noise => generative/components/ln_cfu0}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/ln_cfu0/test_hierarchical.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/ln_cfu0/test_hierarchical_factored.py (100%) rename tests/tfscreen/tfmodel/{components/sample_offset => generative/components/noise}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/noise/test_beta.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/noise/test_logit_normal.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/noise/test_zero.py (100%) rename tests/tfscreen/tfmodel/{components/theta => generative/components/sample_offset}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/sample_offset/test_normal.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/sample_offset/test_zero.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/test_pinning.py (100%) rename tests/tfscreen/tfmodel/{components/theta/thermo/O2_C12_K5_U0_a => generative/components/theta}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/test__simple.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/test_categorical_geno.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/test_hill_geno.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/test_hill_mut.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/test_predict_unmeasured.py (100%) rename tests/tfscreen/tfmodel/{components/theta/thermo/O2_C12_K5_U1_a => generative/components/theta/thermo/O2_C12_K5_U0_a}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py (100%) rename tests/tfscreen/tfmodel/{components/theta/thermo/O2_C4_K3_U0_a => generative/components/theta/thermo/O2_C12_K5_U1_a}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py (100%) rename tests/tfscreen/tfmodel/{components/theta/thermo/O2_C4_K3_U1_a => generative/components/theta/thermo/O2_C4_K3_U0_a}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py (100%) rename tests/tfscreen/tfmodel/{components/theta/thermo => generative/components/theta/thermo/O2_C4_K3_U1_a}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py (100%) rename tests/tfscreen/tfmodel/{components/theta_rescale => generative/components/theta/thermo}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/test_features.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/test_horseshoe.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/test_io.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/test_nn.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta/thermo/test_prior.py (100%) rename tests/tfscreen/tfmodel/{components/transformation => generative/components/theta_rescale}/__init__.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta_rescale/test_logit.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/theta_rescale/test_passthrough.py (100%) create mode 100644 tests/tfscreen/tfmodel/generative/components/transformation/__init__.py rename tests/tfscreen/tfmodel/{ => generative}/components/transformation/test__congression.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/transformation/test_empirical.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/transformation/test_logit_norm.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/components/transformation/test_single.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/observe/test_observe_binding.py (100%) rename tests/tfscreen/tfmodel/{ => generative}/observe/test_observe_growth.py (100%) rename tests/tfscreen/tfmodel/scripts/{test_run_prefit_calibration.py => test_prefit_calibration_cli.py} (100%) rename tests/tfscreen/tfmodel/scripts/{test_prior_predictive_cli.py => test_sample_prior_cli.py} (100%) rename tests/tfscreen/tfmodel/scripts/{test_subset_genotypes.py => test_subset_genotypes_cli.py} (100%) diff --git a/pyproject.toml b/pyproject.toml index 02d47bae..7c12e73e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,16 +73,16 @@ docs = ["pydata-sphinx-theme"] tfs-process-fastq = "tfscreen.process_raw.scripts.process_fastq_cli:main" tfs-process-counts = "tfscreen.process_raw.scripts.process_counts_cli:main" tfs-configure-model = "tfscreen.tfmodel.scripts.configure_model_cli:main" -tfs-prefit-calibration = "tfscreen.tfmodel.scripts.run_prefit_calibration_cli:main" +tfs-prefit-calibration = "tfscreen.tfmodel.scripts.prefit_calibration_cli:main" tfs-fit-model = "tfscreen.tfmodel.scripts.fit_model_cli:main" tfs-sample-posterior = "tfscreen.tfmodel.scripts.sample_posterior_cli:main" -tfs-sample-prior = "tfscreen.tfmodel.scripts.prior_predictive_cli:main" +tfs-sample-prior = "tfscreen.tfmodel.scripts.sample_prior_cli:main" tfs-extract-params = "tfscreen.tfmodel.scripts.extract_params_cli:main" tfs-predict-growth = "tfscreen.tfmodel.scripts.predict_growth_cli:main" tfs-predict-theta = "tfscreen.tfmodel.scripts.predict_theta_cli:main" -tfs-cat-response = "tfscreen.analysis.cat_response.cat_response_cli:main" +tfs-cat-response = "tfscreen.analysis.cat_response.scripts.cat_response_cli:main" tfs-diagnose-nan = "tfscreen.tfmodel.scripts.diagnose_nan_cli:main" -tfs-simulate = "tfscreen.simulate.scripts.run_simulation_cli:main" +tfs-simulate = "tfscreen.simulate.scripts.simulate_cli:main" tfs-setup-sim-grid = "tfscreen.simulate.scripts.setup_sim_grid_cli:main" tfs-setup-grid = "tfscreen.tfmodel.scripts.setup_grid_cli:main" tfs-summarize-grid = "tfscreen.tfmodel.scripts.summarize_grid_cli:main" diff --git a/reports/flake.txt b/reports/flake.txt index a9e5ef62..3d07d14a 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -5052,73 +5052,73 @@ ./tests/tfscreen/process_raw/test_fastq_to_counts.py:1091:1: W293 blank line contains whitespace ./tests/tfscreen/process_raw/test_fastq_to_counts.py:1099:1: W293 blank line contains whitespace ./tests/tfscreen/process_raw/test_fastq_to_counts.py:1104:45: W292 no newline at end of file -./tests/tfscreen/process_raw/test_process_counts.py:16:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:25:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:44:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:68:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:82:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:89:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:96:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:102:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_counts.py:108:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:2:1: F401 'unittest.mock.call' imported but unused -./tests/tfscreen/process_raw/test_process_fastq.py:22:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:31:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:38:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:39:26: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:33: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:40: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:47: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:54: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:61: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:68: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:75: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:82: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:39:89: E231 missing whitespace after ':' -./tests/tfscreen/process_raw/test_process_fastq.py:44:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:55:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:72:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:77:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:84:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:99:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:110:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:117:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:122:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:135:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:149:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:151:62: E231 missing whitespace after ',' -./tests/tfscreen/process_raw/test_process_fastq.py:153:62: E231 missing whitespace after ',' -./tests/tfscreen/process_raw/test_process_fastq.py:154:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:177:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:183:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:193:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:200:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:209:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:215:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:218:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:233:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:240:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:250:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:253:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:257:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:260:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:267:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:273:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:280:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:284:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:288:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:298:38: E261 at least two spaces before inline comment -./tests/tfscreen/process_raw/test_process_fastq.py:300:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:310:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:317:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:321:128: E501 line too long (132 > 127 characters) -./tests/tfscreen/process_raw/test_process_fastq.py:332:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:343:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:348:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:350:26: E261 at least two spaces before inline comment -./tests/tfscreen/process_raw/test_process_fastq.py:357:1: W293 blank line contains whitespace -./tests/tfscreen/process_raw/test_process_fastq.py:360:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/process_raw/test_process_fastq.py:368:78: W292 no newline at end of file +./tests/tfscreen/process_raw/test_process_counts_cli.py:16:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:25:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:44:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:82:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:89:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:96:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:102:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_counts_cli.py:108:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:2:1: F401 'unittest.mock.call' imported but unused +./tests/tfscreen/process_raw/test_process_fastq_cli.py:22:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:31:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:38:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:26: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:33: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:40: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:47: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:54: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:61: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:68: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:75: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:82: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:39:89: E231 missing whitespace after ':' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:44:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:55:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:72:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:77:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:84:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:99:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:110:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:117:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:122:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:135:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:149:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:151:62: E231 missing whitespace after ',' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:153:62: E231 missing whitespace after ',' +./tests/tfscreen/process_raw/test_process_fastq_cli.py:154:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:177:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:183:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:193:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:200:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:209:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:215:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:218:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:233:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:240:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:250:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:253:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:257:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:260:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:267:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:273:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:280:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:284:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:288:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:298:38: E261 at least two spaces before inline comment +./tests/tfscreen/process_raw/test_process_fastq_cli.py:300:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:310:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:317:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:321:128: E501 line too long (132 > 127 characters) +./tests/tfscreen/process_raw/test_process_fastq_cli.py:332:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:343:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:348:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:350:26: E261 at least two spaces before inline comment +./tests/tfscreen/process_raw/test_process_fastq_cli.py:357:1: W293 blank line contains whitespace +./tests/tfscreen/process_raw/test_process_fastq_cli.py:360:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/process_raw/test_process_fastq_cli.py:368:78: W292 no newline at end of file ./tests/tfscreen/simulate/growth/test_growth_linkage.py:50:16: E221 multiple spaces before operator ./tests/tfscreen/simulate/growth/test_growth_linkage.py:95:16: E221 multiple spaces before operator ./tests/tfscreen/simulate/growth/test_growth_linkage.py:154:16: E221 multiple spaces before operator @@ -5209,9 +5209,6 @@ ./tests/tfscreen/simulate/test_run_simulation.py:113:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_run_simulation.py:116:5: F841 local variable 'results' is assigned to but never used ./tests/tfscreen/simulate/test_run_simulation.py:117:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_run_simulation_cli.py:3:1: F401 'os' imported but unused -./tests/tfscreen/simulate/test_run_simulation_cli.py:165:5: F401 'jax' imported but unused -./tests/tfscreen/simulate/test_run_simulation_cli.py:230:5: F401 'tfscreen.simulate.scripts.simulate_cli as cli_mod' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:4:1: F401 'numpy.ma' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:6:1: F401 'scipy.stats.gmean' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5363,6 +5360,9 @@ ./tests/tfscreen/simulate/test_selection_experiment.py:1269:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:1272:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:1313:27: W292 no newline at end of file +./tests/tfscreen/simulate/test_simulate_cli.py:3:1: F401 'os' imported but unused +./tests/tfscreen/simulate/test_simulate_cli.py:165:5: F401 'jax' imported but unused +./tests/tfscreen/simulate/test_simulate_cli.py:230:5: F401 'tfscreen.simulate.scripts.simulate_cli as cli_mod' imported but unused ./tests/tfscreen/simulate/test_thermo_to_growth.py:28:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:32:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:41:1: E302 expected 2 blank lines, found 1 @@ -5377,1118 +5377,1118 @@ ./tests/tfscreen/simulate/test_thermo_to_growth.py:1251:9: F841 local variable 'G_out' is assigned to but never used ./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:313:9: F401 'matplotlib.axes.Axes' imported but unused ./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:355:9: F401 'matplotlib.axes.Axes' imported but unused -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:42:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:49:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:57:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:64:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:66:45: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:67:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:72:19: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:73:24: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:76:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:80:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:83:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:86:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:90:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:94:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:97:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_fixed.py:113:42: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:3:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:23:17: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:27:18: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:28:18: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:31:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:40:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:44:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:47:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:50:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:54:29: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:55:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:69:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:76:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:82:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:89:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:97:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:103:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:106:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:111:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:118:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:120:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:121:55: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:126:19: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:127:24: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:130:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:134:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:137:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:142:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:147:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:153:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py:356:6: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/activity/test_hierarchical_mut.py:36:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:3:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:26:18: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:27:18: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:30:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:39:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:43:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:46:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:49:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:53:29: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:54:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:68:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:75:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:81:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:85:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:87:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:91:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:94:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:97:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:100:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:104:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:110:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:113:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:117:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:124:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:126:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:127:55: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:132:19: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:133:24: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:136:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:140:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:143:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:147:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:152:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:158:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:174:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py:176:42: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/activity/test_horseshoe_mut.py:36:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:29:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:42:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:49:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:57:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:64:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:66:44: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:67:49: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:72:19: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:73:24: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:76:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:80:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:83:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:86:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:90:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:94:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:97:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py:113:41: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:4:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:78:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:82:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:120:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:121:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:183:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:197:34: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:282:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:283:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:314:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:352:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:353:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:370:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:383:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:386:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:389:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:393:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:409:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:410:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:411:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:412:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:415:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py:426:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/growth/test_linear.py:7:1: F401 'tfscreen.tfmodel.generative.components.growth.linear.LinearParams' imported but unused -./tests/tfscreen/tfmodel/components/growth/test_linear.py:55:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/growth/test_linear.py:132:28: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/growth/test_linear.py:134:28: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/growth/test_power.py:3:1: F401 'numpyro.handlers.substitute' imported but unused -./tests/tfscreen/tfmodel/components/growth/test_power.py:6:1: F401 'tfscreen.tfmodel.generative.components.growth.power.PowerParams' imported but unused -./tests/tfscreen/tfmodel/components/growth/test_power.py:23:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/growth/test_saturation.py:6:1: F401 'tfscreen.tfmodel.generative.components.growth.saturation.SaturationParams' imported but unused -./tests/tfscreen/tfmodel/components/growth/test_saturation.py:23:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/growth_noise/test_normal_kt.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/growth_noise/test_normal_kt.py:3:1: F401 'jax.numpy as jnp' imported but unused -./tests/tfscreen/tfmodel/components/growth_noise/test_normal_kt.py:4:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/growth_noise/test_normal_kt.py:5:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/growth_noise/test_zero.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:8:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:12:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:19:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:23:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:28:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:30:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:35:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:38:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:42:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:47:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:49:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_instant.py:51:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/growth_transition/test_memory.py:99:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/growth_transition/test_memory.py:100:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/growth_transition/test_memory.py:101:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/growth_transition/test_memory.py:102:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/growth_transition/test_memory.py:103:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:5:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:6:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:38:35: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:74:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:76:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:77:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:80:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:107:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:109:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:110:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:113:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:139:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:141:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:142:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:145:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:181:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:183:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:184:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:187:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:221:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:223:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:224:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:227:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:232:77: E202 whitespace before ']' -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:455:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:471:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:508:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:509:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:555:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:556:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:574:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:576:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:577:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:618:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:621:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:669:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:671:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:672:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:675:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:715:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:718:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:723:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:748:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:750:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:752:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:760:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:773:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:775:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:777:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:786:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:798:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:799:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:802:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:816:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:817:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:835:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:837:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:838:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:899:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:901:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:902:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:958:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:962:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:994:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:1015:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:1136:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py:1254:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:5:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:6:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:115:77: E202 whitespace before ']' -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:144:32: E131 continuation line unaligned for hanging indent -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:146:32: E131 continuation line unaligned for hanging indent -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:149:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:201:12: F541 f-string is missing placeholders -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:208:12: F541 f-string is missing placeholders -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:254:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:256:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:258:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:267:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:269:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:288:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:290:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:293:7: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:294:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:306:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:310:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:311:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:312:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:323:7: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:324:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:338:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:343:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:363:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:370:7: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:385:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:393:7: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:404:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:412:7: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:425:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:426:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:442:7: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:462:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:474:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:476:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:488:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:490:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:502:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:514:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:516:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:526:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:528:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:538:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:556:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:582:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:616:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:634:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py:653:9: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/noise/test_beta.py:20:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:24:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:30:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:35:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:41:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:45:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:47:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:51:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:54:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:60:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:66:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:69:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:77:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:85:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:88:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:100:19: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:101:25: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:104:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:109:30: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/noise/test_beta.py:110:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:114:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:118:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:121:65: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/noise/test_beta.py:122:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:125:64: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/noise/test_beta.py:126:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:131:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:137:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:158:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:161:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:165:58: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:166:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:170:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:175:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_beta.py:183:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:192:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:197:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:201:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:206:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:211:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:215:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_beta.py:217:58: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/noise/test_logit_normal.py:4:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/noise/test_zero.py:3:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/noise/test_zero.py:5:1: F401 'collections.namedtuple' imported but unused -./tests/tfscreen/tfmodel/components/noise/test_zero.py:19:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_zero.py:22:18: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_zero.py:32:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_zero.py:39:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_zero.py:44:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_zero.py:51:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:54:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:56:42: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:57:51: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:61:37: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/noise/test_zero.py:63:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:66:19: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:67:28: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:70:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:75:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/noise/test_zero.py:82:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:85:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:87:35: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:88:44: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/noise/test_zero.py:93:46: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/sample_offset/test_normal.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/sample_offset/test_normal.py:5:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/sample_offset/test_normal.py:6:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/sample_offset/test_zero.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/sample_offset/test_zero.py:2:1: F401 'jax.numpy as jnp' imported but unused -./tests/tfscreen/tfmodel/components/test_pinning.py:4:1: F401 'numpyro as pyro' imported but unused -./tests/tfscreen/tfmodel/components/theta/test__simple.py:6:1: F401 'tfscreen.tfmodel.generative.components.theta._simple._THETA_EPS' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:5:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:6:1: F401 'numpyro.handlers.trace' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:10:1: F401 'tfscreen.tfmodel.generative.components.theta.categorical_geno.ThetaParam' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:38:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:46:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:51:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:55:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:69:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:77:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:86:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:91:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:96:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:102:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:107:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:119:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:125:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:130:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:146:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:153:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:158:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:167:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:172:21: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:174:34: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:175:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:176:50: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:180:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:186:37: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:188:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:190:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:193:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:200:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:220:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py:379:44: W292 no newline at end of file -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:4:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:5:1: F401 'numpyro.handlers.trace' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:41:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:176:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:179:36: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:180:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:205:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:248:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:252:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:253:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:278:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:279:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:285:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:285:64: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:304:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:320:20: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:329:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:331:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:355:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:357:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:358:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:477:51: E222 multiple spaces after operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:492:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:537:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:540:36: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_geno.py:541:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:12:1: F401 'tfscreen.tfmodel.generative.components.theta.hill_mut._population_moments' imported but unused -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:55:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:61:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:68:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:125:15: F841 local variable 'G' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:136:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:140:15: F841 local variable 'G' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:140:18: F841 local variable 'P' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:246:9: F841 local variable 'M' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:254:62: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:453:56: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:480:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:482:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:492:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:494:26: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:509:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:512:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:513:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:528:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:530:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_hill_mut.py:530:52: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:25:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:30:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:155:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:156:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:157:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:158:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:159:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:160:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:161:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:162:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:164:36: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:166:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:167:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:204:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:205:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py:301:19: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:41:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:58:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:195:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:248:6: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:280:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:315:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:332:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:333:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:346:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:347:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:424:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:425:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:426:45: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:427:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:428:34: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:429:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:430:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:443:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:485:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:596:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:615:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:615:28: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:615:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:616:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:617:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:617:28: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:617:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:618:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:618:28: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:618:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:685:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:693:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:43:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:97:45: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:138:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:195:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:197:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:223:38: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:245:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:266:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:277:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:308:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:327:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:391:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:473:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:478:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:509:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:528:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:528:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:528:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:529:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:530:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:530:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:530:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:531:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:531:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:531:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:588:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:591:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:595:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:599:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:602:15: E222 multiple spaces after operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:754:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:755:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:756:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:757:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:759:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:42:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:113:45: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:165:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:249:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:251:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:299:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:305:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:306:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:338:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:349:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:380:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:418:26: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:426:25: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:457:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:463:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:464:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:543:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:545:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:571:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:590:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:590:28: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:590:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:591:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:592:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:592:28: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:592:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:593:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:593:28: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:593:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:647:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:5:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:21:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:32:14: E222 multiple spaces after operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:258:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:35:6: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:217:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:217:33: E201 whitespace after '[' -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:219:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:49:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:66:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:203:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:275:6: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:276:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:277:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:278:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:292:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:299:45: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:306:46: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:321:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:360:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:361:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:378:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:379:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:394:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:395:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:421:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:422:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:509:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:520:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:521:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:523:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:524:34: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:525:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:526:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:540:39: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:541:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:542:45: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:543:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:544:34: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:545:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:546:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:559:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:606:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:607:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:617:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:709:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:745:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:764:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:764:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:764:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:765:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:766:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:766:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:766:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:767:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:767:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:767:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:795:24: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:803:24: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:812:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:820:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:830:28: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:844:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:852:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:53:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:107:45: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:147:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:204:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:206:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:234:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:239:38: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:272:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:285:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:294:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:305:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:351:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:380:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:399:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:433:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:438:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:531:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:536:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:567:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:586:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:586:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:586:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:587:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:588:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:588:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:588:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:589:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:589:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:589:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:616:24: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:648:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:651:1: E402 module level import not at top of file -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:655:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:658:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:659:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:662:15: E222 multiple spaces after operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:667:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:694:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:695:29: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:696:29: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:697:29: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:698:29: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:699:29: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:820:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:49:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:121:45: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:163:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:230:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:232:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:263:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:287:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:288:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:298:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:299:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:320:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:329:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:340:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:386:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:427:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:430:26: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:447:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:453:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:454:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:544:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:546:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:572:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:591:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:591:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:591:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:592:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:593:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:593:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:593:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:594:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:594:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:594:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:621:24: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:646:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:28:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:39:6: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:43:8: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:310:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:310:33: E201 whitespace after '[' -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:312:32: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:465:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:486:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:514:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:516:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:47:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:48:25: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:61:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:68:1: E305 expected 2 blank lines after class or function definition, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:68:6: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:282:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:283:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:284:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:343:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:344:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:345:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:346:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:347:34: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:348:46: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:349:46: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:361:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:362:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:363:42: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:364:42: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:365:42: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:366:41: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:376:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:47:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:98:45: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:123:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:134:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:265:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:274:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:292:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:308:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:383:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:448:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:467:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:467:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:467:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:468:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:469:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:469:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:469:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:470:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:470:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:470:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:504:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:9:1: F401 'functools.partial' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:40:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:211:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:212:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:213:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:214:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:220:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:221:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:237:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:270:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:271:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:272:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:273:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:278:27: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:279:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:280:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:281:32: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:282:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:283:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:284:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:318:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:325:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:326:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:343:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:381:38: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:383:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:491:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:503:28: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:513:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:5:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:21:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:33:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:51:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:52:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:53:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:54:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:58:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:71:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:72:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:73:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:74:27: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:78:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:125:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:244:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:66:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:73:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:82:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:89:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:98:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:101:19: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:259:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:263:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:47:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:48:25: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:54:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:61:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:68:1: E305 expected 2 blank lines after class or function definition, found 1 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:68:6: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:313:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:314:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:315:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:326:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:327:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:328:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:348:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:349:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:350:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:351:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:353:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:354:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:400:38: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:401:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:402:44: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:403:35: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:404:34: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:405:46: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:406:46: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:417:37: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:418:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:419:43: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:431:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:432:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:433:42: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:434:42: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:435:42: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:436:41: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:438:41: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:448:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:450:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:506:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:549:46: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:550:45: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:551:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:554:48: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:555:47: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:556:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:558:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:564:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:47:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:100:45: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:215:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:220:38: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:229:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:253:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:265:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:266:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:275:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:286:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:310:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:311:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:323:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:340:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:365:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:432:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:433:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:461:37: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:494:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:495:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:496:23: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:499:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:530:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:549:17: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:549:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:549:40: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:550:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:551:18: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:551:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:551:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:552:19: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:552:29: E701 multiple statements on one line (colon) -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:552:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:9:1: F401 'functools.partial' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:39:24: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:208:41: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:218:42: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:229:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:230:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:231:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:232:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:233:34: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:244:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:245:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:261:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:272:5: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:324:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:326:33: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:343:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:350:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:351:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:400:28: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:448:38: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:449:40: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:450:38: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:451:40: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:452:39: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:460:37: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:488:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:489:32: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:510:30: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:582:26: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:594:28: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:604:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:67:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:75:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:84:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:91:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:100:14: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:103:19: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:109:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:165:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:171:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:216:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:310:12: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:319:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:323:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:62:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:63:22: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:150:19: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:168:9: F841 local variable 'sdata' is assigned to but never used -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:177:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:178:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_features.py:179:33: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:6:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:7:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:30:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:37:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:38:31: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:39:30: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:43:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:49:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:61:18: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:71:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:76:15: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:82:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py:89:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:39:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:41:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:130:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:147:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:166:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:167:11: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:172:31: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:229:49: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:236:49: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:241:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:244:55: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:272:13: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_io.py:377:49: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:3:1: F401 'jax' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:7:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:9:1: F401 'tfscreen.tfmodel.generative.components.theta.thermo.nn._DEFAULT_HIDDEN_SIZE' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:24:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:26:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:39:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:41:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:54:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:72:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:74:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:77:9: E306 expected 1 blank line before a nested definition, found 0 -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:95:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py:97:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:6:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:7:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:29:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:36:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:43:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:51:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:75:21: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:84:16: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py:91:17: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/components/theta_rescale/test_logit.py:2:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:3:1: F401 'numpy as np' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:5:1: F401 'numpyro.distributions as dist' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:6:1: F401 'unittest.mock.patch' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:16:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:22:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:24:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:29:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:33:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:37:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:39:32: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:40:36: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:41:47: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:53:26: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:54:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:56:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:58:24: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:63:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:65:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:68:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:71:26: W291 trailing whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:73:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:75:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:79:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:82:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:84:35: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:85:38: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:86:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:88:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:93:55: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:103:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:104:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:109:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:126:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:131:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:135:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:141:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:148:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:161:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:166:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:169:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test__congression.py:185:1: W391 blank line at end of file -./tests/tfscreen/tfmodel/components/transformation/test_empirical.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test_logit_norm.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test_logit_norm.py:3:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test_single.py:1:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test_single.py:4:1: F401 'collections.namedtuple' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test_single.py:7:1: F401 'tfscreen.tfmodel.data_class.GrowthData' imported but unused -./tests/tfscreen/tfmodel/components/transformation/test_single.py:9:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test_single.py:15:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test_single.py:21:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test_single.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test_single.py:31:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test_single.py:36:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/components/transformation/test_single.py:39:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/components/transformation/test_single.py:43:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:3:1: F401 'numpyro' imported but unused -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:25:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:32:22: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:33:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:36:51: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:37:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:38:26: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:52:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:62:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:69:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:73:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:75:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:79:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:83:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:87:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:92:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:95:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:97:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:105:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:108:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:115:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_binding.py:117:41: W292 no newline at end of file -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:22:20: W291 trailing whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:28:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:32:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:42:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:45:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:47:76: W291 trailing whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:49:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:52:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:55:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:58:31: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:58:33: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:58:35: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:58:37: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:58:39: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:58:41: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:75:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:99:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:107:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:112:56: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:113:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:118:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:125:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:127:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:133:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:137:24: W291 trailing whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:138:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:146:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:151:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:154:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:156:35: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:156:37: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:156:39: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:156:41: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:156:43: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:156:45: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:162:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:168:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:171:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:173:23: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:173:25: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:173:27: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:173:29: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:173:31: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:173:33: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:174:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:176:23: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:176:25: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:176:27: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:176:29: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:176:31: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:176:33: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:178:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:184:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:192:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:197:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/observe/test_observe_growth.py:259:57: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:29:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:42:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:49:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:54:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:57:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:64:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:66:45: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:67:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:72:19: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:73:24: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:76:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:80:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:83:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:86:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:90:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:94:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:97:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:113:42: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:3:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:23:17: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:27:18: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:28:18: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:31:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:40:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:44:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:47:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:50:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:54:29: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:55:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:69:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:76:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:82:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:89:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:97:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:103:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:106:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:111:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:118:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:120:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:121:55: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:126:19: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:127:24: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:130:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:134:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:137:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:142:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:147:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:153:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py:356:6: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_mut.py:36:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:3:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:26:18: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:27:18: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:30:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:39:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:43:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:46:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:49:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:53:29: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:54:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:75:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:81:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:85:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:87:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:91:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:94:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:97:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:100:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:104:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:110:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:113:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:117:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:124:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:126:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:127:55: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:132:19: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:133:24: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:136:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:140:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:143:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:147:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:152:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:158:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:174:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py:176:42: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_mut.py:36:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:29:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:36:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:42:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:49:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:54:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:57:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:64:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:66:44: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:67:49: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:72:19: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:73:24: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:76:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:80:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:83:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:86:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:90:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:94:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:97:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py:113:41: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:4:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:78:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:82:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:120:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:121:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:183:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:197:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:282:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:283:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:314:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:352:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:353:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:370:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:383:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:386:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:389:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:393:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:409:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:410:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:411:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:412:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:415:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:426:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:7:1: F401 'tfscreen.tfmodel.generative.components.growth.linear.LinearParams' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:55:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:132:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:134:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/growth/test_power.py:3:1: F401 'numpyro.handlers.substitute' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth/test_power.py:6:1: F401 'tfscreen.tfmodel.generative.components.growth.power.PowerParams' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth/test_power.py:23:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py:6:1: F401 'tfscreen.tfmodel.generative.components.growth.saturation.SaturationParams' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py:23:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/growth_noise/test_normal_kt.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth_noise/test_normal_kt.py:3:1: F401 'jax.numpy as jnp' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth_noise/test_normal_kt.py:4:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth_noise/test_normal_kt.py:5:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth_noise/test_zero.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:8:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:12:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:19:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:23:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:28:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:30:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:35:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:38:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:42:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:47:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:49:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py:51:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py:99:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py:100:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py:101:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py:102:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py:103:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:5:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:6:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:38:35: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:74:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:76:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:77:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:80:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:107:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:109:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:110:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:113:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:139:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:141:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:142:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:145:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:181:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:183:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:184:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:187:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:221:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:223:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:224:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:227:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:232:77: E202 whitespace before ']' +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:455:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:471:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:508:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:509:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:555:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:556:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:574:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:576:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:577:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:618:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:621:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:669:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:671:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:672:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:675:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:715:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:718:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:723:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:748:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:750:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:752:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:760:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:773:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:775:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:777:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:786:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:798:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:799:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:802:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:816:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:817:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:835:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:837:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:838:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:899:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:901:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:902:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:958:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:962:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:994:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:1015:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:1136:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py:1254:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:5:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:6:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:115:77: E202 whitespace before ']' +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:144:32: E131 continuation line unaligned for hanging indent +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:146:32: E131 continuation line unaligned for hanging indent +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:149:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:201:12: F541 f-string is missing placeholders +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:208:12: F541 f-string is missing placeholders +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:254:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:256:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:258:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:267:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:269:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:288:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:290:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:293:7: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:294:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:306:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:310:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:311:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:312:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:323:7: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:324:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:338:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:343:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:363:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:370:7: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:385:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:393:7: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:404:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:412:7: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:425:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:426:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:442:7: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:462:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:474:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:476:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:488:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:490:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:502:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:514:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:516:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:526:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:528:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:538:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:556:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:582:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:616:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:634:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py:653:9: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:20:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:24:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:30:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:35:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:41:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:45:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:47:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:51:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:54:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:60:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:66:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:69:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:77:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:85:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:88:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:100:19: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:101:25: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:104:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:109:30: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:110:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:114:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:118:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:121:65: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:122:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:125:64: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:126:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:131:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:137:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:158:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:161:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:165:58: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:166:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:170:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:175:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:183:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:192:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:197:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:201:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:206:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:211:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:215:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_beta.py:217:58: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/noise/test_logit_normal.py:4:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:3:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:5:1: F401 'collections.namedtuple' imported but unused +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:19:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:22:18: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:26:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:32:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:39:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:44:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:51:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:54:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:56:42: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:57:51: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:61:37: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:63:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:66:19: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:67:28: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:70:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:75:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:82:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:85:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:87:35: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:88:44: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/noise/test_zero.py:93:46: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/sample_offset/test_normal.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/sample_offset/test_normal.py:5:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/sample_offset/test_normal.py:6:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/sample_offset/test_zero.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/sample_offset/test_zero.py:2:1: F401 'jax.numpy as jnp' imported but unused +./tests/tfscreen/tfmodel/generative/components/test_pinning.py:4:1: F401 'numpyro as pyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test__simple.py:6:1: F401 'tfscreen.tfmodel.generative.components.theta._simple._THETA_EPS' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:5:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:6:1: F401 'numpyro.handlers.trace' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:10:1: F401 'tfscreen.tfmodel.generative.components.theta.categorical_geno.ThetaParam' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:38:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:46:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:51:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:55:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:69:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:77:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:86:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:91:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:96:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:102:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:107:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:119:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:125:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:130:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:146:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:153:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:158:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:167:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:172:21: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:174:34: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:175:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:176:50: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:180:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:186:37: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:188:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:190:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:193:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:200:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:220:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:379:44: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:4:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:5:1: F401 'numpyro.handlers.trace' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:41:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:176:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:179:36: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:180:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:205:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:248:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:252:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:253:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:278:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:279:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:285:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:285:64: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:304:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:320:20: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:329:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:331:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:355:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:357:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:358:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:477:51: E222 multiple spaces after operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:492:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:537:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:540:36: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py:541:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:12:1: F401 'tfscreen.tfmodel.generative.components.theta.hill_mut._population_moments' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:55:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:61:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:125:15: F841 local variable 'G' is assigned to but never used +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:136:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:140:15: F841 local variable 'G' is assigned to but never used +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:140:18: F841 local variable 'P' is assigned to but never used +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:246:9: F841 local variable 'M' is assigned to but never used +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:254:62: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:453:56: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:480:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:482:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:492:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:494:26: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:509:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:512:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:513:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:528:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:530:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:530:52: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:25:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:30:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:155:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:156:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:157:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:158:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:159:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:160:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:161:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:162:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:164:36: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:166:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:167:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:204:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:205:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:301:19: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:41:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:58:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:195:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:248:6: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:280:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:315:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:332:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:333:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:346:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:347:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:424:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:425:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:426:45: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:427:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:428:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:429:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:430:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:443:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:485:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:596:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:615:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:615:28: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:615:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:616:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:617:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:617:28: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:617:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:618:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:618:28: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:618:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:685:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py:693:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:43:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:97:45: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:138:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:195:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:197:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:223:38: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:245:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:266:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:277:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:308:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:327:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:391:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:473:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:478:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:509:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:528:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:528:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:528:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:529:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:530:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:530:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:530:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:531:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:531:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:531:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:588:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:591:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:595:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:599:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:602:15: E222 multiple spaces after operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:754:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:755:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:756:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:757:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py:759:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:42:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:113:45: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:165:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:249:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:251:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:299:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:305:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:306:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:338:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:349:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:380:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:418:26: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:426:25: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:457:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:463:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:464:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:543:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:545:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:571:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:590:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:590:28: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:590:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:591:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:592:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:592:28: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:592:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:593:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:593:28: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:593:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py:647:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:5:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:21:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:32:14: E222 multiple spaces after operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py:258:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:26:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:35:6: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:217:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:217:33: E201 whitespace after '[' +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py:219:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:49:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:66:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:203:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:275:6: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:276:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:277:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:278:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:292:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:299:45: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:306:46: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:321:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:360:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:361:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:378:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:379:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:394:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:395:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:421:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:422:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:509:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:520:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:521:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:523:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:524:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:525:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:526:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:540:39: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:541:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:542:45: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:543:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:544:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:545:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:546:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:559:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:606:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:607:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:617:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:709:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:745:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:764:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:764:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:764:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:765:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:766:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:766:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:766:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:767:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:767:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:767:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:795:24: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:803:24: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:812:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:820:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:830:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:844:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py:852:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:53:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:107:45: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:147:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:204:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:206:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:234:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:239:38: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:272:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:285:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:294:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:305:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:351:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:380:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:399:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:433:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:438:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:531:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:536:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:567:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:586:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:586:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:586:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:587:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:588:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:588:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:588:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:589:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:589:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:589:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:616:24: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:648:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:651:1: E402 module level import not at top of file +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:655:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:658:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:659:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:662:15: E222 multiple spaces after operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:667:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:694:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:695:29: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:696:29: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:697:29: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:698:29: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:699:29: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py:820:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:49:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:121:45: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:163:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:230:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:232:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:263:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:287:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:288:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:298:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:299:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:320:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:329:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:340:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:386:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:427:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:430:26: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:447:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:453:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:454:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:544:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:546:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:572:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:591:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:591:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:591:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:592:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:593:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:593:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:593:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:594:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:594:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:594:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:621:24: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py:646:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:28:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:39:6: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:43:8: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:310:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:310:33: E201 whitespace after '[' +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:312:32: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:465:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:486:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:514:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py:516:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:47:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:48:25: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:54:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:61:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:68:1: E305 expected 2 blank lines after class or function definition, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:68:6: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:282:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:283:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:284:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:343:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:344:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:345:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:346:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:347:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:348:46: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:349:46: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:361:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:362:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:363:42: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:364:42: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:365:42: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:366:41: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py:376:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:47:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:98:45: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:123:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:134:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:265:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:274:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:292:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:308:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:383:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:448:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:467:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:467:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:467:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:468:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:469:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:469:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:469:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:470:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:470:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:470:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py:504:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:9:1: F401 'functools.partial' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:40:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:211:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:212:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:213:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:214:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:220:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:221:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:237:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:270:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:271:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:272:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:273:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:278:27: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:279:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:280:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:281:32: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:282:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:283:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:284:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:318:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:325:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:326:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:343:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:381:38: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:383:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:491:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:503:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py:513:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:5:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:21:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:33:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:51:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:52:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:53:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:54:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:58:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:71:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:72:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:73:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:74:27: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:78:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:125:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py:244:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:66:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:73:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:82:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:89:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:98:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:101:19: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:259:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py:263:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:47:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:48:25: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:54:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:61:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:68:1: E305 expected 2 blank lines after class or function definition, found 1 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:68:6: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:313:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:314:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:315:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:326:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:327:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:328:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:348:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:349:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:350:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:351:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:353:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:354:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:400:38: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:401:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:402:44: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:403:35: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:404:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:405:46: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:406:46: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:417:37: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:418:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:419:43: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:431:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:432:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:433:42: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:434:42: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:435:42: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:436:41: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:438:41: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:448:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:450:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:506:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:549:46: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:550:45: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:551:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:554:48: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:555:47: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:556:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:558:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py:564:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:47:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:100:45: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:215:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:220:38: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:229:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:253:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:265:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:266:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:275:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:286:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:310:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:311:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:323:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:340:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:365:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:432:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:433:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:461:37: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:494:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:495:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:496:23: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:499:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:530:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:549:17: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:549:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:549:40: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:550:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:551:18: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:551:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:551:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:552:19: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:552:29: E701 multiple statements on one line (colon) +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py:552:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:9:1: F401 'functools.partial' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:39:24: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:208:41: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:218:42: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:229:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:230:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:231:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:232:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:233:34: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:244:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:245:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:261:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:272:5: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:324:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:326:33: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:343:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:350:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:351:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:400:28: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:448:38: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:449:40: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:450:38: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:451:40: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:452:39: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:460:37: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:488:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:489:32: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:510:30: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:582:26: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:594:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py:604:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:67:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:75:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:84:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:91:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:100:14: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:103:19: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:109:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:165:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:171:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:216:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:310:12: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:319:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py:323:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:62:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:63:22: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:150:19: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:168:9: F841 local variable 'sdata' is assigned to but never used +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:177:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:178:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py:179:33: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:6:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:7:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:30:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:37:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:38:31: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:39:30: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:43:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:49:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:61:18: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:71:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:76:15: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:82:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py:89:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:39:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:41:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:130:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:147:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:166:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:167:11: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:172:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:229:49: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:236:49: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:241:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:244:55: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:272:13: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py:377:49: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:3:1: F401 'jax' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:7:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:9:1: F401 'tfscreen.tfmodel.generative.components.theta.thermo.nn._DEFAULT_HIDDEN_SIZE' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:24:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:26:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:39:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:41:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:54:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:72:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:74:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:77:9: E306 expected 1 blank line before a nested definition, found 0 +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:95:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py:97:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:6:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:7:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:29:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:36:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:43:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:51:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:75:21: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:84:16: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py:91:17: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta_rescale/test_logit.py:2:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:3:1: F401 'numpy as np' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:5:1: F401 'numpyro.distributions as dist' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:6:1: F401 'unittest.mock.patch' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:16:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:22:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:24:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:29:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:33:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:37:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:39:32: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:40:36: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:41:47: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:53:26: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:54:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:56:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:58:24: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:59:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:63:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:65:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:68:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:71:26: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:73:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:75:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:79:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:82:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:84:35: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:85:38: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:86:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:88:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:93:55: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:103:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:104:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:109:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:126:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:131:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:135:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:141:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:148:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:161:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:166:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:169:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py:185:1: W391 blank line at end of file +./tests/tfscreen/tfmodel/generative/components/transformation/test_empirical.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test_logit_norm.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test_logit_norm.py:3:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:1:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:4:1: F401 'collections.namedtuple' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:7:1: F401 'tfscreen.tfmodel.data_class.GrowthData' imported but unused +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:9:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:15:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:21:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:26:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:31:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:36:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:39:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/components/transformation/test_single.py:43:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:3:1: F401 'numpyro' imported but unused +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:25:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:32:22: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:33:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:36:51: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:37:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:38:26: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:52:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:59:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:62:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:69:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:73:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:75:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:79:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:83:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:87:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:92:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:95:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:97:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:105:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:108:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:115:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py:117:41: W292 no newline at end of file +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:22:20: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:28:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:32:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:42:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:45:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:47:76: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:49:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:52:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:55:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:58:31: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:58:33: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:58:35: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:58:37: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:58:39: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:58:41: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:59:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:75:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:99:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:107:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:112:56: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:113:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:118:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:125:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:127:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:133:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:137:24: W291 trailing whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:138:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:146:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:151:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:154:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:156:35: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:156:37: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:156:39: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:156:41: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:156:43: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:156:45: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:162:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:168:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:171:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:173:23: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:173:25: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:173:27: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:173:29: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:173:31: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:173:33: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:174:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:176:23: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:176:25: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:176:27: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:176:29: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:176:31: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:176:33: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:178:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:184:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:192:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:197:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py:259:57: W292 no newline at end of file ./tests/tfscreen/tfmodel/scripts/test_diagnose_nan_cli.py:77:64: E203 whitespace before ',' ./tests/tfscreen/tfmodel/scripts/test_extract_params_cli.py:14:1: F401 'numpyro.infer.svi.SVIState' imported but unused ./tests/tfscreen/tfmodel/scripts/test_fit_model_cli.py:5:1: F401 'dill' imported but unused @@ -6500,27 +6500,27 @@ ./tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py:4:1: F401 'os' imported but unused ./tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py:7:1: F401 'numpy as np' imported but unused ./tests/tfscreen/tfmodel/scripts/test_predict_theta_cli.py:8:1: F401 'unittest.mock.call' imported but unused -./tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py:3:1: F401 'pytest' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:11:1: F401 'dataclasses' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:13:1: F401 'shutil' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:14:1: F401 'sys' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:21:1: F401 'jax' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:26:1: F401 'tfscreen.tfmodel.scripts.prefit_calibration_cli._identify_field_mapping' imported but unused -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:649:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:650:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:651:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:652:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:653:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:654:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:655:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:656:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:657:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1009:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1028:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py:1043:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:11:1: F401 'dataclasses' imported but unused +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:13:1: F401 'shutil' imported but unused +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:14:1: F401 'sys' imported but unused +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:21:1: F401 'jax' imported but unused +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:26:1: F401 'tfscreen.tfmodel.scripts.prefit_calibration_cli._identify_field_mapping' imported but unused +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:649:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:650:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:651:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:652:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:653:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:654:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:655:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:656:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:657:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1009:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1028:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1043:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused -./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:84:31: E741 ambiguous variable name 'l' -./tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py:598:28: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_sample_prior_cli.py:3:1: F401 'pytest' imported but unused +./tests/tfscreen/tfmodel/scripts/test_subset_genotypes_cli.py:84:31: E741 ambiguous variable name 'l' +./tests/tfscreen/tfmodel/scripts/test_subset_genotypes_cli.py:598:28: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:16:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:17:1: E402 module level import not at top of file ./tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py:18:1: E402 module level import not at top of file diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index c0fe10a4..25fa1cdc 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/tfscreen/process_raw/test_process_counts.py b/tests/tfscreen/process_raw/test_process_counts_cli.py similarity index 100% rename from tests/tfscreen/process_raw/test_process_counts.py rename to tests/tfscreen/process_raw/test_process_counts_cli.py diff --git a/tests/tfscreen/process_raw/test_process_fastq.py b/tests/tfscreen/process_raw/test_process_fastq_cli.py similarity index 100% rename from tests/tfscreen/process_raw/test_process_fastq.py rename to tests/tfscreen/process_raw/test_process_fastq_cli.py diff --git a/tests/tfscreen/simulate/test_run_simulation_cli.py b/tests/tfscreen/simulate/test_simulate_cli.py similarity index 100% rename from tests/tfscreen/simulate/test_run_simulation_cli.py rename to tests/tfscreen/simulate/test_simulate_cli.py diff --git a/tests/tfscreen/tfmodel/components/activity/__init__.py b/tests/tfscreen/tfmodel/generative/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/activity/__init__.py rename to tests/tfscreen/tfmodel/generative/__init__.py diff --git a/tests/tfscreen/tfmodel/components/dk_geno/__init__.py b/tests/tfscreen/tfmodel/generative/components/activity/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/dk_geno/__init__.py rename to tests/tfscreen/tfmodel/generative/components/activity/__init__.py diff --git a/tests/tfscreen/tfmodel/components/activity/test_fixed.py b/tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py similarity index 100% rename from tests/tfscreen/tfmodel/components/activity/test_fixed.py rename to tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py diff --git a/tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py b/tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py similarity index 100% rename from tests/tfscreen/tfmodel/components/activity/test_hierarchical_geno.py rename to tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_geno.py diff --git a/tests/tfscreen/tfmodel/components/activity/test_hierarchical_mut.py b/tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_mut.py similarity index 100% rename from tests/tfscreen/tfmodel/components/activity/test_hierarchical_mut.py rename to tests/tfscreen/tfmodel/generative/components/activity/test_hierarchical_mut.py diff --git a/tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py b/tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py similarity index 100% rename from tests/tfscreen/tfmodel/components/activity/test_horseshoe_geno.py rename to tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_geno.py diff --git a/tests/tfscreen/tfmodel/components/activity/test_horseshoe_mut.py b/tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_mut.py similarity index 100% rename from tests/tfscreen/tfmodel/components/activity/test_horseshoe_mut.py rename to tests/tfscreen/tfmodel/generative/components/activity/test_horseshoe_mut.py diff --git a/tests/tfscreen/tfmodel/components/growth/__init__.py b/tests/tfscreen/tfmodel/generative/components/dk_geno/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth/__init__.py rename to tests/tfscreen/tfmodel/generative/components/dk_geno/__init__.py diff --git a/tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py b/tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py similarity index 100% rename from tests/tfscreen/tfmodel/components/dk_geno/test_fixed.py rename to tests/tfscreen/tfmodel/generative/components/dk_geno/test_fixed.py diff --git a/tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py b/tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py similarity index 100% rename from tests/tfscreen/tfmodel/components/dk_geno/test_hierarchical_geno.py rename to tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py diff --git a/tests/tfscreen/tfmodel/components/growth_noise/__init__.py b/tests/tfscreen/tfmodel/generative/components/growth/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_noise/__init__.py rename to tests/tfscreen/tfmodel/generative/components/growth/__init__.py diff --git a/tests/tfscreen/tfmodel/components/growth/test_linear.py b/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth/test_linear.py rename to tests/tfscreen/tfmodel/generative/components/growth/test_linear.py diff --git a/tests/tfscreen/tfmodel/components/growth/test_power.py b/tests/tfscreen/tfmodel/generative/components/growth/test_power.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth/test_power.py rename to tests/tfscreen/tfmodel/generative/components/growth/test_power.py diff --git a/tests/tfscreen/tfmodel/components/growth/test_saturation.py b/tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth/test_saturation.py rename to tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/__init__.py b/tests/tfscreen/tfmodel/generative/components/growth_noise/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/__init__.py rename to tests/tfscreen/tfmodel/generative/components/growth_noise/__init__.py diff --git a/tests/tfscreen/tfmodel/components/growth_noise/test_normal_kt.py b/tests/tfscreen/tfmodel/generative/components/growth_noise/test_normal_kt.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_noise/test_normal_kt.py rename to tests/tfscreen/tfmodel/generative/components/growth_noise/test_normal_kt.py diff --git a/tests/tfscreen/tfmodel/components/growth_noise/test_zero.py b/tests/tfscreen/tfmodel/generative/components/growth_noise/test_zero.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_noise/test_zero.py rename to tests/tfscreen/tfmodel/generative/components/growth_noise/test_zero.py diff --git a/tests/tfscreen/tfmodel/components/ln_cfu0/__init__.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/ln_cfu0/__init__.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/__init__.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/test_baranyi.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/test_baranyi.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/test_baranyi.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/test_baranyi.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/test_baranyi_k.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/test_baranyi_k.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/test_baranyi_k.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/test_baranyi_k.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/test_baranyi_tau.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/test_baranyi_tau.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/test_baranyi_tau.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/test_baranyi_tau.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/test_instant.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/test_instant.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/test_instant.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/test_memory.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/test_memory.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/test_memory.py diff --git a/tests/tfscreen/tfmodel/components/growth_transition/test_two_pop.py b/tests/tfscreen/tfmodel/generative/components/growth_transition/test_two_pop.py similarity index 100% rename from tests/tfscreen/tfmodel/components/growth_transition/test_two_pop.py rename to tests/tfscreen/tfmodel/generative/components/growth_transition/test_two_pop.py diff --git a/tests/tfscreen/tfmodel/components/noise/__init__.py b/tests/tfscreen/tfmodel/generative/components/ln_cfu0/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/noise/__init__.py rename to tests/tfscreen/tfmodel/generative/components/ln_cfu0/__init__.py diff --git a/tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py b/tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py similarity index 100% rename from tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical.py rename to tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical.py diff --git a/tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py b/tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py similarity index 100% rename from tests/tfscreen/tfmodel/components/ln_cfu0/test_hierarchical_factored.py rename to tests/tfscreen/tfmodel/generative/components/ln_cfu0/test_hierarchical_factored.py diff --git a/tests/tfscreen/tfmodel/components/sample_offset/__init__.py b/tests/tfscreen/tfmodel/generative/components/noise/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/sample_offset/__init__.py rename to tests/tfscreen/tfmodel/generative/components/noise/__init__.py diff --git a/tests/tfscreen/tfmodel/components/noise/test_beta.py b/tests/tfscreen/tfmodel/generative/components/noise/test_beta.py similarity index 100% rename from tests/tfscreen/tfmodel/components/noise/test_beta.py rename to tests/tfscreen/tfmodel/generative/components/noise/test_beta.py diff --git a/tests/tfscreen/tfmodel/components/noise/test_logit_normal.py b/tests/tfscreen/tfmodel/generative/components/noise/test_logit_normal.py similarity index 100% rename from tests/tfscreen/tfmodel/components/noise/test_logit_normal.py rename to tests/tfscreen/tfmodel/generative/components/noise/test_logit_normal.py diff --git a/tests/tfscreen/tfmodel/components/noise/test_zero.py b/tests/tfscreen/tfmodel/generative/components/noise/test_zero.py similarity index 100% rename from tests/tfscreen/tfmodel/components/noise/test_zero.py rename to tests/tfscreen/tfmodel/generative/components/noise/test_zero.py diff --git a/tests/tfscreen/tfmodel/components/theta/__init__.py b/tests/tfscreen/tfmodel/generative/components/sample_offset/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/__init__.py rename to tests/tfscreen/tfmodel/generative/components/sample_offset/__init__.py diff --git a/tests/tfscreen/tfmodel/components/sample_offset/test_normal.py b/tests/tfscreen/tfmodel/generative/components/sample_offset/test_normal.py similarity index 100% rename from tests/tfscreen/tfmodel/components/sample_offset/test_normal.py rename to tests/tfscreen/tfmodel/generative/components/sample_offset/test_normal.py diff --git a/tests/tfscreen/tfmodel/components/sample_offset/test_zero.py b/tests/tfscreen/tfmodel/generative/components/sample_offset/test_zero.py similarity index 100% rename from tests/tfscreen/tfmodel/components/sample_offset/test_zero.py rename to tests/tfscreen/tfmodel/generative/components/sample_offset/test_zero.py diff --git a/tests/tfscreen/tfmodel/components/test_pinning.py b/tests/tfscreen/tfmodel/generative/components/test_pinning.py similarity index 100% rename from tests/tfscreen/tfmodel/components/test_pinning.py rename to tests/tfscreen/tfmodel/generative/components/test_pinning.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta/test__simple.py b/tests/tfscreen/tfmodel/generative/components/theta/test__simple.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/test__simple.py rename to tests/tfscreen/tfmodel/generative/components/theta/test__simple.py diff --git a/tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py b/tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/test_categorical_geno.py rename to tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py diff --git a/tests/tfscreen/tfmodel/components/theta/test_hill_geno.py b/tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/test_hill_geno.py rename to tests/tfscreen/tfmodel/generative/components/theta/test_hill_geno.py diff --git a/tests/tfscreen/tfmodel/components/theta/test_hill_mut.py b/tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/test_hill_mut.py rename to tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py diff --git a/tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py b/tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/test_predict_unmeasured.py rename to tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PK.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PddG.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_PnnC.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_predict_unmeasured.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/test_thermo.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PK.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PddG.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_PnnC.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U1_a/test_thermo.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PK.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PddG.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_PnnC.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_predict_unmeasured.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U0_a/test_thermo.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PK.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PddG.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_PnnC.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/O2_C4_K3_U1_a/test_thermo.py diff --git a/tests/tfscreen/tfmodel/components/theta_rescale/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta_rescale/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/test_features.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/test_features.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/test_features.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/test_horseshoe.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/test_horseshoe.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/test_io.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/test_io.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/test_io.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/test_nn.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/test_nn.py diff --git a/tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py b/tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta/thermo/test_prior.py rename to tests/tfscreen/tfmodel/generative/components/theta/thermo/test_prior.py diff --git a/tests/tfscreen/tfmodel/components/transformation/__init__.py b/tests/tfscreen/tfmodel/generative/components/theta_rescale/__init__.py similarity index 100% rename from tests/tfscreen/tfmodel/components/transformation/__init__.py rename to tests/tfscreen/tfmodel/generative/components/theta_rescale/__init__.py diff --git a/tests/tfscreen/tfmodel/components/theta_rescale/test_logit.py b/tests/tfscreen/tfmodel/generative/components/theta_rescale/test_logit.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta_rescale/test_logit.py rename to tests/tfscreen/tfmodel/generative/components/theta_rescale/test_logit.py diff --git a/tests/tfscreen/tfmodel/components/theta_rescale/test_passthrough.py b/tests/tfscreen/tfmodel/generative/components/theta_rescale/test_passthrough.py similarity index 100% rename from tests/tfscreen/tfmodel/components/theta_rescale/test_passthrough.py rename to tests/tfscreen/tfmodel/generative/components/theta_rescale/test_passthrough.py diff --git a/tests/tfscreen/tfmodel/generative/components/transformation/__init__.py b/tests/tfscreen/tfmodel/generative/components/transformation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tfscreen/tfmodel/components/transformation/test__congression.py b/tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py similarity index 100% rename from tests/tfscreen/tfmodel/components/transformation/test__congression.py rename to tests/tfscreen/tfmodel/generative/components/transformation/test__congression.py diff --git a/tests/tfscreen/tfmodel/components/transformation/test_empirical.py b/tests/tfscreen/tfmodel/generative/components/transformation/test_empirical.py similarity index 100% rename from tests/tfscreen/tfmodel/components/transformation/test_empirical.py rename to tests/tfscreen/tfmodel/generative/components/transformation/test_empirical.py diff --git a/tests/tfscreen/tfmodel/components/transformation/test_logit_norm.py b/tests/tfscreen/tfmodel/generative/components/transformation/test_logit_norm.py similarity index 100% rename from tests/tfscreen/tfmodel/components/transformation/test_logit_norm.py rename to tests/tfscreen/tfmodel/generative/components/transformation/test_logit_norm.py diff --git a/tests/tfscreen/tfmodel/components/transformation/test_single.py b/tests/tfscreen/tfmodel/generative/components/transformation/test_single.py similarity index 100% rename from tests/tfscreen/tfmodel/components/transformation/test_single.py rename to tests/tfscreen/tfmodel/generative/components/transformation/test_single.py diff --git a/tests/tfscreen/tfmodel/observe/test_observe_binding.py b/tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py similarity index 100% rename from tests/tfscreen/tfmodel/observe/test_observe_binding.py rename to tests/tfscreen/tfmodel/generative/observe/test_observe_binding.py diff --git a/tests/tfscreen/tfmodel/observe/test_observe_growth.py b/tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py similarity index 100% rename from tests/tfscreen/tfmodel/observe/test_observe_growth.py rename to tests/tfscreen/tfmodel/generative/observe/test_observe_growth.py diff --git a/tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py b/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py similarity index 100% rename from tests/tfscreen/tfmodel/scripts/test_run_prefit_calibration.py rename to tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py diff --git a/tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py b/tests/tfscreen/tfmodel/scripts/test_sample_prior_cli.py similarity index 100% rename from tests/tfscreen/tfmodel/scripts/test_prior_predictive_cli.py rename to tests/tfscreen/tfmodel/scripts/test_sample_prior_cli.py diff --git a/tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py b/tests/tfscreen/tfmodel/scripts/test_subset_genotypes_cli.py similarity index 100% rename from tests/tfscreen/tfmodel/scripts/test_subset_genotypes.py rename to tests/tfscreen/tfmodel/scripts/test_subset_genotypes_cli.py From b62dc8fada8300115959cc9077d31cb0dd2c50ff Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 15 Jun 2026 18:32:07 -0700 Subject: [PATCH 16/24] cleaned up some priors in growth --- docs/badges/tests-badge.svg | 2 +- examples/simulate/simulate_config.yaml | 5 +- examples/simulate/simulate_grid.yaml | 6 +- reports/flake.txt | 512 +++++++++--------- reports/junit/junit.xml | 2 +- .../generative/components/growth/linear.py | 6 +- .../generative/components/growth/power.py | 4 +- .../components/growth/saturation.py | 8 +- src/tfscreen/tfmodel/model_orchestrator.py | 6 + .../components/growth/test_linear.py | 4 +- .../components/growth/test_power.py | 4 +- .../components/growth/test_saturation.py | 4 +- .../tfmodel/test_model_orchestrator.py | 21 +- 13 files changed, 303 insertions(+), 281 deletions(-) diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index d34a1886..e5274206 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3353tests3353 \ No newline at end of file +tests: 3354tests3354 \ No newline at end of file diff --git a/examples/simulate/simulate_config.yaml b/examples/simulate/simulate_config.yaml index e1f9b431..33362a67 100644 --- a/examples/simulate/simulate_config.yaml +++ b/examples/simulate/simulate_config.yaml @@ -129,9 +129,6 @@ theta_component: "thermo.O2_C12_K5_U0_a.PK" # theta_sim_priors: # epi_tau_scale: 0.0 -# Integer seed for the JAX RNG used when sampling theta from the prior. -theta_rng_seed: 0 - # --------------------------------------------------------------------------- # Conditions # --------------------------------------------------------------------------- @@ -278,7 +275,7 @@ total_num_reads: 25_000_000 prob_index_hop: 0.05 # Random seed for reproducibility. Set to null for a random seed each run. -random_seed: null +seed: null # --------------------------------------------------------------------------- # Binding data simulation (optional) diff --git a/examples/simulate/simulate_grid.yaml b/examples/simulate/simulate_grid.yaml index 24a7cd74..a842023b 100644 --- a/examples/simulate/simulate_grid.yaml +++ b/examples/simulate/simulate_grid.yaml @@ -17,7 +17,7 @@ base_config: simulate_config.yaml # in the same examples/simulate/ directory # Directory name for each run. Variables from both 'simulate' and 'template' # blocks are available. The |basename filter strips directory paths from # file-valued variables. -run_name: "{{ theta_component }}__noise{{ tube_noise_sigma }}__seed{{ random_seed }}" +run_name: "{{ theta_component }}__noise{{ tube_noise_sigma }}__seed{{ seed }}" # Jinja2 template file (relative to this YAML) rendered into each subdir. output_file: run.sh # in the same examples/simulate/ directory @@ -65,8 +65,8 @@ simulate: - name: seed variants: - - random_seed: 0 - - random_seed: 42 + - seed: 0 + - seed: 42 # --------------------------------------------------------------------------- diff --git a/reports/flake.txt b/reports/flake.txt index 3d07d14a..df437a9a 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -3417,195 +3417,195 @@ ./src/tfscreen/tfmodel/model_orchestrator.py:79:42: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:83:65: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:83:88: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:88:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:92:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:92:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:92:55: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:92:63: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:93:52: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:103:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:107:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:111:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:112:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:114:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/model_orchestrator.py:149:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:152:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:152:73: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:153:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:154:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:155:61: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:156:61: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:157:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:158:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:160:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:161:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:162:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:163:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:166:63: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:167:73: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:168:73: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:170:59: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:171:70: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:172:72: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:176:63: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:179:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:180:15: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:181:42: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:181:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:182:29: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:183:40: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:184:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:184:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:184:72: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:225:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:234:33: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:234:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:235:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:238:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:244:27: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:281:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/model_orchestrator.py:313:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:314:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:315:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:317:23: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:318:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:319:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:323:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:403:12: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:463:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:466:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:471:52: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:472:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:479:30: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:483:38: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:499:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:500:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/model_orchestrator.py:520:82: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:525:87: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:528:84: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:533:79: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:615:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) -./src/tfscreen/tfmodel/model_orchestrator.py:615:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:640:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:641:28: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:643:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:644:35: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:644:48: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:644:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:645:46: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:645:66: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:94:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:98:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:98:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:98:55: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:98:63: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:99:52: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:109:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:113:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:117:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:118:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:120:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/model_orchestrator.py:155:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:158:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:158:73: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:159:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:160:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:161:61: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:162:61: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:163:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:164:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:166:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:167:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:168:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:169:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:172:63: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:173:73: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:174:73: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:176:59: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:177:70: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:178:72: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:182:63: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:185:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:186:15: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:187:42: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:187:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:188:29: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:189:40: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:190:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:190:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:190:72: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:231:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:240:33: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:240:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:241:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:244:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:250:27: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:287:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/tfmodel/model_orchestrator.py:319:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:320:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:321:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:323:23: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:324:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:325:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:329:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:409:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:469:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:472:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:477:52: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:478:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:485:30: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:489:38: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:505:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:506:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/model_orchestrator.py:526:82: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:531:87: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:534:84: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:539:79: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:621:5: C901 'ModelOrchestrator._initialize_data' is too complex (17) +./src/tfscreen/tfmodel/model_orchestrator.py:621:5: E303 too many blank lines (2) ./src/tfscreen/tfmodel/model_orchestrator.py:646:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:650:39: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:651:34: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:656:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:663:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:665:10: E114 indentation is not a multiple of 4 (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:665:10: E116 unexpected indentation (comment) -./src/tfscreen/tfmodel/model_orchestrator.py:667:76: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:670:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:672:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:674:49: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:686:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:694:57: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:716:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:717:41: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:717:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:718:44: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:718:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:719:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:719:58: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:720:46: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:720:71: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:722:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:729:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:734:39: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:734:45: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:734:53: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:741:40: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:787:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:788:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:789:26: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:790:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:830:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:833:78: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:834:32: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:835:60: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:839:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:840:36: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:841:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:842:38: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:844:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:855:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:855:62: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:858:74: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:859:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:864:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:865:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:866:58: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:868:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:873:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:877:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:881:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:884:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:890:86: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:892:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:899:77: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:955:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:956:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:957:35: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:958:39: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:965:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:966:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:967:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1043:25: E221 multiple spaces before operator -./src/tfscreen/tfmodel/model_orchestrator.py:1044:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1046:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1119:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) -./src/tfscreen/tfmodel/model_orchestrator.py:1139:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1141:67: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1142:71: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1144:68: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1145:77: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1146:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1147:40: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1164:42: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1165:43: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1166:40: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1166:43: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1166:53: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1166:56: E231 missing whitespace after ',' -./src/tfscreen/tfmodel/model_orchestrator.py:1166:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1214:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1217:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1221:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:647:28: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:649:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:650:35: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:650:48: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:650:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:651:46: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:651:66: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:652:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:656:39: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:657:34: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:662:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:669:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:671:10: E114 indentation is not a multiple of 4 (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:671:10: E116 unexpected indentation (comment) +./src/tfscreen/tfmodel/model_orchestrator.py:673:76: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:676:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:678:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:680:49: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:692:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:700:57: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:722:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:723:41: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:723:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:724:44: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:724:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:725:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:725:58: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:726:46: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:726:71: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:728:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:735:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:740:39: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:740:45: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:740:53: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:747:40: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:793:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:794:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:795:26: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:796:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:836:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:839:78: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:840:32: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:841:60: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:845:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:846:36: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:847:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:848:38: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:850:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:861:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:861:62: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:864:74: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:865:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:870:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:871:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:872:58: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:874:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:879:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:883:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:887:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:890:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:896:86: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:898:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:905:77: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:961:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:962:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:963:35: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:964:39: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:971:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:972:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:973:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1049:25: E221 multiple spaces before operator +./src/tfscreen/tfmodel/model_orchestrator.py:1050:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1052:31: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/model_orchestrator.py:1125:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) +./src/tfscreen/tfmodel/model_orchestrator.py:1145:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1147:67: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1148:71: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1150:68: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1151:77: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1152:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1153:40: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1170:42: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1171:43: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1172:40: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1172:43: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1172:53: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1172:56: E231 missing whitespace after ',' +./src/tfscreen/tfmodel/model_orchestrator.py:1172:64: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1220:75: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:1223:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1233:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1235:9: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1288:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1293:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1307:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1317:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1327:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1341:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1344:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1373:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1385:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1397:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1403:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1404:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1405:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1406:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1407:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1408:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1409:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1410:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1411:29: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1412:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1413:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1414:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1415:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1416:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1417:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1227:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1229:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1239:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1241:9: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1294:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1299:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1313:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1323:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1333:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1347:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1350:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1379:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1391:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1403:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1409:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1410:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1411:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1412:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1413:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1414:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1415:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1416:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1417:29: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1418:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1419:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1420:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1421:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1422:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1423:31: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:192:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:193:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:194:22: E128 continuation line under-indented for visual indent @@ -7017,75 +7017,75 @@ ./tests/tfscreen/tfmodel/test_model_orchestrator.py:103:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_model_orchestrator.py:109:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:113:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:120:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:129:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:146:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:276:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:299:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:305:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:307:76: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:330:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:338:128: E501 line too long (184 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:339:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:125:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:148:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:165:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:295:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:318:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:324:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:326:76: W291 trailing whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:349:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:48: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:50: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:52: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:54: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:56: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:354:58: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:355:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:363:112: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:363:128: E501 line too long (191 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:368:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:371:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:375:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:382:128: E501 line too long (184 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:401:128: E501 line too long (198 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:406:128: E501 line too long (167 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:454:79: E712 comparison to False should be 'if cond is False:' or 'if not cond:' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:455:64: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:458:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:466:51: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:77: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:98: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:125: E231 missing whitespace after ':' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:472:128: E501 line too long (131 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:473:128: E501 line too long (138 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:475:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:477:86: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:489:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:494:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:502:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:510:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:521:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:524:128: E501 line too long (129 > 127 characters) -./tests/tfscreen/tfmodel/test_model_orchestrator.py:531:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:538:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:546:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:551:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:556:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:562:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:571:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:575:26: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:585:30: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:586:25: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:588:34: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:589:33: W291 trailing whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:601:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:43: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:45: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:47: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:49: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:51: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:53: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:603:55: E231 missing whitespace after ',' -./tests/tfscreen/tfmodel/test_model_orchestrator.py:607:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/tfmodel/test_model_orchestrator.py:616:26: E261 at least two spaces before inline comment -./tests/tfscreen/tfmodel/test_model_orchestrator.py:617:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:357:128: E501 line too long (184 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:358:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:368:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:48: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:50: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:52: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:54: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:56: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:373:58: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:374:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:382:112: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:382:128: E501 line too long (191 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:387:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:390:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:394:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:401:128: E501 line too long (184 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:420:128: E501 line too long (198 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:425:128: E501 line too long (167 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:473:79: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:474:64: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:477:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:485:51: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:491:77: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:491:98: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:491:125: E231 missing whitespace after ':' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:491:128: E501 line too long (131 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:492:128: E501 line too long (138 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:494:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:496:86: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:508:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:513:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:521:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:529:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:540:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:543:128: E501 line too long (129 > 127 characters) +./tests/tfscreen/tfmodel/test_model_orchestrator.py:550:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:557:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:565:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:570:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:575:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:581:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:590:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:594:26: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:604:30: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:605:25: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:607:34: W291 trailing whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:608:33: W291 trailing whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:620:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:623:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:800:67: W292 no newline at end of file +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:43: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:45: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:47: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:49: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:51: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:53: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:622:55: E231 missing whitespace after ',' +./tests/tfscreen/tfmodel/test_model_orchestrator.py:626:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/tfmodel/test_model_orchestrator.py:635:26: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_model_orchestrator.py:636:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:639:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:642:1: W293 blank line contains whitespace +./tests/tfscreen/tfmodel/test_model_orchestrator.py:819:67: W292 no newline at end of file ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:16: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:31: E272 multiple spaces before keyword ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:31:16: E221 multiple spaces before operator @@ -7679,7 +7679,7 @@ 2 E266 too many leading '#' for block comment 148 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -799 E302 expected 2 blank lines, found 1 +800 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) 16 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 @@ -7698,6 +7698,6 @@ 40 F841 local variable 'e' is assigned to but never used 669 W291 trailing whitespace 86 W292 no newline at end of file -1674 W293 blank line contains whitespace +1673 W293 blank line contains whitespace 29 W391 blank line at end of file 7654 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 25fa1cdc..3d4ca73e 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/tfmodel/generative/components/growth/linear.py b/src/tfscreen/tfmodel/generative/components/growth/linear.py index e63d8115..2289d4c7 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/linear.py +++ b/src/tfscreen/tfmodel/generative/components/growth/linear.py @@ -151,10 +151,10 @@ def get_hyperparameters(): Get default values for the model hyperparameters. """ parameters = {} - parameters["k_loc"] = 0.025 + parameters["k_loc"] = 0.020 parameters["k_scale"] = 0.1 parameters["m_loc"] = 0.0 - parameters["m_scale"] = 0.05 + parameters["m_scale"] = 0.01 return parameters @@ -170,7 +170,7 @@ def get_guesses(name, data): Falls back to hard-coded defaults when ``data.ln_cfu`` or ``data.t_sel`` are not 7-D tensors. """ - _DEFAULT_K = 0.025 + _DEFAULT_K = 0.020 _DEFAULT_SCALE = 0.01 num_cond_rep = data.num_condition_rep diff --git a/src/tfscreen/tfmodel/generative/components/growth/power.py b/src/tfscreen/tfmodel/generative/components/growth/power.py index a6e2f952..8eb3e57b 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/power.py +++ b/src/tfscreen/tfmodel/generative/components/growth/power.py @@ -156,7 +156,7 @@ def get_hyperparameters(): Get default values for the model hyperparameters. """ parameters = {} - parameters["k_loc"] = 0.025 + parameters["k_loc"] = 0.020 parameters["k_scale"] = 0.1 parameters["m_loc"] = 0.0 parameters["m_scale"] = 0.01 @@ -174,7 +174,7 @@ def get_guesses(name, data): _DEFAULT_SCALE = 0.01 guesses = {} - guesses[f"{name}_k_locs"] = jnp.full(num_cond_rep, 0.025, dtype=float) + guesses[f"{name}_k_locs"] = jnp.full(num_cond_rep, 0.020, dtype=float) guesses[f"{name}_k_scales"] = jnp.full(num_cond_rep, _DEFAULT_SCALE, dtype=float) guesses[f"{name}_m_locs"] = jnp.zeros(num_cond_rep, dtype=float) guesses[f"{name}_m_scales"] = jnp.full(num_cond_rep, _DEFAULT_SCALE, dtype=float) diff --git a/src/tfscreen/tfmodel/generative/components/growth/saturation.py b/src/tfscreen/tfmodel/generative/components/growth/saturation.py index 87daca34..393cade4 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/saturation.py +++ b/src/tfscreen/tfmodel/generative/components/growth/saturation.py @@ -128,9 +128,9 @@ def get_hyperparameters(): Get default values for the model hyperparameters. """ parameters = {} - parameters["min_loc"] = 0.025 + parameters["min_loc"] = 0.020 parameters["min_scale"] = 0.1 - parameters["max_loc"] = 0.025 + parameters["max_loc"] = 0.030 parameters["max_scale"] = 0.1 return parameters @@ -144,9 +144,9 @@ def get_guesses(name, data): _DEFAULT_SCALE = 0.01 guesses = {} - guesses[f"{name}_min_locs"] = jnp.full(num_cond_rep, 0.025, dtype=float) + guesses[f"{name}_min_locs"] = jnp.full(num_cond_rep, 0.020, dtype=float) guesses[f"{name}_min_scales"] = jnp.full(num_cond_rep, _DEFAULT_SCALE, dtype=float) - guesses[f"{name}_max_locs"] = jnp.full(num_cond_rep, 0.025, dtype=float) + guesses[f"{name}_max_locs"] = jnp.full(num_cond_rep, 0.030, dtype=float) guesses[f"{name}_max_scales"] = jnp.full(num_cond_rep, _DEFAULT_SCALE, dtype=float) return guesses diff --git a/src/tfscreen/tfmodel/model_orchestrator.py b/src/tfscreen/tfmodel/model_orchestrator.py index 6b3101ec..ed9c7c26 100644 --- a/src/tfscreen/tfmodel/model_orchestrator.py +++ b/src/tfscreen/tfmodel/model_orchestrator.py @@ -82,6 +82,12 @@ def _read_growth_df(growth_df, growth_df = tfscreen.util.io.read_dataframe(growth_df) growth_df = tfscreen.util.dataframe.get_scaled_cfu(growth_df,need_columns=["ln_cfu","ln_cfu_std"]) + # Ensure time columns are float so that merge/concat with float prediction + # grids (e.g. linspace t_sel) doesn't produce int/float dtype warnings. + for _col in ("t_pre", "t_sel"): + if _col in growth_df.columns: + growth_df[_col] = growth_df[_col].astype(float) + # make a replicate column if not defined if "replicate" not in growth_df.columns: growth_df["replicate"] = 1 diff --git a/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py b/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py index d8caa731..91b386f7 100644 --- a/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py +++ b/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py @@ -113,13 +113,13 @@ def test_get_hyperparameters(): assert "k_scale" in params assert "m_loc" in params assert "m_scale" in params - assert params["k_loc"] == 0.025 + assert params["k_loc"] == 0.020 def test_get_priors(): priors = get_priors() assert isinstance(priors, ModelPriors) - assert priors.k_loc == 0.025 + assert priors.k_loc == 0.020 assert priors.m_loc == 0.0 assert not hasattr(priors, "pinned") diff --git a/tests/tfscreen/tfmodel/generative/components/growth/test_power.py b/tests/tfscreen/tfmodel/generative/components/growth/test_power.py index 937eceba..1614d9a4 100644 --- a/tests/tfscreen/tfmodel/generative/components/growth/test_power.py +++ b/tests/tfscreen/tfmodel/generative/components/growth/test_power.py @@ -39,14 +39,14 @@ def test_get_hyperparameters(): assert isinstance(params, dict) assert "k_loc" in params assert "n_loc" in params - assert params["k_loc"] == 0.025 + assert params["k_loc"] == 0.020 assert params["n_loc"] == 0.0 def test_get_priors(): priors = get_priors() assert isinstance(priors, ModelPriors) - assert priors.k_loc == 0.025 + assert priors.k_loc == 0.020 assert priors.n_loc == 0.0 assert not hasattr(priors, "pinned") diff --git a/tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py b/tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py index 3d1a7c20..2d6b7a2b 100644 --- a/tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py +++ b/tests/tfscreen/tfmodel/generative/components/growth/test_saturation.py @@ -39,13 +39,13 @@ def test_get_hyperparameters(): assert isinstance(params, dict) assert "min_loc" in params assert "max_loc" in params - assert params["min_loc"] == 0.025 + assert params["min_loc"] == 0.020 def test_get_priors(): priors = get_priors() assert isinstance(priors, ModelPriors) - assert priors.min_loc == 0.025 + assert priors.min_loc == 0.020 assert not hasattr(priors, "pinned") diff --git a/tests/tfscreen/tfmodel/test_model_orchestrator.py b/tests/tfscreen/tfmodel/test_model_orchestrator.py index bdb843c5..610cd545 100644 --- a/tests/tfscreen/tfmodel/test_model_orchestrator.py +++ b/tests/tfscreen/tfmodel/test_model_orchestrator.py @@ -117,11 +117,30 @@ def test_read_growth_df_no_replicate(mocker, base_growth_df): mocker.patch("tfscreen.util.dataframe.get_scaled_cfu", return_value=df_no_rep) mocker.patch("tfscreen.util.dataframe.check_columns") mocker.patch("tfscreen.tfmodel.model_orchestrator.add_group_columns", return_value=df_no_rep) - + res = _read_growth_df("path.csv") assert "replicate" in res.columns assert res["replicate"].iloc[0] == 1 +def test_read_growth_df_coerces_time_to_float(mocker, base_growth_df): + """t_pre and t_sel stored as int64 (e.g. from pd.read_csv with integer values) + must be cast to float64 so that downstream merges against float prediction grids + don't produce int/float dtype warnings.""" + df_int_time = base_growth_df.copy() + df_int_time["t_pre"] = df_int_time["t_pre"].astype(int) + df_int_time["t_sel"] = df_int_time["t_sel"].astype(int) + assert df_int_time["t_pre"].dtype == int + assert df_int_time["t_sel"].dtype == int + + mocker.patch("tfscreen.util.io.read_dataframe", return_value=df_int_time) + mocker.patch("tfscreen.util.dataframe.get_scaled_cfu", return_value=df_int_time) + mocker.patch("tfscreen.util.dataframe.check_columns") + mocker.patch("tfscreen.tfmodel.model_orchestrator.add_group_columns", return_value=df_int_time) + + res = _read_growth_df("path.csv") + assert res["t_pre"].dtype == float, f"expected float, got {res['t_pre'].dtype}" + assert res["t_sel"].dtype == float, f"expected float, got {res['t_sel'].dtype}" + # ---------------------------------------------------------------------------- # 5. Tests for _read_binding_df # ---------------------------------------------------------------------------- From 9a1dd9e303e740a2fce5ee41b402f773cfd296cb Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 16 Jun 2026 08:33:43 -0700 Subject: [PATCH 17/24] small plot change --- src/tfscreen/tfmodel/analysis/error_calibration.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tfscreen/tfmodel/analysis/error_calibration.py b/src/tfscreen/tfmodel/analysis/error_calibration.py index 5d786464..28cffb9b 100644 --- a/src/tfscreen/tfmodel/analysis/error_calibration.py +++ b/src/tfscreen/tfmodel/analysis/error_calibration.py @@ -238,6 +238,7 @@ def plot_pit_histogram(pit_vals, ax=None, title=None, n_bins=10): color="steelblue", alpha=0.75, edgecolor="white") ax.axhline(1.0, color="firebrick", lw=1.5, ls="--", label="Uniform") ax.set_xlim(0, 1) + ax.set_ylim(0, 10) ax.set_xlabel("PIT value") ax.set_ylabel("Density") if title is not None: @@ -278,14 +279,15 @@ def plot_calibration_curve(calibration_dict, ax=None, label=None): nominals = np.array(sorted(calibration_dict.keys())) empiricals = np.array([calibration_dict[k] for k in nominals]) - ax.plot([0, 1], [0, 1], "k--", lw=1, label="Ideal") + ax.plot([-0.05, 1.05], [-0.05, 1.05], "k--", lw=1, label="Ideal") ax.plot(nominals, empiricals, "o-", color="steelblue", label=label or "Model") - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) + ax.set_xlim(-0.05, 1.05) + ax.set_ylim(-0.05, 1.05) ax.set_xlabel("Nominal coverage") ax.set_ylabel("Empirical coverage") ax.legend(fontsize=8) + ax.set_aspect("equal") return ax From a408faab4b24c30815d35a07b5017f9c965af3db Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 16 Jun 2026 14:45:31 -0700 Subject: [PATCH 18/24] improved binding genotype simulation sampling --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 80 ++++++----- reports/junit/junit.xml | 2 +- src/tfscreen/simulate/library_prediction.py | 62 ++++++++- src/tfscreen/simulate/run_simulation.py | 2 +- src/tfscreen/simulate/sample_theta.py | 131 ++++++++++++++++++ src/tfscreen/simulate/scripts/simulate_cli.py | 70 +++------- src/tfscreen/simulate/thermo_to_growth.py | 33 +++-- .../tfmodel/scripts/predict_growth_cli.py | 4 +- .../tfmodel/scripts/predict_theta_cli.py | 4 +- .../tfmodel/scripts/subset_genotypes_cli.py | 4 +- .../tfmodel/scripts/summarize_fit_cli.py | 10 +- .../simulate/test_generate_presplit_data.py | 4 +- .../simulate/test_library_prediction.py | 3 +- .../tfscreen/simulate/test_run_simulation.py | 4 +- tests/tfscreen/simulate/test_sample_theta.py | 130 ++++++++++++++++- tests/tfscreen/simulate/test_simulate_cli.py | 65 +++++---- .../tfmodel/scripts/test_summarize_fit_cli.py | 46 +++--- 19 files changed, 497 insertions(+), 161 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index c24babeb..90b87d80 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.12%coverage96.12% \ No newline at end of file +coverage: 96.08%coverage96.08% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index e5274206..bc5b626d 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3354tests3354 \ No newline at end of file +tests: 3363tests3363 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index df437a9a..e8078875 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1270,9 +1270,9 @@ ./src/tfscreen/simulate/build_sample_dataframes.py:83:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:87:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:95:21: W292 no newline at end of file -./src/tfscreen/simulate/library_prediction.py:17:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/library_prediction.py:18:43: E252 missing whitespace around parameter equals -./src/tfscreen/simulate/library_prediction.py:18:44: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:20:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/library_prediction.py:21:43: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:21:44: E252 missing whitespace around parameter equals ./src/tfscreen/simulate/run_simulation.py:12:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/simulate/run_simulation.py:17:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:18:36: E231 missing whitespace after ',' @@ -1311,26 +1311,34 @@ ./src/tfscreen/simulate/run_simulation.py:101:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:106:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:107:1: W391 blank line at end of file +./src/tfscreen/simulate/sample_theta.py:159:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:160:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:161:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:162:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:163:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:164:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:165:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:166:30: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/scripts/setup_sim_grid_cli.py:101:1: C901 'setup_sim_grid' is too complex (17) -./src/tfscreen/simulate/scripts/simulate_cli.py:143:15: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:144:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:145:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:146:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:150:20: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:151:22: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:152:21: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:171:43: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/scripts/simulate_cli.py:172:14: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:173:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:178:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:191:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:194:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:195:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:196:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:199:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:201:25: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:203:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:217:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:113:15: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:114:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:115:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:116:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:120:20: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:121:22: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:122:21: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:141:43: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/scripts/simulate_cli.py:142:14: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:143:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:148:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:161:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:164:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:165:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:166:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:169:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:171:25: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:173:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:187:19: E221 multiple spaces before operator ./src/tfscreen/simulate/selection_experiment.py:42:38: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:43:37: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:44:36: E231 missing whitespace after ':' @@ -1471,7 +1479,8 @@ ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/thermo_to_growth.py:603:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/thermo_to_growth.py:289:1: C901 'thermo_to_growth' is too complex (13) +./src/tfscreen/simulate/thermo_to_growth.py:618:41: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/__init__.py:2:73: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:3:20: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:18:65: W291 trailing whitespace @@ -1485,8 +1494,8 @@ ./src/tfscreen/tfmodel/__init__.py:41:77: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:43:56: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:46:1: W391 blank line at end of file -./src/tfscreen/tfmodel/analysis/error_calibration.py:472:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/analysis/error_calibration.py:509:1: C901 'summarize_sbc' is too complex (16) +./src/tfscreen/tfmodel/analysis/error_calibration.py:474:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/analysis/error_calibration.py:511:1: C901 'summarize_sbc' is too complex (16) ./src/tfscreen/tfmodel/analysis/extraction.py:81:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:85:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/analysis/extraction.py:103:1: E302 expected 2 blank lines, found 1 @@ -5188,7 +5197,7 @@ ./tests/tfscreen/simulate/test_generate_presplit_data.py:227:31: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_generate_presplit_data.py:229:33: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_library_prediction.py:47:41: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_library_prediction.py:289:5: F401 'numpy as np' imported but unused +./tests/tfscreen/simulate/test_library_prediction.py:290:5: F401 'numpy as np' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:3:1: F401 'os' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:5:1: F401 'unittest.mock.MagicMock' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:7:1: F401 'tfscreen.util' imported but unused @@ -5209,6 +5218,12 @@ ./tests/tfscreen/simulate/test_run_simulation.py:113:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_run_simulation.py:116:5: F841 local variable 'results' is assigned to but never used ./tests/tfscreen/simulate/test_run_simulation.py:117:1: W293 blank line contains whitespace +./tests/tfscreen/simulate/test_sample_theta.py:239:17: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:242:30: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:251:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:264:27: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:269:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:284:21: E221 multiple spaces before operator ./tests/tfscreen/simulate/test_selection_experiment.py:4:1: F401 'numpy.ma' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:6:1: F401 'scipy.stats.gmean' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5361,8 +5376,7 @@ ./tests/tfscreen/simulate/test_selection_experiment.py:1272:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_selection_experiment.py:1313:27: W292 no newline at end of file ./tests/tfscreen/simulate/test_simulate_cli.py:3:1: F401 'os' imported but unused -./tests/tfscreen/simulate/test_simulate_cli.py:165:5: F401 'jax' imported but unused -./tests/tfscreen/simulate/test_simulate_cli.py:230:5: F401 'tfscreen.simulate.scripts.simulate_cli as cli_mod' imported but unused +./tests/tfscreen/simulate/test_simulate_cli.py:247:5: F401 'tfscreen.simulate.scripts.simulate_cli as cli_mod' imported but unused ./tests/tfscreen/simulate/test_thermo_to_growth.py:28:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:32:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_thermo_to_growth.py:41:1: E302 expected 2 blank lines, found 1 @@ -7652,7 +7666,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -47 C901 'cat_fit' is too complex (19) +48 C901 'cat_fit' is too complex (19) 15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7660,14 +7674,14 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -164 E127 continuation line over-indented for visual indent +172 E127 continuation line over-indented for visual indent 79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1766 E221 multiple spaces before operator +1772 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1115 E231 missing whitespace after ',' @@ -7692,7 +7706,7 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 20 E741 ambiguous variable name 'l' -228 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +227 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 3 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used @@ -7700,4 +7714,4 @@ 86 W292 no newline at end of file 1673 W293 blank line contains whitespace 29 W391 blank line at end of file -7654 +7668 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 3d4ca73e..2c3092ef 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index 07a83de3..49aa132e 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -6,10 +6,13 @@ thermo_to_growth, ) from tfscreen.simulate.sim_data_class import build_sim_data +from tfscreen.simulate.sample_theta import sample_theta_stratified from tfscreen.genetics import library_manager +from tfscreen.genetics import standardize_genotypes import jax import numpy as np +import pandas as pd from typing import Any, Dict, Union from pathlib import Path @@ -47,6 +50,13 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], one row per unique genotype; columns ``dk_geno``, ``activity``, and any scalar per-genotype fields from the theta component (e.g. ``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n``) + binding_theta_df : pandas.DataFrame or None + Present when ``binding_data`` is in the config. One row per + (genotype, titrant_conc) for the calibration genotypes listed in + ``binding_data.genotypes``, with columns ``genotype``, + ``titrant_name``, ``titrant_conc``, ``theta_true``. Theta values + here are stratified (greedy maximin) across the binding concentrations + and are consistent with the growth phenotypes in ``phenotype_df``. """ # ------------------------------------------------------------------------- @@ -91,7 +101,56 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], dk_geno_hyper_scale = cf['dk_geno_hyper_scale'] dk_geno_hyper_shift = cf['dk_geno_hyper_shift'] + # ------------------------------------------------------------------------- + # Stratified theta sampling for binding calibration genotypes + + binding_cfg = cf.get('binding_data') + theta_gc_override = None + binding_theta_df = None + + if binding_cfg is not None: + binding_genotypes = list(standardize_genotypes(binding_cfg['genotypes'])) + binding_concs = binding_cfg['titrant_conc'] + titrant_name = binding_cfg['titrant_name'] + + binding_sample_df = pd.DataFrame({"titrant_conc": binding_concs}) + + selected_binding_gc, selected_growth_gc = sample_theta_stratified( + component_name=cf['theta_component'], + binding_sample_df=binding_sample_df, + growth_sample_df=sample_df, + rng_key=theta_rng_key, + n_select=len(binding_genotypes), + thermo_data=cf.get('thermo_data'), + pool_size=cf.get('binding_stratify_pool_size', 500), + priors_overrides=cf.get('theta_priors'), + sim_priors_overrides=cf.get('theta_sim_priors'), + ) + + # Build override dict for thermo_to_growth (theta at growth concentrations) + theta_gc_override = { + g: selected_growth_gc[i] + for i, g in enumerate(binding_genotypes) + } + + # Build binding_theta_df (theta at binding concentrations, pre-noise) + sorted_binding_concs = np.sort(np.unique(binding_concs)) + conc_to_col = {float(c): j for j, c in enumerate(sorted_binding_concs)} + rows = [] + for i, g in enumerate(binding_genotypes): + for conc in binding_concs: + col_idx = conc_to_col[float(conc)] + rows.append({ + "genotype": g, + "titrant_name": titrant_name, + "titrant_conc": float(conc), + "theta_true": float(selected_binding_gc[i, col_idx]), + }) + binding_theta_df = pd.DataFrame(rows) + + # ------------------------------------------------------------------------- # Calculate phenotype for each genotype across all conditions in sample_df + phenotype_df, genotype_theta_df, parameters_df = thermo_to_growth( genotypes=library_df["genotype"], sim_data=sim_data, @@ -111,6 +170,7 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], activity_component=cf.get('activity_component', 'fixed'), activity_priors_overrides=cf.get('activity_priors'), theta_rescale=cf.get('theta_rescale', 'passthrough'), + theta_gc_override=theta_gc_override, ) - return library_df, phenotype_df, genotype_theta_df, parameters_df + return library_df, phenotype_df, genotype_theta_df, parameters_df, binding_theta_df diff --git a/src/tfscreen/simulate/run_simulation.py b/src/tfscreen/simulate/run_simulation.py index 8d44a9fb..374210f1 100644 --- a/src/tfscreen/simulate/run_simulation.py +++ b/src/tfscreen/simulate/run_simulation.py @@ -82,7 +82,7 @@ def run_simulation(cf: Union[Dict[str, Any], str, Path], file_dict = _setup_file_output(output_dir,output_prefix) # Build library and predict its phenotypes - library_df, phenotype_df, genotype_theta_df, parameters_df = library_prediction(cf) + library_df, phenotype_df, genotype_theta_df, parameters_df, _ = library_prediction(cf) # Perform selection experiment sample_df, counts_df = selection_experiment(cf,library_df,phenotype_df) diff --git a/src/tfscreen/simulate/sample_theta.py b/src/tfscreen/simulate/sample_theta.py index a4faaa81..d073dc29 100644 --- a/src/tfscreen/simulate/sample_theta.py +++ b/src/tfscreen/simulate/sample_theta.py @@ -6,13 +6,22 @@ path: instead of reading fixed thermodynamic parameters from a file, we draw parameters from the model prior, making the simulation consistent with the model family being inferred. + +sample_theta_stratified draws a large pool of independent theta curves from +the prior at the binding concentrations, selects n_select curves that +maximally span the theta-curve space (greedy maximin), then returns the +selected curves at both binding and growth concentrations. This ensures +calibration genotypes cover diverse binding behaviors rather than clustering +by chance. """ import inspect import numpy as np +import pandas as pd from numpyro import handlers from tfscreen.tfmodel.generative.registry import model_registry +from tfscreen.simulate.sim_data_class import build_sim_data # Components excluded from simulation (calibration-only, no generative prior). _EXCLUDED = frozenset({"_simple"}) @@ -106,3 +115,125 @@ def sample_theta_prior(component_name, theta_gc = np.array(theta_tcg[0]).T # (G, C) return theta_gc, theta_param + + +def _greedy_maximin(theta_gc, n_select): + """ + Select n_select rows from theta_gc that maximally span the theta-curve space. + + Uses greedy farthest-point selection (maximin): starts with the row of + greatest dynamic range (max - min across concentrations), then repeatedly + adds the row that maximises the minimum Euclidean distance to the + already-selected set. + + Parameters + ---------- + theta_gc : np.ndarray, shape (pool_size, n_conc) + n_select : int + + Returns + ------- + np.ndarray of int, shape (n_select,) + Indices into theta_gc of the selected rows, in selection order. + """ + pool_size = theta_gc.shape[0] + if n_select >= pool_size: + return np.arange(pool_size, dtype=int) + + theta_range = theta_gc.max(axis=1) - theta_gc.min(axis=1) + selected = [int(np.argmax(theta_range))] + + while len(selected) < n_select: + selected_arr = theta_gc[selected] # (k, C) + # Squared distance from every pool member to its nearest selected point + diff = theta_gc[:, None, :] - selected_arr[None, :, :] # (pool, k, C) + sq_dist = (diff ** 2).sum(axis=-1) # (pool, k) + min_sq_dist = sq_dist.min(axis=1) # (pool,) + min_sq_dist[selected] = -1.0 # exclude already selected + selected.append(int(np.argmax(min_sq_dist))) + + return np.array(selected, dtype=int) + + +def sample_theta_stratified(component_name, + binding_sample_df, + growth_sample_df, + rng_key, + n_select, + thermo_data=None, + pool_size=500, + priors_overrides=None, + sim_priors_overrides=None): + """ + Sample a pool of theta curves from the prior and select n_select that + maximally span the theta-curve space (greedy maximin on binding concentrations). + + Builds an internal pool library of pool_size "wt" genotypes. For per-genotype + theta components (e.g. hill_geno), each pool member receives an independent + i.i.d. draw from the prior. Mutation-decomposed components are not supported. + + The same JAX rng_key is used for both the binding-concentration and + growth-concentration sampling calls. Because per-genotype parameter draws + in the NumPyro trace depend only on plate sizes (not on concentration values), + both calls produce the same underlying parameter sets evaluated at their + respective concentrations, so the returned curves are mutually consistent. + + Parameters + ---------- + component_name : str + A key in model_registry["theta"]. + binding_sample_df : pd.DataFrame + Must contain a "titrant_conc" column (mM). These concentrations are + used to select which parameter sets maximally span the binding space. + growth_sample_df : pd.DataFrame + Must contain a "titrant_conc" column (mM). The selected parameter sets + are also evaluated here so they can be injected into the growth simulation. + rng_key : jax.random.PRNGKey + n_select : int + Number of genotypes to select (= number of calibration binding genotypes). + thermo_data : str or None + Forwarded to build_sim_data. + pool_size : int, default 500 + Number of candidate parameter sets to draw before selection. + priors_overrides : dict or None + Forwarded to sample_theta_prior (prior-predictive path). + sim_priors_overrides : dict or None + Forwarded to sample_theta_prior (perturbation path). + + Returns + ------- + binding_theta_gc : np.ndarray, shape (n_select, n_binding_concs) + Selected theta curves at binding concentrations (sorted ascending). + growth_theta_gc : np.ndarray, shape (n_select, n_growth_concs) + Same selected parameter sets evaluated at growth concentrations. + + Raises + ------ + ValueError + If n_select > pool_size. + """ + if n_select > pool_size: + raise ValueError( + f"n_select ({n_select}) must not exceed pool_size ({pool_size}). " + f"Increase pool_size or reduce the number of binding genotypes." + ) + + pool_library_df = pd.DataFrame({"genotype": ["wt"] * pool_size}) + + binding_sim_data = build_sim_data(pool_library_df, binding_sample_df, + thermo_data=thermo_data, skip_pairs=True) + pool_binding_gc, _ = sample_theta_prior(component_name, binding_sim_data, + rng_key, priors_overrides, + sim_priors_overrides) + # pool_binding_gc: (pool_size, n_binding_concs) + + selected_indices = _greedy_maximin(pool_binding_gc, n_select) + + growth_sim_data = build_sim_data(pool_library_df, growth_sample_df, + thermo_data=thermo_data, skip_pairs=True) + pool_growth_gc, _ = sample_theta_prior(component_name, growth_sim_data, + rng_key, priors_overrides, + sim_priors_overrides) + # pool_growth_gc: (pool_size, n_growth_concs) + + return pool_binding_gc[selected_indices], pool_growth_gc[selected_indices] diff --git a/src/tfscreen/simulate/scripts/simulate_cli.py b/src/tfscreen/simulate/scripts/simulate_cli.py index 7a83b220..3da8efdd 100644 --- a/src/tfscreen/simulate/scripts/simulate_cli.py +++ b/src/tfscreen/simulate/scripts/simulate_cli.py @@ -2,32 +2,24 @@ import yaml import numpy as np import pandas as pd -import jax import tfscreen from tfscreen.simulate import library_prediction, selection_experiment from tfscreen.simulate.selection_experiment import _sim_index_hop -from tfscreen.simulate.sim_data_class import build_sim_data -from tfscreen.simulate.sample_theta import sample_theta_prior from tfscreen.genetics import standardize_genotypes from tfscreen.process_raw import counts_to_lncfu from tfscreen.util.cli.generalized_main import generalized_main -def _generate_binding_data(cf, library_df, binding_cfg, rng): +def _generate_binding_data(binding_cfg, rng, binding_theta_df): """ Generate simulated binding curve data for specific genotypes. - Samples theta from the same prior used by library_prediction (same - theta_component and theta_rng_seed) but evaluated at the binding - concentrations. Adds Gaussian noise to the ground-truth theta values. + Uses pre-computed (stratified) theta values from library_prediction and + adds Gaussian noise to produce observed theta values. Parameters ---------- - cf : dict - Full run configuration (already read from YAML). - library_df : pandas.DataFrame - Library returned by library_prediction (has a 'genotype' column). binding_cfg : dict The 'binding_data' sub-dict from the config. Must contain: genotypes : list of genotype strings @@ -35,6 +27,9 @@ def _generate_binding_data(cf, library_df, binding_cfg, rng): titrant_conc: list of concentrations (mM) noise : float, sigma for Gaussian noise on theta_obs rng : numpy.random.Generator + binding_theta_df : pandas.DataFrame + Pre-computed binding theta from library_prediction. Must contain + columns ``genotype``, ``titrant_conc``, ``theta_true``. Returns ------- @@ -46,47 +41,22 @@ def _generate_binding_data(cf, library_df, binding_cfg, rng): titrant_conc = list(binding_cfg["titrant_conc"]) noise = float(binding_cfg.get("noise", 0.0)) - # Build a SimData for just the binding concentrations - binding_sample_df = pd.DataFrame({ - "titrant_conc": titrant_conc, - "condition_pre": "binding", - "condition_sel": "binding", - }) - sim_data = build_sim_data( - library_df=library_df, - sample_df=binding_sample_df, - thermo_data=cf.get("thermo_data"), - ) - - # Sample theta using the same seed as the main library_prediction run so - # binding theta values are consistent with the ground-truth phenotypes. - seed = cf.get("seed", None) - theta_rng_key = jax.random.PRNGKey(seed if seed is not None else 0) - theta_gc, _ = sample_theta_prior( - component_name=cf["theta_component"], - sim_data=sim_data, - rng_key=theta_rng_key, - priors_overrides=cf.get("theta_priors"), - ) - - # Build lookup: genotype string → row index in library_df (sim_data order) - all_genotypes = library_df["genotype"].tolist() - geno_to_idx = {g: i for i, g in enumerate(all_genotypes)} - - # Sorted unique concentrations (same order as sim_data.titrant_conc) - sorted_concs = np.sort(np.unique(titrant_conc)) - conc_to_col = {float(c): i for i, c in enumerate(sorted_concs)} + # Build lookup: (genotype, conc) → theta_true + theta_lookup = { + (row["genotype"], float(row["titrant_conc"])): row["theta_true"] + for _, row in binding_theta_df.iterrows() + } rows = [] for g in genotypes: - if g not in geno_to_idx: - raise ValueError( - f"Genotype '{g}' in binding_data.genotypes is not in the library." - ) - row_idx = geno_to_idx[g] for conc in titrant_conc: - col_idx = conc_to_col[float(conc)] - theta_true = float(theta_gc[row_idx, col_idx]) + key = (g, float(conc)) + if key not in theta_lookup: + raise ValueError( + f"No pre-computed theta for genotype '{g}' at conc {conc}. " + f"Ensure binding_data.genotypes and titrant_conc match the config." + ) + theta_true = float(theta_lookup[key]) if noise > 0: theta_obs = float(np.clip(theta_true + rng.normal(0, noise), 0, 1)) else: @@ -294,7 +264,7 @@ def out_path(name): # ------------------------------------------------------------------------- # Ground-truth library and phenotypes (deterministic given the config) - library_df, phenotype_df, genotype_theta_df, parameters_df = library_prediction(cf) + library_df, phenotype_df, genotype_theta_df, parameters_df, binding_theta_df = library_prediction(cf) # ------------------------------------------------------------------------- # Simulate independent replicates @@ -347,7 +317,7 @@ def out_path(name): print(f"\nWrote: {', '.join(out_path(n) for n in ['library', 'parameters', 'genotype_theta', 'growth'])}") if "binding_data" in cf: - binding_df = _generate_binding_data(cf, library_df, cf["binding_data"], rng) + binding_df = _generate_binding_data(cf["binding_data"], rng, binding_theta_df) binding_df.to_csv(out_path("binding"), index=False) print(f"Wrote: {out_path('binding')}") diff --git a/src/tfscreen/simulate/thermo_to_growth.py b/src/tfscreen/simulate/thermo_to_growth.py index 4557e524..95bce1e5 100644 --- a/src/tfscreen/simulate/thermo_to_growth.py +++ b/src/tfscreen/simulate/thermo_to_growth.py @@ -306,6 +306,7 @@ def thermo_to_growth( activity_component: str = "fixed", activity_priors_overrides: Optional[dict] = None, theta_rescale: str = "passthrough", + theta_gc_override: Optional[dict] = None, ) -> tuple[pd.DataFrame, pd.DataFrame]: """ Generate phenotypes from genotypes via prior-predictive theta sampling. @@ -385,6 +386,13 @@ def thermo_to_growth( the logit scale ``log(θ/(1−θ))``. The ``"theta"`` column in ``phenotype_df`` and ``genotype_theta_df`` always store the pre-rescale (0–1) value. + theta_gc_override : dict[str, np.ndarray] or None, default None + Per-genotype theta overrides. Keys are genotype strings; values are + 1-D arrays of theta at the growth concentrations (same sorted-unique + order as ``sample_df["titrant_conc"]``). Overrides are applied after + prior-predictive sampling but before noise and all downstream + calculations. Used by ``library_prediction`` to inject + stratified theta values for calibration genotypes. Returns ------- @@ -446,6 +454,22 @@ def thermo_to_growth( print("Done.", flush=True) + # ── Index lookups (shared by genotype_theta_df, phenotype_df, parameters_df) ─ + # Computed here (before noise) so that theta_gc_override can be applied + # using the same index map. Duplicates in library_df are handled by + # mapping each unique genotype to its last occurrence in sim_data order. + + all_genotypes = list(genotypes) # sim_data order (may have duplicates) + geno_to_sim_idx = {g: i for i, g in enumerate(all_genotypes)} + unique_sim_indices = np.array([geno_to_sim_idx[g] for g in unique_genotypes]) + + # ── Inject stratified theta for calibration genotypes ───────────────────── + if theta_gc_override: + theta_gc = np.array(theta_gc) # ensure a mutable copy + for g, theta_row in theta_gc_override.items(): + if g in geno_to_sim_idx: + theta_gc[geno_to_sim_idx[g], :] = theta_row + # ── Apply logit-normal noise to theta ───────────────────────────────────── if theta_noise_sigma_logit > 0.0: theta_safe = np.clip(theta_gc, 1e-6, 1.0 - 1e-6) @@ -453,15 +477,6 @@ def thermo_to_growth( epsilon = rng.normal(0.0, theta_noise_sigma_logit, size=theta_gc.shape) theta_gc = 1.0 / (1.0 + np.exp(-(logit_theta + epsilon))) - # ── Index lookups (shared by genotype_theta_df, phenotype_df, parameters_df) ─ - # Computed once here so that genotype_theta_df can be built from unique - # genotypes rather than the full sim_data order (which may contain duplicates - # when the same sequence appears in more than one sub-library). - - all_genotypes = list(genotypes) # sim_data order (may have duplicates) - geno_to_sim_idx = {g: i for i, g in enumerate(all_genotypes)} - unique_sim_indices = np.array([geno_to_sim_idx[g] for g in unique_genotypes]) - # ── Map sample_df concentrations to unique-concentration indices ────────── # sample_df may have duplicate concentrations across rows; each row maps to # one column of theta_gc. diff --git a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py index 381e4524..72d4f0ed 100644 --- a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py @@ -6,7 +6,7 @@ def predict_growth(config_file, param_file, - out_prefix="tfs_growth_pred", + out_prefix="tfs_pred_growth", genotypes_file=None, titrant_names_file=None, titrant_concs_file=None, @@ -51,7 +51,7 @@ def predict_growth(config_file, tfs-sample-posterior first. out_prefix : str, optional Prefix for the output CSV file. Written to {out_prefix}.csv. - Default 'tfs_growth_pred'. + Default 'tfs_pred_growth'. genotypes_file : str or None, optional Plain-text file with one genotype per line (slash-separated mutations, e.g. 'M42I/K84L', or 'wt'). These genotypes are unioned with all diff --git a/src/tfscreen/tfmodel/scripts/predict_theta_cli.py b/src/tfscreen/tfmodel/scripts/predict_theta_cli.py index 7d1caa6b..5e7b04a2 100644 --- a/src/tfscreen/tfmodel/scripts/predict_theta_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_theta_cli.py @@ -11,7 +11,7 @@ def predict_theta(config_file, param_file, - out_prefix="tfs_theta_pred", + out_prefix="tfs_pred_theta", genotypes_file=None, titrant_names_file=None, titrant_concs_file=None, @@ -60,7 +60,7 @@ def predict_theta(config_file, tfs-sample-posterior first. out_prefix : str, optional Prefix for the output CSV file. Written to {out_prefix}.csv. - Default 'tfs_theta_pred'. + Default 'tfs_pred_theta'. genotypes_file : str or None, optional Plain-text file with one genotype per line (slash-separated mutations, e.g. 'M42I/K84L', or 'wt'). These genotypes are unioned with all diff --git a/src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py b/src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py index 23f8e74a..2da39f26 100644 --- a/src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py +++ b/src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py @@ -41,7 +41,7 @@ def subset_genotypes( data_file, n_singles, n_steps, - out_prefix="genotype_subset", + out_prefix="tfs_subset", whitelist_file=None, blacklist_file=None, random_seed=42, @@ -70,7 +70,7 @@ def subset_genotypes( included in training are spaced linearly from 0 to all possible, with duplicate counts collapsed. out_prefix : str, optional - Prefix for all output files. Default 'genotype_subset'. + Prefix for all output files. Default 'tfs_subset'. whitelist_file : str, optional Path to a plain-text file (one genotype per line, # comments allowed) of genotypes that must be included in the selected singles. diff --git a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py index e3f8f944..c7e4d88a 100644 --- a/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py +++ b/src/tfscreen/tfmodel/scripts/summarize_fit_cli.py @@ -524,7 +524,7 @@ def summarize_fit(run_dir, """ Evaluate theta prediction quality for a tfs-fit-model run directory. - Scans run_dir for a *_config.yaml, *_theta_pred.csv, *_growth_pred.csv + Scans run_dir for a *_config.yaml, *_pred_theta.csv, *_pred_growth.csv (optional), and *_losses.txt (optional). Computes prediction statistics and writes output files to the summary directory. @@ -546,9 +546,9 @@ def summarize_fit(run_dir, - ``{out_prefix}_theta_corr.pdf`` — two-panel correlation plot for theta (training left, test right). - ``{out_prefix}_growth_corr.csv`` — relative symlink to the - *_growth_pred.csv in run_dir (only created when that file is present). + *_pred_growth.csv in run_dir (only created when that file is present). - ``{out_prefix}_growth_corr.pdf`` — correlation plot for ln_cfu (only - written when *_growth_pred.csv is present). + written when *_pred_growth.csv is present). - ``{out_prefix}_{genotype}_theta_fits.csv`` / ``.pdf`` — per-genotype joined observation + prediction data and fit plot (one pair per genotype present in both binding and prediction CSVs). @@ -600,9 +600,9 @@ def summarize_fit(run_dir, # --- Locate files in run_dir --- config_file = _find_unique(run_dir, "_config.yaml", "config") - theta_pred_file = _find_unique(run_dir, "_theta_pred.csv", "theta predictions") + theta_pred_file = _find_unique(run_dir, "_pred_theta.csv", "theta predictions") losses_file = _find_unique(run_dir, "_losses.txt", "losses", warn_missing=False) - growth_pred_file = _find_unique(run_dir, "_growth_pred.csv", "growth predictions", + growth_pred_file = _find_unique(run_dir, "_pred_growth.csv", "growth predictions", warn_missing=False) # --- Parse YAML config once --- diff --git a/tests/tfscreen/simulate/test_generate_presplit_data.py b/tests/tfscreen/simulate/test_generate_presplit_data.py index eec39165..83d3cf7c 100644 --- a/tests/tfscreen/simulate/test_generate_presplit_data.py +++ b/tests/tfscreen/simulate/test_generate_presplit_data.py @@ -199,7 +199,7 @@ def test_run_simulation_writes_presplit_csv(tmp_path): with patch("tfscreen.util.read_yaml", return_value=cf), \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)), \ + return_value=(lib_df, pheno_df, theta_df, params_df, None)), \ patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", @@ -234,7 +234,7 @@ def test_run_simulation_no_presplit_without_config(tmp_path): with patch("tfscreen.util.read_yaml", return_value=cf), \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)), \ + return_value=(lib_df, pheno_df, theta_df, params_df, None)), \ patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", diff --git a/tests/tfscreen/simulate/test_library_prediction.py b/tests/tfscreen/simulate/test_library_prediction.py index a1d68c65..c276d1ed 100644 --- a/tests/tfscreen/simulate/test_library_prediction.py +++ b/tests/tfscreen/simulate/test_library_prediction.py @@ -54,7 +54,7 @@ def test_library_prediction_success(mocker, mock_config): mock_jax_key = mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="mock_key") - lib_df, pheno_df, theta_df, params_df = library_prediction(cf="config.yaml") + lib_df, pheno_df, theta_df, params_df, binding_df = library_prediction(cf="config.yaml") tfscreen.util.read_yaml.assert_called_once_with("config.yaml", override_keys=None) mock_lm_cls.assert_called_once_with(mock_config) @@ -82,6 +82,7 @@ def test_library_prediction_success(mocker, mock_config): assert pheno_df.equals(mock_phenotype_df) assert theta_df.equals(mock_genotype_theta_df) assert params_df.equals(mock_parameters_df) + assert binding_df is None def test_library_prediction_config_error(): diff --git a/tests/tfscreen/simulate/test_run_simulation.py b/tests/tfscreen/simulate/test_run_simulation.py index 2a538cb8..4ebf74c8 100644 --- a/tests/tfscreen/simulate/test_run_simulation.py +++ b/tests/tfscreen/simulate/test_run_simulation.py @@ -74,7 +74,7 @@ def test_run_simulation_success(mocker, mock_config, mock_result_dfs): mocker.patch("tfscreen.util.read_yaml", return_value=mock_config) mock_lib_pred = mocker.patch( "tfscreen.simulate.run_simulation.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df), + return_value=(lib_df, pheno_df, theta_df, params_df, None), ) mock_sel_exp = mocker.patch("tfscreen.simulate.run_simulation.selection_experiment", return_value=(sample_df, counts_df)) @@ -104,7 +104,7 @@ def test_run_simulation_no_output(mocker, mock_config, mock_result_dfs): mocker.patch("tfscreen.util.read_yaml", return_value=mock_config) mocker.patch( "tfscreen.simulate.run_simulation.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df), + return_value=(lib_df, pheno_df, theta_df, params_df, None), ) mocker.patch("tfscreen.simulate.run_simulation.selection_experiment", return_value=(sample_df, counts_df)) diff --git a/tests/tfscreen/simulate/test_sample_theta.py b/tests/tfscreen/simulate/test_sample_theta.py index 4d1c9a56..3bd7a23f 100644 --- a/tests/tfscreen/simulate/test_sample_theta.py +++ b/tests/tfscreen/simulate/test_sample_theta.py @@ -1,8 +1,9 @@ import pytest import numpy as np +import pandas as pd from unittest.mock import MagicMock, patch -from tfscreen.simulate.sample_theta import sample_theta_prior, _EXCLUDED +from tfscreen.simulate.sample_theta import sample_theta_prior, _EXCLUDED, _greedy_maximin, sample_theta_stratified # ---------------------------------------------------------------------------- @@ -170,3 +171,130 @@ def test_sim_priors_overrides_applied(mock_sim_data): call_kwargs = mock_module.SimPriors.call_args[1] assert call_kwargs["alpha"] == 9.9 + + +# ---------------------------------------------------------------------------- +# _greedy_maximin +# ---------------------------------------------------------------------------- + +def test_greedy_maximin_returns_correct_count(): + rng = np.random.default_rng(0) + theta_gc = rng.uniform(0, 1, (20, 5)) + selected = _greedy_maximin(theta_gc, 4) + assert len(selected) == 4 + + +def test_greedy_maximin_no_duplicates(): + rng = np.random.default_rng(1) + theta_gc = rng.uniform(0, 1, (50, 8)) + selected = _greedy_maximin(theta_gc, 10) + assert len(selected) == len(set(selected.tolist())) + + +def test_greedy_maximin_selects_spread_curves(): + """The selected set should span the theta space broadly.""" + # 50 curves spread across [0,1]; 2 selected should have large distance between them + rng = np.random.default_rng(7) + theta_gc = rng.uniform(0, 1, (50, 4)) + selected = _greedy_maximin(theta_gc, 2) + a, b = theta_gc[selected[0]], theta_gc[selected[1]] + dist = np.sqrt(np.sum((a - b) ** 2)) + # Two randomly chosen points from 50 uniform in [0,1]^4 would average ~0.87 + # in max-pairwise distance; our greedy selection should comfortably exceed 0.5 + assert dist > 0.5 + + +def test_greedy_maximin_all_when_n_equals_pool(): + theta_gc = np.eye(5) + selected = _greedy_maximin(theta_gc, 5) + assert set(selected.tolist()) == set(range(5)) + + +def test_greedy_maximin_all_when_n_exceeds_pool(): + theta_gc = np.eye(3) + selected = _greedy_maximin(theta_gc, 10) + assert list(selected) == [0, 1, 2] + + +# ---------------------------------------------------------------------------- +# sample_theta_stratified +# ---------------------------------------------------------------------------- + +def _make_stratified_mock_module(pool_size, n_binding_concs, n_growth_concs): + """ + Return a mock theta module for sample_theta_stratified tests. + + run_model is called twice (once for binding sim_data, once for growth + sim_data). We return distinct matrices so tests can verify which call + produced which output. + """ + mock_module = MagicMock() + mock_module.get_hyperparameters.return_value = {} + mock_module.ModelPriors.return_value = MagicMock() + mock_module.define_model.return_value = MagicMock(name="theta_param") + + # Alternating call sides: binding first, then growth. + rng = np.random.default_rng(42) + binding_theta = rng.uniform(0, 1, (1, n_binding_concs, pool_size)) + growth_theta = rng.uniform(0, 1, (1, n_growth_concs, pool_size)) + mock_module.run_model.side_effect = [binding_theta, growth_theta] + mock_module._binding_theta = binding_theta + mock_module._growth_theta = growth_theta + return mock_module + + +def test_stratified_output_shapes(): + pool_size, n_select, n_binding_concs, n_growth_concs = 30, 4, 6, 3 + mock_module = _make_stratified_mock_module(pool_size, n_binding_concs, n_growth_concs) + + binding_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_binding_concs)}) + growth_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_growth_concs)}) + + with patch("tfscreen.simulate.sample_theta.model_registry", + {"theta": {"hill_geno": mock_module}}), \ + patch("tfscreen.simulate.sample_theta.handlers"), \ + patch("tfscreen.simulate.sample_theta.build_sim_data", return_value=MagicMock( + num_genotype=pool_size, num_titrant_conc=n_binding_concs)): + binding_gc, growth_gc = sample_theta_stratified( + "hill_geno", binding_sample_df, growth_sample_df, + rng_key=0, n_select=n_select, pool_size=pool_size, + ) + + assert binding_gc.shape == (n_select, n_binding_concs) + assert growth_gc.shape == (n_select, n_growth_concs) + + +def test_stratified_raises_when_n_select_exceeds_pool(): + binding_sample_df = pd.DataFrame({"titrant_conc": [0.0, 1.0]}) + growth_sample_df = pd.DataFrame({"titrant_conc": [0.5]}) + + with pytest.raises(ValueError, match="n_select"): + sample_theta_stratified( + "hill_geno", binding_sample_df, growth_sample_df, + rng_key=0, n_select=10, pool_size=5, + ) + + +def test_stratified_selected_rows_come_from_pool(): + """Selected binding rows must be a subset of the pool's binding rows.""" + pool_size, n_select, n_binding_concs, n_growth_concs = 20, 3, 4, 2 + mock_module = _make_stratified_mock_module(pool_size, n_binding_concs, n_growth_concs) + + binding_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_binding_concs)}) + growth_sample_df = pd.DataFrame({"titrant_conc": [0.0, 1.0]}) + + with patch("tfscreen.simulate.sample_theta.model_registry", + {"theta": {"hill_geno": mock_module}}), \ + patch("tfscreen.simulate.sample_theta.handlers"), \ + patch("tfscreen.simulate.sample_theta.build_sim_data", return_value=MagicMock( + num_genotype=pool_size, num_titrant_conc=n_binding_concs)): + binding_gc, growth_gc = sample_theta_stratified( + "hill_geno", binding_sample_df, growth_sample_df, + rng_key=0, n_select=n_select, pool_size=pool_size, + ) + + # Each selected binding row must exist in the full pool binding matrix + pool_binding = mock_module._binding_theta[0].T # (pool_size, n_binding_concs) + for row in binding_gc: + match = np.any(np.all(np.isclose(pool_binding, row[None, :]), axis=1)) + assert match, "Selected binding row not found in pool" diff --git a/tests/tfscreen/simulate/test_simulate_cli.py b/tests/tfscreen/simulate/test_simulate_cli.py index 41bc962f..c448de1f 100644 --- a/tests/tfscreen/simulate/test_simulate_cli.py +++ b/tests/tfscreen/simulate/test_simulate_cli.py @@ -72,7 +72,7 @@ def patched_simulation(tmp_path): with patch("tfscreen.util.read_yaml", return_value={"seed": 99}) as mock_yaml, \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)), \ + return_value=(lib_df, pheno_df, theta_df, params_df, None)), \ patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", @@ -118,7 +118,7 @@ def capture_csv(self_df, path, **kwargs): with patch("tfscreen.util.read_yaml", return_value={"seed": 0}), \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)), \ + return_value=(lib_df, pheno_df, theta_df, params_df, None)), \ patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", @@ -142,7 +142,7 @@ def capture_csv(self_df, path, **kwargs): with patch("tfscreen.util.read_yaml", return_value={"seed": 0}), \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)), \ + return_value=(lib_df, pheno_df, theta_df, params_df, None)), \ patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", @@ -156,38 +156,55 @@ def capture_csv(self_df, path, **kwargs): # --------------------------------------------------------------------------- -# _generate_binding_data: JAX key derived from seed +# _generate_binding_data: uses pre-computed theta from library_prediction # --------------------------------------------------------------------------- -def test_generate_binding_data_uses_seed_for_jax_key(tmp_path): - """_generate_binding_data must derive the JAX PRNGKey from seed, - not from a separate theta_rng_seed key.""" - import jax +def test_generate_binding_data_uses_precomputed_theta(): + """_generate_binding_data must use theta_true from binding_theta_df, + not re-sample from the prior.""" import numpy as np - from unittest.mock import patch, MagicMock from tfscreen.simulate.scripts.simulate_cli import _generate_binding_data - library_df = pd.DataFrame({"genotype": ["wt"]}) binding_cfg = { - "genotypes": ["wt"], + "genotypes": ["wt", "M1A"], "titrant_name": "iptg", "titrant_conc": [0.0, 1.0], "noise": 0.0, } - cf = {"seed": 55, "theta_component": "mock", "thermo_data": None} rng = np.random.default_rng(0) + binding_theta_df = pd.DataFrame([ + {"genotype": "wt", "titrant_conc": 0.0, "theta_true": 0.1}, + {"genotype": "wt", "titrant_conc": 1.0, "theta_true": 0.9}, + {"genotype": "M1A", "titrant_conc": 0.0, "theta_true": 0.2}, + {"genotype": "M1A", "titrant_conc": 1.0, "theta_true": 0.8}, + ]) - with patch("tfscreen.simulate.scripts.simulate_cli.jax.random.PRNGKey", - return_value=MagicMock()) as mock_key, \ - patch("tfscreen.simulate.scripts.simulate_cli.build_sim_data", - return_value=MagicMock()), \ - patch("tfscreen.simulate.scripts.simulate_cli.sample_theta_prior", - return_value=(np.ones((1, 2)), MagicMock())), \ - patch("tfscreen.simulate.scripts.simulate_cli.standardize_genotypes", - return_value=["wt"]): - _generate_binding_data(cf, library_df, binding_cfg, rng) + result = _generate_binding_data(binding_cfg, rng, binding_theta_df) - mock_key.assert_called_once_with(55) + assert set(result.columns) >= {"genotype", "titrant_name", "titrant_conc", + "theta_obs", "theta_std"} + wt_row = result[(result["genotype"] == "wt") & (result["titrant_conc"] == 1.0)] + assert float(wt_row["theta_obs"].iloc[0]) == pytest.approx(0.9) + + +def test_generate_binding_data_missing_genotype_raises(): + """Raises ValueError when a genotype/conc pair is absent from binding_theta_df.""" + import numpy as np + from tfscreen.simulate.scripts.simulate_cli import _generate_binding_data + + binding_cfg = { + "genotypes": ["wt", "A2V"], # A2V is valid but not in binding_theta_df + "titrant_name": "iptg", + "titrant_conc": [1.0], + "noise": 0.0, + } + rng = np.random.default_rng(0) + binding_theta_df = pd.DataFrame([ + {"genotype": "wt", "titrant_conc": 1.0, "theta_true": 0.5}, + ]) + + with pytest.raises(ValueError, match="No pre-computed theta"): + _generate_binding_data(binding_cfg, rng, binding_theta_df) # --------------------------------------------------------------------------- @@ -236,7 +253,7 @@ def capture_dump(data, fh, **kwargs): with patch("tfscreen.util.read_yaml", return_value={"seed": 5}), \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)), \ + return_value=(lib_df, pheno_df, theta_df, params_df, None)), \ patch("tfscreen.simulate.scripts.simulate_cli.selection_experiment", return_value=(sample_df, counts_df)), \ patch("tfscreen.simulate.scripts.simulate_cli.counts_to_lncfu", @@ -258,7 +275,7 @@ def test_input_config_yaml_existence_check(tmp_path): with patch("tfscreen.util.read_yaml", return_value={"seed": 0}), \ patch("tfscreen.simulate.scripts.simulate_cli.library_prediction", - return_value=(lib_df, pheno_df, theta_df, params_df)) as mock_lib: + return_value=(lib_df, pheno_df, theta_df, params_df, None)) as mock_lib: with pytest.raises(FileExistsError, match="input-config.yaml"): run_simulation_from_config("config.yaml", str(tmp_path)) diff --git a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py index 86053c2b..f17612c6 100644 --- a/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_summarize_fit_cli.py @@ -126,7 +126,7 @@ def run_dir(tmp_path): binding_path = str(tmp_path / "binding.csv") _make_binding_csv(binding_path) - _make_pred_csv(str(tmp_path / "run_theta_pred.csv")) + _make_pred_csv(str(tmp_path / "run_pred_theta.csv")) _make_guesses_csv(str(tmp_path / "run_guesses.csv"), n_params=25) _make_losses_txt(str(tmp_path / "run_losses.txt"), final_loss=-4321.0) _make_config_yaml( @@ -403,7 +403,7 @@ def test_growth_training_stats_populated_when_file_present(self, run_dir): "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), "q0.5": np.linspace(8.1, 13.1, 6), - }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + }).to_csv(os.path.join(run_dir, "tfs_pred_growth.csv"), index=False) summarize_fit(run_dir) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) @@ -419,7 +419,7 @@ def test_growth_training_drops_nan_ln_cfu(self, run_dir): "ln_cfu": [8.0, np.nan, 9.0, np.nan, 10.0, 11.0], "q0.5": np.linspace(8.1, 13.1, 6), }) - df.to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + df.to_csv(os.path.join(run_dir, "tfs_pred_growth.csv"), index=False) summarize_fit(run_dir) with open(os.path.join(run_dir, "summary", "tfs_summarize_fit_summary.json")) as fh: data = json.load(fh) @@ -435,7 +435,7 @@ def test_growth_corr_pdf_written_when_file_present(self, run_dir): "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), "q0.5": np.linspace(8.1, 13.1, 6), - }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + }).to_csv(os.path.join(run_dir, "tfs_pred_growth.csv"), index=False) summarize_fit(run_dir) assert os.path.exists(os.path.join(run_dir, "summary", "tfs_summarize_growth_corr.pdf")) @@ -477,7 +477,7 @@ def test_theta_corr_test_csv_not_written_without_ref_theta(self, run_dir): ) def test_theta_corr_test_csv_written_with_ref_theta_file(self, run_dir, ref_theta_file): - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) summarize_fit(run_dir, ref_theta_file=ref_theta_file) csv_path = os.path.join(run_dir, "summary", "tfs_summarize_theta_corr_test.csv") @@ -488,7 +488,7 @@ def test_theta_corr_test_csv_written_with_ref_theta_file(self, run_dir, ref_thet assert len(df) == len(TITRANT_CONCS) def test_growth_corr_csv_written(self, run_dir): - growth_pred_path = os.path.join(run_dir, "tfs_growth_pred.csv") + growth_pred_path = os.path.join(run_dir, "tfs_pred_growth.csv") pd.DataFrame({ "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), @@ -512,7 +512,7 @@ def test_growth_corr_csv_not_written_when_file_absent(self, run_dir): ) def test_growth_corr_csv_overwritten_on_rerun(self, run_dir): - growth_pred_path = os.path.join(run_dir, "tfs_growth_pred.csv") + growth_pred_path = os.path.join(run_dir, "tfs_pred_growth.csv") pd.DataFrame({ "genotype": ["wt"] * 3, "ln_cfu": [8.0, 9.0, 10.0], @@ -539,7 +539,7 @@ class TestSummarizeFitWithRefTheta: def test_theta_test_stats_populated(self, run_dir, ref_theta_file): # Add out-of-training genotype to pred CSV - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) summarize_fit(run_dir, ref_theta_file=ref_theta_file) @@ -549,7 +549,7 @@ def test_theta_test_stats_populated(self, run_dir, ref_theta_file): assert data["metadata"]["n_theta_test_points"] == len(TITRANT_CONCS) def test_theta_test_pearson_r_reasonable(self, run_dir, ref_theta_file): - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) summarize_fit(run_dir, ref_theta_file=ref_theta_file) @@ -561,7 +561,7 @@ def test_theta_test_pearson_r_reasonable(self, run_dir, ref_theta_file): def test_theta_col_fallback_to_theta(self, run_dir, tmp_path): """Ref theta file with 'theta' column (no 'theta_obs') still works.""" - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) gt_path = str(tmp_path / "ref_theta_col.csv") @@ -575,7 +575,7 @@ def test_theta_col_fallback_to_theta(self, run_dir, tmp_path): def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): """Ref theta file with no theta column issues a warning and skips test stats.""" - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) gt_path = str(tmp_path / "ref_no_theta.csv") @@ -598,7 +598,7 @@ def test_missing_theta_col_warns_and_skips(self, run_dir, tmp_path): def test_auto_discovers_sim_genotype_theta_in_run_dir(self, run_dir): """*_sim_genotype_theta.csv in run_dir is used automatically when no arg given.""" - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) _make_ref_theta_csv( os.path.join(run_dir, "tfs_sim_genotype_theta.csv"), theta_col="theta_obs" @@ -613,7 +613,7 @@ def test_auto_discovers_sim_genotype_theta_in_run_dir(self, run_dir): def test_ref_theta_file_arg_overrides_auto_discovery(self, run_dir, tmp_path): """Explicit ref_theta_file takes precedence over auto-discovered file.""" - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) # Auto-discoverable file: has no matching genotypes → would give 0 test points @@ -636,7 +636,7 @@ def test_ref_theta_file_arg_overrides_auto_discovery(self, run_dir, tmp_path): def test_metadata_ref_theta_file_recorded(self, run_dir, ref_theta_file): """ref_theta_file path is recorded in JSON metadata.""" - pred_path = os.path.join(run_dir, "run_theta_pred.csv") + pred_path = os.path.join(run_dir, "run_pred_theta.csv") _make_pred_csv(pred_path, n_training=3, extra_genotypes=["E4F"]) summarize_fit(run_dir, ref_theta_file=ref_theta_file) @@ -659,7 +659,7 @@ def test_metadata_ref_theta_file_null_when_nothing_found(self, run_dir): class TestSummarizeFitGraceful: def test_missing_theta_pred_still_writes_json(self, run_dir): - os.remove(os.path.join(run_dir, "run_theta_pred.csv")) + os.remove(os.path.join(run_dir, "run_pred_theta.csv")) with warnings.catch_warnings(record=True): warnings.simplefilter("always") summarize_fit(run_dir) @@ -946,7 +946,7 @@ def test_summarize_fit_calls_plot_theta_fits_per_training_genotype(self, run_dir assert mock_plot.call_count == 3 def test_summarize_fit_skips_theta_fits_when_no_pred_csv(self, run_dir): - os.remove(os.path.join(run_dir, "run_theta_pred.csv")) + os.remove(os.path.join(run_dir, "run_pred_theta.csv")) with patch(_PATCH_PLOT_THETA_FITS) as mock_plot: with warnings.catch_warnings(record=True): warnings.simplefilter("always") @@ -1852,7 +1852,7 @@ class TestCalibrationIntegration: # ------------------------------------------------------------------ def test_calibration_called_for_theta_training_with_quantile_cols(self, run_dir): - _make_pred_csv_with_quantiles(os.path.join(run_dir, "run_theta_pred.csv")) + _make_pred_csv_with_quantiles(os.path.join(run_dir, "run_pred_theta.csv")) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: summarize_fit(run_dir) prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] @@ -1868,7 +1868,7 @@ def test_calibration_skipped_for_theta_training_without_quantile_cols(self, run_ "titrant_conc": tc, "q0.5": 0.5, "in_training_data": 1, }) pd.DataFrame(rows).to_csv( - os.path.join(run_dir, "run_theta_pred.csv"), index=False + os.path.join(run_dir, "run_pred_theta.csv"), index=False ) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: summarize_fit(run_dir) @@ -1883,7 +1883,7 @@ def test_calibration_called_for_theta_test_with_quantile_cols( self, run_dir, ref_theta_file ): _make_pred_csv_with_quantiles( - os.path.join(run_dir, "run_theta_pred.csv"), + os.path.join(run_dir, "run_pred_theta.csv"), n_training=3, extra_genotypes=["E4F"], ) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: @@ -1892,7 +1892,7 @@ def test_calibration_called_for_theta_test_with_quantile_cols( assert any("theta_test" in p for p in prefixes) def test_calibration_skipped_for_theta_test_when_no_ref_theta_file(self, run_dir): - _make_pred_csv_with_quantiles(os.path.join(run_dir, "run_theta_pred.csv")) + _make_pred_csv_with_quantiles(os.path.join(run_dir, "run_pred_theta.csv")) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: summarize_fit(run_dir) prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] @@ -1909,7 +1909,7 @@ def test_calibration_called_for_growth_with_quantile_cols(self, run_dir): "q0.025": np.linspace(7.5, 12.5, 6), "q0.5": np.linspace(8.0, 13.0, 6), "q0.975": np.linspace(8.5, 13.5, 6), - }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + }).to_csv(os.path.join(run_dir, "tfs_pred_growth.csv"), index=False) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: summarize_fit(run_dir) prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] @@ -1922,7 +1922,7 @@ def test_calibration_uses_ref_col_for_growth(self, run_dir): "q0.025": np.linspace(7.5, 12.5, 6), "q0.5": np.linspace(8.0, 13.0, 6), "q0.975": np.linspace(8.5, 13.5, 6), - }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + }).to_csv(os.path.join(run_dir, "tfs_pred_growth.csv"), index=False) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: summarize_fit(run_dir) growth_calls = [c for c in mock_cal.call_args_list @@ -1939,7 +1939,7 @@ def test_calibration_skipped_for_growth_without_quantile_cols(self, run_dir): "genotype": ["wt"] * 6, "ln_cfu": np.linspace(8.0, 13.0, 6), "q0.5": np.linspace(8.0, 13.0, 6), - }).to_csv(os.path.join(run_dir, "tfs_growth_pred.csv"), index=False) + }).to_csv(os.path.join(run_dir, "tfs_pred_growth.csv"), index=False) with patch(_PATCH_CALIBRATION_SUMMARY) as mock_cal: summarize_fit(run_dir) prefixes = [call.kwargs["out_prefix"] for call in mock_cal.call_args_list] From 4e8dd546e63a2ba2c5a1acf2b8d2e54fb91e9aa4 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Fri, 19 Jun 2026 11:11:41 -0700 Subject: [PATCH 19/24] working to fix theta undershoot in SVI --- CLAUDE.md | 63 ++ docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 393 +++++++----- reports/junit/junit.xml | 2 +- src/tfscreen/simulate/binding_params.py | 481 ++++++++++++++ src/tfscreen/simulate/library_prediction.py | 163 ++++- src/tfscreen/simulate/sample_theta.py | 73 ++- src/tfscreen/simulate/scripts/simulate_cli.py | 10 +- src/tfscreen/simulate/thermo_to_growth.py | 20 + .../tfmodel/analysis/error_calibration.py | 4 +- .../generative/components/theta/_simple.py | 74 ++- .../generative/components/theta/hill_mut.py | 41 +- .../tfmodel/scripts/prefit_calibration_cli.py | 43 +- .../tfscreen/simulate/test_binding_params.py | 585 ++++++++++++++++++ .../simulate/test_library_prediction.py | 172 +++++ tests/tfscreen/simulate/test_sample_theta.py | 108 +++- .../simulate/test_thermo_to_growth.py | 274 ++++++++ .../components/theta/test__simple.py | 137 +++- .../components/theta/test_hill_mut.py | 109 ++++ .../scripts/test_prefit_calibration_cli.py | 128 +++- 21 files changed, 2556 insertions(+), 328 deletions(-) create mode 100644 src/tfscreen/simulate/binding_params.py create mode 100644 tests/tfscreen/simulate/test_binding_params.py diff --git a/CLAUDE.md b/CLAUDE.md index b0e7c095..453392f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,6 +131,69 @@ The hierarchical Bayesian inference engine. Key files: See **YAML Standards** below for the full conventions. The tfmodel config (`tfs_configure_config.yaml`) is generated by `tfs-configure-model` and drives `tfs-fit-model`. It contains `data`, `components`, `priors_file`, and `guesses_file` sections. Do not hand-edit it. +## Simulation Internals (`simulate/`) + +### Overview + +The simulation pipeline generates ground-truth phenotypes for a synthetic TF screen experiment. The top-level entry point is `library_prediction` (`simulate/library_prediction.py`), which returns five dataframes: + +| Return value | Content | +|---|---| +| `library_df` | One row per genotype in the library | +| `phenotype_df` | Long-form growth predictions (one row per genotype × condition) | +| `genotype_theta_df` | Long-form theta predictions (one row per genotype × titrant_conc) | +| `parameters_df` | One row per genotype; per-genotype Hill/theta params + dk_geno + activity | +| `binding_theta_df` | Theta at binding concentrations for calibration genotypes (`None` if not configured) | + +The core calculation happens in `thermo_to_growth` (`simulate/thermo_to_growth.py`): + +1. **Prior-predictive theta sampling** — `sample_theta_prior` draws a `theta_gc` matrix of shape `(G, C)` where G = number of library genotypes and C = number of unique titrant concentrations. The genotype order here matches the library (`sim_data`) order. +2. **theta_gc_override injection** — specific rows of `theta_gc` can be replaced before any further computation (see below). +3. **Growth rate calculation** — theta is mapped to growth rates via the configured `growth_params`. +4. **parameters_df assembly** — per-genotype Hill parameters extracted from the `theta_param` pytree, then patched with `theta_params_override` (see below). + +### Binding data and calibration genotypes + +The optional `binding_data` YAML block configures calibration genotypes for which measured binding curves are available. It has two sub-paths that can coexist: + +**Path 1: Simulated genotypes** (`binding_data.genotypes` list) +- `wt` gets its natural unperturbed prior-predictive reference curve. +- Non-wt entries are drawn via a stratified (greedy maximin) algorithm across binding concentrations, ensuring diverse coverage. +- The selected theta values at *growth* concentrations are injected via `theta_gc_override` so that the simulated growth data is consistent with the binding data. + +**Path 2: Measured Hill parameters** (`binding_data.genotype_params_file`) +- Reads a CSV with columns `genotype, theta_low, theta_high, log_hill_K, hill_n`. +- Only supported for Hill-based theta components (`hill_geno`, `hill_mut`). +- **`theta_low` and `theta_high` are clamped to `[1e-4, 1-1e-4]` at read time** with a `UserWarning`. This is critical: a value of e.g. `1.000004` (a common float-rounding artefact) maps to `logit ≈ +16`, making all per-mutation deltas ~13–15 σ under the `HalfNormal(1)` prior on delta scales and preventing inference from recovering reasonable theta values. +- For `hill_mut`: the function `build_theta_gc_override_hill_mut` assembles theta for **all library genotypes** (not just the measured ones) by additively combining per-mutation logit-space deltas. The WT reference is taken from `SimPriors` defaults. Multi-mutant genotypes not directly measured in the CSV are assembled from single-mutant deltas; directly-measured multi-mutants use their CSV values directly. +- Measured genotypes override any earlier simulated-path values in `theta_gc_override`. + +### theta_gc_override and theta_params_override + +These two dicts are the mechanism by which binding data is "pinned" into the growth simulation. + +**`theta_gc_override`** (`dict[str, np.ndarray]`): +- Keys are genotype strings; values are 1-D arrays of theta at the growth titrant concentrations (sorted-unique order from `sample_df`). +- Applied in `thermo_to_growth` *after* prior-predictive sampling but *before* noise and all downstream computations. +- Overwrites the corresponding row of `theta_gc` in-place using `geno_to_sim_idx` (which maps genotype → its index in the original library list). + +**`theta_params_override`** (`dict[str, dict[str, float]]`): +- Keys are genotype strings; values are dicts with Hill parameter keys (`theta_low`, `theta_high`, `log_hill_K`, `hill_n`). +- Applied in `thermo_to_growth` *after* `parameters_df` is assembled from the `theta_param` pytree, overwriting the relevant columns. +- Purpose: the `theta_param` from `sample_theta_prior` reflects the prior-predictive draw, not the override. Without this patch, `tfs_sim_parameters.csv` would show the pre-override values, making the saved parameters inconsistent with what was actually simulated. +- Only columns already present in `parameters_df` are updated; unknown keys are silently skipped. +- Genotype keys not found in `parameters_df` are silently skipped. + +### Key files + +| File | Role | +|---|---| +| `simulate/library_prediction.py` | Top-level orchestrator; assembles override dicts and calls `thermo_to_growth` | +| `simulate/thermo_to_growth.py` | Prior-predictive sampling, theta injection, growth calculation, parameters_df assembly | +| `simulate/binding_params.py` | CSV reading (with theta clipping), per-component override builders, binding theta assemblers | +| `simulate/sample_theta.py` | `sample_theta_prior` (prior-predictive) and `sample_theta_stratified` (greedy maximin) | +| `simulate/sim_data_class.py` | `SimData` container and `build_sim_data` factory | + ## YAML Standards All YAML files in this codebase follow these conventions. Apply them when creating or modifying any YAML file. diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 90b87d80..f23c57bb 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.08%coverage96.08% \ No newline at end of file +coverage: 96.14%coverage96.14% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index bc5b626d..a69536ec 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3363tests3363 \ No newline at end of file +tests: 3443tests3443 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index e8078875..b2468a96 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1264,15 +1264,46 @@ ./src/tfscreen/process_raw/scripts/process_fastq_cli.py:418:33: E231 missing whitespace after ',' ./src/tfscreen/process_raw/scripts/process_fastq_cli.py:420:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/simulate/__init__.py:29:2: W292 no newline at end of file +./src/tfscreen/simulate/binding_params.py:225:1: C901 'build_theta_gc_override_hill_mut' is too complex (15) +./src/tfscreen/simulate/binding_params.py:289:17: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:290:18: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:292:13: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:293:13: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:296:16: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:298:12: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:299:12: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:317:20: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:318:21: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:321:27: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:321:43: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:323:23: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:324:23: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:330:32: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:332:28: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:333:28: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:336:16: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:350:14: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:350:31: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:352:10: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:352:27: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:353:10: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:353:27: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:383:22: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:385:18: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:386:18: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:389:18: E221 multiple spaces before operator +./src/tfscreen/simulate/binding_params.py:391:15: E221 multiple spaces before operator ./src/tfscreen/simulate/build_sample_dataframes.py:4:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/simulate/build_sample_dataframes.py:56:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:68:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:83:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:87:1: W293 blank line contains whitespace ./src/tfscreen/simulate/build_sample_dataframes.py:95:21: W292 no newline at end of file -./src/tfscreen/simulate/library_prediction.py:20:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/simulate/library_prediction.py:21:43: E252 missing whitespace around parameter equals -./src/tfscreen/simulate/library_prediction.py:21:44: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:28:1: C901 'library_prediction' is too complex (14) +./src/tfscreen/simulate/library_prediction.py:28:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/simulate/library_prediction.py:29:43: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:29:44: E252 missing whitespace around parameter equals +./src/tfscreen/simulate/library_prediction.py:123:9: F841 local variable 'binding_noise' is assigned to but never used ./src/tfscreen/simulate/run_simulation.py:12:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/simulate/run_simulation.py:17:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:18:36: E231 missing whitespace after ',' @@ -1311,34 +1342,35 @@ ./src/tfscreen/simulate/run_simulation.py:101:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:106:1: W293 blank line contains whitespace ./src/tfscreen/simulate/run_simulation.py:107:1: W391 blank line at end of file -./src/tfscreen/simulate/sample_theta.py:159:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:160:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:161:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:162:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:163:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:164:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:165:30: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/sample_theta.py:166:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:170:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:171:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:172:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:173:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:174:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:175:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:176:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:177:30: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/sample_theta.py:270:19: E221 multiple spaces before operator ./src/tfscreen/simulate/scripts/setup_sim_grid_cli.py:101:1: C901 'setup_sim_grid' is too complex (17) -./src/tfscreen/simulate/scripts/simulate_cli.py:113:15: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:114:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:115:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:116:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:120:20: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:121:22: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:122:21: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:141:43: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/scripts/simulate_cli.py:142:14: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:143:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:148:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:161:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:164:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:165:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:166:16: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:169:17: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:171:25: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:173:19: E221 multiple spaces before operator -./src/tfscreen/simulate/scripts/simulate_cli.py:187:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:119:15: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:120:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:121:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:122:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:126:20: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:127:22: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:128:21: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:147:43: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/scripts/simulate_cli.py:148:14: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:149:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:154:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:167:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:170:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:171:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:172:16: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:175:17: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:177:25: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:179:19: E221 multiple spaces before operator +./src/tfscreen/simulate/scripts/simulate_cli.py:193:19: E221 multiple spaces before operator ./src/tfscreen/simulate/selection_experiment.py:42:38: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:43:37: E231 missing whitespace after ':' ./src/tfscreen/simulate/selection_experiment.py:44:36: E231 missing whitespace after ':' @@ -1479,8 +1511,8 @@ ./src/tfscreen/simulate/sim_data_class.py:97:1: E303 too many blank lines (3) ./src/tfscreen/simulate/thermo_to_growth.py:102:36: E127 continuation line over-indented for visual indent ./src/tfscreen/simulate/thermo_to_growth.py:103:36: E127 continuation line over-indented for visual indent -./src/tfscreen/simulate/thermo_to_growth.py:289:1: C901 'thermo_to_growth' is too complex (13) -./src/tfscreen/simulate/thermo_to_growth.py:618:41: E127 continuation line over-indented for visual indent +./src/tfscreen/simulate/thermo_to_growth.py:289:1: C901 'thermo_to_growth' is too complex (18) +./src/tfscreen/simulate/thermo_to_growth.py:626:41: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/__init__.py:2:73: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:3:20: W291 trailing whitespace ./src/tfscreen/tfmodel/__init__.py:18:65: W291 trailing whitespace @@ -2046,99 +2078,99 @@ ./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:882:11: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/hill_geno.py:885:14: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:193:76: E261 at least two spaces before inline comment -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:293:31: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:294:31: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:295:31: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:333:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:333:30: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:335:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:335:28: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:336:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:336:28: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:347:22: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:347:38: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:349:23: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:349:36: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:350:23: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:350:36: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:360:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:362:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:363:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:366:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:366:44: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:368:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:368:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:369:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:369:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:601:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:603:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:604:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:24: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:44: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:73: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:612:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:612:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:612:71: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:613:19: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:613:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:613:71: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:693:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:695:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:698:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:705:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:705:31: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:707:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:708:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:708:27: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:741:22: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:743:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:744:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:748:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:749:70: E261 at least two spaces before inline comment -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:750:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:754:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:18: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:21: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:758:19: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:758:21: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:759:19: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:759:21: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:760:15: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:760:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:760:21: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:11: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:16: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:762:14: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:762:16: E251 unexpected spaces around keyword / parameter equals -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:839:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1006:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1008:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1009:13: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1010:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1011:12: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1012:8: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1013:8: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1016:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1016:43: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1018:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1018:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1019:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1019:39: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1022:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1024:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1025:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1026:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1028:14: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1029:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:300:31: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:301:31: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:302:31: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:340:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:340:30: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:342:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:342:28: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:343:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:343:28: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:354:22: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:354:38: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:356:23: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:356:36: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:357:23: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:357:36: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:367:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:369:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:370:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:373:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:373:44: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:375:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:375:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:376:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:376:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:608:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:610:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:611:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:617:24: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:617:44: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:617:73: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:619:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:619:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:619:71: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:620:19: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:620:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:620:71: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:700:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:702:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:705:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:712:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:712:31: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:714:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:715:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:715:27: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:748:22: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:750:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:751:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:755:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:756:70: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:757:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:761:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:764:18: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:764:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:764:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:765:19: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:765:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:766:19: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:766:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:767:15: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:767:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:767:21: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:768:11: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:768:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:768:16: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:769:14: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:769:16: E251 unexpected spaces around keyword / parameter equals +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:846:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1013:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1015:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1016:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1017:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1018:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1019:8: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1020:8: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1023:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1023:43: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1025:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1025:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1026:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1026:39: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1029:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1031:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1032:14: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1033:18: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1035:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1036:15: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1079:40: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1134:17: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1147:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1150:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1035:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1036:14: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1040:18: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1042:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1043:15: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1086:40: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1141:17: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1154:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/theta/hill_mut.py:1157:14: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/PK.py:38:1: F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused ./src/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/PK.py:38:1: F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.get_population_moments' imported but unused ./src/tfscreen/tfmodel/generative/components/theta/thermo/O2_C12_K5_U0_a/PK.py:171:6: E221 multiple spaces before operator @@ -3667,14 +3699,17 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:245:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:246:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:247:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:250:1: C901 '_inject_calibration_priors' is too complex (21) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:441:1: C901 '_build_csv_updates' is too complex (11) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:535:1: C901 '_apply_guesses_updates' is too complex (12) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:687:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:691:1: E302 expected 2 blank lines, found 3 +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:184:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:185:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:190:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:255:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:256:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:259:1: C901 '_inject_calibration_priors' is too complex (21) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:450:1: C901 '_build_csv_updates' is too complex (11) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:544:1: C901 '_apply_guesses_updates' is too complex (12) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:696:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:700:1: E302 expected 2 blank lines, found 3 ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used @@ -5159,6 +5194,37 @@ ./tests/tfscreen/simulate/growth/test_transition_linkage.py:566:14: E221 multiple spaces before operator ./tests/tfscreen/simulate/growth/test_transition_linkage.py:567:14: E221 multiple spaces before operator ./tests/tfscreen/simulate/growth/test_transition_linkage.py:568:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:5:1: F401 'io' imported but unused +./tests/tfscreen/simulate/test_binding_params.py:8:1: F401 'pandas as pd' imported but unused +./tests/tfscreen/simulate/test_binding_params.py:11:1: F401 'tfscreen.simulate.binding_params._logit' imported but unused +./tests/tfscreen/simulate/test_binding_params.py:83:20: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:85:16: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:86:17: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:87:25: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:89:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:90:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:91:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:93:19: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:203:22: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:205:32: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:215:35: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:217:32: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:229:35: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:237:35: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:302:38: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:304:37: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:313:5: F811 redefinition of unused 'pd' from line 8 +./tests/tfscreen/simulate/test_binding_params.py:316:15: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:317:14: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:422:43: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_binding_params.py:442:56: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_binding_params.py:443:56: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_binding_params.py:463:46: E127 continuation line over-indented for visual indent +./tests/tfscreen/simulate/test_binding_params.py:484:38: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:485:39: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:487:37: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:505:45: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_binding_params.py:584:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:9:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:33:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/simulate/test_build_sample_dataframes.py:52:1: W293 blank line contains whitespace @@ -5198,6 +5264,7 @@ ./tests/tfscreen/simulate/test_generate_presplit_data.py:229:33: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_library_prediction.py:47:41: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_library_prediction.py:290:5: F401 'numpy as np' imported but unused +./tests/tfscreen/simulate/test_library_prediction.py:439:16: E272 multiple spaces before keyword ./tests/tfscreen/simulate/test_run_simulation.py:3:1: F401 'os' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:5:1: F401 'unittest.mock.MagicMock' imported but unused ./tests/tfscreen/simulate/test_run_simulation.py:7:1: F401 'tfscreen.util' imported but unused @@ -5218,12 +5285,11 @@ ./tests/tfscreen/simulate/test_run_simulation.py:113:1: W293 blank line contains whitespace ./tests/tfscreen/simulate/test_run_simulation.py:116:5: F841 local variable 'results' is assigned to but never used ./tests/tfscreen/simulate/test_run_simulation.py:117:1: W293 blank line contains whitespace -./tests/tfscreen/simulate/test_sample_theta.py:239:17: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_sample_theta.py:242:30: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_sample_theta.py:251:21: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_sample_theta.py:264:27: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_sample_theta.py:269:21: E221 multiple spaces before operator -./tests/tfscreen/simulate/test_sample_theta.py:284:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:260:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:277:27: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:282:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:297:21: E221 multiple spaces before operator +./tests/tfscreen/simulate/test_sample_theta.py:338:21: E221 multiple spaces before operator ./tests/tfscreen/simulate/test_selection_experiment.py:4:1: F401 'numpy.ma' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:6:1: F401 'scipy.stats.gmean' imported but unused ./tests/tfscreen/simulate/test_selection_experiment.py:9:1: F401 'unittest.mock.MagicMock' imported but unused @@ -5387,8 +5453,8 @@ ./tests/tfscreen/simulate/test_thermo_to_growth.py:195:14: E131 continuation line unaligned for hanging indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:222:44: E127 continuation line over-indented for visual indent ./tests/tfscreen/simulate/test_thermo_to_growth.py:229:35: E127 continuation line over-indented for visual indent -./tests/tfscreen/simulate/test_thermo_to_growth.py:1163:1: E402 module level import not at top of file -./tests/tfscreen/simulate/test_thermo_to_growth.py:1251:9: F841 local variable 'G_out' is assigned to but never used +./tests/tfscreen/simulate/test_thermo_to_growth.py:1437:1: E402 module level import not at top of file +./tests/tfscreen/simulate/test_thermo_to_growth.py:1525:9: F841 local variable 'G_out' is assigned to but never used ./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:313:9: F401 'matplotlib.axes.Axes' imported but unused ./tests/tfscreen/tfmodel/analysis/test_error_calibration.py:355:9: F401 'matplotlib.axes.Axes' imported but unused ./tests/tfscreen/tfmodel/generative/components/activity/test_fixed.py:23:1: E302 expected 2 blank lines, found 1 @@ -5745,6 +5811,7 @@ ./tests/tfscreen/tfmodel/generative/components/sample_offset/test_zero.py:2:1: F401 'jax.numpy as jnp' imported but unused ./tests/tfscreen/tfmodel/generative/components/test_pinning.py:4:1: F401 'numpyro as pyro' imported but unused ./tests/tfscreen/tfmodel/generative/components/theta/test__simple.py:6:1: F401 'tfscreen.tfmodel.generative.components.theta._simple._THETA_EPS' imported but unused +./tests/tfscreen/tfmodel/generative/components/theta/test__simple.py:447:34: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:5:1: F401 'numpyro.distributions as dist' imported but unused ./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:6:1: F401 'numpyro.handlers.trace' imported but unused ./tests/tfscreen/tfmodel/generative/components/theta/test_categorical_geno.py:10:1: F401 'tfscreen.tfmodel.generative.components.theta.categorical_geno.ThetaParam' imported but unused @@ -5825,6 +5892,10 @@ ./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:528:21: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:530:32: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:530:52: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:719:34: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:722:31: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:730:36: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py:730:70: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:25:11: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:30:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/generative/components/theta/test_predict_unmeasured.py:155:31: E221 multiple spaces before operator @@ -6519,18 +6590,18 @@ ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:14:1: F401 'sys' imported but unused ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:21:1: F401 'jax' imported but unused ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:26:1: F401 'tfscreen.tfmodel.scripts.prefit_calibration_cli._identify_field_mapping' imported but unused -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:649:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:650:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:651:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:652:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:653:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:654:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:655:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:656:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:657:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1009:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1028:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1043:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:728:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:729:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:730:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:731:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:732:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:733:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:734:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:735:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:736:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1089:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1108:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1123:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused ./tests/tfscreen/tfmodel/scripts/test_sample_prior_cli.py:3:1: F401 'pytest' imported but unused ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes_cli.py:84:31: E741 ambiguous variable name 'l' @@ -7666,7 +7737,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -48 C901 'cat_fit' is too complex (19) +50 C901 'cat_fit' is too complex (19) 15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7674,14 +7745,14 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -172 E127 continuation line over-indented for visual indent +177 E127 continuation line over-indented for visual indent 79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1772 E221 multiple spaces before operator +1829 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1115 E231 missing whitespace after ',' @@ -7691,9 +7762,9 @@ 1 E262 inline comment should start with '# ' 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment -148 E272 multiple spaces before keyword +149 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -800 E302 expected 2 blank lines, found 1 +801 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) 16 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 @@ -7706,12 +7777,12 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 20 E741 ambiguous variable name 'l' -227 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders -3 F811 redefinition of unused 'np' from line 1 -40 F841 local variable 'e' is assigned to but never used +4 F811 redefinition of unused 'np' from line 1 +41 F841 local variable 'e' is assigned to but never used 669 W291 trailing whitespace 86 W292 no newline at end of file 1673 W293 blank line contains whitespace 29 W391 blank line at end of file -7668 +7739 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 2c3092ef..c9fe6bce 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/binding_params.py b/src/tfscreen/simulate/binding_params.py new file mode 100644 index 00000000..201c8b55 --- /dev/null +++ b/src/tfscreen/simulate/binding_params.py @@ -0,0 +1,481 @@ +""" +Utilities for injecting measured Hill binding parameters into the simulation. + +When real binding data (theta curves from fluorescence, SPR, ITC, etc.) are +available for specific genotypes, supply a CSV of per-genotype Hill parameters +via the ``binding_data.genotype_params_file`` config key. This module reads +those parameters and computes theta-value overrides for injection into the +simulation via the ``theta_gc_override`` mechanism in ``thermo_to_growth``. + +Supported theta components: ``hill_geno``, ``hill_mut``. + +CSV format (``genotype_params_file``) +-------------------------------------- +Columns: ``genotype``, plus any subset of + ``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n``. + +One row per genotype. ``NaN`` in any parameter column → the WT reference +value for that parameter is used (from ``theta_sim_priors`` in the config, +or overridden by the ``wt`` row in the CSV itself). + +Example:: + + genotype,theta_low,theta_high,log_hill_K,hill_n + wt,0.99,0.01,-4.1,2.0 + A47V,0.97,0.03,-3.8,1.8 + K84L,,,−3.5, + +``hill_mut`` note +----------------- +Single-mutant rows are back-converted to per-mutation deltas (relative to the +WT reference) and injected into a full ``hill_mut``-style simulation. Deltas +for mutations absent from the CSV are drawn from the ``SimPriors`` as usual. +Pairwise epistasis is sampled from the regularized horseshoe prior and applied +consistently to the whole library. The function returns theta for every +library genotype — including doubles assembled from measured singles — so +downstream growth rates are fully consistent with the measured binding data. +""" + +import warnings +import numpy as np +import pandas as pd +from pathlib import Path +from typing import Union + +_EPS = 1e-7 +_THETA_CLIP = 1e-4 # hard bounds for theta_low / theta_high from CSV +_ZERO_CONC_SENTINEL = 1e-20 + +HILL_PARAM_COLS = frozenset({"theta_low", "theta_high", "log_hill_K", "hill_n"}) +SUPPORTED_COMPONENTS = frozenset({"hill_geno", "hill_mut"}) + + +# --------------------------------------------------------------------------- +# CSV I/O +# --------------------------------------------------------------------------- + +def read_binding_genotype_params(csv_path: Union[str, Path]) -> dict: + """ + Read a per-genotype Hill parameter CSV. + + Parameters + ---------- + csv_path : str or Path + + Returns + ------- + dict[str, dict[str, float]] + ``{genotype: {param: value}}``. ``NaN`` values are preserved as + ``float("nan")``; the caller fills them from the WT reference. + + Raises + ------ + ValueError + If the CSV lacks a ``genotype`` column, has no recognised parameter + columns, or contains unrecognised column names. + """ + df = pd.read_csv(csv_path) + + if "genotype" not in df.columns: + raise ValueError( + f"'{csv_path}' must have a 'genotype' column." + ) + + param_cols_present = [c for c in df.columns if c in HILL_PARAM_COLS] + if not param_cols_present: + raise ValueError( + f"'{csv_path}' must have at least one parameter column: " + f"{sorted(HILL_PARAM_COLS)}" + ) + + unknown = set(df.columns) - {"genotype"} - HILL_PARAM_COLS + if unknown: + raise ValueError( + f"Unrecognised columns in '{csv_path}': {sorted(unknown)}. " + f"Expected: genotype + subset of {sorted(HILL_PARAM_COLS)}" + ) + + result = {} + for _, row in df.iterrows(): + g = str(row["genotype"]) + params = {} + for col in param_cols_present: + v = row[col] + if pd.isna(v): + params[col] = float("nan") + else: + v = float(v) + if col in ("theta_low", "theta_high"): + clipped = float(np.clip(v, _THETA_CLIP, 1.0 - _THETA_CLIP)) + if clipped != v: + warnings.warn( + f"'{csv_path}': {col} for genotype '{g}' " + f"({v:.6g}) is outside [{_THETA_CLIP}, " + f"{1.0 - _THETA_CLIP}] and has been clipped to " + f"{clipped:.6g}. theta values must lie strictly " + f"between 0 and 1.", + UserWarning, + stacklevel=2, + ) + v = clipped + params[col] = v + result[g] = params + + return result + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +def _logit(x): + xc = np.clip(np.asarray(x, dtype=float), _EPS, 1.0 - _EPS) + return np.log(xc / (1.0 - xc)) + + +def _to_log_conc(concs): + c = np.asarray(concs, dtype=float) + return np.log(np.where(c == 0.0, _ZERO_CONC_SENTINEL, c)) + + +def _hill_theta(theta_low, theta_high, log_hill_K, hill_n, log_conc): + """Evaluate Hill equation at log concentrations. Returns shape (len(log_conc),).""" + lc = np.asarray(log_conc, dtype=float) + occupancy = 1.0 / (1.0 + np.exp(-hill_n * (lc - log_hill_K))) + return float(theta_low) + (float(theta_high) - float(theta_low)) * occupancy + + +def _fill_params_from_wt(params: dict, wt_params: dict) -> dict: + """Return a copy of *params* with NaN entries replaced from *wt_params*.""" + filled = dict(params) + for k in list(HILL_PARAM_COLS): + v = filled.get(k) + if v is None or (isinstance(v, float) and np.isnan(v)): + if k in wt_params: + filled[k] = wt_params[k] + return filled + + +def _wt_params_from_sim_priors(sim_priors) -> dict: + """ + Extract WT reference in the CSV parameter space from a SimPriors object. + + Works with both ``hill_geno.SimPriors`` and ``hill_mut.SimPriors``; + both expose ``wt_theta_low``, ``wt_theta_high``, ``wt_log_K``, ``wt_hill_n``. + """ + return { + "theta_low": float(sim_priors.wt_theta_low), + "theta_high": float(sim_priors.wt_theta_high), + "log_hill_K": float(sim_priors.wt_log_K), + "hill_n": float(sim_priors.wt_hill_n), + } + + +# --------------------------------------------------------------------------- +# hill_geno override +# --------------------------------------------------------------------------- + +def build_theta_gc_override_hill_geno( + params_dict: dict, + log_conc_growth: np.ndarray, + wt_params: dict, +) -> dict: + """ + Compute theta at growth concentrations for measured genotypes (``hill_geno``). + + Each entry in *params_dict* contributes one key to the returned override + dict. ``NaN`` parameter values are filled from *wt_params*. + + Parameters + ---------- + params_dict : dict[str, dict[str, float]] + Output of :func:`read_binding_genotype_params`. + log_conc_growth : np.ndarray, shape (C,) + Log concentrations at growth conditions (from ``sim_data.log_titrant_conc``). + wt_params : dict + WT reference: ``{theta_low, theta_high, log_hill_K, hill_n}``. + From :func:`_wt_params_from_sim_priors`. + + Returns + ------- + theta_dict : dict[str, np.ndarray] + ``{genotype: theta_array}`` where theta_array has shape (C,). + effective_params : dict[str, dict[str, float]] + ``{genotype: {theta_low, theta_high, log_hill_K, hill_n}}`` — the + actual Hill parameters used for each genotype (NaN fields filled from + *wt_params*). + """ + theta_dict = {} + effective_params = {} + for g, params in params_dict.items(): + filled = _fill_params_from_wt(params, wt_params) + theta_dict[g] = _hill_theta( + filled["theta_low"], filled["theta_high"], + filled["log_hill_K"], filled["hill_n"], + log_conc_growth, + ) + effective_params[g] = {k: filled[k] for k in HILL_PARAM_COLS} + return theta_dict, effective_params + + +# --------------------------------------------------------------------------- +# hill_mut override +# --------------------------------------------------------------------------- + +def build_theta_gc_override_hill_mut( + params_dict: dict, + library_genotypes: list, + sim_data, + sim_priors, + log_conc: np.ndarray, + rng: np.random.Generator, +) -> dict: + """ + Run a ``hill_mut``-style simulation with per-mutation deltas pinned for + measured genotypes and return theta for every library genotype. + + Single-mutation entries in *params_dict* are back-converted to per-mutation + deltas relative to the WT reference (CSV ``wt`` row wins; otherwise + *sim_priors* WT values are used). Mutations absent from *params_dict* draw + deltas from the prior as usual. Pairwise epistasis is sampled from the + regularized horseshoe prior for all pairs consistently. Multi-mutation + entries in *params_dict* override the assembled theta value directly. + + Parameters + ---------- + params_dict : dict[str, dict[str, float]] + library_genotypes : list[str] + Genotype strings in the same order as *sim_data* was built from. + sim_data : SimData + Must have ``mut_nnz_mut_idx``, ``mut_nnz_geno_idx``, + ``pair_nnz_pair_idx``, ``pair_nnz_geno_idx``, ``num_pair``. + sim_priors : hill_mut.SimPriors + Used as fallback WT reference and prior widths for unmeasured mutations. + log_conc : np.ndarray, shape (C,) + Log concentrations to evaluate theta at. + rng : np.random.Generator + + Returns + ------- + theta_dict : dict[str, np.ndarray] + ``{genotype: theta_array}`` for every genotype in *library_genotypes*. + Caller should add all entries to ``theta_gc_override``. + effective_params : dict[str, dict[str, float]] + ``{genotype: {theta_low, theta_high, log_hill_K, hill_n}}`` — the + effective Hill parameters used for each genotype in the simulation. + """ + import jax.numpy as jnp + from tfscreen.genetics.build_mut_geno_matrix import ( + build_mut_geno_matrix, + apply_mut_matrix, + apply_pair_matrix, + ) + + log_conc = np.asarray(log_conc, dtype=float) + G = len(library_genotypes) + + # Get mutation labels in the same order as sim_data was built + mut_labels, pair_labels, _, _, _ = build_mut_geno_matrix(library_genotypes) + M = len(mut_labels) + P = len(pair_labels) + + # WT reference in logit/log space --- + # If 'wt' row is in params_dict, it overrides sim_priors WT values. + wt_params = _wt_params_from_sim_priors(sim_priors) + if "wt" in params_dict: + wt_from_csv = _fill_params_from_wt(params_dict["wt"], wt_params) + wt_params = wt_from_csv # CSV WT wins + + wt_logit_low = float(_logit(wt_params["theta_low"])) + wt_logit_high = float(_logit(wt_params["theta_high"])) + wt_logit_delta = wt_logit_high - wt_logit_low + wt_log_K = float(wt_params["log_hill_K"]) + wt_log_n = float(np.log(wt_params["hill_n"])) + + # Build per-mutation delta arrays (NaN = needs to be sampled from prior) + d_logit_low = np.full(M, np.nan) + d_logit_delta = np.full(M, np.nan) + d_log_K = np.full(M, np.nan) + d_log_n = np.full(M, np.nan) + + def _parse_muts(g): + return [p for p in g.split("/") if p and p.lower() != "wt"] + + mut_label_to_idx = {m: i for i, m in enumerate(mut_labels)} + + for g, params in params_dict.items(): + muts = _parse_muts(g) + if len(muts) != 1: + continue # skip wt and multi-mutants for delta back-calculation + m_label = muts[0] + if m_label not in mut_label_to_idx: + continue # this mutation is not in the library + + m_idx = mut_label_to_idx[m_label] + filled = _fill_params_from_wt(params, wt_params) + + g_logit_low = float(_logit(filled["theta_low"])) + g_logit_high = float(_logit(filled["theta_high"])) + g_logit_delta = g_logit_high - g_logit_low + + d_logit_low[m_idx] = g_logit_low - wt_logit_low + d_logit_delta[m_idx] = g_logit_delta - wt_logit_delta + d_log_K[m_idx] = float(filled["log_hill_K"]) - wt_log_K + d_log_n[m_idx] = float(np.log(filled["hill_n"])) - wt_log_n + + # Sample deltas for unmeasured mutations from prior + unmeasured = np.isnan(d_logit_low) + n_un = int(unmeasured.sum()) + if n_un > 0: + d_logit_low[unmeasured] = rng.normal(0.0, sim_priors.sigma_d_logit_low, n_un) + d_logit_delta[unmeasured] = rng.normal(0.0, sim_priors.sigma_d_logit_delta, n_un) + d_log_K[unmeasured] = rng.normal(0.0, sim_priors.sigma_d_log_K, n_un) + d_log_n[unmeasured] = rng.normal(0.0, sim_priors.sigma_d_log_n, n_un) + + # Scatter helpers: (1, M) or (1, P) → (1, G) + mut_nnz_mut = jnp.array(sim_data.mut_nnz_mut_idx) + mut_nnz_geno = jnp.array(sim_data.mut_nnz_geno_idx) + + def _scatter_mut(d_1d): + return np.array( + apply_mut_matrix( + jnp.array(d_1d, dtype=float)[None, :], + mut_nnz_mut_idx=mut_nnz_mut, + mut_nnz_geno_idx=mut_nnz_geno, + num_genotype=G, + ) + )[0] + + # Assemble per-genotype parameters: (G,) + logit_low = wt_logit_low + _scatter_mut(d_logit_low) + logit_delta = wt_logit_delta + _scatter_mut(d_logit_delta) + log_K = wt_log_K + _scatter_mut(d_log_K) + log_n = wt_log_n + _scatter_mut(d_log_n) + + # Horseshoe epistasis for pairs (mirrors hill_mut.simulate exactly) + if P > 0 and sim_data.pair_nnz_pair_idx is not None: + pair_nnz_pair = jnp.array(sim_data.pair_nnz_pair_idx) + pair_nnz_geno = jnp.array(sim_data.pair_nnz_geno_idx) + + def _scatter_pair(e_1d): + return np.array( + apply_pair_matrix( + jnp.array(e_1d, dtype=float)[None, :], + pair_nnz_pair_idx=pair_nnz_pair, + pair_nnz_geno_idx=pair_nnz_geno, + num_genotype=G, + ) + )[0] + + tau = float(np.abs(rng.standard_cauchy())) * sim_priors.epi_tau_scale + + if tau > 0.0: + c2 = 1.0 / rng.gamma( + shape=sim_priors.epi_slab_df / 2.0, + scale=2.0 / (sim_priors.epi_slab_df * sim_priors.epi_slab_scale ** 2), + ) + + def _horseshoe(size): + lam = np.abs(rng.standard_cauchy(size)) + lam_tilde = np.sqrt(c2 * lam ** 2 / (c2 + tau ** 2 * lam ** 2)) + return rng.standard_normal(size) * tau * lam_tilde + + logit_low += _scatter_pair(_horseshoe(P)) + logit_delta += _scatter_pair(_horseshoe(P)) + log_K += _scatter_pair(_horseshoe(P)) + log_n += _scatter_pair(_horseshoe(P)) + + # Convert to theta: (G, C) + theta_low_arr = 1.0 / (1.0 + np.exp(-logit_low)) + theta_high_arr = 1.0 / (1.0 + np.exp(-(logit_low + logit_delta))) + hill_n_arr = np.exp(log_n) + + occupancy = 1.0 / (1.0 + np.exp( + -hill_n_arr[:, None] * (log_conc[None, :] - log_K[:, None]) + )) + theta_gc = theta_low_arr[:, None] + (theta_high_arr - theta_low_arr)[:, None] * occupancy + + # Build result dict: last occurrence wins for duplicates (matches theta_gc_override semantics) + result = {} + effective_params = {} + for i, g in enumerate(library_genotypes): + result[g] = theta_gc[i] + effective_params[g] = { + "theta_low": float(theta_low_arr[i]), + "theta_high": float(theta_high_arr[i]), + "log_hill_K": float(log_K[i]), + "hill_n": float(hill_n_arr[i]), + } + + # Directly-measured multi-mutant entries override the assembled values + for g, params in params_dict.items(): + muts = _parse_muts(g) + if len(muts) > 1 and g in result: + filled = _fill_params_from_wt(params, wt_params) + result[g] = _hill_theta( + filled["theta_low"], filled["theta_high"], + filled["log_hill_K"], filled["hill_n"], + log_conc, + ) + effective_params[g] = {k: filled[k] for k in HILL_PARAM_COLS} + + return result, effective_params + + +# --------------------------------------------------------------------------- +# Binding theta output +# --------------------------------------------------------------------------- + +def build_binding_theta_from_params( + params_dict: dict, + binding_concs, + titrant_name: str, + noise: float, + rng: np.random.Generator, + wt_params: dict, +) -> pd.DataFrame: + """ + Compute binding theta rows for measured genotypes. + + Parameters + ---------- + params_dict : dict[str, dict[str, float]] + binding_concs : array-like + Concentrations (mM) at which binding theta is reported. + titrant_name : str + noise : float + Gaussian noise sigma applied to theta (in [0, 1] space). + Set to 0 to disable. + rng : np.random.Generator + wt_params : dict + ``{theta_low, theta_high, log_hill_K, hill_n}`` used to fill NaN params. + + Returns + ------- + pd.DataFrame + Columns: ``genotype``, ``titrant_name``, ``titrant_conc``, ``theta_true``. + """ + binding_concs = np.asarray(binding_concs, dtype=float) + log_concs = _to_log_conc(binding_concs) + + rows = [] + for g, params in params_dict.items(): + filled = _fill_params_from_wt(params, wt_params) + theta_vals = _hill_theta( + filled["theta_low"], filled["theta_high"], + filled["log_hill_K"], filled["hill_n"], + log_concs, + ) + for conc, theta in zip(binding_concs, theta_vals): + if noise > 0: + theta_out = float(np.clip(theta + rng.normal(0.0, noise), 0.0, 1.0)) + else: + theta_out = float(theta) + rows.append({ + "genotype": g, + "titrant_name": titrant_name, + "titrant_conc": float(conc), + "theta_true": theta_out, + }) + + return pd.DataFrame(rows) diff --git a/src/tfscreen/simulate/library_prediction.py b/src/tfscreen/simulate/library_prediction.py index 49aa132e..df3911d1 100644 --- a/src/tfscreen/simulate/library_prediction.py +++ b/src/tfscreen/simulate/library_prediction.py @@ -6,7 +6,15 @@ thermo_to_growth, ) from tfscreen.simulate.sim_data_class import build_sim_data -from tfscreen.simulate.sample_theta import sample_theta_stratified +from tfscreen.simulate.sample_theta import sample_theta_stratified, sample_theta_prior +from tfscreen.simulate.binding_params import ( + SUPPORTED_COMPONENTS as _BINDING_PARAMS_SUPPORTED, + read_binding_genotype_params, + build_theta_gc_override_hill_geno, + build_theta_gc_override_hill_mut, + build_binding_theta_from_params, + _wt_params_from_sim_priors, +) from tfscreen.genetics import library_manager from tfscreen.genetics import standardize_genotypes @@ -105,48 +113,146 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], # Stratified theta sampling for binding calibration genotypes binding_cfg = cf.get('binding_data') - theta_gc_override = None + theta_gc_override = {} + theta_params_override = {} binding_theta_df = None if binding_cfg is not None: - binding_genotypes = list(standardize_genotypes(binding_cfg['genotypes'])) binding_concs = binding_cfg['titrant_conc'] titrant_name = binding_cfg['titrant_name'] + binding_noise = binding_cfg.get('noise', 0.0) binding_sample_df = pd.DataFrame({"titrant_conc": binding_concs}) - - selected_binding_gc, selected_growth_gc = sample_theta_stratified( - component_name=cf['theta_component'], - binding_sample_df=binding_sample_df, - growth_sample_df=sample_df, - rng_key=theta_rng_key, - n_select=len(binding_genotypes), - thermo_data=cf.get('thermo_data'), - pool_size=cf.get('binding_stratify_pool_size', 500), - priors_overrides=cf.get('theta_priors'), - sim_priors_overrides=cf.get('theta_sim_priors'), - ) - - # Build override dict for thermo_to_growth (theta at growth concentrations) - theta_gc_override = { - g: selected_growth_gc[i] - for i, g in enumerate(binding_genotypes) - } - - # Build binding_theta_df (theta at binding concentrations, pre-noise) sorted_binding_concs = np.sort(np.unique(binding_concs)) conc_to_col = {float(c): j for j, c in enumerate(sorted_binding_concs)} - rows = [] - for i, g in enumerate(binding_genotypes): + + rows = [] # accumulated rows for binding_theta_df + + # --- Section 1: Simulated genotypes (existing 'genotypes' key) --- + binding_genotypes = list(standardize_genotypes(binding_cfg.get('genotypes', []))) + + # wt gets its natural unperturbed reference parameters — not a random + # draw from the pool. Only non-wt binding genotypes are stratified. + non_wt_genotypes = [g for g in binding_genotypes if g != "wt"] + + # wt reference curve (if wt is a simulated binding genotype) + wt_binding_gc = None + if "wt" in binding_genotypes: + single_wt_df = pd.DataFrame({"genotype": ["wt"]}) + binding_wt_sim = build_sim_data(single_wt_df, binding_sample_df, + thermo_data=cf.get('thermo_data'), + skip_pairs=True) + # Perturbation path for wt (M=0): always returns the fixed sim-priors + # reference curve regardless of rng_key, so the binding data matches + # the growth simulation exactly. + wt_binding_gc, _ = sample_theta_prior( + cf['theta_component'], binding_wt_sim, theta_rng_key, + sim_priors_overrides=cf.get('theta_sim_priors'), + ) + + # Stratified curves for non-wt simulated binding genotypes + selected_binding_gc = None + selected_growth_gc = None + if non_wt_genotypes: + selected_binding_gc, selected_growth_gc = sample_theta_stratified( + component_name=cf['theta_component'], + binding_sample_df=binding_sample_df, + growth_sample_df=sample_df, + rng_key=theta_rng_key, + n_select=len(non_wt_genotypes), + thermo_data=cf.get('thermo_data'), + pool_size=cf.get('binding_stratify_pool_size', 500), + priors_overrides=cf.get('theta_priors'), + sim_priors_overrides=cf.get('theta_sim_priors'), + ) + + # Override theta at growth concentrations for non-wt binding genotypes only. + # wt is deliberately excluded so thermo_to_growth uses its natural curve. + if non_wt_genotypes and selected_growth_gc is not None: + theta_gc_override.update({ + g: selected_growth_gc[i] + for i, g in enumerate(non_wt_genotypes) + }) + + # Accumulate binding rows for simulated genotypes + if "wt" in binding_genotypes and wt_binding_gc is not None: + for conc in binding_concs: + rows.append({ + "genotype": "wt", + "titrant_name": titrant_name, + "titrant_conc": float(conc), + "theta_true": float(wt_binding_gc[0, conc_to_col[float(conc)]]), + }) + for i, g in enumerate(non_wt_genotypes): for conc in binding_concs: - col_idx = conc_to_col[float(conc)] rows.append({ "genotype": g, "titrant_name": titrant_name, "titrant_conc": float(conc), - "theta_true": float(selected_binding_gc[i, col_idx]), + "theta_true": float(selected_binding_gc[i, conc_to_col[float(conc)]]), }) - binding_theta_df = pd.DataFrame(rows) + + # --- Section 2: Measured genotype params from CSV --- + params_file = binding_cfg.get('genotype_params_file') + if params_file is not None: + theta_component = cf['theta_component'] + if theta_component not in _BINDING_PARAMS_SUPPORTED: + raise ValueError( + f"genotype_params_file is only supported with Hill-based theta " + f"components: {sorted(_BINDING_PARAMS_SUPPORTED)}. " + f"Got: '{theta_component}'." + ) + + # Build sim_priors for WT reference / NaN filling + from tfscreen.tfmodel.generative.registry import model_registry + theta_module = model_registry["theta"][theta_component] + sim_params = theta_module.get_sim_hyperparameters() + if cf.get('theta_sim_priors'): + sim_params.update(cf['theta_sim_priors']) + sim_priors = theta_module.SimPriors(**sim_params) + + wt_params = _wt_params_from_sim_priors(sim_priors) + + params_dict = read_binding_genotype_params(params_file) + + # Compute theta overrides for measured genotypes + log_conc_growth = np.array(sim_data.log_titrant_conc) + + if theta_component == "hill_geno": + measured_override, measured_params = build_theta_gc_override_hill_geno( + params_dict, log_conc_growth, wt_params + ) + else: # hill_mut + measured_override, measured_params = build_theta_gc_override_hill_mut( + params_dict=params_dict, + library_genotypes=library_df["genotype"].tolist(), + sim_data=sim_data, + sim_priors=sim_priors, + log_conc=log_conc_growth, + rng=rng, + ) + + # Measured data takes precedence over stratified simulated data + theta_gc_override.update(measured_override) + theta_params_override.update(measured_params) + + # Accumulate binding rows for measured genotypes (overrides simulated rows) + # Store noise-free theta_true; noise is applied later by simulate_cli + # _generate_binding_data, consistent with the simulated-genotypes path. + measured_binding_df = build_binding_theta_from_params( + params_dict=params_dict, + binding_concs=binding_concs, + titrant_name=titrant_name, + noise=0.0, + rng=rng, + wt_params=wt_params, + ) + # Remove any existing rows for genotypes now covered by measured data + measured_genotypes = set(params_dict.keys()) + rows = [r for r in rows if r["genotype"] not in measured_genotypes] + rows.extend(measured_binding_df.to_dict("records")) + + binding_theta_df = pd.DataFrame(rows) if rows else None # ------------------------------------------------------------------------- # Calculate phenotype for each genotype across all conditions in sample_df @@ -171,6 +277,7 @@ def library_prediction(cf: Union[Dict[str, Any], str, Path], activity_priors_overrides=cf.get('activity_priors'), theta_rescale=cf.get('theta_rescale', 'passthrough'), theta_gc_override=theta_gc_override, + theta_params_override=theta_params_override or None, ) return library_df, phenotype_df, genotype_theta_df, parameters_df, binding_theta_df diff --git a/src/tfscreen/simulate/sample_theta.py b/src/tfscreen/simulate/sample_theta.py index d073dc29..43398531 100644 --- a/src/tfscreen/simulate/sample_theta.py +++ b/src/tfscreen/simulate/sample_theta.py @@ -18,6 +18,8 @@ import inspect import numpy as np import pandas as pd +import tqdm +import jax from numpyro import handlers from tfscreen.tfmodel.generative.registry import model_registry @@ -31,17 +33,23 @@ def sample_theta_prior(component_name, sim_data, rng_key, priors_overrides=None, - sim_priors_overrides=None): + sim_priors_overrides=None, + force_prior_predictive=False): """ Draw one sample of theta from a registered theta component. If the component defines a ``simulate`` function (detected via - ``inspect.isfunction``), the perturbation-based path is used: wildtype - reference parameters are drawn from ``SimPriors`` built from - ``get_sim_hyperparameters()`` plus any ``sim_priors_overrides``. + ``inspect.isfunction``) and ``force_prior_predictive`` is False, the + perturbation-based path is used: wildtype reference parameters are drawn + from ``SimPriors`` built from ``get_sim_hyperparameters()`` plus any + ``sim_priors_overrides``. Otherwise the prior-predictive path is used: ``define_model`` is called inside a seeded NumPyro trace, and ``run_model`` produces ``theta_gc``. + Set ``force_prior_predictive=True`` to always use this path — this is + required for mutation-decomposed components (e.g. ``hill_mut``) when the + pool library has only the wt genotype and M=0 mutations, because the + perturbation path would produce zero deltas and a homogeneous pool. Parameters ---------- @@ -59,6 +67,9 @@ def sample_theta_prior(component_name, sim_priors_overrides : dict or None Overrides for ``SimPriors`` (perturbation path only). Ignored when the component does not provide ``simulate``. + force_prior_predictive : bool, default False + When True, always use the prior-predictive path (``define_model`` + + ``handlers.seed``), even if the component provides ``simulate``. Returns ------- @@ -90,7 +101,7 @@ def sample_theta_prior(component_name, module = theta_registry[component_name] # Perturbation-based path: component defines a simulate() function. - if inspect.isfunction(getattr(module, "simulate", None)): + if not force_prior_predictive and inspect.isfunction(getattr(module, "simulate", None)): sim_params = module.get_sim_hyperparameters() if sim_priors_overrides: sim_params.update(sim_priors_overrides) @@ -218,22 +229,46 @@ def sample_theta_stratified(component_name, f"Increase pool_size or reduce the number of binding genotypes." ) - pool_library_df = pd.DataFrame({"genotype": ["wt"] * pool_size}) - - binding_sim_data = build_sim_data(pool_library_df, binding_sample_df, + if isinstance(rng_key, int): + rng_key = jax.random.PRNGKey(rng_key) + + # Build sim_data for a single genotype at each concentration set. + # The pool is built by calling sample_theta_prior once per member with a + # unique key derived via fold_in. This gives each member its own + # hyperprior draw (loc, scale, …) rather than sharing one draw across all + # pool members — which would concentrate the pool around a single binding + # regime regardless of pool_size. + single_library_df = pd.DataFrame({"genotype": ["wt"]}) + binding_sim_data = build_sim_data(single_library_df, binding_sample_df, thermo_data=thermo_data, skip_pairs=True) - pool_binding_gc, _ = sample_theta_prior(component_name, binding_sim_data, - rng_key, priors_overrides, - sim_priors_overrides) - # pool_binding_gc: (pool_size, n_binding_concs) + growth_sim_data = build_sim_data(single_library_df, growth_sample_df, + thermo_data=thermo_data, skip_pairs=True) - selected_indices = _greedy_maximin(pool_binding_gc, n_select) + # Always use the prior-predictive path for pool building. The perturbation + # path (simulate()) requires mutations to generate diversity; with a wt-only + # single-genotype pool library (M=0), it produces zero deltas and an + # entirely homogeneous pool regardless of pool_size. The prior-predictive + # path samples WT-level parameters (log_K_wt, logit_low_wt, …) from their + # broad Normal priors, giving real diversity across pool members. + print(f"Building stratified pool ({pool_size} candidates)... ", + end="", flush=True) + pool_binding_rows = [] + pool_growth_rows = [] + for i in tqdm.tqdm(range(pool_size)): + key_i = jax.random.fold_in(rng_key, i) + theta_b, _ = sample_theta_prior(component_name, binding_sim_data, key_i, + priors_overrides, sim_priors_overrides, + force_prior_predictive=True) + theta_g, _ = sample_theta_prior(component_name, growth_sim_data, key_i, + priors_overrides, sim_priors_overrides, + force_prior_predictive=True) + pool_binding_rows.append(theta_b[0]) # (C_b,) — single genotype + pool_growth_rows.append(theta_g[0]) # (C_g,) + print("Done.", flush=True) + + pool_binding_gc = np.stack(pool_binding_rows) # (pool_size, C_b) + pool_growth_gc = np.stack(pool_growth_rows) # (pool_size, C_g) - growth_sim_data = build_sim_data(pool_library_df, growth_sample_df, - thermo_data=thermo_data, skip_pairs=True) - pool_growth_gc, _ = sample_theta_prior(component_name, growth_sim_data, - rng_key, priors_overrides, - sim_priors_overrides) - # pool_growth_gc: (pool_size, n_growth_concs) + selected_indices = _greedy_maximin(pool_binding_gc, n_select) return pool_binding_gc[selected_indices], pool_growth_gc[selected_indices] diff --git a/src/tfscreen/simulate/scripts/simulate_cli.py b/src/tfscreen/simulate/scripts/simulate_cli.py index 3da8efdd..713c2809 100644 --- a/src/tfscreen/simulate/scripts/simulate_cli.py +++ b/src/tfscreen/simulate/scripts/simulate_cli.py @@ -36,7 +36,6 @@ def _generate_binding_data(binding_cfg, rng, binding_theta_df): pandas.DataFrame Columns: genotype, titrant_name, titrant_conc, theta_obs, theta_std """ - genotypes = list(standardize_genotypes(binding_cfg["genotypes"])) titrant_name = binding_cfg["titrant_name"] titrant_conc = list(binding_cfg["titrant_conc"]) noise = float(binding_cfg.get("noise", 0.0)) @@ -47,6 +46,13 @@ def _generate_binding_data(binding_cfg, rng, binding_theta_df): for _, row in binding_theta_df.iterrows() } + # Genotypes to output: explicit list from config, or every genotype in binding_theta_df. + raw_genotypes = binding_cfg.get("genotypes") + if raw_genotypes: + genotypes = list(standardize_genotypes(raw_genotypes)) + else: + genotypes = list(binding_theta_df["genotype"].unique()) + rows = [] for g in genotypes: for conc in titrant_conc: @@ -54,7 +60,7 @@ def _generate_binding_data(binding_cfg, rng, binding_theta_df): if key not in theta_lookup: raise ValueError( f"No pre-computed theta for genotype '{g}' at conc {conc}. " - f"Ensure binding_data.genotypes and titrant_conc match the config." + f"Ensure binding_data genotypes and titrant_conc match the config." ) theta_true = float(theta_lookup[key]) if noise > 0: diff --git a/src/tfscreen/simulate/thermo_to_growth.py b/src/tfscreen/simulate/thermo_to_growth.py index 95bce1e5..ddc47773 100644 --- a/src/tfscreen/simulate/thermo_to_growth.py +++ b/src/tfscreen/simulate/thermo_to_growth.py @@ -307,6 +307,7 @@ def thermo_to_growth( activity_priors_overrides: Optional[dict] = None, theta_rescale: str = "passthrough", theta_gc_override: Optional[dict] = None, + theta_params_override: Optional[dict] = None, ) -> tuple[pd.DataFrame, pd.DataFrame]: """ Generate phenotypes from genotypes via prior-predictive theta sampling. @@ -393,6 +394,13 @@ def thermo_to_growth( prior-predictive sampling but before noise and all downstream calculations. Used by ``library_prediction`` to inject stratified theta values for calibration genotypes. + theta_params_override : dict[str, dict[str, float]] or None, default None + Effective Hill parameters for each overridden genotype. Keys are + genotype strings; values are dicts with keys ``theta_low``, + ``theta_high``, ``log_hill_K``, ``hill_n``. When provided, the + corresponding rows in ``parameters_df`` are updated so that the + saved CSV reflects what was actually used in the simulation rather + than the prior-predictive sample. Returns ------- @@ -627,6 +635,18 @@ def thermo_to_growth( parameters_df = parameters_df[_front + _other] parameters_df = set_categorical_genotype(parameters_df) + # Overwrite theta columns with the effective (post-override) Hill parameters + # so that parameters_df reflects what was actually simulated, not the + # prior-predictive sample that was later replaced. + if theta_params_override: + for g, effective in theta_params_override.items(): + mask = parameters_df["genotype"] == g + if not mask.any(): + continue + for col, val in effective.items(): + if col in parameters_df.columns: + parameters_df.loc[mask, col] = float(val) + print("Done.", flush=True) return phenotype_df, genotype_theta_df, parameters_df diff --git a/src/tfscreen/tfmodel/analysis/error_calibration.py b/src/tfscreen/tfmodel/analysis/error_calibration.py index 28cffb9b..ea6f3cf3 100644 --- a/src/tfscreen/tfmodel/analysis/error_calibration.py +++ b/src/tfscreen/tfmodel/analysis/error_calibration.py @@ -238,7 +238,7 @@ def plot_pit_histogram(pit_vals, ax=None, title=None, n_bins=10): color="steelblue", alpha=0.75, edgecolor="white") ax.axhline(1.0, color="firebrick", lw=1.5, ls="--", label="Uniform") ax.set_xlim(0, 1) - ax.set_ylim(0, 10) + ax.set_ylim(0, n_bins) ax.set_xlabel("PIT value") ax.set_ylabel("Density") if title is not None: @@ -364,7 +364,7 @@ def calibration_summary(true_vals, quantile_matrix, quantile_levels, # --- Two-panel PDF --- pdf_path = f"{out_prefix}_calibration.pdf" try: - fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + fig, axes = plt.subplots(1, 2, figsize=(10, 5)) hist_title = f"PIT histogram — {label}" if label else "PIT histogram" curve_title = f"Calibration curve — {label}" if label else "Calibration curve" plot_pit_histogram(pit_vals, ax=axes[0], title=hist_title) diff --git a/src/tfscreen/tfmodel/generative/components/theta/_simple.py b/src/tfscreen/tfmodel/generative/components/theta/_simple.py index 5a804438..37940331 100644 --- a/src/tfscreen/tfmodel/generative/components/theta/_simple.py +++ b/src/tfscreen/tfmodel/generative/components/theta/_simple.py @@ -4,8 +4,8 @@ This component is intended for the *calibration* / pre-fit pass of the two-stage hierarchical workflow. It treats the per-condition fractional occupancy ``theta`` as a fully known, deterministic quantity supplied by -the caller — typically computed from a thermodynamic model evaluated at -the wildtype TF parameters. +the caller — typically the per-genotype observed binding values from the +calibration dataset. There are no sampled latent variables in this component: - ``define_model`` registers zero ``pyro.sample`` sites and only emits a @@ -13,19 +13,26 @@ - ``guide`` is the same pure no-op (no ``pyro.param`` and no ``pyro.sample`` sites), keeping model/guide sample sets symmetric. -Because ``theta`` is identical across all genotypes, the broadcast tensor -returned in ``ThetaParam.theta`` has shape -``(num_titrant_name, num_titrant_conc, num_genotype)`` but is constant -along the genotype axis. +``theta_values`` in ``ModelPriors`` may have one of two shapes: -The population moments ``(mu, sigma)`` are derived from the supplied -``theta_values``: -- ``mu`` = ``logit(theta_values)`` (with eps clipping) -- ``sigma`` = a small floor (``priors.sigma_floor``) +* ``(num_titrant_name, num_titrant_conc)`` — a single shared curve + broadcast identically to every genotype. Legacy / backward-compatible + path; rarely useful in practice. -These match the contract used by downstream transformations (e.g. -``transformation.logit_norm``) that need a population mean / scale even -when theta is "known". +* ``(num_titrant_name, num_titrant_conc, num_genotype)`` — per-genotype + theta values. The calibration pre-fit uses this path so that each + genotype's observed binding curve is used independently rather than + being replaced by a population average. + +The population moments ``(mu, sigma)`` are derived from ``theta_values``: + +* 2-D input: ``mu = logit(theta_values)[..., None]``; + ``sigma = priors.sigma_floor`` (scalar floor broadcast). +* 3-D input: ``mu = mean(logit(theta_values), axis=-1, keepdims=True)``; + ``sigma = max(std(logit(theta_values), axis=-1, keepdims=True), floor)``. + +Both variants return moments of shape ``(T, C, 1)`` to satisfy the +contract used by downstream transformations (e.g. ``transformation.logit_norm``). """ import jax.numpy as jnp @@ -64,6 +71,8 @@ class ModelPriors: theta_values: jnp.ndarray sigma_floor: float = field(pytree_node=False, default=0.01) + # theta_values may be shape (T, C) or (T, C, G). + # See module docstring for semantics of each shape. @dataclass(frozen=True) @@ -89,9 +98,9 @@ class ThetaParam: (passed through from ``data.titrant_conc``). """ - theta: jnp.ndarray - mu: jnp.ndarray - sigma: jnp.ndarray + theta: jnp.ndarray # (T, C, G) — per-genotype after construction + mu: jnp.ndarray # (T, C, 1) — population mean of logit(theta) + sigma: jnp.ndarray # (T, C, 1) — population scale of logit(theta) concentrations: jnp.ndarray @@ -106,19 +115,30 @@ def _build_theta_param(name: str, difference is that ``define_model`` emits a ``pyro.deterministic`` site for inspection, while ``guide`` does not (sites in a guide must not be observed). - """ - theta_values = priors.theta_values # (num_titrant_name, num_titrant_conc) + Handles two ``priors.theta_values`` shapes: - # Broadcast across genotype axis -> (Name, Conc, Genotype). - theta = jnp.broadcast_to( - theta_values[..., None], - (data.num_titrant_name, data.num_titrant_conc, data.num_genotype), - ) - - # Population moments in logit-space, shape (Name, Conc, 1) - mu = _logit(theta_values)[..., None] - sigma = jnp.full_like(mu, priors.sigma_floor) + * ``(T, C)`` — broadcast identically to all G genotypes. + * ``(T, C, G)`` — per-genotype; used as-is (no broadcast). + """ + theta_values = priors.theta_values + + if theta_values.ndim == 2: + # Legacy / shared-curve path: broadcast (T, C) → (T, C, G). + theta = jnp.broadcast_to( + theta_values[..., None], + (data.num_titrant_name, data.num_titrant_conc, data.num_genotype), + ) + # Population moments from the shared curve. + mu = _logit(theta_values)[..., None] # (T, C, 1) + sigma = jnp.full_like(mu, priors.sigma_floor) + else: + # Per-genotype path: theta_values already (T, C, G). + theta = theta_values + logit_tv = _logit(theta_values) # (T, C, G) + mu = jnp.mean(logit_tv, axis=-1, keepdims=True) # (T, C, 1) + std = jnp.std(logit_tv, axis=-1, keepdims=True) # (T, C, 1) + sigma = jnp.maximum(std, priors.sigma_floor) if register_deterministic: pyro.deterministic(f"{name}_theta", theta) diff --git a/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py b/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py index b8beb30b..d3b66b51 100644 --- a/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py +++ b/src/tfscreen/tfmodel/generative/components/theta/hill_mut.py @@ -274,16 +274,23 @@ def define_model(name: str, # ------------------------------------------------------------------ # Mutation delta offsets: shape (T, num_mutation) # ------------------------------------------------------------------ - with pyro.plate(f"{name}_titrant_mut_outer_plate", T, dim=-2): - with pyro.plate(f"{name}_mutation_plate", num_mut, dim=-1): - d_low_off = pyro.sample( - f"{name}_d_logit_low_offset", dist.Normal(0.0, 1.0)) - d_delta_off = pyro.sample( - f"{name}_d_logit_delta_offset", dist.Normal(0.0, 1.0)) - d_K_off = pyro.sample( - f"{name}_d_log_hill_K_offset", dist.Normal(0.0, 1.0)) - d_n_off = pyro.sample( - f"{name}_d_log_hill_n_offset", dist.Normal(0.0, 1.0)) + if num_mut > 0: + with pyro.plate(f"{name}_titrant_mut_outer_plate", T, dim=-2): + with pyro.plate(f"{name}_mutation_plate", num_mut, dim=-1): + d_low_off = pyro.sample( + f"{name}_d_logit_low_offset", dist.Normal(0.0, 1.0)) + d_delta_off = pyro.sample( + f"{name}_d_logit_delta_offset", dist.Normal(0.0, 1.0)) + d_K_off = pyro.sample( + f"{name}_d_log_hill_K_offset", dist.Normal(0.0, 1.0)) + d_n_off = pyro.sample( + f"{name}_d_log_hill_n_offset", dist.Normal(0.0, 1.0)) + else: + # num_mut == 0 (wt-only library, e.g. stratified pool building): + # numpyro.plate requires size > 0, so skip the plate and return + # empty offset arrays. apply_mut_matrix with empty COO indices + # produces zeros, so per-genotype params equal the WT values. + d_low_off = d_delta_off = d_K_off = d_n_off = jnp.zeros((T, 0)) # ------------------------------------------------------------------ # Optional epistasis: shape (T, num_pair) @@ -802,16 +809,16 @@ def get_population_moments(theta_param: ThetaParam, data) -> tuple: def get_hyperparameters() -> Dict[str, Any]: p = {} - p["theta_logit_low_wt_loc"] = 2.0 + p["theta_logit_low_wt_loc"] = 5.0 p["theta_logit_low_wt_scale"] = 2.0 - p["theta_logit_delta_wt_loc"] = -4.0 + p["theta_logit_delta_wt_loc"] = -9.0 p["theta_logit_delta_wt_scale"] = 2.0 p["theta_log_hill_K_wt_loc"] = -4.1 p["theta_log_hill_K_wt_scale"] = 2.0 p["theta_log_hill_n_wt_loc"] = 0.7 p["theta_log_hill_n_wt_scale"] = 0.3 - p["theta_sigma_d_logit_low_scale"] = 1.0 - p["theta_sigma_d_logit_delta_scale"] = 1.0 + p["theta_sigma_d_logit_low_scale"] = 5.0 + p["theta_sigma_d_logit_delta_scale"] = 5.0 p["theta_sigma_d_log_hill_K_scale"] = 1.0 p["theta_sigma_d_log_hill_n_scale"] = 0.5 p["theta_epi_tau_scale"] = 0.1 @@ -852,12 +859,12 @@ def get_guesses(name: str, data: GrowthData) -> Dict[str, Any]: T = data.num_titrant_name M = data.num_mutation guesses = {} - guesses[f"{name}_logit_low_wt"] = jnp.full(T, 2.0) - guesses[f"{name}_logit_delta_wt"] = jnp.full(T, -4.0) + guesses[f"{name}_logit_low_wt"] = jnp.full(T, 5.0) + guesses[f"{name}_logit_delta_wt"] = jnp.full(T, -9.0) guesses[f"{name}_log_hill_K_wt"] = jnp.full(T, -4.1) guesses[f"{name}_log_hill_n_wt"] = jnp.full(T, 0.7) guesses[f"{name}_sigma_d_logit_low"] = jnp.full(T, 0.5) - guesses[f"{name}_sigma_d_logit_delta"] = jnp.full(T, 0.5) + guesses[f"{name}_sigma_d_logit_delta"] = jnp.full(T, 4.0) guesses[f"{name}_sigma_d_log_hill_K"] = jnp.full(T, 0.5) guesses[f"{name}_sigma_d_log_hill_n"] = jnp.full(T, 0.3) guesses[f"{name}_d_logit_low_offset"] = jnp.zeros((T, M)) diff --git a/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py index 90606237..c982b4a5 100644 --- a/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py @@ -166,37 +166,46 @@ def _intersect_data(growth_df, binding_df): def _compute_theta_values(orchestrator_cal, binding_df_cal): """ - Build a (num_titrant_name, num_titrant_conc) theta tensor for the - simple-theta component of the calibration model. + Build a per-genotype ``(T, C, G)`` theta tensor for the simple-theta + component of the calibration model. - Each cell is the inverse-variance weighted mean of ``theta_obs`` - across the genotypes that contributed observations at that - (titrant_name, titrant_conc) cell. Cells with no usable observations - fall back to a plain mean of ``theta_obs``; if that is also empty the - cell is set to 0.5 (uninformative midpoint). + Each cell ``[i, j, k]`` holds the inverse-variance weighted mean of + ``theta_obs`` for titrant_name ``i``, titrant_conc ``j``, and genotype + ``k``. Cells with no usable observations fall back to a plain mean; + if that is also empty the cell is set to 0.5 (uninformative midpoint). The dimension ordering is taken from ``orchestrator_cal.binding_tm.tensor_dim_labels`` so the resulting array - matches the layout the simple-theta component expects. + matches the layout the simple-theta component expects. The genotype + axis ordering mirrors the calibration model's internal genotype index so + that ``theta_values[:, :, k]`` is the correct curve for the k-th + calibration genotype. """ - tn_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("titrant_name") - tc_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("titrant_conc") + tn_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("titrant_name") + tc_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("titrant_conc") + geno_idx = orchestrator_cal.binding_tm.tensor_dim_names.index("genotype") + titrant_name_labels = list(orchestrator_cal.binding_tm.tensor_dim_labels[tn_idx]) titrant_conc_labels = list(orchestrator_cal.binding_tm.tensor_dim_labels[tc_idx]) + genotype_labels = list(orchestrator_cal.binding_tm.tensor_dim_labels[geno_idx]) n_name = len(titrant_name_labels) n_conc = len(titrant_conc_labels) - theta_values = np.full((n_name, n_conc), 0.5, dtype=float) + n_geno = len(genotype_labels) + theta_values = np.full((n_name, n_conc, n_geno), 0.5, dtype=float) name_to_i = {str(n): i for i, n in enumerate(titrant_name_labels)} conc_to_j = {float(c): j for j, c in enumerate(titrant_conc_labels)} + geno_to_k = {str(g): k for k, g in enumerate(genotype_labels)} - grouped = binding_df_cal.groupby(["titrant_name", "titrant_conc"], - observed=True) - for (tn, tc), grp in grouped: + grouped = binding_df_cal.groupby( + ["titrant_name", "titrant_conc", "genotype"], observed=True + ) + for (tn, tc, geno), grp in grouped: i = name_to_i.get(str(tn)) j = conc_to_j.get(float(tc)) - if i is None or j is None: + k = geno_to_k.get(str(geno)) + if i is None or j is None or k is None: continue theta_obs = grp["theta_obs"].to_numpy(dtype=float) @@ -205,11 +214,11 @@ def _compute_theta_values(orchestrator_cal, binding_df_cal): & (theta_std > 0)) if valid.any(): w = 1.0 / np.square(theta_std[valid]) - theta_values[i, j] = float(np.sum(theta_obs[valid] * w) / np.sum(w)) + theta_values[i, j, k] = float(np.sum(theta_obs[valid] * w) / np.sum(w)) else: usable = np.isfinite(theta_obs) if usable.any(): - theta_values[i, j] = float(np.mean(theta_obs[usable])) + theta_values[i, j, k] = float(np.mean(theta_obs[usable])) np.clip(theta_values, _THETA_EPS, 1.0 - _THETA_EPS, out=theta_values) return jnp.asarray(theta_values, dtype=jnp.float32) diff --git a/tests/tfscreen/simulate/test_binding_params.py b/tests/tfscreen/simulate/test_binding_params.py new file mode 100644 index 00000000..240e1ca4 --- /dev/null +++ b/tests/tfscreen/simulate/test_binding_params.py @@ -0,0 +1,585 @@ +""" +Unit tests for tfscreen.simulate.binding_params. +""" + +import io +import pytest +import numpy as np +import pandas as pd +from unittest.mock import MagicMock + +from tfscreen.simulate.binding_params import ( + read_binding_genotype_params, + build_theta_gc_override_hill_geno, + build_theta_gc_override_hill_mut, + build_binding_theta_from_params, + _hill_theta, + _fill_params_from_wt, + _logit, + _to_log_conc, + _wt_params_from_sim_priors, + SUPPORTED_COMPONENTS, + HILL_PARAM_COLS, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def simple_csv(tmp_path): + """CSV with all four Hill parameter columns.""" + content = ( + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,0.01,-4.1,2.0\n" + "A47V,0.97,0.03,-3.8,1.8\n" + "K84L,0.98,0.02,-3.5,2.2\n" + ) + p = tmp_path / "params.csv" + p.write_text(content) + return str(p) + + +@pytest.fixture +def partial_csv(tmp_path): + """CSV with only two parameter columns; others should NaN.""" + content = ( + "genotype,log_hill_K,hill_n\n" + "wt,-4.1,2.0\n" + "A47V,-3.8,1.8\n" + ) + p = tmp_path / "partial.csv" + p.write_text(content) + return str(p) + + +@pytest.fixture +def nan_csv(tmp_path): + """CSV with explicit NaN values.""" + content = ( + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,0.01,-4.1,2.0\n" + "A47V,,,-3.8,1.8\n" + ) + p = tmp_path / "nan_params.csv" + p.write_text(content) + return str(p) + + +@pytest.fixture +def wt_params(): + return { + "theta_low": 0.99, + "theta_high": 0.01, + "log_hill_K": -4.1, + "hill_n": 2.0, + } + + +@pytest.fixture +def mock_sim_priors(wt_params): + sp = MagicMock() + sp.wt_theta_low = wt_params["theta_low"] + sp.wt_theta_high = wt_params["theta_high"] + sp.wt_log_K = wt_params["log_hill_K"] + sp.wt_hill_n = wt_params["hill_n"] + sp.sigma_d_logit_low = 0.3 + sp.sigma_d_logit_delta = 0.5 + sp.sigma_d_log_K = 0.5 + sp.sigma_d_log_n = 0.3 + sp.epi_tau_scale = 0.0 # no epistasis by default in tests + sp.epi_slab_scale = 2.0 + sp.epi_slab_df = 4.0 + return sp + + +@pytest.fixture +def rng(): + return np.random.default_rng(42) + + +# --------------------------------------------------------------------------- +# read_binding_genotype_params +# --------------------------------------------------------------------------- + +class TestReadBindingGenotypeParams: + + def test_reads_all_params(self, simple_csv): + d = read_binding_genotype_params(simple_csv) + assert set(d.keys()) == {"wt", "A47V", "K84L"} + assert d["wt"]["theta_low"] == pytest.approx(0.99) + assert d["A47V"]["log_hill_K"] == pytest.approx(-3.8) + + def test_reads_partial_columns(self, partial_csv): + d = read_binding_genotype_params(partial_csv) + assert "theta_low" not in d["wt"] + assert d["wt"]["log_hill_K"] == pytest.approx(-4.1) + + def test_nan_preserved(self, nan_csv): + d = read_binding_genotype_params(nan_csv) + assert np.isnan(d["A47V"]["theta_low"]) + assert np.isnan(d["A47V"]["theta_high"]) + assert d["A47V"]["log_hill_K"] == pytest.approx(-3.8) + + def test_raises_no_genotype_column(self, tmp_path): + p = tmp_path / "bad.csv" + p.write_text("theta_low,theta_high\n0.99,0.01\n") + with pytest.raises(ValueError, match="genotype"): + read_binding_genotype_params(str(p)) + + def test_raises_no_param_columns(self, tmp_path): + p = tmp_path / "bad.csv" + p.write_text("genotype\nwt\n") + with pytest.raises(ValueError, match="parameter column"): + read_binding_genotype_params(str(p)) + + def test_raises_unknown_columns(self, tmp_path): + p = tmp_path / "bad.csv" + p.write_text("genotype,theta_low,extra_col\nwt,0.99,foo\n") + with pytest.raises(ValueError, match="Unrecognised"): + read_binding_genotype_params(str(p)) + + def test_clips_theta_above_one(self, tmp_path): + """theta_low > 1 (float rounding) must be clipped and trigger a warning.""" + p = tmp_path / "over.csv" + p.write_text("genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,1.000004,0.01,-4.1,2.0\n") + with pytest.warns(UserWarning, match="theta_low"): + d = read_binding_genotype_params(str(p)) + assert d["wt"]["theta_low"] <= 1.0 - 1e-4 + + def test_clips_theta_below_zero(self, tmp_path): + """theta_high < 0 must be clipped and trigger a warning.""" + p = tmp_path / "under.csv" + p.write_text("genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,-0.0001,-4.1,2.0\n") + with pytest.warns(UserWarning, match="theta_high"): + d = read_binding_genotype_params(str(p)) + assert d["wt"]["theta_high"] >= 1e-4 + + def test_no_warning_for_valid_values(self, simple_csv): + """Valid theta values in (0, 1) must not trigger a warning.""" + import warnings as _w + with _w.catch_warnings(): + _w.simplefilter("error", UserWarning) + read_binding_genotype_params(simple_csv) # should not raise + + def test_nan_theta_not_clipped(self, nan_csv): + """NaN theta values must pass through without triggering a warning.""" + import warnings as _w + with _w.catch_warnings(): + _w.simplefilter("error", UserWarning) + d = read_binding_genotype_params(nan_csv) + assert np.isnan(d["A47V"]["theta_low"]) + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +class TestHillTheta: + + def test_wt_curve_shape(self, wt_params): + log_conc = np.linspace(-10, 0, 20) + theta = _hill_theta(**wt_params, log_conc=log_conc) + assert theta.shape == (20,) + assert np.all(theta >= 0) and np.all(theta <= 1) + + def test_decreasing_for_repressor(self, wt_params): + """With theta_low > theta_high, theta decreases as ligand increases.""" + log_conc = np.linspace(-10, 0, 10) + theta = _hill_theta(**wt_params, log_conc=log_conc) + assert theta[0] > theta[-1] + + def test_zero_concentration_handled(self, wt_params): + """Zero concentrations are passed as the sentinel; no NaN.""" + log_conc = _to_log_conc([0.0, 0.001, 0.01]) + theta = _hill_theta(**wt_params, log_conc=log_conc) + assert np.all(np.isfinite(theta)) + + def test_endpoints(self, wt_params): + """Very low conc → theta_low; very high conc → theta_high.""" + theta_low_end = _hill_theta(**wt_params, log_conc=np.array([-30.0])) + theta_high_end = _hill_theta(**wt_params, log_conc=np.array([10.0])) + assert theta_low_end[0] == pytest.approx(wt_params["theta_low"], abs=1e-4) + assert theta_high_end[0] == pytest.approx(wt_params["theta_high"], abs=1e-4) + + +class TestFillParamsFromWt: + + def test_fills_nan_from_wt(self, wt_params): + params = {"theta_low": float("nan"), "log_hill_K": -3.8, + "theta_high": float("nan"), "hill_n": float("nan")} + filled = _fill_params_from_wt(params, wt_params) + assert filled["theta_low"] == wt_params["theta_low"] + assert filled["theta_high"] == wt_params["theta_high"] + assert filled["hill_n"] == wt_params["hill_n"] + assert filled["log_hill_K"] == pytest.approx(-3.8) + + def test_does_not_overwrite_present(self, wt_params): + params = {"theta_low": 0.5, "theta_high": 0.5, + "log_hill_K": -3.0, "hill_n": 1.5} + filled = _fill_params_from_wt(params, wt_params) + assert filled["theta_low"] == pytest.approx(0.5) + + def test_missing_key_uses_wt(self, wt_params): + params = {"log_hill_K": -3.8, "hill_n": 1.8} + filled = _fill_params_from_wt(params, wt_params) + assert filled["theta_low"] == wt_params["theta_low"] + assert filled["theta_high"] == wt_params["theta_high"] + + +class TestWtParamsFromSimPriors: + + def test_extracts_correct_keys(self, mock_sim_priors, wt_params): + result = _wt_params_from_sim_priors(mock_sim_priors) + assert result["theta_low"] == pytest.approx(wt_params["theta_low"]) + assert result["log_hill_K"] == pytest.approx(wt_params["log_hill_K"]) + assert set(result.keys()) == HILL_PARAM_COLS + + +# --------------------------------------------------------------------------- +# build_theta_gc_override_hill_geno +# --------------------------------------------------------------------------- + +class TestBuildThetaGcOverrideHillGeno: + + def test_returns_correct_genotypes(self, simple_csv, wt_params): + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.linspace(-10, 0, 5) + override, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + assert set(override.keys()) == {"wt", "A47V", "K84L"} + + def test_theta_shape(self, simple_csv, wt_params): + params_dict = read_binding_genotype_params(simple_csv) + C = 8 + log_conc = np.linspace(-10, 0, C) + override, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + for g, theta in override.items(): + assert theta.shape == (C,), f"Shape mismatch for {g}" + + def test_wt_uses_wt_params(self, simple_csv, wt_params): + """WT row in CSV should produce the exact WT curve.""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.linspace(-10, 0, 20) + override, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + expected = _hill_theta(**wt_params, log_conc=log_conc) + np.testing.assert_allclose(override["wt"], expected, rtol=1e-6) + + def test_mutant_uses_measured_params(self, simple_csv, wt_params): + """Non-WT row should use the measured parameters, not WT.""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.linspace(-10, 0, 20) + override, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + wt_curve = _hill_theta(**wt_params, log_conc=log_conc) + # A47V has a different log_hill_K, so its curve should differ from WT + assert not np.allclose(override["A47V"], wt_curve) + + def test_nan_params_filled_from_wt(self, nan_csv, wt_params): + """NaN theta_low / theta_high should fall back to WT values.""" + params_dict = read_binding_genotype_params(nan_csv) + log_conc = np.linspace(-10, 0, 5) + override, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + # A47V has NaN theta_low / theta_high → WT values used + # its log_hill_K is -3.8 (different from WT -4.1), so not identical to WT + assert override["A47V"].shape == (5,) + assert np.all(np.isfinite(override["A47V"])) + + def test_theta_in_unit_interval(self, simple_csv, wt_params): + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.linspace(-15, 5, 30) + override, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + for theta in override.values(): + assert np.all(theta >= 0) and np.all(theta <= 1) + + def test_effective_params_returned(self, simple_csv, wt_params): + """effective_params matches the CSV values (NaN filled from wt_params).""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.linspace(-10, 0, 5) + _, eff = build_theta_gc_override_hill_geno(params_dict, log_conc, wt_params) + assert set(eff.keys()) == {"wt", "A47V", "K84L"} + assert eff["wt"]["theta_low"] == pytest.approx(0.99) + assert eff["A47V"]["log_hill_K"] == pytest.approx(-3.8) + assert eff["K84L"]["hill_n"] == pytest.approx(2.2) + + +# --------------------------------------------------------------------------- +# build_theta_gc_override_hill_mut +# --------------------------------------------------------------------------- + +def _make_sim_data_for_genotypes(genotypes): + """Build a real SimData for a small genotype list (no thermo data).""" + import pandas as pd + from tfscreen.simulate.sim_data_class import build_sim_data + + library_df = pd.DataFrame({"genotype": genotypes}) + sample_df = pd.DataFrame({"titrant_conc": [0.0, 0.001, 0.01, 0.1, 1.0]}) + return build_sim_data(library_df, sample_df) + + +class TestBuildThetaGcOverrideHillMut: + + @pytest.fixture + def small_genotypes(self): + return ["wt", "A47V", "K84L", "A47V/K84L"] + + @pytest.fixture + def small_sim_data(self, small_genotypes): + return _make_sim_data_for_genotypes(small_genotypes) + + def test_returns_all_genotypes(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + assert set(result.keys()) == set(small_genotypes) + + def test_theta_shape(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + C = len(log_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + for g, theta in result.items(): + assert theta.shape == (C,), f"Shape mismatch for {g}" + + def test_theta_in_unit_interval(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + for g, theta in result.items(): + assert np.all(theta >= 0) and np.all(theta <= 1), f"Out of [0,1] for {g}" + + def test_wt_receives_exact_wt_curve(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + """WT genotype has no mutations so its assembled curve = WT reference curve.""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + # WT from CSV: theta_low=0.99, theta_high=0.01, log_hill_K=-4.1, hill_n=2.0 + expected_wt = _hill_theta(0.99, 0.01, -4.1, 2.0, log_conc) + np.testing.assert_allclose(result["wt"], expected_wt, rtol=1e-5) + + def test_single_mutant_uses_measured_params(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + """A single-mutant entry in the CSV should produce its measured curve exactly.""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + expected_a47v = _hill_theta(0.97, 0.03, -3.8, 1.8, log_conc) + np.testing.assert_allclose(result["A47V"], expected_a47v, rtol=1e-5) + + def test_double_differs_from_wt(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + """Double mutant should differ from WT (deltas from both singles applied).""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + expected_wt = _hill_theta(0.99, 0.01, -4.1, 2.0, log_conc) + assert not np.allclose(result["A47V/K84L"], expected_wt) + + def test_unmeasured_mut_uses_prior(self, wt_params, mock_sim_priors, rng): + """A mutation not in the CSV gets a random delta from the prior.""" + genotypes = ["wt", "M99A"] # M99A not in CSV + sim_data = _make_sim_data_for_genotypes(genotypes) + params_dict = {"wt": wt_params} # only WT in CSV + log_conc = np.array(sim_data.log_titrant_conc) + + r1, _ = build_theta_gc_override_hill_mut( + params_dict, genotypes, sim_data, mock_sim_priors, + log_conc, np.random.default_rng(1), + ) + r2, _ = build_theta_gc_override_hill_mut( + params_dict, genotypes, sim_data, mock_sim_priors, + log_conc, np.random.default_rng(2), + ) + # WT is pinned: same both times + np.testing.assert_allclose(r1["wt"], r2["wt"]) + # M99A is random: different seeds → different curves + assert not np.allclose(r1["M99A"], r2["M99A"]) + + def test_csv_wt_overrides_sim_priors(self, small_genotypes, small_sim_data, + mock_sim_priors, rng, tmp_path): + """When CSV has a 'wt' row, that WT reference overrides sim_priors.""" + # Use a WT with very different log_hill_K + content = ( + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.95,0.05,-6.0,1.5\n" # dramatically different from sim_priors WT + ) + p = tmp_path / "alt_wt.csv" + p.write_text(content) + params_dict = read_binding_genotype_params(str(p)) + + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + expected_wt = _hill_theta(0.95, 0.05, -6.0, 1.5, log_conc) + np.testing.assert_allclose(result["wt"], expected_wt, rtol=1e-5) + + def test_direct_double_in_csv_overrides_assembled(self, small_genotypes, + small_sim_data, mock_sim_priors, + rng, tmp_path): + """A directly-measured double mutant in the CSV overrides the assembled value.""" + content = ( + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,0.01,-4.1,2.0\n" + "A47V/K84L,0.90,0.10,-5.0,3.0\n" # direct measurement for the double + ) + p = tmp_path / "with_double.csv" + p.write_text(content) + params_dict = read_binding_genotype_params(str(p)) + + log_conc = np.array(small_sim_data.log_titrant_conc) + result, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + expected_double = _hill_theta(0.90, 0.10, -5.0, 3.0, log_conc) + np.testing.assert_allclose(result["A47V/K84L"], expected_double, rtol=1e-5) + + def test_effective_params_all_genotypes(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + """effective_params covers all library genotypes.""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + _, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + assert set(eff.keys()) == set(small_genotypes) + for g, p in eff.items(): + assert set(p.keys()) == {"theta_low", "theta_high", "log_hill_K", "hill_n"} + + def test_effective_params_measured_singles(self, simple_csv, small_genotypes, + small_sim_data, mock_sim_priors, rng): + """effective_params for CSV-measured singles matches their CSV values.""" + params_dict = read_binding_genotype_params(simple_csv) + log_conc = np.array(small_sim_data.log_titrant_conc) + _, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + assert eff["wt"]["theta_low"] == pytest.approx(0.99, abs=1e-4) + assert eff["wt"]["theta_high"] == pytest.approx(0.01, abs=1e-4) + assert eff["A47V"]["log_hill_K"] == pytest.approx(-3.8, abs=1e-4) + assert eff["K84L"]["hill_n"] == pytest.approx(2.2, abs=1e-4) + + def test_effective_params_direct_double(self, small_genotypes, small_sim_data, + mock_sim_priors, rng, tmp_path): + """effective_params for a directly-measured double uses the CSV values.""" + content = ( + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,0.01,-4.1,2.0\n" + "A47V/K84L,0.90,0.10,-5.0,3.0\n" + ) + p = tmp_path / "with_double.csv" + p.write_text(content) + params_dict = read_binding_genotype_params(str(p)) + log_conc = np.array(small_sim_data.log_titrant_conc) + _, eff = build_theta_gc_override_hill_mut( + params_dict, small_genotypes, small_sim_data, + mock_sim_priors, log_conc, rng, + ) + assert eff["A47V/K84L"]["theta_low"] == pytest.approx(0.90, abs=1e-4) + assert eff["A47V/K84L"]["log_hill_K"] == pytest.approx(-5.0, abs=1e-4) + + +# --------------------------------------------------------------------------- +# build_binding_theta_from_params +# --------------------------------------------------------------------------- + +class TestBuildBindingThetaFromParams: + + @pytest.fixture + def params_dict(self, simple_csv): + return read_binding_genotype_params(simple_csv) + + def test_output_shape(self, params_dict, wt_params, rng): + binding_concs = [0.0, 0.001, 0.01, 0.1, 1.0] + df = build_binding_theta_from_params( + params_dict, binding_concs, "iptg", noise=0.0, rng=rng, wt_params=wt_params + ) + assert set(df.columns) == {"genotype", "titrant_name", "titrant_conc", "theta_true"} + # 3 genotypes × 5 concentrations = 15 rows + assert len(df) == 3 * len(binding_concs) + + def test_correct_titrant_name(self, params_dict, wt_params, rng): + df = build_binding_theta_from_params( + params_dict, [0.01], "iptg", noise=0.0, rng=rng, wt_params=wt_params + ) + assert (df["titrant_name"] == "iptg").all() + + def test_no_noise(self, params_dict, wt_params, rng): + """With noise=0, theta_true should exactly match Hill equation output.""" + binding_concs = [0.0, 0.001, 0.01] + log_concs = _to_log_conc(binding_concs) + df = build_binding_theta_from_params( + params_dict, binding_concs, "iptg", noise=0.0, rng=rng, wt_params=wt_params + ) + wt_rows = df[df["genotype"] == "wt"].sort_values("titrant_conc") + expected = _hill_theta(0.99, 0.01, -4.1, 2.0, log_concs) + np.testing.assert_allclose(wt_rows["theta_true"].values, expected, rtol=1e-6) + + def test_noise_changes_values(self, params_dict, wt_params): + """With noise > 0, two runs with different seeds produce different theta_true.""" + binding_concs = [0.0, 0.001, 0.01, 0.1] + df1 = build_binding_theta_from_params( + params_dict, binding_concs, "iptg", noise=0.05, + rng=np.random.default_rng(1), wt_params=wt_params, + ) + df2 = build_binding_theta_from_params( + params_dict, binding_concs, "iptg", noise=0.05, + rng=np.random.default_rng(2), wt_params=wt_params, + ) + assert not np.allclose(df1["theta_true"].values, df2["theta_true"].values) + + def test_theta_in_unit_interval(self, params_dict, wt_params): + """theta_true must remain in [0, 1] even with noise.""" + binding_concs = np.linspace(0.0, 1.0, 20) + df = build_binding_theta_from_params( + params_dict, binding_concs, "iptg", noise=0.1, + rng=np.random.default_rng(99), wt_params=wt_params, + ) + assert (df["theta_true"] >= 0).all() and (df["theta_true"] <= 1).all() + + def test_nan_filled_from_wt(self, nan_csv, wt_params, rng): + """Genotypes with NaN params should produce finite theta using WT fallback.""" + params_dict = read_binding_genotype_params(nan_csv) + df = build_binding_theta_from_params( + params_dict, [0.0, 0.01, 1.0], "iptg", noise=0.0, rng=rng, wt_params=wt_params, + ) + assert np.all(np.isfinite(df["theta_true"].values)) + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +def test_supported_components(): + assert "hill_geno" in SUPPORTED_COMPONENTS + assert "hill_mut" in SUPPORTED_COMPONENTS + +def test_hill_param_cols(): + assert HILL_PARAM_COLS == {"theta_low", "theta_high", "log_hill_K", "hill_n"} diff --git a/tests/tfscreen/simulate/test_library_prediction.py b/tests/tfscreen/simulate/test_library_prediction.py index c276d1ed..464c8bd0 100644 --- a/tests/tfscreen/simulate/test_library_prediction.py +++ b/tests/tfscreen/simulate/test_library_prediction.py @@ -318,3 +318,175 @@ def capture_rng(*args, **kwargs): draw_a = captured_rngs[0].integers(0, 2**32) draw_b = captured_rngs[1].integers(0, 2**32) assert draw_a == draw_b, "Same seed must produce identical RNG state" + + +# --------------------------------------------------------------------------- +# genotype_params_file integration +# --------------------------------------------------------------------------- + +def _make_params_csv(tmp_path, content): + p = tmp_path / "params.csv" + p.write_text(content) + return str(p) + + +def _base_config_with_binding(params_path, genotypes=None): + binding = { + "titrant_name": "iptg", + "titrant_conc": [0.0, 0.001, 0.01], + "noise": 0.0, + "genotype_params_file": params_path, + } + if genotypes is not None: + binding["genotypes"] = genotypes + return { + "condition_blocks": [{"some": "block"}], + "theta_component": "hill_geno", + "seed": 7, + "thermo_data": None, + "theta_priors": None, + "growth": {"cond_A": {"m": 1.0, "b": 0.0}}, + "dk_geno_hyper_loc": -3.5, + "dk_geno_hyper_scale": 1.0, + "dk_geno_hyper_shift": 0.02, + "binding_data": binding, + } + + +def _patch_for_params_file(mocker, config): + """Patch all heavy deps; returns (mock_thermo, mock_sim_data).""" + mocker.patch("tfscreen.util.read_yaml", return_value=config) + + mock_library_df = pd.DataFrame({"genotype": ["wt", "A47V"]}) + mocker.patch( + "tfscreen.simulate.library_prediction.library_manager.LibraryManager" + ).return_value.build_library_df.return_value = mock_library_df + + import numpy as np + import jax.numpy as jnp + mock_sim_data = MagicMock() + mock_sim_data.log_titrant_conc = jnp.array( + [np.log(1e-20), np.log(0.001), np.log(0.01)] + ) + mock_sim_data.num_mutation = 1 + mock_sim_data.num_pair = 0 + mock_sim_data.mut_nnz_mut_idx = np.array([0], dtype=np.int32) + mock_sim_data.mut_nnz_geno_idx = np.array([1], dtype=np.int32) + mock_sim_data.pair_nnz_pair_idx = None + mock_sim_data.pair_nnz_geno_idx = None + mocker.patch( + "tfscreen.simulate.library_prediction.build_sample_dataframes", + return_value=pd.DataFrame({"titrant_conc": [0.0, 0.001, 0.01]}), + ) + mocker.patch( + "tfscreen.simulate.library_prediction.build_sim_data", + return_value=mock_sim_data, + ) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + + mock_thermo = mocker.patch( + "tfscreen.simulate.library_prediction.thermo_to_growth", + return_value=( + pd.DataFrame({"theta": [0.5, 0.3]}), + pd.DataFrame({"genotype": ["wt", "A47V"]}), + pd.DataFrame({ + "genotype": ["wt", "A47V"], + "dk_geno": [0.0, -0.01], + "activity": [1.0, 1.0], + }), + ), + ) + return mock_thermo, mock_sim_data + + +def test_params_file_produces_binding_theta_df(mocker, tmp_path): + """When genotype_params_file is set, binding_theta_df is populated.""" + csv = _make_params_csv( + tmp_path, + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,0.01,-4.1,2.0\n" + "A47V,0.97,0.03,-3.8,1.8\n", + ) + config = _base_config_with_binding(csv) + _patch_for_params_file(mocker, config) + + # hill_geno module must be importable for sim_priors + from tfscreen.tfmodel.generative.registry import model_registry + assert "hill_geno" in model_registry["theta"] + + _, _, _, _, binding_df = library_prediction(cf="config.yaml") + + assert binding_df is not None + assert set(binding_df["genotype"].unique()) == {"wt", "A47V"} + assert set(binding_df.columns) >= {"genotype", "titrant_name", "titrant_conc", "theta_true"} + + +def test_params_file_theta_gc_override_passed_to_thermo(mocker, tmp_path): + """theta_gc_override passed to thermo_to_growth contains measured genotypes.""" + csv = _make_params_csv( + tmp_path, + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "wt,0.99,0.01,-4.1,2.0\n" + "A47V,0.97,0.03,-3.8,1.8\n", + ) + config = _base_config_with_binding(csv) + mock_thermo, _ = _patch_for_params_file(mocker, config) + + library_prediction(cf="config.yaml") + + _, kwargs = mock_thermo.call_args + override = kwargs.get("theta_gc_override", {}) + assert "wt" in override + assert "A47V" in override + + +def test_unsupported_theta_component_raises(mocker, tmp_path): + """genotype_params_file with a non-Hill component must raise ValueError.""" + csv = _make_params_csv( + tmp_path, + "genotype,theta_low,theta_high,log_hill_K,hill_n\nwt,0.99,0.01,-4.1,2.0\n", + ) + config = _base_config_with_binding(csv) + config["theta_component"] = "thermo.O2_C12_K5_U0_a.PK" + + mocker.patch("tfscreen.util.read_yaml", return_value=config) + mocker.patch( + "tfscreen.simulate.library_prediction.library_manager.LibraryManager" + ).return_value.build_library_df.return_value = pd.DataFrame({"genotype": ["wt"]}) + mocker.patch( + "tfscreen.simulate.library_prediction.build_sample_dataframes", + return_value=pd.DataFrame({"titrant_conc": [0.0]}), + ) + mocker.patch("tfscreen.simulate.library_prediction.build_sim_data", return_value=MagicMock()) + mocker.patch("tfscreen.simulate.library_prediction.jax.random.PRNGKey", return_value="k") + + with pytest.raises(ValueError, match="hill"): + library_prediction(cf="config.yaml") + + +def test_params_file_and_genotypes_coexist(mocker, tmp_path): + """genotype_params_file and genotypes can coexist; both appear in binding_df.""" + csv = _make_params_csv( + tmp_path, + "genotype,theta_low,theta_high,log_hill_K,hill_n\n" + "A47V,0.97,0.03,-3.8,1.8\n", + ) + config = _base_config_with_binding(csv, genotypes=["wt"]) + mock_thermo, mock_sim_data = _patch_for_params_file(mocker, config) + + # Patch sample_theta_prior (called for simulated WT in the 'genotypes' path) + import numpy as np + mock_wt_gc = np.array([[0.99, 0.50, 0.01]]) # shape (1, 3): 1 genotype × 3 concs + mock_theta_param = MagicMock() + mocker.patch( + "tfscreen.simulate.library_prediction.sample_theta_prior", + return_value=(mock_wt_gc, mock_theta_param), + ) + + _, _, _, _, binding_df = library_prediction(cf="config.yaml") + + assert binding_df is not None + genos = set(binding_df["genotype"].unique()) + # WT from simulated path + A47V from params file + assert "A47V" in genos + assert "wt" in genos diff --git a/tests/tfscreen/simulate/test_sample_theta.py b/tests/tfscreen/simulate/test_sample_theta.py index 3bd7a23f..a55c7605 100644 --- a/tests/tfscreen/simulate/test_sample_theta.py +++ b/tests/tfscreen/simulate/test_sample_theta.py @@ -220,41 +220,54 @@ def test_greedy_maximin_all_when_n_exceeds_pool(): # sample_theta_stratified # ---------------------------------------------------------------------------- -def _make_stratified_mock_module(pool_size, n_binding_concs, n_growth_concs): +def _make_stratified_mock_module(n_binding_concs, n_growth_concs): """ Return a mock theta module for sample_theta_stratified tests. - run_model is called twice (once for binding sim_data, once for growth - sim_data). We return distinct matrices so tests can verify which call - produced which output. + sample_theta_stratified now calls sample_theta_prior 2*pool_size times: + pool_size calls at binding concentrations, then pool_size calls at growth + concentrations, alternating binding/growth per pool member. Each call + returns a (1, C) array for the single-genotype sim_data. """ mock_module = MagicMock() mock_module.get_hyperparameters.return_value = {} mock_module.ModelPriors.return_value = MagicMock() mock_module.define_model.return_value = MagicMock(name="theta_param") - # Alternating call sides: binding first, then growth. rng = np.random.default_rng(42) - binding_theta = rng.uniform(0, 1, (1, n_binding_concs, pool_size)) - growth_theta = rng.uniform(0, 1, (1, n_growth_concs, pool_size)) - mock_module.run_model.side_effect = [binding_theta, growth_theta] - mock_module._binding_theta = binding_theta - mock_module._growth_theta = growth_theta + + def run_model_side_effect(theta_param, sim_data): + # Detect which sim_data is being used by its concentration count + n_conc = sim_data.num_titrant_conc + return rng.uniform(0, 1, (1, n_conc, 1)) + + mock_module.run_model.side_effect = run_model_side_effect return mock_module +def _make_stratified_sim_data(n_conc): + sd = MagicMock() + sd.num_genotype = 1 + sd.num_titrant_conc = n_conc + return sd + + def test_stratified_output_shapes(): - pool_size, n_select, n_binding_concs, n_growth_concs = 30, 4, 6, 3 - mock_module = _make_stratified_mock_module(pool_size, n_binding_concs, n_growth_concs) + pool_size, n_select, n_binding_concs, n_growth_concs = 8, 3, 6, 3 + mock_module = _make_stratified_mock_module(n_binding_concs, n_growth_concs) binding_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_binding_concs)}) growth_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_growth_concs)}) + def build_sim_data_side(library_df, sample_df, thermo_data=None, skip_pairs=False): + n_conc = len(sample_df["titrant_conc"].unique()) + return _make_stratified_sim_data(n_conc) + with patch("tfscreen.simulate.sample_theta.model_registry", {"theta": {"hill_geno": mock_module}}), \ patch("tfscreen.simulate.sample_theta.handlers"), \ - patch("tfscreen.simulate.sample_theta.build_sim_data", return_value=MagicMock( - num_genotype=pool_size, num_titrant_conc=n_binding_concs)): + patch("tfscreen.simulate.sample_theta.build_sim_data", + side_effect=build_sim_data_side): binding_gc, growth_gc = sample_theta_stratified( "hill_geno", binding_sample_df, growth_sample_df, rng_key=0, n_select=n_select, pool_size=pool_size, @@ -275,26 +288,67 @@ def test_stratified_raises_when_n_select_exceeds_pool(): ) -def test_stratified_selected_rows_come_from_pool(): - """Selected binding rows must be a subset of the pool's binding rows.""" - pool_size, n_select, n_binding_concs, n_growth_concs = 20, 3, 4, 2 - mock_module = _make_stratified_mock_module(pool_size, n_binding_concs, n_growth_concs) +def test_stratified_pool_size_calls(): + """sample_theta_prior must be called 2*pool_size times (once per member per conc set).""" + pool_size, n_binding_concs, n_growth_concs = 5, 4, 2 + mock_module = _make_stratified_mock_module(n_binding_concs, n_growth_concs) binding_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_binding_concs)}) growth_sample_df = pd.DataFrame({"titrant_conc": [0.0, 1.0]}) + def build_sim_data_side(library_df, sample_df, thermo_data=None, skip_pairs=False): + return _make_stratified_sim_data(len(sample_df["titrant_conc"].unique())) + with patch("tfscreen.simulate.sample_theta.model_registry", {"theta": {"hill_geno": mock_module}}), \ patch("tfscreen.simulate.sample_theta.handlers"), \ - patch("tfscreen.simulate.sample_theta.build_sim_data", return_value=MagicMock( - num_genotype=pool_size, num_titrant_conc=n_binding_concs)): - binding_gc, growth_gc = sample_theta_stratified( + patch("tfscreen.simulate.sample_theta.build_sim_data", + side_effect=build_sim_data_side): + sample_theta_stratified( "hill_geno", binding_sample_df, growth_sample_df, - rng_key=0, n_select=n_select, pool_size=pool_size, + rng_key=0, n_select=2, pool_size=pool_size, + ) + + assert mock_module.run_model.call_count == 2 * pool_size + + +def test_stratified_ignores_simulate_path(): + """sample_theta_stratified must use prior-predictive even when simulate() is present. + + Mutation-decomposed components (e.g. hill_mut) with a wt-only pool library + have M=0 mutations and their simulate() path produces zero deltas for every + pool member. The prior-predictive path must be used instead so that WT-level + parameters are sampled independently per pool member. + """ + pool_size, n_binding_concs, n_growth_concs = 4, 3, 2 + mock_module = _make_stratified_mock_module(n_binding_concs, n_growth_concs) + + # Add a real simulate() function — stratified path must NOT call it. + simulate_called = [] + + def fake_simulate(name, data, sim_priors, rng_key): + simulate_called.append(True) + return np.ones((data.num_genotype, data.num_titrant_conc)) * 0.5, MagicMock() + + mock_module.simulate = fake_simulate + mock_module.get_sim_hyperparameters.return_value = {} + mock_module.SimPriors.return_value = MagicMock() + + binding_sample_df = pd.DataFrame({"titrant_conc": np.linspace(0, 1, n_binding_concs)}) + growth_sample_df = pd.DataFrame({"titrant_conc": [0.0, 1.0]}) + + def build_sim_data_side(library_df, sample_df, thermo_data=None, skip_pairs=False): + return _make_stratified_sim_data(len(sample_df["titrant_conc"].unique())) + + with patch("tfscreen.simulate.sample_theta.model_registry", + {"theta": {"hill_geno": mock_module}}), \ + patch("tfscreen.simulate.sample_theta.handlers"), \ + patch("tfscreen.simulate.sample_theta.build_sim_data", + side_effect=build_sim_data_side): + sample_theta_stratified( + "hill_geno", binding_sample_df, growth_sample_df, + rng_key=0, n_select=2, pool_size=pool_size, ) - # Each selected binding row must exist in the full pool binding matrix - pool_binding = mock_module._binding_theta[0].T # (pool_size, n_binding_concs) - for row in binding_gc: - match = np.any(np.all(np.isclose(pool_binding, row[None, :]), axis=1)) - assert match, "Selected binding row not found in pool" + assert not simulate_called, "simulate() must not be called from sample_theta_stratified" + assert mock_module.run_model.call_count == 2 * pool_size diff --git a/tests/tfscreen/simulate/test_thermo_to_growth.py b/tests/tfscreen/simulate/test_thermo_to_growth.py index e83b3954..d4b3a56b 100644 --- a/tests/tfscreen/simulate/test_thermo_to_growth.py +++ b/tests/tfscreen/simulate/test_thermo_to_growth.py @@ -533,6 +533,280 @@ def test_thermo_to_growth_dk_geno_zero_false_passes_none( assert mock_assign_dk.call_args.kwargs.get("fixed_value") is None +# ============================================================================ +# test thermo_to_growth — theta_gc_override +# ============================================================================ + +class TestThetaGcOverride: + """theta_gc_override must replace theta rows for the named genotypes.""" + + def test_overridden_genotype_has_new_theta( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.zeros((3, 2)) # all-zero baseline + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + override_values = np.array([0.8, 0.9]) + _, genotype_theta_df, _ = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_gc_override={"A1B": override_values}, + ) + + a1b_rows = genotype_theta_df[genotype_theta_df["genotype"] == "A1B"] + a1b_rows = a1b_rows.sort_values("titrant_conc").reset_index(drop=True) + np.testing.assert_allclose(a1b_rows["theta"].values, override_values) + + def test_non_overridden_genotypes_unchanged( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.zeros((3, 2)) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + _, genotype_theta_df, _ = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_gc_override={"A1B": np.array([0.8, 0.9])}, + ) + + for geno in ("wt", "A1B/C2D"): + rows = genotype_theta_df[genotype_theta_df["genotype"] == geno] + np.testing.assert_array_equal(rows["theta"].values, 0.0) + + def test_none_override_is_noop( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.3) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + _, genotype_theta_df, _ = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_gc_override=None, + ) + + np.testing.assert_array_equal(genotype_theta_df["theta"].values, 0.3) + + def test_empty_override_is_noop( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.3) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + _, genotype_theta_df, _ = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_gc_override={}, + ) + + np.testing.assert_array_equal(genotype_theta_df["theta"].values, 0.3) + + def test_unknown_genotype_in_override_is_ignored( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.zeros((3, 2)) + sim_data = _make_sim_data_mock(test_genotypes, concs) + _patch_thermo_deps(mocker, test_genotypes, test_sample_df, theta_gc) + + _, genotype_theta_df, _ = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_gc_override={"NOTINGENO": np.array([0.99, 0.99])}, + ) + + np.testing.assert_array_equal(genotype_theta_df["theta"].values, 0.0) + + +# ============================================================================ +# test thermo_to_growth — theta_params_override +# ============================================================================ + +def _patch_thermo_deps_with_param(mocker, theta_gc, theta_param): + """Like _patch_thermo_deps but accepts a custom theta_param object.""" + mocker.patch( + "tfscreen.simulate.thermo_to_growth.sample_theta_prior", + return_value=(theta_gc, theta_param), + ) + mocker.patch( + "tfscreen.simulate.thermo_to_growth.set_categorical_genotype", + side_effect=lambda df: df, + ) + + +class TestThetaParamsOverride: + """theta_params_override must update Hill columns in parameters_df.""" + + def _make_hill_param(self, n_geno): + """Return a _MockThetaParam2D with shape (1, n_geno) and known values.""" + rng = np.random.default_rng(5) + return _MockThetaParam2D( + theta_low=rng.random((1, n_geno)), + theta_high=rng.random((1, n_geno)), + log_hill_K=rng.standard_normal((1, n_geno)), + hill_n=np.abs(rng.standard_normal((1, n_geno))) + 0.5, + ) + + def test_overridden_genotype_columns_updated( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.5) + theta_param = self._make_hill_param(3) + _patch_thermo_deps_with_param(mocker, theta_gc, theta_param) + sim_data = _make_sim_data_mock(test_genotypes, concs) + + override = {"A1B": {"theta_low": 0.99, "theta_high": 0.01, + "log_hill_K": -4.0, "hill_n": 2.0}} + _, _, parameters_df = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_params_override=override, + ) + + row = parameters_df[parameters_df["genotype"] == "A1B"].iloc[0] + assert np.isclose(row["theta_low"], 0.99) + assert np.isclose(row["theta_high"], 0.01) + assert np.isclose(row["log_hill_K"], -4.0) + assert np.isclose(row["hill_n"], 2.0) + + def test_non_overridden_genotypes_retain_original_values( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.5) + theta_param = self._make_hill_param(3) + sim_data = _make_sim_data_mock(test_genotypes, concs) + + # First: baseline call with no override to get reference values + _patch_thermo_deps_with_param(mocker, theta_gc, theta_param) + _, _, baseline_df = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + ) + wt_theta_low_baseline = float( + baseline_df[baseline_df["genotype"] == "wt"]["theta_low"].iloc[0] + ) + double_theta_low_baseline = float( + baseline_df[baseline_df["genotype"] == "A1B/C2D"]["theta_low"].iloc[0] + ) + + # Second: override call for A1B only + _patch_thermo_deps_with_param(mocker, theta_gc, theta_param) + _, _, parameters_df = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_params_override={"A1B": {"theta_low": 0.99}}, + ) + + wt_row = parameters_df[parameters_df["genotype"] == "wt"].iloc[0] + assert np.isclose(wt_row["theta_low"], wt_theta_low_baseline) + double_row = parameters_df[parameters_df["genotype"] == "A1B/C2D"].iloc[0] + assert np.isclose(double_row["theta_low"], double_theta_low_baseline) + + def test_none_override_is_noop( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.5) + theta_param = self._make_hill_param(3) + sim_data = _make_sim_data_mock(test_genotypes, concs) + + _patch_thermo_deps_with_param(mocker, theta_gc, theta_param) + _, _, parameters_df_no_override = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + rng=np.random.default_rng(0), + theta_params_override=None, + ) + + _patch_thermo_deps_with_param(mocker, theta_gc, theta_param) + _, _, parameters_df_empty = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + rng=np.random.default_rng(0), + theta_params_override={}, + ) + + pd.testing.assert_frame_equal( + parameters_df_no_override.reset_index(drop=True), + parameters_df_empty.reset_index(drop=True), + ) + + def test_unknown_column_in_override_is_silently_skipped( + self, mocker, test_genotypes, test_sample_df, simple_growth_params + ): + concs = np.array([10.0, 100.0]) + theta_gc = np.full((3, 2), 0.5) + theta_param = self._make_hill_param(3) + _patch_thermo_deps_with_param(mocker, theta_gc, theta_param) + sim_data = _make_sim_data_mock(test_genotypes, concs) + + # "nonexistent_col" is not in parameters_df; must not raise + _, _, parameters_df = thermo_to_growth( + genotypes=test_genotypes, + sim_data=sim_data, + sample_df=test_sample_df, + theta_component="mock", + theta_rng_key=0, + growth_params=simple_growth_params, + theta_params_override={"A1B": {"theta_low": 0.99, + "nonexistent_col": 42.0}}, + ) + + assert "nonexistent_col" not in parameters_df.columns + row = parameters_df[parameters_df["genotype"] == "A1B"].iloc[0] + assert np.isclose(row["theta_low"], 0.99) + + # ============================================================================ # test _sample_horseshoe_activity # ============================================================================ diff --git a/tests/tfscreen/tfmodel/generative/components/theta/test__simple.py b/tests/tfscreen/tfmodel/generative/components/theta/test__simple.py index f5555fe2..cda478a6 100644 --- a/tests/tfscreen/tfmodel/generative/components/theta/test__simple.py +++ b/tests/tfscreen/tfmodel/generative/components/theta/test__simple.py @@ -54,7 +54,7 @@ def mock_data(): def mock_priors(): """ A non-trivial theta_values tensor of shape (num_titrant_name=2, - num_titrant_conc=3) with two saturating Hill-like rows. + num_titrant_conc=3) with two saturating Hill-like rows. 2-D path. """ theta_values = jnp.array([ [0.10, 0.50, 0.90], @@ -63,6 +63,29 @@ def mock_priors(): return ModelPriors(theta_values=theta_values, sigma_floor=0.05) +@pytest.fixture +def mock_priors_3d(mock_data): + """ + Per-genotype theta_values of shape (T=2, C=3, G=4). + + Each genotype has a distinct sigmoid-like curve so we can verify that + values are NOT broadcast across the genotype axis. + Axis order: (titrant_name, titrant_conc, genotype). + """ + # shape = (T=2, C=3, G=4): outer=titrant, middle=conc, inner=genotype. + theta_values = jnp.array([ + # titrant 0 — shape (C=3, G=4) + [[0.90, 0.80, 0.70, 0.60], # conc 0, genotypes 0-3 + [0.50, 0.40, 0.30, 0.20], # conc 1 + [0.10, 0.05, 0.08, 0.03]], # conc 2 + # titrant 1 + [[0.85, 0.75, 0.65, 0.55], + [0.45, 0.35, 0.25, 0.15], + [0.12, 0.07, 0.09, 0.04]], + ]) # shape (2, 3, 4) = (T, C, G) + return ModelPriors(theta_values=theta_values, sigma_floor=0.05) + + # --------------------------------------------------------------------------- # Helper / sanity tests # --------------------------------------------------------------------------- @@ -342,3 +365,115 @@ def test_extreme_theta_values_finite_mu(mock_data): assert jnp.isclose(theta_param.mu[0, 1, 0], 0.0) assert theta_param.mu[0, 0, 0] < -10.0 assert theta_param.mu[0, 2, 0] > 10.0 + + +# --------------------------------------------------------------------------- +# 3-D theta_values: per-genotype path +# --------------------------------------------------------------------------- + +class TestPerGenotype: + """Tests for the (T, C, G) theta_values path introduced for calibration.""" + + def test_define_model_returns_per_genotype_theta(self, mock_data, mock_priors_3d): + """Each genotype should get its own theta column, not a broadcast copy.""" + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + assert theta_param.theta.shape == ( + mock_data.num_titrant_name, + mock_data.num_titrant_conc, + mock_data.num_genotype, + ) + # The per-genotype theta must match the input exactly. + assert jnp.allclose(theta_param.theta, mock_priors_3d.theta_values) + # Genotypes must differ from each other — no broadcast collapse. + assert not jnp.allclose(theta_param.theta[..., 0], theta_param.theta[..., 1]) + + def test_define_model_3d_population_moment_shapes(self, mock_data, mock_priors_3d): + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + expected = (mock_data.num_titrant_name, mock_data.num_titrant_conc, 1) + assert theta_param.mu.shape == expected + assert theta_param.sigma.shape == expected + + def test_define_model_3d_mu_is_mean_over_genotypes(self, mock_data, mock_priors_3d): + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + expected_mu = jnp.mean( + _logit(mock_priors_3d.theta_values), axis=-1, keepdims=True + ) + assert jnp.allclose(theta_param.mu, expected_mu, atol=1e-5) + + def test_define_model_3d_sigma_at_least_floor(self, mock_data, mock_priors_3d): + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + assert jnp.all(theta_param.sigma >= mock_priors_3d.sigma_floor) + + def test_define_model_3d_sigma_is_floor_when_genotypes_identical(self, mock_data): + """When all genotypes share the same theta, sigma collapses to the floor.""" + tv = jnp.broadcast_to( + jnp.array([[0.3, 0.5, 0.7], [0.2, 0.4, 0.6]])[..., None], + (2, 3, mock_data.num_genotype), + ) + floor = 0.05 + priors = ModelPriors(theta_values=tv, sigma_floor=floor) + theta_param = define_model(name="pg", data=mock_data, priors=priors) + assert jnp.allclose(theta_param.sigma, floor, atol=1e-5) + + def test_define_model_3d_registers_no_sample_sites(self, mock_data, mock_priors_3d): + with seed(rng_seed=0): + tr = trace(define_model).get_trace( + name="pg", data=mock_data, priors=mock_priors_3d, + ) + sample_sites = [n for n, s in tr.items() if s["type"] == "sample"] + assert sample_sites == [] + assert "pg_theta" in tr + + def test_guide_3d_matches_model(self, mock_data, mock_priors_3d): + model_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + guide_param = guide(name="pg", data=mock_data, priors=mock_priors_3d) + assert jnp.allclose(model_param.theta, guide_param.theta) + assert jnp.allclose(model_param.mu, guide_param.mu) + + def test_run_model_3d_slices_correct_genotypes(self, mock_data, mock_priors_3d): + """run_model must pick the right per-genotype curve using geno_theta_idx.""" + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + out = run_model(theta_param, mock_data._replace(scatter_theta=0)) + + expected_shape = ( + mock_data.num_titrant_name, + mock_data.num_titrant_conc, + mock_data.geno_theta_idx.shape[0], + ) + assert out.shape == expected_shape + # Each output column must match the theta_values column for that genotype. + for col, geno_idx in enumerate(mock_data.geno_theta_idx.tolist()): + assert jnp.allclose(out[..., col], + mock_priors_3d.theta_values[..., geno_idx]) + + def test_run_model_3d_concentration_mapping(self, mock_data, mock_priors_3d): + """Concentration remapping must work correctly with per-genotype theta.""" + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + # Request concentrations [1.0, 0.0] — subset + reorder of [0, 1, 10]. + new_conc = jnp.array([1.0, 0.0]) + out = run_model( + theta_param, + mock_data._replace(titrant_conc=new_conc, scatter_theta=0), + ) + # Expected: columns [1, 0] from theta_values, then slice by geno_theta_idx. + expected = mock_priors_3d.theta_values[..., mock_data.geno_theta_idx][:, [1, 0], :] + assert jnp.allclose(out, expected) + + def test_run_model_3d_scatter(self, mock_data, mock_priors_3d): + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + out = run_model(theta_param, mock_data) + expected_shape = ( + 1, 1, 1, 1, + mock_data.num_titrant_name, + mock_data.num_titrant_conc, + mock_data.geno_theta_idx.shape[0], + ) + assert out.shape == expected_shape + + def test_get_population_moments_3d(self, mock_data, mock_priors_3d): + theta_param = define_model(name="pg", data=mock_data, priors=mock_priors_3d) + mu, sigma = get_population_moments(theta_param, mock_data) + expected_shape = (mock_data.num_titrant_name, mock_data.num_titrant_conc, 1) + assert mu.shape == expected_shape + assert sigma.shape == expected_shape + assert jnp.all(sigma > 0) diff --git a/tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py b/tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py index c3ecf0be..d17fbc18 100644 --- a/tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py +++ b/tests/tfscreen/tfmodel/generative/components/theta/test_hill_mut.py @@ -669,3 +669,112 @@ def test_deterministic_same_key(self, mock_data_epi): t1, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(11)) t2, _ = simulate("theta", mock_data_epi, sp, jax.random.PRNGKey(11)) np.testing.assert_array_equal(t1, t2) + + +# --------------------------------------------------------------------------- +# define_model with num_mutation == 0 (wt-only / stratified-pool case) +# --------------------------------------------------------------------------- +# +# This was the bug: numpyro.plate requires size > 0, so calling define_model +# with a wt-only library (M=0) raised AssertionError inside the mutation plate. +# The fix guards the plate block with `if num_mut > 0` and returns zero-shape +# offset arrays when M=0. These tests lock in that behaviour. + +@pytest.fixture +def mock_data_no_mutation(): + """1 genotype (wt only), 0 mutations, 0 pairs — the stratified pool case.""" + return MockData( + num_titrant_name=1, + num_titrant_conc=4, + num_genotype=1, + log_titrant_conc=jnp.linspace(-5, 0, 4), + geno_theta_idx=jnp.array([0], dtype=jnp.int32), + scatter_theta=0, + num_mutation=0, + num_pair=0, + mut_geno_matrix=np.zeros((0, 1), dtype=np.float32), + mut_nnz_mut_idx=np.zeros(0, dtype=np.int32), + mut_nnz_geno_idx=np.zeros(0, dtype=np.int32), + pair_nnz_pair_idx=np.zeros(0, dtype=np.int32), + pair_nnz_geno_idx=np.zeros(0, dtype=np.int32), + batch_idx=jnp.array([0], dtype=jnp.int32), + ) + + +class TestDefineModelNoMutation: + + def test_does_not_raise(self, mock_data_no_mutation): + """define_model must not raise when num_mutation == 0.""" + priors = get_priors() + with seed(rng_seed=jax.random.PRNGKey(0)): + tp = define_model("theta", mock_data_no_mutation, priors) + assert isinstance(tp, ThetaParam) + + def test_output_shapes(self, mock_data_no_mutation): + """ThetaParam fields must have shape (T=1, G=1) with M=0.""" + priors = get_priors() + with seed(rng_seed=jax.random.PRNGKey(1)): + tp = define_model("theta", mock_data_no_mutation, priors) + T, G = mock_data_no_mutation.num_titrant_name, mock_data_no_mutation.num_genotype + assert tp.theta_low.shape == (T, G) + assert tp.theta_high.shape == (T, G) + assert tp.log_hill_K.shape == (T, G) + assert tp.hill_n.shape == (T, G) + + def test_per_genotype_equals_wt(self, mock_data_no_mutation): + """With M=0 the single genotype column must equal the WT parameters exactly.""" + priors = get_priors() + with seed(rng_seed=jax.random.PRNGKey(2)): + tp = define_model("theta", mock_data_no_mutation, priors) + # theta values must be valid probabilities + assert jnp.all(tp.theta_low >= 0.0) and jnp.all(tp.theta_low <= 1.0) + assert jnp.all(tp.theta_high >= 0.0) and jnp.all(tp.theta_high <= 1.0) + + def test_different_keys_give_different_curves(self, mock_data_no_mutation): + """Independent rng_keys must produce different WT parameter draws.""" + priors = get_priors() + with seed(rng_seed=jax.random.PRNGKey(10)): + tp1 = define_model("theta", mock_data_no_mutation, priors) + with seed(rng_seed=jax.random.PRNGKey(11)): + tp2 = define_model("theta", mock_data_no_mutation, priors) + assert not jnp.allclose(tp1.log_hill_K, tp2.log_hill_K), ( + "Different seeds should produce different log_K draws") + + def test_force_prior_predictive_path_does_not_raise(self, mock_data_no_mutation): + """sample_theta_prior with force_prior_predictive=True must work for M=0. + + This is the exact call path used by sample_theta_stratified when building + the diversity pool for mutation-decomposed components like hill_mut. + """ + from tfscreen.simulate.sample_theta import sample_theta_prior + theta_gc, theta_param = sample_theta_prior( + "hill_mut", mock_data_no_mutation, + rng_key=jax.random.PRNGKey(42), + force_prior_predictive=True, + ) + assert theta_gc.shape == (mock_data_no_mutation.num_genotype, + mock_data_no_mutation.num_titrant_conc) + + def test_pool_members_diverse_under_force_prior_predictive(self, mock_data_no_mutation): + """Multiple pool members must produce different curves (not all wt-identical). + + This is the regression test for the original bug: the perturbation path + with M=0 returned the same fixed wt curve for every pool member. + """ + from tfscreen.simulate.sample_theta import sample_theta_prior + import jax + rng_key = jax.random.PRNGKey(0) + curves = [] + for i in range(10): + key_i = jax.random.fold_in(rng_key, i) + theta_gc, _ = sample_theta_prior( + "hill_mut", mock_data_no_mutation, + rng_key=key_i, + force_prior_predictive=True, + ) + curves.append(theta_gc[0]) # single genotype → (C,) + + curves = np.stack(curves) # (10, C) + # At least some curves must differ (pool is not homogeneous) + assert curves.std(axis=0).max() > 0.01, ( + "Pool members are all identical — prior-predictive path is not sampling") diff --git a/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py b/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py index 6a76ce3c..f6c45bca 100644 --- a/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py @@ -227,67 +227,146 @@ def test_missing_column_in_binding_raises(self): class TestComputeThetaValues: - def _make_orchestrator_cal(self, titrant_names, titrant_concs): + def _make_orchestrator_cal(self, titrant_names, titrant_concs, genotypes): """Return a MagicMock with the binding_tm shape used by the helper.""" orchestrator = MagicMock() - # Order matters; the helper reads names then concs by index lookup. - orchestrator.binding_tm.tensor_dim_names = ["titrant_name", "titrant_conc"] - orchestrator.binding_tm.tensor_dim_labels = [list(titrant_names), - list(titrant_concs)] + # Order matters; the helper reads names, concs, and genotypes by index. + orchestrator.binding_tm.tensor_dim_names = [ + "titrant_name", "titrant_conc", "genotype" + ] + orchestrator.binding_tm.tensor_dim_labels = [ + list(titrant_names), + list(titrant_concs), + list(genotypes), + ] return orchestrator - def test_inverse_variance_weighted_mean(self): - orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0]) - # Two genotypes: theta=0.2 (sigma=0.1) and theta=0.8 (sigma=0.01) - # Inverse-variance weights heavily favor the second, so the - # consensus should be ~0.8 (not the unweighted 0.5). + def test_per_genotype_theta_extracted_correctly(self): + """Each genotype must get its own theta value, not the cross-genotype average.""" + orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0], ["wt", "mut"]) + # wt has theta=0.8, mut has theta=0.2; output must preserve the distinction. + binding_df = pd.DataFrame({ + "titrant_name": ["IPTG", "IPTG"], + "titrant_conc": [1.0, 1.0], + "genotype": ["wt", "mut"], + "theta_obs": [0.8, 0.2], + "theta_std": [0.01, 0.01], + }) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) + assert theta.shape == (1, 1, 2) + assert theta[0, 0, 0] > 0.75 # wt ≈ 0.8 + assert theta[0, 0, 1] < 0.25 # mut ≈ 0.2 + + def test_inverse_variance_weighting_within_genotype(self): + """Multiple rows for the same genotype/cell must be IVW-averaged.""" + orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0], ["wt"]) + # Two replicate measurements: theta=0.2 (noisy) and theta=0.8 (precise). binding_df = pd.DataFrame({ "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [1.0, 1.0], + "genotype": ["wt", "wt"], "theta_obs": [0.2, 0.8], "theta_std": [0.1, 0.01], }) theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) - assert theta.shape == (1, 1) - # Closer to 0.8 than to the midpoint 0.5 - assert theta[0, 0] > 0.7 + assert theta.shape == (1, 1, 1) + # Inverse-variance weighting heavily favours the precise observation. + assert theta[0, 0, 0] > 0.7 def test_falls_back_to_plain_mean_when_all_stds_zero(self): - orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0]) + orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0], ["wt"]) binding_df = pd.DataFrame({ "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [1.0, 1.0], + "genotype": ["wt", "wt"], "theta_obs": [0.3, 0.7], "theta_std": [0.0, 0.0], # invalid weights → fallback to mean }) theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) - assert theta[0, 0] == pytest.approx(0.5) + assert theta[0, 0, 0] == pytest.approx(0.5) + + def test_unobserved_genotype_cell_defaults_to_midpoint(self): + """A genotype present in labels but absent from binding_df gets 0.5.""" + orchestrator = self._make_orchestrator_cal(["IPTG"], [1.0], ["wt", "missing"]) + binding_df = pd.DataFrame({ + "titrant_name": ["IPTG"], + "titrant_conc": [1.0], + "genotype": ["wt"], + "theta_obs": [0.9], + "theta_std": [0.01], + }) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) + assert theta.shape == (1, 1, 2) + assert theta[0, 0, 0] > 0.85 # wt observed + assert theta[0, 0, 1] == pytest.approx(0.5) # "missing" → midpoint - def test_unobserved_cell_defaults_to_midpoint(self): - orchestrator = self._make_orchestrator_cal(["IPTG"], [0.0, 1.0]) + def test_unobserved_concentration_cell_defaults_to_midpoint(self): + """A concentration present in labels but absent from binding_df gets 0.5.""" + orchestrator = self._make_orchestrator_cal(["IPTG"], [0.0, 1.0], ["wt"]) # Only the IPTG=1.0 cell is observed. binding_df = pd.DataFrame({ "titrant_name": ["IPTG"], "titrant_conc": [1.0], + "genotype": ["wt"], "theta_obs": [0.9], "theta_std": [0.01], }) theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) - assert theta[0, 0] == pytest.approx(0.5) - assert theta[0, 1] > 0.85 + assert theta.shape == (1, 2, 1) + assert theta[0, 0, 0] == pytest.approx(0.5) # conc=0.0 not observed + assert theta[0, 1, 0] > 0.85 # conc=1.0 observed + + def test_output_shape(self): + """Output shape must be (n_titrant_name, n_titrant_conc, n_genotype).""" + orchestrator = self._make_orchestrator_cal( + ["IPTG", "arabinose"], [0.0, 0.01, 1.0], ["wt", "A1T", "G2P"] + ) + binding_df = pd.DataFrame({ + "titrant_name": ["IPTG"] * 3, + "titrant_conc": [0.0, 0.01, 1.0], + "genotype": ["wt", "A1T", "G2P"], + "theta_obs": [0.9, 0.5, 0.1], + "theta_std": [0.01, 0.01, 0.01], + }) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) + assert theta.shape == (2, 3, 3) def test_clip_to_open_interval(self): - orchestrator = self._make_orchestrator_cal(["IPTG"], [0.0, 1.0]) + orchestrator = self._make_orchestrator_cal(["IPTG"], [0.0, 1.0], ["wt"]) binding_df = pd.DataFrame({ "titrant_name": ["IPTG", "IPTG"], "titrant_conc": [0.0, 1.0], + "genotype": ["wt", "wt"], "theta_obs": [0.0, 1.0], "theta_std": [0.01, 0.01], }) theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) + assert theta.shape == (1, 2, 1) # Zero and one would blow up the downstream logit; expect clipping. - assert theta[0, 0] > 0.0 - assert theta[0, 1] < 1.0 + assert theta[0, 0, 0] > 0.0 + assert theta[0, 1, 0] < 1.0 + + def test_genotype_ordering_matches_labels(self): + """Values must be placed at the index matching the genotype label order.""" + orchestrator = self._make_orchestrator_cal( + ["IPTG"], [1.0], ["wt", "A1T", "G2P"] + ) + binding_df = pd.DataFrame({ + "titrant_name": ["IPTG", "IPTG", "IPTG"], + "titrant_conc": [1.0, 1.0, 1.0], + # Deliberately supply rows in a different order than the labels. + "genotype": ["G2P", "wt", "A1T"], + "theta_obs": [0.1, 0.9, 0.5], + "theta_std": [0.01, 0.01, 0.01], + }) + theta = np.asarray(_compute_theta_values(orchestrator, binding_df)) + assert theta.shape == (1, 1, 3) + # wt is index 0 in labels → theta ≈ 0.9 + assert theta[0, 0, 0] > 0.85 + # A1T is index 1 → theta ≈ 0.5 + assert 0.45 < theta[0, 0, 1] < 0.55 + # G2P is index 2 → theta ≈ 0.1 + assert theta[0, 0, 2] < 0.15 # --------------------------------------------------------------------------- @@ -793,7 +872,8 @@ def test_dk_geno_not_pinned_after_inject(self): def test_theta_values_are_set(self): orchestrator_cal, orchestrator_prod = self._make_models() - theta_values = np.array([[0.1, 0.9]]) + # theta_values is now (T, C, G); _inject_calibration_priors is shape-agnostic. + theta_values = np.array([[[0.1, 0.9], [0.3, 0.7]]]) # shape (1, 2, 2) _inject_calibration_priors(orchestrator_cal, orchestrator_prod, theta_values) result = orchestrator_cal._priors.theta.theta_values assert np.allclose(np.asarray(result), theta_values) @@ -842,7 +922,7 @@ def _patch_pipeline(self, mocker, orchestrator_cal=None, params=None, mocker.patch( "tfscreen.tfmodel.scripts" ".prefit_calibration_cli._compute_theta_values", - return_value=np.array([[0.5]]), + return_value=np.array([[[0.5]]]), # shape (T=1, C=1, G=1) ) mocker.patch( "tfscreen.tfmodel.scripts" From 5771dd1b9e86ac26be4a3673c34709fd4903e5e3 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 22 Jun 2026 10:48:10 -0700 Subject: [PATCH 20/24] improved symmetry breaking pre/sel growth calibration --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 171 +++++++------ reports/junit/junit.xml | 2 +- .../generative/components/growth/linear.py | 127 +++++++++- src/tfscreen/tfmodel/model_orchestrator.py | 17 +- .../tfmodel/scripts/prefit_calibration_cli.py | 176 +++++++++++++- tests/smoke-tests/test_configure_run_smoke.py | 8 +- .../components/growth/test_linear.py | 200 +++++++++++++++- .../scripts/test_prefit_calibration_cli.py | 226 +++++++++++++++++- .../tfmodel/test_extraction_robustness.py | 10 +- tests/tfscreen/tfmodel/test_h5_indexing.py | 4 +- .../tfmodel/test_model_orchestrator.py | 152 +++++++++++- .../tfscreen/tfmodel/test_needs_mut_wiring.py | 4 +- tests/tfscreen/tfmodel/test_prediction.py | 12 +- tests/tfscreen/tfmodel/test_presplit.py | 10 +- .../tfscreen/tfmodel/test_prior_predictive.py | 4 +- .../tfscreen/tfmodel/test_share_replicates.py | 10 +- .../tfscreen/tfmodel/test_spiked_genotypes.py | 4 +- tests/tfscreen/tfmodel/test_struct_wiring.py | 4 +- 20 files changed, 1012 insertions(+), 133 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index f23c57bb..92cb9a75 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.14%coverage96.14% \ No newline at end of file +coverage: 96.17%coverage96.17% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index a69536ec..c9b16aaa 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 3443tests3443 \ No newline at end of file +tests: 3491tests3491 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index b2468a96..b653ba9c 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1815,19 +1815,21 @@ ./src/tfscreen/tfmodel/generative/components/dk_geno/hierarchical_geno.py:374:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/generative/components/dk_geno/hierarchical_geno.py:374:5: W292 no newline at end of file ./src/tfscreen/tfmodel/generative/components/growth/linear.py:28:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/generative/components/growth/linear.py:179:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:193:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:199:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:201:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:202:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:203:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:204:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:205:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:206:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:207:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:208:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:209:10: E221 multiple spaces before operator -./src/tfscreen/tfmodel/generative/components/growth/linear.py:232:12: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:92:13: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:237:40: E261 at least two spaces before inline comment +./src/tfscreen/tfmodel/generative/components/growth/linear.py:260:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:274:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:280:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:282:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:283:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:284:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:285:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:286:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:287:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:288:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:289:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:290:10: E221 multiple spaces before operator +./src/tfscreen/tfmodel/generative/components/growth/linear.py:313:12: E221 multiple spaces before operator ./src/tfscreen/tfmodel/generative/components/growth/power.py:25:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/generative/components/growth/saturation.py:23:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/generative/components/growth_transition/baranyi.py:133:30: E261 at least two spaces before inline comment @@ -3601,7 +3603,7 @@ ./src/tfscreen/tfmodel/model_orchestrator.py:1049:25: E221 multiple spaces before operator ./src/tfscreen/tfmodel/model_orchestrator.py:1050:31: E127 continuation line over-indented for visual indent ./src/tfscreen/tfmodel/model_orchestrator.py:1052:31: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/model_orchestrator.py:1125:5: C901 'ModelOrchestrator._initialize_classes' is too complex (15) +./src/tfscreen/tfmodel/model_orchestrator.py:1125:5: C901 'ModelOrchestrator._initialize_classes' is too complex (16) ./src/tfscreen/tfmodel/model_orchestrator.py:1145:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:1147:67: W291 trailing whitespace ./src/tfscreen/tfmodel/model_orchestrator.py:1148:71: W291 trailing whitespace @@ -3616,37 +3618,37 @@ ./src/tfscreen/tfmodel/model_orchestrator.py:1172:53: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/model_orchestrator.py:1172:56: E231 missing whitespace after ',' ./src/tfscreen/tfmodel/model_orchestrator.py:1172:64: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1220:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1223:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1227:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1229:69: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1239:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1241:9: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1294:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1299:5: E303 too many blank lines (2) -./src/tfscreen/tfmodel/model_orchestrator.py:1313:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1323:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1333:75: W291 trailing whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1347:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1350:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1379:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1391:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1403:1: W293 blank line contains whitespace -./src/tfscreen/tfmodel/model_orchestrator.py:1409:25: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1410:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1411:31: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1412:32: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1413:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1414:22: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1415:23: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1416:20: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1417:29: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1418:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1419:33: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1420:34: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1421:27: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1422:28: E231 missing whitespace after ':' -./src/tfscreen/tfmodel/model_orchestrator.py:1423:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1229:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1232:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1236:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1238:69: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1248:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1250:9: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1303:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1308:5: E303 too many blank lines (2) +./src/tfscreen/tfmodel/model_orchestrator.py:1322:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1332:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1342:75: W291 trailing whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1356:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1359:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1388:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1400:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1412:1: W293 blank line contains whitespace +./src/tfscreen/tfmodel/model_orchestrator.py:1418:25: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1419:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1420:31: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1421:32: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1422:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1423:22: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1424:23: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1425:20: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1426:29: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1427:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1428:33: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1429:34: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1430:27: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1431:28: E231 missing whitespace after ':' +./src/tfscreen/tfmodel/model_orchestrator.py:1432:31: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:192:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:193:22: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/configure_model_cli.py:194:22: E128 continuation line under-indented for visual indent @@ -3699,17 +3701,21 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:522:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:1: E305 expected 2 blank lines after class or function definition, found 1 ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:184:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:185:11: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:190:20: E221 multiple spaces before operator -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:254:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:255:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:256:24: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:259:1: C901 '_inject_calibration_priors' is too complex (21) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:450:1: C901 '_build_csv_updates' is too complex (11) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:544:1: C901 '_apply_guesses_updates' is too complex (12) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:696:1: E303 too many blank lines (3) -./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:700:1: E302 expected 2 blank lines, found 3 +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:106:23: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:107:23: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:198:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:199:11: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:204:20: E221 multiple spaces before operator +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:268:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:269:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:270:24: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:273:1: C901 '_inject_calibration_priors' is too complex (21) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:464:1: C901 '_build_csv_updates' is too complex (11) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:522:35: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:523:35: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:662:1: C901 '_apply_guesses_updates' is too complex (12) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:814:1: E303 too many blank lines (3) +./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:818:1: E302 expected 2 blank lines, found 3 ./src/tfscreen/tfmodel/scripts/setup_grid_cli.py:160:1: C901 'setup_grid' is too complex (17) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:40:1: C901 'subset_genotypes' is too complex (23) ./src/tfscreen/tfmodel/scripts/subset_genotypes_cli.py:93:5: F841 local variable 'all_genotypes' is assigned to but never used @@ -5590,9 +5596,9 @@ ./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:415:21: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/generative/components/dk_geno/test_hierarchical_geno.py:426:9: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:7:1: F401 'tfscreen.tfmodel.generative.components.growth.linear.LinearParams' imported but unused -./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:55:10: E221 multiple spaces before operator -./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:132:28: E272 multiple spaces before keyword -./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:134:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:56:10: E221 multiple spaces before operator +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:138:28: E272 multiple spaces before keyword +./tests/tfscreen/tfmodel/generative/components/growth/test_linear.py:140:28: E272 multiple spaces before keyword ./tests/tfscreen/tfmodel/generative/components/growth/test_power.py:3:1: F401 'numpyro.handlers.substitute' imported but unused ./tests/tfscreen/tfmodel/generative/components/growth/test_power.py:6:1: F401 'tfscreen.tfmodel.generative.components.growth.power.PowerParams' imported but unused ./tests/tfscreen/tfmodel/generative/components/growth/test_power.py:23:1: E302 expected 2 blank lines, found 1 @@ -6590,18 +6596,32 @@ ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:14:1: F401 'sys' imported but unused ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:21:1: F401 'jax' imported but unused ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:26:1: F401 'tfscreen.tfmodel.scripts.prefit_calibration_cli._identify_field_mapping' imported but unused -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:728:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:729:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:730:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:731:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:732:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:733:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:734:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:735:29: E128 continuation line under-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:736:29: E128 continuation line under-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1089:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1108:14: E127 continuation line over-indented for visual indent -./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1123:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:737:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:738:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:739:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:740:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:741:29: E128 continuation line under-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1117:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1123:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1129:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1136:47: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1144:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1150:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1158:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1164:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1170:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1177:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1184:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1198:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1212:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1224:128: E501 line too long (136 > 127 characters) +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1309:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1328:14: E127 continuation line over-indented for visual indent +./tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py:1343:14: E127 continuation line over-indented for visual indent ./tests/tfscreen/tfmodel/scripts/test_sample_posterior_cli.py:5:1: F401 'unittest.mock.call' imported but unused ./tests/tfscreen/tfmodel/scripts/test_sample_prior_cli.py:3:1: F401 'pytest' imported but unused ./tests/tfscreen/tfmodel/scripts/test_subset_genotypes_cli.py:84:31: E741 ambiguous variable name 'l' @@ -7170,7 +7190,8 @@ ./tests/tfscreen/tfmodel/test_model_orchestrator.py:636:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:639:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_model_orchestrator.py:642:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_model_orchestrator.py:819:67: W292 no newline at end of file +./tests/tfscreen/tfmodel/test_model_orchestrator.py:899:9: F401 'inspect' imported but unused +./tests/tfscreen/tfmodel/test_model_orchestrator.py:969:72: W292 no newline at end of file ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:16: E221 multiple spaces before operator ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:30:31: E272 multiple spaces before keyword ./tests/tfscreen/tfmodel/test_needs_mut_wiring.py:31:16: E221 multiple spaces before operator @@ -7439,7 +7460,7 @@ ./tests/tfscreen/tfmodel/test_share_replicates.py:7:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/tfmodel/test_share_replicates.py:10:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_share_replicates.py:32:1: W293 blank line contains whitespace -./tests/tfscreen/tfmodel/test_share_replicates.py:35:77: E261 at least two spaces before inline comment +./tests/tfscreen/tfmodel/test_share_replicates.py:35:78: E261 at least two spaces before inline comment ./tests/tfscreen/tfmodel/test_share_replicates.py:40:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_share_replicates.py:42:1: W293 blank line contains whitespace ./tests/tfscreen/tfmodel/test_share_replicates.py:45:1: W293 blank line contains whitespace @@ -7745,20 +7766,20 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -177 E127 continuation line over-indented for visual indent +180 E127 continuation line over-indented for visual indent 79 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 5 E211 whitespace before '(' -1829 E221 multiple spaces before operator +1832 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator 1115 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals -228 E261 at least two spaces before inline comment +229 E261 at least two spaces before inline comment 1 E262 inline comment should start with '# ' 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment @@ -7770,14 +7791,14 @@ 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line 37 E402 module level import not at top of file -71 E501 line too long (145 > 127 characters) +84 E501 line too long (145 > 127 characters) 46 E701 multiple statements on one line (colon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' 2 E713 test for membership should be 'not in' 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 20 E741 ambiguous variable name 'l' -230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +231 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 41 F841 local variable 'e' is assigned to but never used @@ -7785,4 +7806,4 @@ 86 W292 no newline at end of file 1673 W293 blank line contains whitespace 29 W391 blank line at end of file -7739 +7760 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index c9fe6bce..52224a25 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/tfmodel/generative/components/growth/linear.py b/src/tfscreen/tfmodel/generative/components/growth/linear.py index 2289d4c7..73c10503 100644 --- a/src/tfscreen/tfmodel/generative/components/growth/linear.py +++ b/src/tfscreen/tfmodel/generative/components/growth/linear.py @@ -2,8 +2,8 @@ import jax.numpy as jnp import numpyro as pyro import numpyro.distributions as dist -from flax.struct import dataclass -from typing import Tuple +from flax.struct import dataclass, field +from typing import List, Optional, Tuple from tfscreen.tfmodel.data_class import ( GrowthData @@ -39,12 +39,83 @@ class ModelPriors: m_loc : float Mean of the Normal prior on the per-condition occupancy slope m. m_scale : float - Standard deviation of the Normal prior on m. + Standard deviation of the Normal prior on m (used for all conditions + when ``m_is_selection`` is ``None``). + m_scale_minus : float + Prior scale on m for control ('-') conditions, where theta is not + expected to meaningfully affect growth. Applied per-condition when + ``m_is_selection`` is provided. + m_scale_plus : float + Prior scale on m for selection ('+') conditions, where theta drives + differential growth. Applied per-condition when ``m_is_selection`` + is provided. + m_is_selection : tuple of bool or None + Length-``num_condition_rep`` tuple; ``True`` for selection ('+') + conditions, ``False`` for control ('-') conditions. ``None`` + disables the per-condition logic and uses ``m_scale`` for all + conditions (backward-compatible default). """ k_loc: float k_scale: float m_loc: float m_scale: float + m_scale_minus: float + m_scale_plus: float + m_is_selection: tuple = field(pytree_node=False, default=None) + + +# --------------------------------------------------------------------------- +# Condition-label parsing +# --------------------------------------------------------------------------- + +def _parse_condition_label(label: str) -> bool: + """ + Classify one condition as selection ('+') or control ('-'). + + Parameters + ---------- + label : str + Condition name as it appears in the growth DataFrame. + + Returns + ------- + bool + ``True`` if this is a selection ('+') condition, + ``False`` if it is a control ('-') condition. + + Raises + ------ + ValueError + If the label contains neither '+' nor '-', or contains both. + The error includes a detailed explanation of the naming convention. + """ + has_plus = '+' in label + has_minus = '-' in label + + if has_plus and not has_minus: + return True + if has_minus and not has_plus: + return False + + # Both or neither: ambiguous. + raise ValueError( + f"Condition name '{label}' does not unambiguously identify whether " + "it is a selection or control condition.\n\n" + "The linear growth component uses the presence of '+' or '-' in the " + "condition name to apply different priors on m (the theta-to-growth " + "slope):\n" + " '+' conditions — selection media (e.g. kanR+kan, pheS+4CP): theta " + "meaningfully suppresses or promotes growth, so m gets the full " + "Normal(0, m_scale_plus) prior.\n" + " '-' conditions — control/no-selection media (e.g. kanR-kan, " + "pheS-4CP): theta has negligible effect on growth, so m gets the tight " + "Normal(0, m_scale_minus) prior, preventing the optimizer from " + "incorrectly absorbing selection-phase theta signal into the control " + "condition.\n\n" + "Please rename your condition so that the name contains exactly one of " + "'+' or '-'.\n" + f"Got: '{label}'" + ) def define_model(name: str, @@ -70,9 +141,17 @@ def define_model(name: str, params : LinearParams A dataclass containing k_pre, m_pre, k_sel, and m_sel. """ - with pyro.plate(f"{name}_condition_parameters", data.num_condition_rep): + if priors.m_is_selection is None: + m_scale_arr = jnp.full(data.num_condition_rep, priors.m_scale) + else: + m_scale_arr = jnp.array([ + priors.m_scale_plus if sel else priors.m_scale_minus + for sel in priors.m_is_selection + ]) + + with pyro.plate(f"{name}_condition_parameters", data.num_condition_rep) as idx: growth_k = pyro.sample(f"{name}_k", dist.Normal(priors.k_loc, priors.k_scale)) - growth_m = pyro.sample(f"{name}_m", dist.Normal(priors.m_loc, priors.m_scale)) + growth_m = pyro.sample(f"{name}_m", dist.Normal(priors.m_loc, m_scale_arr[idx])) k_pre = growth_k[data.map_condition_pre] m_pre = growth_m[data.map_condition_pre] @@ -154,7 +233,9 @@ def get_hyperparameters(): parameters["k_loc"] = 0.020 parameters["k_scale"] = 0.1 parameters["m_loc"] = 0.0 - parameters["m_scale"] = 0.01 + parameters["m_scale"] = 0.01 # used when m_is_selection is None + parameters["m_scale_minus"] = 0.001 # tight prior for '-' control conditions + parameters["m_scale_plus"] = 0.01 # normal prior for '+' selection conditions return parameters @@ -241,8 +322,38 @@ def get_guesses(name, data): return guesses -def get_priors(): - return ModelPriors(**get_hyperparameters()) +def get_priors(condition_labels: Optional[List[str]] = None) -> ModelPriors: + """ + Build a :class:`ModelPriors` for the linear growth component. + + When ``condition_labels`` is supplied (a list of condition names in the + same order as the model's ``num_condition_rep`` axis), each label is + parsed for '+' or '-' to set ``m_is_selection``. This enables the + tighter ``m_scale_minus`` prior for control conditions and the normal + ``m_scale_plus`` prior for selection conditions, breaking the + optimization degeneracy that otherwise causes systematic theta + undershoot. + + When ``condition_labels`` is ``None`` (the default), ``m_is_selection`` + is left as ``None`` and ``m_scale`` is used for all conditions + (backward-compatible behaviour). + + Parameters + ---------- + condition_labels : list of str, optional + Ordered condition names, one per ``condition_rep`` index. + Each name must contain exactly one of '+' or '-'. + + Returns + ------- + ModelPriors + """ + hypers = get_hyperparameters() + if condition_labels is not None: + hypers["m_is_selection"] = tuple( + _parse_condition_label(label) for label in condition_labels + ) + return ModelPriors(**hypers) def get_extract_specs(ctx): diff --git a/src/tfscreen/tfmodel/model_orchestrator.py b/src/tfscreen/tfmodel/model_orchestrator.py index ed9c7c26..564705e5 100644 --- a/src/tfscreen/tfmodel/model_orchestrator.py +++ b/src/tfscreen/tfmodel/model_orchestrator.py @@ -1199,11 +1199,20 @@ def _initialize_classes(self): else: component_data = self._data.growth - # Record priors. Components may optionally accept a `data` - # parameter to derive empirical prior values (e.g. fixed - # subgroup scales) from the observed data. + # Record priors. Components may optionally accept special + # keyword arguments: + # condition_labels — ordered condition names used by the + # linear growth component to set per-condition m priors. + # data — component data pytree for empirical prior values. priors_sig = inspect.signature(component_module.get_priors) - if "data" in priors_sig.parameters: + if "condition_labels" in priors_sig.parameters: + cond_rep_df = self.growth_tm.map_groups["condition_rep"] + condition_labels = list( + cond_rep_df.sort_values("map_condition_rep")["condition_rep"] + ) + priors_class_kwargs[prior_group][key] = \ + component_module.get_priors(condition_labels=condition_labels) + elif "data" in priors_sig.parameters: priors_class_kwargs[prior_group][key] = \ component_module.get_priors(data=component_data) else: diff --git a/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py b/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py index c982b4a5..7c0883c9 100644 --- a/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py +++ b/src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py @@ -98,6 +98,20 @@ # downstream logit transform (in the simple-theta component) is finite. _THETA_EPS = 1e-6 +# Default floors for Hessian-derived prior scales. These represent the +# minimum plausible inter-experiment variability in growth-rate parameters +# that the calibration MAP cannot see (e.g., inoculum variation, day-to-day +# media differences). For real data, calibrate these by running the prefit +# on multiple independent replicates and examining the spread of MAP estimates. +_DEFAULT_K_SCALE_FLOOR = 0.002 # hr⁻¹ — baseline growth rate floor +_DEFAULT_M_SCALE_FLOOR = 0.001 # hr⁻¹ — theta-slope floor +# Ceilings match the linear-component default prior scales so the prefit can +# only TIGHTEN (never loosen) the production prior. A degenerate Hessian +# direction (e.g. b ↔ ln_cfu0 with fixed t_pre) can give sigma >> 10, which +# without a ceiling would silently widen the prior far beyond the default. +_DEFAULT_K_SCALE_CEILING = 0.1 # matches linear.get_hyperparameters() k_scale +_DEFAULT_M_SCALE_CEILING = 0.01 # matches linear.get_hyperparameters() m_scale_plus + # --------------------------------------------------------------------------- # Data filtering @@ -504,6 +518,110 @@ def _build_csv_updates(field_mapping, params): return prior_updates, guess_updates +def _build_hessian_scale_updates(field_mapping, hessian_results, + k_scale_floor, m_scale_floor, + k_scale_ceiling, m_scale_ceiling): + """ + Build per-condition scale updates from Hessian-derived sigmas with floors + and ceilings. + + For each array condition_growth site (``k`` and ``m``) that appears in + both ``field_mapping`` and ``hessian_results``, the per-element Hessian + sigma is clipped to ``[floor, ceiling]`` and returned in two dicts: + + * **guess_updates** — per-condition sigma arrays keyed as + ``{site_name}_scales``. Written to the guesses CSV to initialise the + variational posterior's uncertainty near the MAP calibration. + + * **prior_updates** — scalar (max across conditions) written to the priors + CSV. For ``k``, updates ``condition_growth.k_scale``; for ``m``, + updates ``condition_growth.m_scale_plus`` (selection conditions). This + is the constraint that actually prevents the production SVI from drifting + away from the calibration. + + **Why both floor and ceiling?** + + The ceiling is essential. The prefit calibration uses ``growth_noise: + "zero"`` and a fixed ``t_pre``, which creates a near-degenerate Hessian + direction (the baseline growth rate ``b`` and initial count ``ln_cfu0`` are + only weakly separated). The clamping at 1e-3 inside + ``compute_hessian_sigmas`` then gives ``sigma ≈ 1/sqrt(1e-3) ≈ 31`` for + that direction — a huge value that would *loosen* the production prior far + beyond the original default. The ceiling prevents this: if the Hessian + sigma exceeds the original production prior scale, the original scale is + kept. + + The floor prevents over-constraining from the opposite direction: if the + calibration data is very informative and the Hessian gives + ``sigma ≈ 1e-5``, we still keep at least ``floor`` of freedom to represent + irreducible inter-experiment variability. + + Parameters + ---------- + field_mapping : dict + Output of :func:`_identify_field_mapping`. + hessian_results : dict + Output of ``RunInference.compute_hessian_sigmas``; maps site names to + ``{"map": array, "sigma": array}`` dicts. + k_scale_floor : float + Minimum prior scale for baseline growth-rate (k / b) parameters. + m_scale_floor : float + Minimum prior scale for theta-slope (m) parameters. + k_scale_ceiling : float + Maximum prior scale for k. Hessian sigmas above this are capped so + the prefit never *loosens* the production k prior beyond its default. + m_scale_ceiling : float + Maximum prior scale for m_scale_plus. Analogous to ``k_scale_ceiling``. + + Returns + ------- + guess_updates : dict[str, np.ndarray] + Per-condition clipped sigma arrays for the guesses CSV. + prior_updates : dict[str, float] + Scalar (max-across-conditions) prior scale updates for the priors CSV. + """ + guess_updates = {} + prior_updates = {} + + for site_name, info in field_mapping.items(): + if not info.get("is_array", False): + continue + if site_name not in hessian_results: + continue + + sigma = np.asarray(hessian_results[site_name]["sigma"]) + loc_field = info.get("loc_field", "") + scale_field = info.get("scale_field") + component = info["component"] + + if loc_field == "k_loc": + floor = k_scale_floor + ceiling = k_scale_ceiling + prior_field = scale_field # "k_scale" + elif loc_field == "m_loc": + floor = m_scale_floor + ceiling = m_scale_ceiling + prior_field = "m_scale_plus" # tighten the + condition prior + else: + continue + + # Clip: floor prevents over-constraining; ceiling prevents loosening. + clipped_sigma = np.clip(sigma, floor, ceiling) + + # Per-condition scales to guesses CSV (initialise variational uncertainty) + if scale_field is not None: + guess_updates[f"{site_name}_scales"] = clipped_sigma + + # Scalar (max) to priors CSV — the actual model constraint. + # Taking the max rather than the min avoids over-constraining the + # condition with the largest legitimate uncertainty. + if prior_field is not None: + prior_key = _csv_row_name(component, prior_field) + prior_updates[prior_key] = float(np.max(clipped_sigma)) + + return guess_updates, prior_updates + + def _apply_priors_updates(priors_path, prior_updates): """ Overwrite ``parameter == row_name`` rows of the production priors @@ -712,7 +830,12 @@ def run_prefit_calibration(config_file, checkpoint_interval=10, max_num_epochs=100000, init_param_jitter=0.0, - epoch_checkpoint_interval=0): + epoch_checkpoint_interval=0, + k_scale_floor=_DEFAULT_K_SCALE_FLOOR, + m_scale_floor=_DEFAULT_M_SCALE_FLOOR, + k_scale_ceiling=_DEFAULT_K_SCALE_CEILING, + m_scale_ceiling=_DEFAULT_M_SCALE_CEILING, + hessian_chunk_size=64): """ Run the calibration pre-fit and update the production priors / guesses CSVs in place. @@ -779,6 +902,30 @@ def run_prefit_calibration(config_file, Frequency (in epochs) to write numbered epoch checkpoints to a ``checkpoints/`` subdirectory (default 0). Set to 0 or None to disable. + k_scale_floor : float, optional + Minimum prior scale applied to the baseline growth-rate (k/b) + parameters after Hessian estimation (default + ``_DEFAULT_K_SCALE_FLOOR = 0.002``). Represents the irreducible + day-to-day variability in growth rates that the Hessian cannot capture. + For real data, estimate this by running the prefit on multiple + independent calibration replicates and measuring the spread of MAP + estimates. + m_scale_floor : float, optional + Minimum prior scale applied to the theta-slope (m) parameters after + Hessian estimation (default ``_DEFAULT_M_SCALE_FLOOR = 0.001``). + k_scale_ceiling : float, optional + Maximum prior scale for k after Hessian estimation (default + ``_DEFAULT_K_SCALE_CEILING = 0.1``, matching the linear component + default). Prevents degenerate Hessian directions — which arise from + the ``b`` / ``ln_cfu0`` near-degeneracy in the prefit calibration — + from producing a scale larger than the original production prior and + thereby *loosening* the constraint. + m_scale_ceiling : float, optional + Maximum prior scale for m_scale_plus after Hessian estimation (default + ``_DEFAULT_M_SCALE_CEILING = 0.01``, matching the linear default). + hessian_chunk_size : int, optional + Number of Hessian rows computed per device batch (default 64). + Reduce if device runs out of memory during Hessian computation. Returns ------- @@ -834,9 +981,27 @@ def run_prefit_calibration(config_file, epoch_checkpoint_interval=epoch_checkpoint_interval ) - # 5. Map sample sites → CSV fields and apply in-place updates. + # 5. Hessian-derived scale estimation. + # Compute the diagonal of the inverse Hessian at the MAP to get per- + # parameter uncertainty estimates. These are floored and written as + # per-condition scales (guesses) and scalar prior scales (priors) so the + # production SVI cannot drift far from the calibrated growth parameters. + hessian_results = ri.compute_hessian_sigmas( + params, hessian_chunk_size=hessian_chunk_size + ) + + # 6. Map sample sites → CSV fields and apply in-place updates. field_mapping = _identify_field_mapping(orchestrator_cal) prior_updates, guess_updates = _build_csv_updates(field_mapping, params) + + hessian_guess_updates, hessian_prior_updates = _build_hessian_scale_updates( + field_mapping, hessian_results, + k_scale_floor, m_scale_floor, + k_scale_ceiling, m_scale_ceiling, + ) + guess_updates.update(hessian_guess_updates) + prior_updates.update(hessian_prior_updates) + _apply_priors_updates(priors_path, prior_updates) _apply_guesses_updates(guesses_path, guess_updates) @@ -849,7 +1014,12 @@ def main(): manual_arg_types={"config_file": str, "seed": int, "checkpoint_file": str, - "init_param_jitter": float}, + "init_param_jitter": float, + "k_scale_floor": float, + "m_scale_floor": float, + "k_scale_ceiling": float, + "m_scale_ceiling": float, + "hessian_chunk_size": int}, ) diff --git a/tests/smoke-tests/test_configure_run_smoke.py b/tests/smoke-tests/test_configure_run_smoke.py index f107bd4e..6938557c 100644 --- a/tests/smoke-tests/test_configure_run_smoke.py +++ b/tests/smoke-tests/test_configure_run_smoke.py @@ -70,8 +70,8 @@ def test_configure_run_pipeline_smoke(tmpdir): # Create mock data growth_df = pd.DataFrame({ "replicate": ["R1", "R1"], - "condition_pre": ["C1", "C1"], - "condition_sel": ["C2", "C2"], + "condition_pre": ["C1-", "C1-"], + "condition_sel": ["C2+", "C2+"], "genotype": ["A123B", "C456D"], "t": [0, 10], "t_sel": [0, 10], @@ -155,8 +155,8 @@ def test_configure_run_binding_weight_smoke(tmpdir): # Growth data: 20 rows | Binding data: 4 rows → expected auto-weight = 5.0 growth_df = pd.DataFrame({ "replicate": ["R1"] * 20, - "condition_pre": ["C1"] * 20, - "condition_sel": ["C2"] * 20, + "condition_pre": ["C1-"] * 20, + "condition_sel": ["C2+"] * 20, "genotype": ["A123B"] * 10 + ["C456D"] * 10, "t_sel": [10.0] * 20, "t_pre": [12.0] * 20, diff --git a/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py b/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py index 91b386f7..34ce0cab 100644 --- a/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py +++ b/tests/tfscreen/tfmodel/generative/components/growth/test_linear.py @@ -11,6 +11,7 @@ get_hyperparameters, get_guesses, get_priors, + _parse_condition_label, LinearParams, ) @@ -113,14 +114,19 @@ def test_get_hyperparameters(): assert "k_scale" in params assert "m_loc" in params assert "m_scale" in params + assert "m_scale_minus" in params + assert "m_scale_plus" in params assert params["k_loc"] == 0.020 + assert params["m_scale_minus"] < params["m_scale_plus"] # minus is tighter -def test_get_priors(): +def test_get_priors_no_labels_legacy_behavior(): + """With no condition_labels, m_is_selection is None (backward compat).""" priors = get_priors() assert isinstance(priors, ModelPriors) assert priors.k_loc == 0.020 assert priors.m_loc == 0.0 + assert priors.m_is_selection is None assert not hasattr(priors, "pinned") @@ -300,3 +306,195 @@ def test_model_and_guide_have_compatible_sample_sites(mock_data): f" model only: {model_samples - guide_samples}\n" f" guide only: {guide_samples - model_samples}" ) + + +# --------------------------------------------------------------------------- +# _parse_condition_label +# --------------------------------------------------------------------------- + +class TestParseConditionLabel: + + def test_plus_returns_true(self): + assert _parse_condition_label("kanR+kan") is True + + def test_minus_returns_false(self): + assert _parse_condition_label("kanR-kan") is False + + def test_selection_with_description_plus(self): + assert _parse_condition_label("pheS+4CP") is True + + def test_control_with_description_minus(self): + assert _parse_condition_label("pheS-4CP") is False + + def test_no_plus_no_minus_raises(self): + with pytest.raises(ValueError, match="does not unambiguously"): + _parse_condition_label("kanR_no_selection") + + def test_both_plus_and_minus_raises(self): + with pytest.raises(ValueError, match="does not unambiguously"): + _parse_condition_label("weird+condition-name") + + def test_error_includes_label_name(self): + label = "mystery_condition" + with pytest.raises(ValueError, match=label): + _parse_condition_label(label) + + def test_error_explains_convention(self): + with pytest.raises(ValueError, match=r"\+.*selection"): + _parse_condition_label("unlabeled") + + +# --------------------------------------------------------------------------- +# get_priors with condition_labels +# --------------------------------------------------------------------------- + +class TestGetPriorsWithLabels: + + def test_plus_conditions_marked_selection(self): + labels = ["kanR+kan", "pheS+4CP"] + priors = get_priors(condition_labels=labels) + assert priors.m_is_selection == (True, True) + + def test_minus_conditions_marked_control(self): + labels = ["kanR-kan", "pheS-4CP"] + priors = get_priors(condition_labels=labels) + assert priors.m_is_selection == (False, False) + + def test_mixed_labels_correct_classification(self): + labels = ["kanR+kan", "kanR-kan", "pheS+4CP", "pheS-4CP"] + priors = get_priors(condition_labels=labels) + assert priors.m_is_selection == (True, False, True, False) + + def test_single_selection_label(self): + priors = get_priors(condition_labels=["sel+media"]) + assert priors.m_is_selection == (True,) + + def test_invalid_label_propagates_error(self): + with pytest.raises(ValueError, match="unlabeled"): + get_priors(condition_labels=["kanR+kan", "unlabeled"]) + + def test_none_labels_gives_none_is_selection(self): + priors = get_priors(condition_labels=None) + assert priors.m_is_selection is None + + def test_empty_labels_gives_empty_tuple(self): + priors = get_priors(condition_labels=[]) + assert priors.m_is_selection == () + + def test_scales_preserved_in_priors(self): + priors = get_priors(condition_labels=["a+b", "a-b"]) + assert priors.m_scale_minus == pytest.approx(0.001) + assert priors.m_scale_plus == pytest.approx(0.01) + + +# --------------------------------------------------------------------------- +# define_model — per-condition m prior +# --------------------------------------------------------------------------- + +class TestDefineModelSelectionAware: + + def _make_data(self, num_condition_rep): + """Minimal mock with num_condition_rep conditions.""" + num_rep = 1 + num_cond_pre = 1 + num_cond_sel = num_condition_rep + num_tname = 1 + num_tconc = 1 + num_geno = 1 + num_time = 2 + shape = (num_rep, num_time, num_cond_pre, num_cond_sel, + num_tname, num_tconc, num_geno) + + return MockGrowthData( + num_condition_rep=num_condition_rep, + num_replicate=num_rep, + map_condition_pre=jnp.zeros(num_condition_rep, dtype=jnp.int32), + map_condition_sel=jnp.arange(num_condition_rep, dtype=jnp.int32), + ln_cfu=jnp.zeros(shape), + t_sel=jnp.zeros(shape), + good_mask=jnp.ones(shape, dtype=bool), + ) + + def test_selection_aware_prior_tighter_for_control(self): + """ + The Normal prior on m for a '-' condition must be tighter than for + a '+' condition. We verify this by inspecting the log-prob of a + moderate m value under each condition's prior. + """ + labels = ["kanR+kan", "kanR-kan"] + priors = get_priors(condition_labels=labels) + data = self._make_data(num_condition_rep=2) + + # Substitute specific m values and inspect the model trace + m_test_value = 0.005 # moderate m, within normal range but 5× tight range + subs = { + "test_m_k": jnp.zeros(2), + "test_m_m": jnp.full(2, m_test_value), + } + substituted = substitute(define_model, data=subs) + + with seed(rng_seed=0): + tr = trace(substituted).get_trace( + name="test_m", data=data, priors=priors + ) + + m_site = tr["test_m_m"] + # The trace fn should have two m values; we can check the log-prob + # is lower (more penalized) for the control condition. + m_values = m_site["value"] + assert m_values.shape == (2,) + # kanR+kan (index 0) is selection: wider prior → less penalty + # kanR-kan (index 1) is control: tight prior → more penalty + import numpyro.distributions as d + lp_sel = d.Normal(priors.m_loc, priors.m_scale_plus).log_prob(m_test_value) + lp_ctrl = d.Normal(priors.m_loc, priors.m_scale_minus).log_prob(m_test_value) + assert float(lp_sel) > float(lp_ctrl) + + def test_legacy_path_unchanged(self): + """With m_is_selection=None, the single m_scale prior applies.""" + priors = get_priors() # no condition_labels → m_is_selection=None + assert priors.m_is_selection is None + + data = self._make_data(num_condition_rep=2) + with seed(rng_seed=0): + tr = trace(define_model).get_trace( + name="leg", data=data, priors=priors + ) + assert "leg_m" in tr + assert tr["leg_m"]["value"].shape == (2,) + + def test_define_model_shapes_with_labels(self): + """Output shapes should be the same as the legacy path.""" + labels = ["sel+a", "ctrl-b", "sel+c"] + priors = get_priors(condition_labels=labels) + data = self._make_data(num_condition_rep=3) + + with seed(rng_seed=0): + params = define_model(name="sa", data=data, priors=priors) + + assert params.k_pre.shape == data.map_condition_pre.shape + assert params.m_pre.shape == data.map_condition_pre.shape + assert params.k_sel.shape == data.map_condition_sel.shape + assert params.m_sel.shape == data.map_condition_sel.shape + + def test_model_guide_compatible_with_labels(self): + """Model and guide sample sites must still match with labels.""" + labels = ["kanR+kan", "kanR-kan"] + priors = get_priors(condition_labels=labels) + data = self._make_data(num_condition_rep=2) + + with seed(rng_seed=0): + model_trace = trace(define_model).get_trace( + name="compat2", data=data, priors=priors + ) + with seed(rng_seed=0): + guide_trace = trace(guide).get_trace( + name="compat2", data=data, priors=priors + ) + + model_samples = { + n for n, s in model_trace.items() + if s["type"] == "sample" and not s.get("is_observed", False) + } + guide_samples = {n for n, s in guide_trace.items() if s["type"] == "sample"} + assert model_samples == guide_samples diff --git a/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py b/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py index f6c45bca..05c5c3fc 100644 --- a/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_prefit_calibration_cli.py @@ -28,6 +28,7 @@ _apply_priors_updates, _build_calibration_model, _build_csv_updates, + _build_hessian_scale_updates, _compute_theta_values, _csv_row_name, _identify_field_mapping, @@ -37,6 +38,10 @@ main, run_prefit_calibration, _CALIBRATION_OVERRIDES, + _DEFAULT_K_SCALE_CEILING, + _DEFAULT_K_SCALE_FLOOR, + _DEFAULT_M_SCALE_CEILING, + _DEFAULT_M_SCALE_FLOOR, _PINNED_COMPONENTS, ) @@ -1064,12 +1069,227 @@ def test_default_init_param_jitter_is_zero(self, tmp_path, mocker): run_prefit_calibration(config_file=cfg, seed=1) assert mock_run_map.call_args.kwargs["init_param_jitter"] == 0.0 - def test_hessian_not_called(self, tmp_path, mocker): - """compute_hessian_sigmas must not be called after the cleanup.""" + def test_hessian_called_after_map(self, tmp_path, mocker): + """compute_hessian_sigmas must be called exactly once after MAP.""" cfg, _, _ = self._write_yaml_and_csvs(tmp_path) mock_ri, _ = self._patch_pipeline(mocker) run_prefit_calibration(config_file=cfg, seed=1) - mock_ri.compute_hessian_sigmas.assert_not_called() + mock_ri.compute_hessian_sigmas.assert_called_once() + + def test_hessian_chunk_size_forwarded(self, tmp_path, mocker): + """The hessian_chunk_size kwarg must be passed to compute_hessian_sigmas.""" + cfg, _, _ = self._write_yaml_and_csvs(tmp_path) + mock_ri, _ = self._patch_pipeline(mocker) + run_prefit_calibration(config_file=cfg, seed=1, hessian_chunk_size=32) + call_kwargs = mock_ri.compute_hessian_sigmas.call_args.kwargs + assert call_kwargs.get("hessian_chunk_size") == 32 + + +# --------------------------------------------------------------------------- +# _build_hessian_scale_updates +# --------------------------------------------------------------------------- + +class TestBuildHessianScaleUpdates: + """Tests for the Hessian-with-floor scale update builder.""" + + def _make_field_mapping(self, sites): + """Build a minimal field_mapping for the given list of (site, loc_field) pairs.""" + out = {} + for site_name, loc_field in sites: + suffix = loc_field.replace("_loc", "") + out[site_name] = { + "component": "condition_growth", + "dist_class": "Normal", + "loc_field": loc_field, + "scale_field": f"{suffix}_scale", + "is_array": True, + } + return out + + def _make_hessian(self, site_name, sigmas): + return {site_name: {"map": np.zeros_like(sigmas), "sigma": np.array(sigmas)}} + + # --- floor application --- + + def test_k_sigma_above_floor_is_kept(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.01, 0.02]) + g, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + np.testing.assert_allclose(g["condition_growth_k_scales"], [0.01, 0.02]) + + def test_k_sigma_below_floor_is_floored(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.0001, 0.0005]) + g, _ = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert np.all(g["condition_growth_k_scales"] == pytest.approx(0.002)) + + def test_m_sigma_uses_m_floor_not_k_floor(self): + fm = self._make_field_mapping([("condition_growth_m", "m_loc")]) + hr = self._make_hessian("condition_growth_m", [0.0003, 0.0008]) + g, _ = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + np.testing.assert_allclose(g["condition_growth_m_scales"], [0.001, 0.001]) + + def test_floor_applied_elementwise(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.001, 0.005]) + g, _ = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.003, m_scale_floor=0.001, + k_scale_ceiling=0.1, m_scale_ceiling=0.01) + np.testing.assert_allclose(g["condition_growth_k_scales"], [0.003, 0.005]) + + # --- guess output --- + + def test_k_scales_in_guess_updates(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.005, 0.007]) + g, _ = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert "condition_growth_k_scales" in g + + def test_m_scales_in_guess_updates(self): + fm = self._make_field_mapping([("condition_growth_m", "m_loc")]) + hr = self._make_hessian("condition_growth_m", [0.003, 0.004]) + g, _ = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert "condition_growth_m_scales" in g + + # --- prior output --- + + def test_k_prior_update_uses_max_across_conditions(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.003, 0.007]) + _, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert p["growth.condition_growth.k_scale"] == pytest.approx(0.007) + + def test_k_prior_update_respects_floor(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.0001, 0.0002]) + _, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert p["growth.condition_growth.k_scale"] == pytest.approx(0.002) + + def test_m_prior_goes_to_m_scale_plus_not_m_scale(self): + fm = self._make_field_mapping([("condition_growth_m", "m_loc")]) + hr = self._make_hessian("condition_growth_m", [0.004, 0.006]) + _, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert "growth.condition_growth.m_scale_plus" in p + assert "growth.condition_growth.m_scale" not in p + + def test_m_scale_plus_is_max_floored(self): + fm = self._make_field_mapping([("condition_growth_m", "m_loc")]) + hr = self._make_hessian("condition_growth_m", [0.009, 0.002]) + _, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert p["growth.condition_growth.m_scale_plus"] == pytest.approx(0.009) + + # --- edge cases --- + + def test_site_missing_from_hessian_skipped(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + g, p = _build_hessian_scale_updates(fm, {}, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert g == {} and p == {} + + def test_non_array_site_skipped(self): + fm = { + "condition_growth_k_hyper": { + "component": "condition_growth", + "dist_class": "Normal", + "loc_field": "k_hyper_loc", + "scale_field": "k_hyper_scale", + "is_array": False, + } + } + hr = {"condition_growth_k_hyper": {"map": np.array(0.02), "sigma": np.array(0.0001)}} + g, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert g == {} and p == {} + + def test_unknown_loc_field_skipped(self): + fm = { + "condition_growth_tau": { + "component": "condition_growth", + "dist_class": "Normal", + "loc_field": "tau_loc", + "scale_field": "tau_scale", + "is_array": True, + } + } + hr = {"condition_growth_tau": {"map": np.zeros(2), "sigma": np.ones(2) * 0.1}} + g, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert g == {} and p == {} + + def test_joint_k_and_m_both_updated(self): + fm = self._make_field_mapping([ + ("condition_growth_k", "k_loc"), + ("condition_growth_m", "m_loc"), + ]) + hr = { + "condition_growth_k": {"map": np.zeros(2), "sigma": np.array([0.005, 0.008])}, + "condition_growth_m": {"map": np.zeros(2), "sigma": np.array([0.004, 0.002])}, + } + g, p = _build_hessian_scale_updates(fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, k_scale_ceiling=0.1, m_scale_ceiling=0.01) + assert "condition_growth_k_scales" in g + assert "condition_growth_m_scales" in g + assert "growth.condition_growth.k_scale" in p + assert "growth.condition_growth.m_scale_plus" in p + + # --- ceiling application --- + + def test_k_sigma_above_ceiling_is_capped(self): + """A degenerate Hessian giving sigma >> ceiling must not loosen the prior.""" + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [23.5, 31.6]) + g, p = _build_hessian_scale_updates( + fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, + k_scale_ceiling=0.1, m_scale_ceiling=0.01, + ) + assert np.all(g["condition_growth_k_scales"] == pytest.approx(0.1)) + assert p["growth.condition_growth.k_scale"] == pytest.approx(0.1) + + def test_m_sigma_above_ceiling_is_capped(self): + fm = self._make_field_mapping([("condition_growth_m", "m_loc")]) + hr = self._make_hessian("condition_growth_m", [15.0, 22.0]) + g, p = _build_hessian_scale_updates( + fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, + k_scale_ceiling=0.1, m_scale_ceiling=0.01, + ) + assert np.all(g["condition_growth_m_scales"] == pytest.approx(0.01)) + assert p["growth.condition_growth.m_scale_plus"] == pytest.approx(0.01) + + def test_sigma_between_floor_and_ceiling_is_unchanged(self): + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.004, 0.006]) + g, p = _build_hessian_scale_updates( + fm, hr, k_scale_floor=0.002, m_scale_floor=0.001, + k_scale_ceiling=0.1, m_scale_ceiling=0.01, + ) + np.testing.assert_allclose(g["condition_growth_k_scales"], [0.004, 0.006]) + assert p["growth.condition_growth.k_scale"] == pytest.approx(0.006) + + def test_ceiling_below_floor_raises_or_clips_to_ceiling(self): + """When ceiling < floor (degenerate config), clip still works (clip clamps to ceiling).""" + fm = self._make_field_mapping([("condition_growth_k", "k_loc")]) + hr = self._make_hessian("condition_growth_k", [0.05]) + g, _ = _build_hessian_scale_updates( + fm, hr, k_scale_floor=0.05, m_scale_floor=0.001, + k_scale_ceiling=0.05, m_scale_ceiling=0.01, + ) + # floor == ceiling == 0.05 → result must be 0.05 + assert g["condition_growth_k_scales"][0] == pytest.approx(0.05) + + # --- default constant sanity --- + + def test_default_floors_are_sensible_constants(self): + """Floor constants must be positive and k_floor >= m_floor.""" + assert _DEFAULT_K_SCALE_FLOOR > 0 + assert _DEFAULT_M_SCALE_FLOOR > 0 + assert _DEFAULT_K_SCALE_FLOOR >= _DEFAULT_M_SCALE_FLOOR + + def test_default_ceilings_above_floors(self): + """Ceilings must be strictly above floors so the valid range is non-empty.""" + assert _DEFAULT_K_SCALE_CEILING > _DEFAULT_K_SCALE_FLOOR + assert _DEFAULT_M_SCALE_CEILING > _DEFAULT_M_SCALE_FLOOR + + def test_default_ceilings_match_linear_defaults(self): + """Ceilings should match the linear component's default prior scales.""" + from tfscreen.tfmodel.generative.components.growth.linear import get_hyperparameters + h = get_hyperparameters() + assert _DEFAULT_K_SCALE_CEILING == pytest.approx(h["k_scale"]) + assert _DEFAULT_M_SCALE_CEILING == pytest.approx(h["m_scale_plus"]) # --------------------------------------------------------------------------- diff --git a/tests/tfscreen/tfmodel/test_extraction_robustness.py b/tests/tfscreen/tfmodel/test_extraction_robustness.py index 6d5eebb9..c3592876 100644 --- a/tests/tfscreen/tfmodel/test_extraction_robustness.py +++ b/tests/tfscreen/tfmodel/test_extraction_robustness.py @@ -32,8 +32,8 @@ def mock_model(): "titrant_conc": [1.0], "replicate": ["1"], "condition_rep": ["cond1"], - "condition_pre": ["pre1"], - "condition_sel": ["sel1"], + "condition_pre": ["pre-1"], + "condition_sel": ["sel+1"], "map_theta": [0], "map_theta_group": [0], "map_condition_rep": [0], @@ -50,7 +50,7 @@ def mock_model(): 'condition_rep': pd.DataFrame({"replicate": ["1"], "condition_rep": ["cond1"], "map_condition_rep": [0]}) } mock_tm.tensor_dim_names = ["replicate", "time", "condition_pre", "condition_sel", "titrant_name", "titrant_conc", "genotype"] - mock_tm.tensor_dim_labels = [["1"], ["1"], ["pre1"], ["sel1"], ["iptg"], [1.0], ["wt"]] + mock_tm.tensor_dim_labels = [["1"], ["1"], ["pre-1"], ["sel+1"], ["iptg"], [1.0], ["wt"]] model.growth_tm = mock_tm model.training_tm = mock_tm model.mut_labels = [] @@ -342,8 +342,8 @@ def test_extract_growth_predictions_hdf5_multiple_groups(mock_model, tmp_path): mock_model.growth_df = pd.DataFrame({ "replicate": ["1", "2"], "genotype": ["wt", "wt"], - "condition_pre": ["pre1", "pre2"], - "condition_sel": ["sel1", "sel2"], + "condition_pre": ["pre-1", "pre-2"], + "condition_sel": ["sel+1", "sel+2"], "titrant_name": ["iptg", "iptg"], "titrant_conc": [1.0, 2.0], "t_pre": [0.0, 0.0], diff --git a/tests/tfscreen/tfmodel/test_h5_indexing.py b/tests/tfscreen/tfmodel/test_h5_indexing.py index 0dc58a25..af4c540e 100644 --- a/tests/tfscreen/tfmodel/test_h5_indexing.py +++ b/tests/tfscreen/tfmodel/test_h5_indexing.py @@ -14,8 +14,8 @@ def test_predict_h5_indexing(tmp_path): "genotype": ["wt"], "titrant_name": ["tit1"], "titrant_conc": [0.0], - "condition_pre": ["pre1"], - "condition_sel": ["sel1"], + "condition_pre": ["pre-1"], + "condition_sel": ["sel+1"], "t_pre": [10.0], "t_sel": [0.0], "ln_cfu": [0.0], diff --git a/tests/tfscreen/tfmodel/test_model_orchestrator.py b/tests/tfscreen/tfmodel/test_model_orchestrator.py index 610cd545..fe2a541f 100644 --- a/tests/tfscreen/tfmodel/test_model_orchestrator.py +++ b/tests/tfscreen/tfmodel/test_model_orchestrator.py @@ -816,4 +816,154 @@ def capture_populate(cls, sources): assert "scale_vector" in captured, "BindingData populate_dataclass was never called" expected = base_scale * explicit_weight - np.testing.assert_allclose(captured["scale_vector"], expected) \ No newline at end of file + np.testing.assert_allclose(captured["scale_vector"], expected) + + +# --------------------------------------------------------------------------- +# Tests for condition-label extraction logic in _setup_model +# --------------------------------------------------------------------------- + +class TestConditionLabelExtraction: + """ + Verify that _setup_model correctly extracts ordered condition labels from + growth_tm and routes them to get_priors(). + + These tests exercise the extraction logic directly without needing to + build a full ModelOrchestrator, by calling the relevant code path via + a stripped-down mock orchestrator. + """ + + def _make_cond_rep_df(self, names, indices): + """Return a condition_rep map_groups DataFrame.""" + return pd.DataFrame({ + "condition_rep": names, + "map_condition_rep": indices, + }) + + def test_labels_extracted_in_index_order(self): + """ + Labels must be sorted by map_condition_rep so parameter positions + match the ordering established by the TensorManager. + """ + # Rows are intentionally NOT in index order + df = self._make_cond_rep_df( + names=["pheS+4CP", "pheS-4CP", "kanR+kan", "kanR-kan"], + indices=[2, 3, 0, 1], + ) + result = list(df.sort_values("map_condition_rep")["condition_rep"]) + assert result == ["kanR+kan", "kanR-kan", "pheS+4CP", "pheS-4CP"] + + def test_labels_correct_when_already_ordered(self): + df = self._make_cond_rep_df( + names=["kanR+kan", "kanR-kan", "pheS+4CP", "pheS-4CP"], + indices=[0, 1, 2, 3], + ) + result = list(df.sort_values("map_condition_rep")["condition_rep"]) + assert result == ["kanR+kan", "kanR-kan", "pheS+4CP", "pheS-4CP"] + + def test_single_condition(self): + df = self._make_cond_rep_df(["sel+media"], [0]) + result = list(df.sort_values("map_condition_rep")["condition_rep"]) + assert result == ["sel+media"] + + def test_shares_replicates_false_includes_replicate_but_still_sorts(self): + """When shares_replicates=False the df has (replicate, condition_rep) rows.""" + df = pd.DataFrame({ + "replicate": [1, 2, 1, 2], + "condition_rep": ["pheS+4CP", "pheS+4CP", "pheS-4CP", "pheS-4CP"], + "map_condition_rep": [1, 3, 0, 2], + }) + result = list(df.sort_values("map_condition_rep")["condition_rep"]) + # Sorted by index: 0=pheS-4CP, 1=pheS+4CP, 2=pheS-4CP, 3=pheS+4CP + assert result[0] == "pheS-4CP" + assert result[1] == "pheS+4CP" + + def test_linear_get_priors_signature_has_condition_labels(self): + """ + The orchestrator detects condition_labels support via inspect.signature. + Verify that the linear component's get_priors actually has this param. + """ + import inspect + from tfscreen.tfmodel.generative.components.growth.linear import get_priors + sig = inspect.signature(get_priors) + assert "condition_labels" in sig.parameters, ( + "linear.get_priors must have a 'condition_labels' parameter so " + "ModelOrchestrator can pass condition names for per-condition m priors" + ) + + def test_setup_model_extracts_labels_and_calls_get_priors(self, mocker): + """ + _setup_model must call get_priors(condition_labels=...) for a + condition_growth component that accepts that parameter. + """ + import inspect + import tfscreen.tfmodel.model_orchestrator as mo + from tfscreen.tfmodel.generative.components.growth import linear as real_linear + + captured = {} + + def capturing_get_priors(condition_labels=None): + captured["condition_labels"] = condition_labels + return real_linear.get_priors(condition_labels=condition_labels) + + cond_rep_df = pd.DataFrame({ + "condition_rep": ["kanR+kan", "kanR-kan"], + "map_condition_rep": [0, 1], + }) + mock_growth_tm = MagicMock() + mock_growth_tm.map_groups = {"condition_rep": cond_rep_df} + + # Stub ALL registry entries except we intercept condition_growth.get_priors + all_stubs = {} + for comp_key, comp_val in mo.model_registry.items(): + if not isinstance(comp_val, dict): + continue + all_stubs[comp_key] = {} + for variant_name, variant_mod in comp_val.items(): + stub = MagicMock() + stub.get_priors.return_value = MagicMock() + stub.get_guesses.return_value = {} + stub.define_model = MagicMock() + stub.guide = MagicMock() + stub.calculate_growth = MagicMock() + stub.rescale = MagicMock() + stub.observe = MagicMock() + all_stubs[comp_key][variant_name] = stub + + # Override condition_growth / linear with our spy + all_stubs.setdefault("condition_growth", {})["linear"] = MagicMock( + get_priors=capturing_get_priors, + get_guesses=MagicMock(return_value={}), + define_model=MagicMock(), + guide=MagicMock(), + calculate_growth=MagicMock(), + ) + + orchestrator = MagicMock(spec=ModelOrchestrator) + orchestrator.growth_tm = mock_growth_tm + orchestrator._data = MagicMock() + orchestrator._binding_only = False + orchestrator._batch_size = None + orchestrator._condition_growth = "linear" + orchestrator._growth_transition = "instant" + orchestrator._ln_cfu0 = "hierarchical" + orchestrator._dk_geno = "fixed" + orchestrator._activity = "fixed" + orchestrator._theta = "hill_geno" + orchestrator._transformation = "single" + orchestrator._theta_growth_noise = "zero" + orchestrator._theta_binding_noise = "zero" + orchestrator._growth_noise = "zero" + orchestrator._sample_offset = "zero" + orchestrator._theta_rescale = "passthrough" + + with patch.object(mo, "model_registry", all_stubs): + try: + mo.ModelOrchestrator._initialize_classes(orchestrator) + except Exception: + pass # only condition_labels matters + + assert "condition_labels" in captured, ( + "_setup_model did not pass condition_labels to get_priors" + ) + assert captured["condition_labels"] == ["kanR+kan", "kanR-kan"] \ No newline at end of file diff --git a/tests/tfscreen/tfmodel/test_needs_mut_wiring.py b/tests/tfscreen/tfmodel/test_needs_mut_wiring.py index 3bf4654b..3a23842c 100644 --- a/tests/tfscreen/tfmodel/test_needs_mut_wiring.py +++ b/tests/tfscreen/tfmodel/test_needs_mut_wiring.py @@ -52,9 +52,9 @@ def _make_growth_csv(tmp_path, genotypes=_GENOTYPES): "num_muts": num_muts, "counts": 1000, "replicate": rep, - "condition_pre": "pre", + "condition_pre": "pre-cond", "t_pre": 24.0, - "condition_sel": "sel", + "condition_sel": "sel+cond", "t_sel": 48.0, "titrant_name": "iptg", "titrant_conc": conc, diff --git a/tests/tfscreen/tfmodel/test_prediction.py b/tests/tfscreen/tfmodel/test_prediction.py index d069a870..05ce9041 100644 --- a/tests/tfscreen/tfmodel/test_prediction.py +++ b/tests/tfscreen/tfmodel/test_prediction.py @@ -15,8 +15,8 @@ def dummy_orchestrator(): "genotype": ["wt", "wt", "M42V", "M42V"], "titrant_name": ["tit1", "tit1", "tit1", "tit1"], "titrant_conc": [0.0, 1.0, 0.0, 1.0], - "condition_pre": ["pre1", "pre1", "pre1", "pre1"], - "condition_sel": ["sel1", "sel1", "sel1", "sel1"], + "condition_pre": ["pre-1", "pre-1", "pre-1", "pre-1"], + "condition_sel": ["sel+1", "sel+1", "sel+1", "sel+1"], "t_pre": [10.0, 10.0, 10.0, 10.0], "t_sel": [0.0, 20.0, 0.0, 20.0], "ln_cfu": [0.0, 5.0, 0.0, 3.0], @@ -151,8 +151,8 @@ def spiked_orchestrator(): "genotype": ["wt", "wt", "M42V", "M42V", "A1G", "A1G"], "titrant_name": ["tit1"] * 6, "titrant_conc": [0.0, 1.0] * 3, - "condition_pre": ["pre1"] * 6, - "condition_sel": ["sel1"] * 6, + "condition_pre": ["pre-1"] * 6, + "condition_sel": ["sel+1"] * 6, "t_pre": [10.0] * 6, "t_sel": [0.0, 20.0] * 3, "ln_cfu": [0.0, 5.0, 0.0, 3.0, 0.0, 4.0], @@ -188,8 +188,8 @@ def test_copy_orchestrator_spiked_partial_overlap(): "genotype": ["wt", "wt", "M42V", "M42V", "A1G", "A1G", "S5T", "S5T"], "titrant_name": ["tit1"] * 8, "titrant_conc": [0.0, 1.0] * 4, - "condition_pre": ["pre1"] * 8, - "condition_sel": ["sel1"] * 8, + "condition_pre": ["pre-1"] * 8, + "condition_sel": ["sel+1"] * 8, "t_pre": [10.0] * 8, "t_sel": [0.0, 20.0] * 4, "ln_cfu": [0.0, 5.0, 0.0, 3.0, 0.0, 4.0, 0.0, 4.5], diff --git a/tests/tfscreen/tfmodel/test_presplit.py b/tests/tfscreen/tfmodel/test_presplit.py index ac5f1093..edc229a2 100644 --- a/tests/tfscreen/tfmodel/test_presplit.py +++ b/tests/tfscreen/tfmodel/test_presplit.py @@ -35,7 +35,7 @@ def minimal_growth_df(): condition_pre values — enough to exercise the presplit alignment logic.""" rows = [] for rep in [1, 2]: - for cp in ["kanR", "pheS"]: + for cp in ["kanR-cond", "pheS-cond"]: for geno in ["wt", "A1V", "A2V"]: for t_sel in [60.0, 90.0]: rows.append({ @@ -64,7 +64,7 @@ def minimal_presplit_df(): """Pre-split data for all (replicate, condition_pre, genotype) combos.""" rows = [] for rep in [1, 2]: - for cp in ["kanR", "pheS"]: + for cp in ["kanR-cond", "pheS-cond"]: for geno in ["wt", "A1V", "A2V"]: rows.append({ "replicate": rep, @@ -92,7 +92,7 @@ def test_read_presplit_df_passes_through_valid(minimal_presplit_df, def test_read_presplit_df_drops_unknown_genotypes(minimal_presplit_df, minimal_growth_df, capsys): df_extra = minimal_presplit_df.copy() - extra_row = pd.DataFrame([{"replicate": 1, "condition_pre": "kanR", + extra_row = pd.DataFrame([{"replicate": 1, "condition_pre": "kanR-cond", "genotype": "A99V", "ln_cfu": 5.0, "ln_cfu_std": 0.1}]) df_extra = pd.concat([df_extra, extra_row], ignore_index=True) @@ -109,7 +109,7 @@ def test_read_presplit_df_drops_unknown_genotypes(minimal_presplit_df, def test_read_presplit_df_missing_column_raises(minimal_growth_df): bad_df = pd.DataFrame({ - "replicate": [1], "condition_pre": ["kanR"], + "replicate": [1], "condition_pre": ["kanR-cond"], "genotype": ["wt"], "ln_cfu": [9.0] # missing ln_cfu_std }) @@ -154,7 +154,7 @@ def test_build_presplit_tm_genotype_alignment(minimal_presplit_df, def test_build_presplit_tm_partial_coverage(minimal_growth_df, growth_tm): """Genotypes absent from presplit_df get NaN (masked) in the tensor.""" partial_df = pd.DataFrame([ - {"replicate": 1, "condition_pre": "kanR", "genotype": "wt", + {"replicate": 1, "condition_pre": "kanR-cond", "genotype": "wt", "ln_cfu": 9.5, "ln_cfu_std": 0.4}, ]) gdf = _read_growth_df(minimal_growth_df.copy()) diff --git a/tests/tfscreen/tfmodel/test_prior_predictive.py b/tests/tfscreen/tfmodel/test_prior_predictive.py index 2d23e24e..5ba65a11 100644 --- a/tests/tfscreen/tfmodel/test_prior_predictive.py +++ b/tests/tfscreen/tfmodel/test_prior_predictive.py @@ -12,8 +12,8 @@ def dummy_orchestrator(): "genotype": ["wt", "wt", "M42V", "M42V"], "titrant_name": ["tit1", "tit1", "tit1", "tit1"], "titrant_conc": [0.0, 1.0, 0.0, 1.0], - "condition_pre": ["pre1", "pre1", "pre1", "pre1"], - "condition_sel": ["sel1", "sel1", "sel1", "sel1"], + "condition_pre": ["pre-1", "pre-1", "pre-1", "pre-1"], + "condition_sel": ["sel+1", "sel+1", "sel+1", "sel+1"], "t_pre": [10.0, 10.0, 10.0, 10.0], "t_sel": [0.0, 20.0, 0.0, 20.0], "ln_cfu": [0.0, 5.0, 0.0, 3.0], diff --git a/tests/tfscreen/tfmodel/test_share_replicates.py b/tests/tfscreen/tfmodel/test_share_replicates.py index c10fac5a..20b59c6b 100644 --- a/tests/tfscreen/tfmodel/test_share_replicates.py +++ b/tests/tfscreen/tfmodel/test_share_replicates.py @@ -10,8 +10,8 @@ def get_mock_df(): # Let's say we have genotypes A, B for rep in [1, 2]: - for cond_pre in ['pre1', 'pre2']: - for cond_sel in ['sel1']: + for cond_pre in ['pre-1', 'pre-2']: + for cond_sel in ['sel+1']: for tname in ['inducer']: for tconc in [0.0, 1.0]: for geno in ['wt', 'A1T']: @@ -32,7 +32,7 @@ def get_mock_df(): df = pd.DataFrame(data) growth_df = df.copy() - binding_df = df[df['cond_pre'] == 'pre1'].copy() if False else df.copy() # Just pass identical df for mock + binding_df = df[df['cond_pre'] == 'pre-1'].copy() if False else df.copy() # Just pass identical df for mock return growth_df, binding_df @@ -60,7 +60,7 @@ def test_growth_shares_replicates(): df_mapped = model_shared.growth_tm.df # Find rows for rep 1 and rep 2 for the same condition - rep1_map = df_mapped[(df_mapped["replicate"] == 1) & (df_mapped["condition_pre"] == 'pre1')]["map_condition_pre"].iloc[0] - rep2_map = df_mapped[(df_mapped["replicate"] == 2) & (df_mapped["condition_pre"] == 'pre1')]["map_condition_pre"].iloc[0] + rep1_map = df_mapped[(df_mapped["replicate"] == 1) & (df_mapped["condition_pre"] == 'pre-1')]["map_condition_pre"].iloc[0] + rep2_map = df_mapped[(df_mapped["replicate"] == 2) & (df_mapped["condition_pre"] == 'pre-1')]["map_condition_pre"].iloc[0] assert rep1_map == rep2_map, "Replicates did not receive the same parameter mapping index!" diff --git a/tests/tfscreen/tfmodel/test_spiked_genotypes.py b/tests/tfscreen/tfmodel/test_spiked_genotypes.py index e5743ec0..d36591f9 100644 --- a/tests/tfscreen/tfmodel/test_spiked_genotypes.py +++ b/tests/tfscreen/tfmodel/test_spiked_genotypes.py @@ -18,8 +18,8 @@ def dummy_data(): "genotype": g, "ln_cfu": 1.0, "ln_cfu_std": 0.1, - "condition_pre": "pre", - "condition_sel": "sel", + "condition_pre": "pre-cond", + "condition_sel": "sel+cond", "t_pre": 1.0, "t_sel": 10.0, "titrant_name": "iptg", diff --git a/tests/tfscreen/tfmodel/test_struct_wiring.py b/tests/tfscreen/tfmodel/test_struct_wiring.py index a0a0b313..a342bcbd 100644 --- a/tests/tfscreen/tfmodel/test_struct_wiring.py +++ b/tests/tfscreen/tfmodel/test_struct_wiring.py @@ -80,9 +80,9 @@ def _make_growth_csv(tmp_path, genotypes=_GENOTYPES): "num_muts": num_muts, "counts": 1000, "replicate": rep, - "condition_pre": "pre", + "condition_pre": "pre-cond", "t_pre": 24.0, - "condition_sel": "sel", + "condition_sel": "sel+cond", "t_sel": 48.0, "titrant_name": "iptg", "titrant_conc": conc, From 87571c86ec5386821969f2a4ba697e60f3dd26d9 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 22 Jun 2026 15:03:47 -0700 Subject: [PATCH 21/24] added simulation/analysis example and docs --- .../source/_static/saa_growth_calibration.png | Bin 0 -> 38505 bytes docs/source/_static/saa_growth_corr.png | Bin 0 -> 97366 bytes docs/source/_static/saa_params_log_hill_K.png | Bin 0 -> 66626 bytes docs/source/_static/saa_theta_corr.png | Bin 0 -> 230344 bytes docs/source/_static/saa_wt_theta_fits.png | Bin 0 -> 29751 bytes docs/source/analysis.rst | 56 ++- docs/source/index.rst | 8 + docs/source/quickstart.rst | 156 ++++++++ docs/source/summarize-fit.rst | 334 ++++++++++++++++++ examples/simulate-and-analyze/README.md | 33 ++ examples/simulate-and-analyze/hill_params.csv | 5 + examples/simulate-and-analyze/run.sh | 132 +++++++ .../simulate-and-analyze/simulate_config.yaml | 246 +++++++++++++ reports/junit/junit.xml | 2 +- .../tfmodel/scripts/extract_params_cli.py | 9 +- 15 files changed, 971 insertions(+), 10 deletions(-) create mode 100644 docs/source/_static/saa_growth_calibration.png create mode 100644 docs/source/_static/saa_growth_corr.png create mode 100644 docs/source/_static/saa_params_log_hill_K.png create mode 100644 docs/source/_static/saa_theta_corr.png create mode 100644 docs/source/_static/saa_wt_theta_fits.png create mode 100644 docs/source/quickstart.rst create mode 100644 docs/source/summarize-fit.rst create mode 100644 examples/simulate-and-analyze/README.md create mode 100644 examples/simulate-and-analyze/hill_params.csv create mode 100644 examples/simulate-and-analyze/run.sh create mode 100644 examples/simulate-and-analyze/simulate_config.yaml diff --git a/docs/source/_static/saa_growth_calibration.png b/docs/source/_static/saa_growth_calibration.png new file mode 100644 index 0000000000000000000000000000000000000000..13b2c1c7de37e9eeb3ca334ae96e548820359c42 GIT binary patch literal 38505 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/saa_growth_corr.png b/docs/source/_static/saa_growth_corr.png new file mode 100644 index 0000000000000000000000000000000000000000..40fe93736eda379436881705d46fe0172db4e73d GIT binary patch literal 97366 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 Od>vEMRLxMf^!Q&_efUHG literal 0 HcmV?d00001 diff --git a/docs/source/_static/saa_params_log_hill_K.png b/docs/source/_static/saa_params_log_hill_K.png new file mode 100644 index 0000000000000000000000000000000000000000..6ca560c49138249bdf68ffc89835265083d776cd GIT binary patch literal 66626 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

X_9@8-9!s0fGgLYXPGSMUp%F zeZX3fxi3{Se!^*9fZCi=s-`GkCN13JE!g&~iUZ}~zt=5ovqc)LOD^?wC%c%P)4gUR zslOMbK1%H^oF+-aQYb4r(m!A~nGC@Q3e=}b?j$H1FUNxsvIE;P-4$!*3YK8AqW2ZK zJ^Iv(Zhh+cBkF_~BMt5T0IwfU4cG$ENkFX>@X_gLtpXgM7i)gCA%htRj3?2}n~$xX zyMFYST}OuBRk!3#mb1o=cx>ukHm2ZRTHJY6^$MUAE15EF+NsVCyg=uO&bGq)6THcb=qB1XpFEgFG{15o}$7A5}I z;bCN71kzc$D%Mp3Gb{kF67Mh9EG+M_5oP=L1xPO5v6dhr?^qztI%`(G{DSsh>AKbzSp^;xsw3TlM%u&iI(KWR_nUbt2=w|j z68?>gw5l<^{HcG%?eg&lhtBBPNMb+&l|ClQpTei`ZQ}Afa@2NrhYow0{o};~R zMcRNyzEYNjrt+=6nLmHz&*rhmH&3u!svu`aYvvOVW0P`d;ROopYsFN1k&U9A1F2e7({TWP{fDf zmwnu#l5k)!1HXXXj^SEYcqZQeJCajRM_;}``pZtiK<>+*AsmYy z+T7d~}1tPy+RVR#Bh@YqcyDN(3}yW!^r!x!A4G$4|0)%YYR zVxOs(goTaKrItDhzX|9hh2N!rEjp+S3gxX*u7e7pa-}~TjXnfwu(3mh)MY_X&~uSb z)nT(75+hX6WA0+7Ny7JmH4>OV!^L~Z@=2TB41Z~CAOYrb#@976eeujly8&h=x`uC; z5JifiCc7wYwm;*x^DK(Z7W1WIKs6Ga9D7dz5n45^GL5i==y$!B$pzKRRALKt6_oFN zxcALyR3Q{Qi?tqZ@>%Jif&ldJJ&AHH$MGA+r;|5H?xRfah_6~OQ;A-#yIEiIw@Myq z6HOVE0K3=9uN)vzL7_J9hkv=~2P@0Pgd}om*=#10ZXw%SmV$|b+BHR13+b!8<#fde zndW_36w0^p-v9nJeKEYj5UaG(d-O|SAjOFO{V!6~<}+tUA6Ibtxd#eBZgAIra8=Pn zwADnAPpug;+`lBp7B>5+c+6T5Lc0?TJ;z>8?p6)C2S6fNX&QUZ52FsauPc5H=*aP^ zKMN*x7Zp0GFG6AP3l3808BBEj2{U)&aQ>VB*#rnYJX_3a(aRyhFpD9Z0DmaGAg8J4 zI?ZfBd%}GF@jqBy??`-sBApRv6MvTqrCmO>LD zlB^NKpkzt*m=W1hQrSY*$(E4Dl4QhKe)pNa-`_v)```Os*IeUr=FFLMp67nM(cB7ltgC7Pig66RND&gVvYdYMnL1p;BauOt-}!Ui=ZX7k zCVe*%Nhe%RnUoDe8pvxB>2{$|QtV-0vY6j~juZi|IC%vg*FEZNHZbmWA0S;{ToZ3thMsQ~&8Eq004NkvF)L}wFk5wBDUa6Ew%$qw}ND{UR z1lQ;1N)VB5pL(|%gQvjk%n_wO?aPY6p~@bm$#P{NygVYgmsABV^_w6C1hoP4>800d z?7P~Y?N1q91nxK}r#;Z73s0q34G=QdU~SV%uApd!gZj$#pk5l}zzU9_=jDKu!nj5n zJut+FGA%OZ-P_Ta)XB+KU@VEk5?=qIp^|+c7lct~S=tdwhms9De#Dmk%n>rT=;kl26g-gCKab=Wub z&f}+k`TW(wmL)#|UW1p6YkR2O)pFWut7o~VhpWWH7f?pHT-Tj*Y6`qoxj5;hvq@(Gas&)(ep{)_ zvZr4%m0W9BJ*G&kR4#?=Xm-XDl&eXX4FIMGo%HbFw5s$Hi$B3;9U1`bjbv1C#w-55 zKeE^h!3Z`MiQI{7DAdXDHQwyh^Tkq?cHy#dz<{*_i!&3E#^y`iVVAa)e&rG*#HswQtENQxW!3eXQ+twPU`e)S6rbm8ITqgRevh#u=p=a zyqpQ-^&NMxxUOw6M8w?-h+W*?S{wj1P9rCP-k$NCtaq>MkA`NUIm#Oo>|~JAnTGhK zH7D01R;fz1@nz<(vR1Ufhypcd&eQoGZ-aNC%9Q|lvkfzXYKr@yfbJ%EMYZ$vJO&22w6A8* z@}c&G60BpbU^r-Ii~~em z$uIM`wA_98{9F;+)!9uEtIU|356b5=zIN} z6H*F569I7@4s|q4AAp6o2P!|R<^lz#DIK>@EbnDJC-$Iu>4e_mH zYJtfUpg}<*hO{OLkQ#V{RbL)kXB!e1p1P4)uSS}0$kfMy|1d(@-1X^X++5)3@Yr#1 z(=Sndb);00XBZ%td`AgG3W2=Rz5#wHj5KA-_SUekVTlRA#FvdMq3vszZe}a-1<)E% zKANv_{pA@c3)KDd$bbX>K()eQH8e!XE_F;rPGtOYJ6(AR7A z*pZ)#gg~<#lEL0*B>JA6089i*KDw^S0EHdD=paZqJ6@Pbb0bagXO3bjWA;s47klxHUtWwqW;0|tFOrVt8f3o!{Dp1a!8VY?lKi=3@x30~qsLscPf&)0wU^NBB*j zUK{9$c zQdIFy5M2r5lLw}17qp_dKt{&7xkPQlAxVkN3vD8JX_$ZHixL=HK@6)dKXhg);_vJnY5FLEU&JAoeDP@rgRz>`)n0u`&Sq*ot zh{+J1E#od!f`6V5fTiMXf1fU2&b49$BC%q-82C_Xk1A&O`>5aGTX$bimIJjLxpHJO zcdvSkFKE_|=FbiI*r?~`cca&?q7gbU%Kf02ft)t`jL=Ctfe4_Mht6M0_|#p6;q=~5 zdxC(YXo-)vL?AiTbDQqazcX6+3i)Cg^8nQJ-yD*a8bi}0BhS$i-NpBA={BXW z1kcIPE21MB3>s_g=tZZ6PL!q)R@96o(usSfqBcA%3BZm4)c?Yd;sn#WvVDlN? zfJCcs+Qk&7&6LU9R^usoBm*+gK(UBt6gK&WA}h|`ZfAO zUvR|064rd}uPX3RL}yn0@LYenY?KG;`?)PQXnt@*kDxuC!VVBWAVEue7!d&7y4~%g z^)ScmBG)w^>5Aq1@M!?Cc5?(A)*+d_ZZ}tc_W66k^q@Lz0ZA%8{F2r0d$`|Mv!ihi zO23+Rl1E{tQQxmGhJ6ux%So}URS@)j0!IQ^=DU28OD|UZUARN@M6|)VKoJkBifP2K z&S*1AMDAF4Y))1idF69SGpX3M2tG|X4v+hOXSXKH?<_wh_p;stB!51aJ(i$DL+keC z-gxZkFghP_y#FEMMTR;ZAh%EyB-Bo8AWP)tT%Wu_(HVg(y`TSldDkVho5|bWI36t{KiFNZw~w*rOTS2Vk5$ljIfyU z|2uXMOv53Z6etq}BKG$J#}Z|7{{g2r8-(Zou5<*h6b7X!*ahG0jA1imxT!IR8QN_v z@U1?Im3=_;MNv3ZnbYiVX95E!7EWl|dOAc;)ItENE8r+DD*_>kifw1sg@T2krIv%o zhqFNE*lzF5E$4!y7FY?$aNq?Ikl?cCs;|LR^nn?;suYvKATtfO)WeUt%ZP}kN~t4H z+EkY&bT8H=a#_>hztK7r2LeMKG~Y}uEq{eNpqkM9Ui$4C=*XhJq)FL-83Su)3j)R( z5iiuq2Z!&KOvrY&kBMmB$S2XBb^Ij!;;m!tz@iMQ_w3T&c8^?>2t$N8v{L@xVxjLW zxF#vidTIW*wz*ger+T9BM}I33P5YuNkcfy}>-e`qY}nY?ghi_zoWzGbpmIiIE?E)D^4}5R2etl!=!G9gvf8y@GZyIek1K%H-2=1lHNco8{&VcmU&eSXx zcoG!;penP1_{g{0broRcQQiXdqI0>37L)qlPiJyl0%qF*S_xHzDKgY!zFN#d6Lk1b zns)DITyRY~M1oO85cBubG?r~23?GIzy2U$HnJsCmC)EE4+RlyUh?Gl0Zmh)BE}Ax? z#*l(=0~2FaAJn&%{qIoV+uHLe+4z4|0VrEzxLy!FGF!zwfblGWnd6L{s67wDgDQWH zwtJRmiH8n-PSc3UqsSN4nR(qn2v0ol1ATSQ3$cM05}G5^34@-T>JKJw=_1P&#Rpl! z$#&+eJw7qM1O5Y0tP3JVW-e|yAoUp*j3;F1CArPs&Ri<5%Net@@kI-s0NH@P2_c$J&%^p%8qFhw;`gboy-gA zELx*Kf{_LQi^@~5x|5^)-}rEISVNkTJCd>i9(o8x@l+~xeSU1qI4#9 z29q{Bng)ITozpNw!!ZYzTArO zomTaThv1my_nGq`-R{z`%KFoYdS3v87mk)9FuWhw58~HE<{pm9lXb1If-$ru?aNJg z{pcd+B`W+^&cbByfs3{@sG*1Nef_rvPr*Tke+MHD-}C*qUdHz3dP~yQ%bZXcXXitj zWMS14E-ug(*`PnYD1PWa8XKA=wESEAP0EMS6W9J@T|==;k^WvN;uxG}Tw*d%510$8 z>C=_O=4-H8@?}!sUw{o&nF}DeclQ5*EFH=oRp#Sb)v_vuBZ%@Y*M}(A)x!G>qBTl< z+Tc*=%2Bl4i$^c?!Gb`j>^3L{2W_QcNlg65V?n{Aou=LTxF5YJ>_ml}piD1{rfDry z%q>{Swy99fCT#_jD%ksaxxYOgc|O$1NMI*;RtHa$OU2PNy%5@Ry2@)OifJx5PnAH9$)K=-Qi2sQ~)MeL$+lkIe`|NW>VeIZ{5$AkA#+a(HU7#mv1^GIL0 z5G)m9Cs9(2VP772SR^D_vm2gj_a?F~c5Xfxccec6iE3#0a z`DLi$_i)%>*6UGpAXHe=lrpNANfqj2L#M$fhrxUHQ7r`Qk2U zc=`mXqCg6nSD-$@b|hn`roPfjvh}SA2IWc_`fT%zf+6?cID%@sOFyIXuN?@4YPC8& zr?h+%yCF@UEy(U%jk|0HO^q~?=3GZ)oD{ra8=1shp#6j0e0Ukbhapd==rxiNrZOEU zkZsNz(N)~>)}`P1bhbHA>EU=cIbP=GH zk4Bq*-*$O};Dzz=ah-u>Z1&D%F35QL-m@Bg-)2~7bb#5Q?f_YOp73qOtU-M%&QG47 zjXTvnvFoIDe4kWEtNwbY3?1YO_0o8tN??Tm_jX$N19Oot5#XQ4PMJ$fe5|ox$E#P_8X7DEw}^mwQJ6 zILkts6iAKt*J~=A+PqC|Xk6$Ra%)&zws3#7G;m^b>y7jH;)kDq8$y0AD{KY*8X{in zwdTm$-e_O0QEBL!x%U(++ozak@IC6|@1~9SjnUsXU*g_xd;ap+_}wnO7~MEo)N-wA zil^+du5^h(7GoHv-nlNe2OGa`?_Ad2J`K*|GOwT*oBK1@-owu3@0IlvnxmgzF6oDd z+o&M1w(F8uCwO7la;NCr{7yR)Q<34kQfrvW;iaen zdnN9kYX0}fgE)fDzpZ7zaJMd#Qqwc*_hUG!&pvG^DDzL`dB&B8_0}rZF?|l`F=w9a zn^N;_vesoQ4w8}y>|vbF-@*eb)4Y6Iqf)wS1N`+eq`WL%?9YEJzA5mew!&QR#f`l= zGH?H$V6^6Nuk<%H*=>*(jrrott@2mO4&&-}^zd80=UqYUI>8c5#5VPF7zNxXiN*L8 z9C5UB+iTG?p09;~3T-Yr_%T;XbS@&)5Er+*;B!qyeI-L6V?+>8U{}!7VeO_g;xwN#DNN6;B!oBsE&~UW3CtD@t#h+Gq&zUM? z;HhY|sZmi@Q7Ot4%FfPK`IM@bLRwaEOV9Tz8)WjB59S>0=pQo|tqAVJUm}IL_;~IY z%@Hn|aU1lozboO~uz1%yS>&EkZyQ#vg^uA#} zLT1!EE_o*xm*#5SvW0>}Tw0P&^onm+PTf2}#VpnL=yx8Yn}~g`e3UAxL}?Q}lkZwJ zRW~$RWZ3*i)3P&hQ!^*7ZSf|*$e6h0ZARR1EBobV!>2c=uyS6ZLWycV3?++KAETFI zgP#qzDNWni)TH-yrWAD45(htC_sqb=w67gBDRPO_NV zI>$ApS_*LC2a1Z_J9wMf_HHr&{oPg!cI zYH+~iyAs&#yIh`TCHY>k@m3ekOX}w6$l_|*wm@rzod?B# zil{Of@EW^Gm0}%{V%1Nb8VoUA9(t$bOF4aEDfH&bDe^<=lB&x!v)ifo4k794EBR_?3{YGlZFW(olM~+>wRk+Oh%JxQyhCmtUQ^ zBLx>(Y~D&Ke(f5jxKYcxOTQA!`jiShMVyKWr>l#res_nv$hmsgEX-=VQL0Nm#h*Q&IWJ#$ zN>4(|Behc3^|e+$Wr~S}ue9euzeeN z{y>b)oRhrZT-$w4Z=S-87c+$c)ua;Ob&;uCvy7_^7O7uSzI~xAdOy1bd))}#AVjb* zD!wp$#jRQ_P*JoYTC)BlMZUo;!U2+ZUv$&`fZlGpCxV^g2hITUK=89cgkX>3}n9Sx8LR$UjlJvgw>h$$Ei%-SIeU z?W?l9UJ2<^77UnhWs>M%yc+YXTpzv=VU&i}u2FUO2rssu4sZSn2yvV0&@F(jCdE23 zTO^@(q&i`G$jzd58cd^@c(Lh+Mnt{lY|Y%qq^RC^H->LhT+Y^WeB`XM>r)P889w?; z<)@fu9!CEW>EgU&_w{~ijEJrz=NlsEm*c${KSv4fF*o{WlsgvjGE*N49X|Se6kX(x zYwJVj+u9(jD$6{8ic9$v_ab+}Qsk^#Ye-R3aLmlE6y@a?>IO>~{pz1JGPKx4{NiHD1*a^4%SBK=f~8*8*}qKeVv_m$*% zx4*{>Cy`p~0tyHQA-d&QXfH0jPR($`aCu%W!so+YY%Q9Jf)ai4fU(J4?P@b9l@@&Q(LV}`3{9YyAy&68ub_1|%i z3d*{oyF(Ymv{s|!a) z@xO2UJn(p}ooQl2?gPQ34ButsO2V{xEPq{fo5yuUe+lrFb4lk3HpyRmLtLLdxsSf7 z8CS40#iSuZx$gV3q4II-dUw5d-U`M?M|*yVaRQy@P4|i?niF;9BwycNJ(Q^fMi6!7 zU6nElj+MjiwLcAhPK-2X(J_ l7}8)@&ADv;6a%ABn(BILr5A0({s%cOttJ2f literal 0 HcmV?d00001 diff --git a/docs/source/_static/saa_theta_corr.png b/docs/source/_static/saa_theta_corr.png new file mode 100644 index 0000000000000000000000000000000000000000..6e058e9be80f739a032828e9b43aca97941f7c13 GIT binary patch literal 230344 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%0DkOlB1mniv;;Ln9 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

  • KD!FMBH; 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 F{Xe9*;UNG3 literal 0 HcmV?d00001 diff --git a/docs/source/analysis.rst b/docs/source/analysis.rst index 76e8582e..b9c25da4 100644 --- a/docs/source/analysis.rst +++ b/docs/source/analysis.rst @@ -53,8 +53,15 @@ conditions present in both the growth and binding data. After convergence the script updates the production ``{out_prefix}_priors.csv`` and ``{out_prefix}_guesses.csv`` files in place (a ``.bak`` backup is written -first), giving the full production fit a warm start. Diagnostic PDFs (one per -genotype) and a ``{out_prefix}_calib_stats.json`` file are also written. +first), giving the full production fit a warm start. + +Diagnostic artefacts from the calibration MAP run are also written (default +prefix ``tfs_prefit``): + +* ``tfs_prefit_params.npz`` — MAP point estimates in constrained space. +* ``tfs_prefit_checkpoint.pkl`` — optimizer checkpoint (can be passed to + ``--checkpoint_file`` to resume an interrupted calibration run). +* ``tfs_prefit_losses.txt`` — per-epoch loss history. .. code-block:: bash @@ -139,9 +146,32 @@ writes one CSV per parameter group. Outputs (default ``--out_prefix tfs_params``): -* ``tfs_params_activity.csv`` — per-genotype TF activity *A* with posterior quantiles -* ``tfs_params_theta.csv`` — inferred occupancy parameters (Hill *Kd*, *n*, etc.) -* Additional CSVs depending on the components selected during configuration. +* ``tfs_params_log_hill_K.csv`` — per-genotype log₁₀(*Kd*) with posterior quantiles. +* ``tfs_params_theta_low.csv``, ``tfs_params_theta_high.csv`` — per-genotype + lower and upper occupancy plateaux. +* ``tfs_params_dk_geno.csv`` — per-genotype pleiotropic growth effect. +* Additional CSVs depending on the components selected during configuration + (e.g. ``tfs_params_d_log_hill_K.csv`` for per-mutation effects when using + ``hill_mut``; ``tfs_params_epi_*.csv`` for epistasis terms when + ``--epistasis`` is set). + +**Quantile columns** + +Each CSV contains metadata columns (typically ``genotype`` and +``titrant_name``) followed by 17 quantile columns named ``q{level}``: + +.. code-block:: text + + q0.001 q0.005 q0.01 q0.025 q0.05 q0.1 q0.159 q0.25 + q0.5 q0.75 q0.841 q0.9 q0.95 q0.975 q0.99 q0.995 q0.999 + +``q0.5`` is the posterior median (the recommended point estimate). ``q0.025`` +and ``q0.975`` span the 95% credible interval; ``q0.159`` and ``q0.841`` +span the ±1 σ interval under a normal approximation. + +When ``tfs-extract-params`` is given a MAP checkpoint (``.pkl``) instead of +a posterior file, only a single ``q0.5`` column is written (the MAP point +estimate, not a true posterior quantile). Step 6: Predict Growth (``tfs-predict-growth``) ------------------------------------------------ @@ -206,6 +236,22 @@ Output (default ``--out_prefix tfs_cat_response``): * ``tfs_cat_response.csv`` — one row per (genotype, titrant_name) with ``best_model``, AIC weights, and fitted parameters for every model. +Step 9: Summarise Fit (``tfs-summarize-fit``) +---------------------------------------------- + +Collects the outputs of the completed run, computes prediction quality +statistics, and writes diagnostic plots and tables to a ``summary/`` +subdirectory. It can be run after Steps 5–7 are complete and does not +require the full posterior file to be retained. + +.. code-block:: bash + + tfs-summarize-fit out/ + +For a full guide to every output file — including how to read the theta +correlation plots, calibration curves, and parameter-recovery scatter +plots — see :doc:`summarize-fit`. + --- .. _model-components: diff --git a/docs/source/index.rst b/docs/source/index.rst index 394e858c..ad036cab 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,9 +8,11 @@ tfscreen :maxdepth: 2 :caption: Contents: + quickstart simulation process-raw analysis + summarize-fit grid ligandmpnn-features @@ -30,6 +32,12 @@ The thermodynamic observable is operator occupancy (*θ*), inferred from *E. col The model in ``tfscreen.tfmodel`` is designed to jointly capture high-throughput growth data and low-throughput binding data, learning the quantitative relationship between operator occupancy and growth rate in the process. +Quick Start +----------- +New to ``tfscreen``? The :doc:`quickstart` page walks through a complete +simulate-and-analyze run using the bundled example in +``examples/simulate-and-analyze/``. + Simulation ---------- `tfscreen` allows you to simulate high-throughput screens starting from thermodynamic models of transcription factor binding and activity. See the :doc:`simulation` page for more details. diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 00000000..6d8483fc --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,156 @@ +=========== +Quick Start +=========== + +This page walks through a complete simulate-and-analyze run using the +bundled example in ``examples/simulate-and-analyze/``. By the end you +will have simulated a synthetic high-throughput TF-library screen, fitted +the hierarchical Bayesian model, and produced a full set of diagnostic +plots and statistics. + +The example uses a small library (~600 genotypes across three sub-libraries) +so the full pipeline finishes in roughly 20–60 minutes on a laptop CPU or a +few minutes on a GPU. + +Prerequisites +------------- + +Install ``tfscreen`` and verify the entry points are on your PATH: + +.. code-block:: bash + + git clone https://github.com/harmslab/tfscreen + cd tfscreen + pip install -e . + tfs-simulate --help # should print usage + +The example requires JAX and Numpyro, which are installed as dependencies. +On Apple Silicon you may need to install the Metal-accelerated ``jax-metal`` +package separately; on a Linux cluster with a GPU, install the appropriate +``jaxlib`` wheel for your CUDA version. + +Getting the example +------------------- + +Copy the example directory to a working location: + +.. code-block:: bash + + cp -r examples/simulate-and-analyze/ ~/tfscreen-example + cd ~/tfscreen-example + +The directory contains: + +* ``simulate_config.yaml`` — simulation parameters (library genetics, + growth conditions, binding data). +* ``hill_params.csv`` — per-genotype Hill binding parameters that set the + ground-truth θ values used during simulation. +* ``run.sh`` — the pipeline script. + +Running the pipeline +-------------------- + +.. code-block:: bash + + bash run.sh simulate_config.yaml out/ 1 + +The three positional arguments are: + +1. The simulation config file. +2. The output directory (created automatically). +3. The random seed (``1`` reproduces the documented example outputs). + +On a local machine the script uses JAX's CPU parallelism +(``XLA_FLAGS="--xla_force_host_platform_device_count=8"``). On a +cluster, comment that line out and uncomment ``module load cuda/...`` +instead. + +The script runs nine steps in sequence, printing a ``>>>`` header before +each one: + +.. list-table:: + :header-rows: 1 + :widths: 5 30 65 + + * - Step + - Command + - What it does + * - 1 + - ``tfs-simulate`` + - Builds the genotype library, draws per-genotype θ from the Hill + model, converts θ to growth rates, and writes simulated sequencing + data. + * - 2 + - ``tfs-configure-model`` + - Validates inputs, selects model components, writes + ``tfs_configure_config.yaml``. + * - 3 + - ``tfs-prefit-calibration`` + - Fast MAP fit to calibrate the growth-linking-function priors. + * - 4 + - ``tfs-fit-model`` + - Main SVI inference; writes ``tfs_fit_model_checkpoint.pkl``. + * - 5 + - ``tfs-sample-posterior`` + - Draws posterior samples; writes ``tfs_posterior.h5`` (~5 GB). + * - 6 + - ``tfs-extract-params`` + - Summarises the posterior into per-parameter CSV files. + * - 7 + - ``tfs-predict-theta`` + - Predicts θ at every (genotype, titrant concentration) in the + training data. + * - 8 + - ``tfs-predict-growth`` + - Predicts ln(CFU) with posterior quantiles for all training + observations. + * - 9 + - ``tfs-summarize-fit`` + - Computes fit statistics and writes diagnostic plots and CSVs to + ``out/summary/``. + +Expected outputs +---------------- + +After the run completes, ``out/`` will contain: + +.. list-table:: + :header-rows: 1 + :widths: 38 62 + + * - File / directory + - Contents + * - ``tfs_sim_library.csv`` + - All genotypes in the simulated library. + * - ``tfs_sim_growth.csv`` + - Simulated ln(CFU) data (same format as ``tfs-process-counts`` output). + * - ``tfs_sim_binding.csv`` + - Simulated binding curve observations. + * - ``tfs_sim_parameters.csv`` + - Ground-truth per-genotype parameters (θ, dk_geno, etc.). + * - ``tfs_configure_config.yaml`` + - Model configuration read by all downstream steps. + * - ``tfs_fit_model_checkpoint.pkl`` + - Fitted model checkpoint. + * - ``tfs_posterior.h5`` + - Posterior samples (~5 GB; not committed to the repo). + * - ``tfs_params_*.csv`` + - Posterior summaries (quantiles) for each parameter group. + * - ``tfs_pred_theta.csv`` + - Predicted θ with posterior quantiles. + * - ``tfs_pred_growth.csv`` + - Predicted ln(CFU) with posterior quantiles. + * - ``summary/`` + - Diagnostic plots and statistics from ``tfs-summarize-fit``. + +Next steps +---------- + +* **Understand each analysis step** — see :doc:`analysis` for the full CLI + reference covering every ``tfs-*`` command in the pipeline. +* **Interpret the diagnostic outputs** — see :doc:`summarize-fit` for a + guided walkthrough of every plot and statistic in ``out/summary/``. +* **Simulate a parameter sweep** — see :doc:`grid` for how to run a + Cartesian grid of configurations. +* **Use real data** — see :doc:`process-raw` for converting FASTQ reads + into the ``growth.csv`` format that ``tfs-configure-model`` accepts. diff --git a/docs/source/summarize-fit.rst b/docs/source/summarize-fit.rst new file mode 100644 index 00000000..25315f70 --- /dev/null +++ b/docs/source/summarize-fit.rst @@ -0,0 +1,334 @@ +================= +tfs-summarize-fit +================= + +``tfs-summarize-fit`` collects the outputs of a completed model-fitting run, +computes prediction quality statistics, and writes a set of diagnostic plots +and tables to a ``summary/`` subdirectory. + +.. note:: + + **CSV files are the authoritative outputs.** Every PDF written by this + command has a matching CSV containing the exact data used to generate it. + Use the CSVs for downstream quantitative analysis; treat the PDFs as + human-readable summaries. + +Usage +----- + +.. code-block:: bash + + tfs-summarize-fit [options] + +**Positional arguments:** + +* ``run_dir`` — directory containing the completed model-fit outputs (i.e. + the ``out/`` directory produced by ``run.sh``). + +**Optional arguments:** + +* ``--ref_theta_file`` — CSV with columns ``genotype``, ``titrant_name``, + ``titrant_conc``, and a ``theta_obs`` or ``theta`` column containing + known θ values for evaluating out-of-sample predictions. When omitted, + the script automatically looks for ``*_sim_genotype_theta.csv`` in + ``run_dir`` (the file written by ``tfs-simulate``). +* ``--out_prefix`` — prefix for all output files. Defaults to + ``{run_dir}/summary/tfs_summarize``. + +What the script needs +--------------------- + +The script scans ``run_dir`` for: + +* ``*_config.yaml`` — model configuration (required). +* ``*_pred_theta.csv`` — theta predictions from ``tfs-predict-theta`` (required). +* ``*_sim_genotype_theta.csv`` — ground-truth θ from ``tfs-simulate`` + (used for test statistics; absent for real-data runs). +* ``*_pred_growth.csv`` — growth predictions from ``tfs-predict-growth`` + (optional; enables growth statistics and plots). +* ``*_losses.txt`` — training loss history from ``tfs-fit-model`` + (optional; enables the loss-curve plot). +* ``*_params_*.csv`` — parameter summaries from ``tfs-extract-params`` + (optional; enables parameter-recovery plots when ``*_sim_parameters.csv`` + is also present). + +Output file reference +--------------------- + +.. list-table:: + :header-rows: 1 + :widths: 45 55 + + * - File + - Contents + * - ``tfs_summarize_fit_summary.json`` + - Nested statistics dict (see :ref:`fit-summary-json`). + * - ``tfs_summarize_theta_corr.pdf`` / ``.csv`` + - Two-panel θ correlation: training (left) and test (right). + * - ``tfs_summarize_theta_corr_training.csv`` + - Joined (ref, predicted) θ pairs for training genotypes. + * - ``tfs_summarize_theta_corr_test.csv`` + - Same for test genotypes. + * - ``tfs_summarize_growth_corr.pdf`` / ``.csv`` + - Observed vs predicted ln(CFU) across all training points. + * - ``tfs_summarize_{genotype}_theta_fits.pdf`` / ``.csv`` + - Per-genotype θ curve overlaid with binding observations. + * - ``tfs_summarize_{genotype}_trajectory.pdf`` / ``.csv`` + - Per-genotype predicted ln(CFU) vs time across all conditions. + * - ``tfs_summarize_losses.pdf`` + - Training loss curve. + * - ``tfs_summarize_theta_training_calibration.pdf`` + - Calibration plots for training θ (PIT histogram + coverage curve). + * - ``tfs_summarize_theta_test_calibration.pdf`` + - Calibration plots for test θ. + * - ``tfs_summarize_growth_calibration.pdf`` + - Calibration plots for growth predictions. + * - ``tfs_summarize_params_{name}.pdf`` / ``.csv`` + - Per-parameter scatter of simulated vs inferred values (simulated + data only). + * - ``tfs_summarize_params_{name}_calibration.pdf`` + - Calibration plots for each inferred parameter (simulated data only). + +Reading the outputs +------------------- + +.. _fit-summary-json: + +fit_summary.json — at a glance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The JSON file gives a rapid numerical overview of the run. Open it in any +text editor or load it with ``json.load``. + +.. code-block:: json + + { + "metadata": { + "n_parameters": 5005, + "n_theta_training_points": 32, + "n_theta_test_points": 3752, + "n_growth_training_points": 54180, + "final_loss": 7281255.0 + }, + "theta": { + "training": { "pearson_r": 0.999, "r_squared": 0.998, "rmse": 0.016 }, + "test": { "pearson_r": 0.936, "r_squared": 0.876, "rmse": 0.140 } + }, + "growth": { + "training": { "pearson_r": 0.981, "r_squared": 0.963, "rmse": 0.577 } + } + } + +Key fields: + +* ``n_parameters`` — total number of free parameters (from the guesses CSV). + Compare to ``n_growth_training_points`` to check for over-parameterisation. +* ``final_loss`` — the converged SVI ELBO (negative; more negative = better). + Use this to compare runs with identical data but different components or seeds. +* ``theta.training`` — statistics comparing the model's θ predictions against + the direct **binding observations** used as training data. These will + always be near-perfect because the model is conditioned on these values. +* ``theta.test`` — statistics comparing θ predictions against the full + ground-truth θ grid from ``tfs-simulate``. This is the real generalization + diagnostic: the vast majority of test points were never directly observed. +* ``growth.training`` — statistics comparing predicted ln(CFU) against the + observed growth data. + +Useful derived checks: + +* ``theta.test.r_squared > 0.85`` is a reasonable threshold for a well-fitting + model on a library of this size and noise level. +* A large gap between ``theta.training.rmse`` and ``theta.test.rmse`` signals + that the model fits the spiked genotypes well but struggles to generalize + to the broader library. +* ``residual_corr`` and its p-value test for autocorrelation in the residuals. + A significant value indicates systematic structure the model is not capturing. + +Training-loss curve (tfs_summarize_losses.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The loss curve shows the SVI ELBO at each recorded epoch. A healthy run +shows a smooth decrease that levels off to a plateau; a run that has not +converged will still be declining at the final epoch. If the loss spikes +upward mid-run, the learning rate or the prior scales may need adjustment. + +Theta correlation (tfs_summarize_theta_corr.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. figure:: _static/saa_theta_corr.png + :alt: Two-panel theta correlation plot + :width: 100% + + **Left panel** — training θ: the model's posterior-median prediction + vs the direct binding observations used as input. Near-perfect agreement + is expected and is not by itself a measure of model quality. **Right + panel** — test θ: predictions vs the full ground-truth θ grid from + simulation, covering all library genotypes at all concentrations. Points + far from the diagonal indicate genotypes or concentrations where the + mutation-additivity assumption breaks down. + +The two panels serve different purposes: + +* **Training panel** confirms that the model correctly ingest the binding + data — any large deviation here points to a data-loading or configuration + error. +* **Test panel** is the generalization diagnostic. The model must predict θ + for thousands of genotypes that were never directly measured; it does so by + combining per-mutation effects inferred from the spiked controls and the + growth data. Scatter around the diagonal reflects the irreducible noise in + that extrapolation. + +Per-genotype theta fits (tfs_summarize_{genotype}_theta_fits.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. figure:: _static/saa_wt_theta_fits.png + :alt: Wild-type theta fit + :width: 60% + + Wild-type θ curve: posterior-median prediction (line) overlaid with the + binding observations (circles). The x-axis is on a log scale. A sigmoid + transition from high θ at low [IPTG] to low θ at high [IPTG] is the + expected shape for a repressor inactivated by its inducer. + +One plot is produced per genotype that appears in both the binding data and +the theta predictions. For spiked genotypes with measured binding curves +these plots confirm that the Hill model adequately describes the shape of the +induction curve. + +Growth trajectories (tfs_summarize_{genotype}_trajectory.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The trajectory plots show predicted ln(CFU) versus time for every growth +condition, with the observed data points (two replicates) overlaid on the +posterior-median prediction and its 95% credible interval. + +One page is produced per genotype; each page contains one panel per +condition (library × selection × titrant concentration). The dashed +vertical line marks the transition from pre-growth to selection medium. + +Typical patterns to look for: + +* **Pre-growth panels** (``kanR-kan``, ``pheS-4CP`` without selection): + all genotypes should grow at similar rates; large deviations suggest a + problematic ``dk_geno`` estimate. +* **Selection panels at extreme IPTG**: the two markers should respond in + opposite directions as θ moves from ~1 to ~0 with increasing [IPTG]. +* **Replicate agreement**: the two colored lines should be close together; + large replicate-to-replicate variation indicates high tube noise or a + confounded sample. + +Growth correlation (tfs_summarize_growth_corr.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. figure:: _static/saa_growth_corr.png + :alt: Growth prediction correlation + :width: 65% + + Observed vs predicted ln(CFU) across all genotypes, conditions, and + time-points in the training data (54,180 points in the example run). + Points cluster tightly along the diagonal except at low ln(CFU) values + (~3–7), where sequencing noise dominates and the model's predictions + spread somewhat. + +A tight cluster along the diagonal confirms that the growth model +accurately reproduces the data. Systematic vertical or horizontal bands +point to conditions or time-points where the model consistently over- or +under-predicts; these are usually caused by incorrect ``{m, b}`` priors for +those conditions or by tube-noise events in specific samples. + +.. _calibration-plots: + +Calibration plots (tfs_summarize_*_calibration.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. figure:: _static/saa_growth_calibration.png + :alt: Growth calibration plots + :width: 100% + + Growth-prediction calibration. **Left** — PIT histogram; the dashed + red line shows the ideal uniform distribution. **Right** — calibration + curve; the dashed black line is perfect calibration. + +Calibration plots assess whether the model's **posterior uncertainty +intervals** are appropriately sized — neither too narrow (overconfident) +nor too wide (underconfident). + +**PIT histogram** (left panel) + +The Probability Integral Transform (PIT) value for each observation is the +fraction of the posterior predictive distribution that falls below the +observed value. For a perfectly calibrated model, PIT values are +uniformly distributed on [0, 1] and the histogram should be flat. + +* **U-shaped histogram** (spikes near 0 and 1, flat middle): many + observations fall outside the predicted intervals → posterior is + **overconfident** (intervals too narrow). +* **Hump-shaped histogram** (peak in the middle, low edges): observations + cluster inside the predicted intervals → posterior is **underconfident** + (intervals too wide). + +**Calibration curve** (right panel) + +For each nominal coverage level *α* (x-axis), the calibration curve shows +the empirical fraction of observations that fall within the model's *α* +credible interval (y-axis). + +* **Below the diagonal**: empirical coverage < nominal → **overconfident**. +* **Above the diagonal**: empirical coverage > nominal → **underconfident**. +* **On the diagonal**: perfectly calibrated. + +The example growth-calibration plot shows U-shaped PIT and a curve below +the diagonal. This slight overconfidence in growth predictions is typical +of SVI: the mean-field variational approximation tends to underestimate +posterior variance. It does not indicate a model misspecification; the +point predictions (correlation, R²) remain accurate. + +Similar calibration plots are produced for theta (training and test) and for +each parameter group when ground-truth values are available. + +Parameter recovery (tfs_summarize_params_{name}.pdf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. figure:: _static/saa_params_log_hill_K.png + :alt: log_hill_K parameter recovery + :width: 65% + + Simulated vs inferred ``log_hill_K`` (log₁₀ of the Hill dissociation + constant) for all library genotypes. Each point is one genotype; the + dashed line is perfect recovery. Good agreement across the dynamic range + confirms that the model correctly identifies which genotypes have shifted + binding affinities. + +Parameter-recovery plots are produced only when the run directory contains +a ``*_sim_parameters.csv`` file (i.e. when running on simulated data). They +compare the posterior-median inferred value for each parameter against the +ground truth used during simulation. + +One plot is produced per parameter group. Versions are produced for: + +* **Direct parameters** — per-genotype values (e.g. ``log_hill_K``, + ``theta_high``, ``theta_low``, ``hill_n``). +* **Diff parameters** (``_d_`` infix) — per-mutation effect sizes relative to + wild-type (e.g. ``d_log_hill_K``, ``d_logit_low``). +* **Epistasis parameters** (``_epi_`` infix) — pairwise epistatic deviations + from additivity for double-mutant genotypes. + +Each parameter group also has a companion calibration plot +(``*_calibration.pdf``) assessing whether the inferred uncertainty correctly +covers the true value. + +Interpreting parameter recovery +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Tight scatter around the diagonal for all parameters → model recovers the + ground truth; the experimental design has sufficient signal. +* Tight recovery for direct parameters but wide scatter for diff/epi + parameters → the model identifies which genotypes differ from wild-type + but cannot resolve the per-mutation contributions as precisely. +* Systematic bias (all points shifted to one side of the diagonal) → + mis-specified priors, an incorrect model component, or a calibration issue + in the pre-fit step. +* Good recovery for ``log_hill_K`` but poor for ``hill_n`` or ``theta_high`` + → the mid-range of the induction curve is well-constrained but the shape + of the saturation plateaux is not (common when the growth data covers only + a restricted θ range for most genotypes). diff --git a/examples/simulate-and-analyze/README.md b/examples/simulate-and-analyze/README.md new file mode 100644 index 00000000..c3765eb3 --- /dev/null +++ b/examples/simulate-and-analyze/README.md @@ -0,0 +1,33 @@ +# simulate-and-analyze + +A complete end-to-end example: simulate a synthetic TF-library screen and fit +the hierarchical Bayesian model to the simulated data. + +## Contents + +| File | Purpose | +|------|---------| +| `simulate_config.yaml` | Simulation parameters (library genetics, conditions, binding data) | +| `hill_params.csv` | Per-genotype Hill binding parameters used to generate ground-truth θ | +| `run.sh` | Pipeline script (simulate → configure → calibrate → fit → predict → summarise) | + +## Running + +```bash +bash run.sh simulate_config.yaml out/ 1 +``` + +The three arguments are the config file, the output directory (created +automatically), and the random seed. Seed `1` reproduces the documented +example outputs. + +Running time is roughly 20–60 minutes on a laptop CPU or a few minutes on a +GPU. On a cluster, comment out the `XLA_FLAGS` line in `run.sh` and +uncomment `module load cuda/...` instead. + +## Documentation + +See the [quickstart](https://tfscreen.readthedocs.io/en/latest/quickstart.html) +for a guided walkthrough and the +[tfs-summarize-fit reference](https://tfscreen.readthedocs.io/en/latest/summarize-fit.html) +for a detailed guide to interpreting the diagnostic outputs in `out/summary/`. diff --git a/examples/simulate-and-analyze/hill_params.csv b/examples/simulate-and-analyze/hill_params.csv new file mode 100644 index 00000000..a3568471 --- /dev/null +++ b/examples/simulate-and-analyze/hill_params.csv @@ -0,0 +1,5 @@ +genotype,log_hill_K,hill_n,theta_high,theta_low +wt,-4.1,2,0.01,0.99 +M42I,-2.2104779610300698,1.5156149571654287,0.5054105302329879,0.9538933267956679 +H74A,-5.872705439673401,1.2001278450518955,0.05,0.7115380202706272 +K84L,-5.755115159475512,1.0593068151944411,0.1,0.856776050040223 diff --git a/examples/simulate-and-analyze/run.sh b/examples/simulate-and-analyze/run.sh new file mode 100644 index 00000000..65d9b4fb --- /dev/null +++ b/examples/simulate-and-analyze/run.sh @@ -0,0 +1,132 @@ +#!/bin/bash -l +#SBATCH --account=harmslab ### change this to your actual account for charging +#SBATCH --job-name=tfscreen ### job name +#SBATCH --output=hostname.out ### file in which to store job stdout +#SBATCH --error=hostname.err ### file in which to store job stderr +#SBATCH --partition=gpu +#SBATCH --time=01-00:00:00 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=8 +#SBATCH --gpus=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=0 + +# On a cluster: comment out the XLA_FLAGS line and uncomment "module load cuda/..." +# On a local CPU: keep the XLA_FLAGS line (sets JAX to use 8 virtual CPU devices). +#module load cuda/12.4.1 +export XLA_FLAGS="--xla_force_host_platform_device_count=8" + +# Stop immediately on any error. +set -e + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +config_file="${1}" +run_dir="${2}" +if [[ ! "${run_dir}" || ! "${config_file}" ]]; then + echo "Usage: run.sh config_file out_dir [seed]" + exit 1 +fi + +seed="${3}" +if [[ ! "${seed}" ]]; then + seed=1 +fi + +# --------------------------------------------------------------------------- +# 1. Simulate library +# --------------------------------------------------------------------------- +# tfs-simulate reads the config YAML and writes simulated growth data, +# binding curves, and ground-truth parameter CSVs to run_dir. +echo ">>> Simulate library" +tfs-simulate "${config_file}" "${run_dir}" --seed "${seed}" + +cd "${run_dir}" + +# --------------------------------------------------------------------------- +# 2. Configure model +# --------------------------------------------------------------------------- +# tfs-configure-model validates the data, maps labels to indices, selects +# model components, and writes tfs_configure_config.yaml + priors/guesses CSVs. +# Edit the flags here to change which model components are used. +echo ">>> Configure model" +tfs-configure-model \ + tfs_sim_binding.csv \ + --growth_df tfs_sim_growth.csv \ + --presplit_df tfs_sim_presplit.csv \ + --condition_growth_model linear \ + --growth_transition_model instant \ + --ln_cfu0_model hierarchical_factored \ + --dk_geno_model hierarchical_geno \ + --activity_model fixed \ + --theta_model hill_mut \ + --transformation_model single \ + --theta_rescale_model passthrough \ + --theta_growth_noise_model logit_normal \ + --theta_binding_noise_model zero \ + --growth_noise_model normal_kt \ + --spiked wt M42I H74A K84L M42I/H74A M42I/K84L H74A/K84L D88A \ + --growth_shares_replicates \ + --epistasis + +# --------------------------------------------------------------------------- +# 3. Pre-fit calibration +# --------------------------------------------------------------------------- +# A fast MAP fit on a simplified model calibrates the growth-linking-function +# priors (m and b) before the full inference run. Updates priors.csv and +# guesses.csv in place. +echo ">>> Pre-fit calibration" +tfs-prefit-calibration tfs_configure_config.yaml --seed "${seed}" + +# --------------------------------------------------------------------------- +# 4. Fit model (SVI) +# --------------------------------------------------------------------------- +# Main hierarchical Bayesian inference. SVI produces a full approximate +# posterior; for a point estimate only, use --analysis_method map instead. +echo ">>> Fit model" +tfs-fit-model \ + tfs_configure_config.yaml \ + --seed "${seed}" \ + --analysis_method svi \ + --convergence_tolerance 0.0001 + +# --------------------------------------------------------------------------- +# 5. Sample posterior +# --------------------------------------------------------------------------- +# Draw posterior samples from the SVI variational distribution and write +# them to an HDF5 file used by the prediction steps below. +echo ">>> Sample posterior" +tfs-sample-posterior tfs_configure_config.yaml tfs_fit_model_checkpoint.pkl + +# --------------------------------------------------------------------------- +# 6. Extract parameter estimates +# --------------------------------------------------------------------------- +# Summarise the posterior into per-parameter CSV files (quantiles, means). +echo ">>> Extract parameter estimates" +tfs-extract-params tfs_configure_config.yaml tfs_posterior.h5 + +# --------------------------------------------------------------------------- +# 7. Predict theta +# --------------------------------------------------------------------------- +# Predict operator occupancy θ as a function of titrant concentration for +# every genotype in the training data. +echo ">>> Predict theta" +tfs-predict-theta tfs_configure_config.yaml tfs_posterior.h5 + +# --------------------------------------------------------------------------- +# 8. Predict growth +# --------------------------------------------------------------------------- +# Predict ln(CFU) with posterior uncertainty for every training observation. +echo ">>> Predict growth" +tfs-predict-growth tfs_configure_config.yaml tfs_posterior.h5 --num_marginal_samples=500 + +# --------------------------------------------------------------------------- +# 9. Summarise fit +# --------------------------------------------------------------------------- +# Collects all outputs, computes statistics, and writes diagnostic PDFs and +# CSVs to the summary/ subdirectory. +echo ">>> Summarise fit" +tfs-summarize-fit . + +cd .. diff --git a/examples/simulate-and-analyze/simulate_config.yaml b/examples/simulate-and-analyze/simulate_config.yaml new file mode 100644 index 00000000..a0ea8afa --- /dev/null +++ b/examples/simulate-and-analyze/simulate_config.yaml @@ -0,0 +1,246 @@ + +# --------------------------------------------------------------------------- +# Library genetic information +# --------------------------------------------------------------------------- + +# Reading frame 0, 1, 2. No reverse (negative) strand allowed. +reading_frame: 0 + +# Amino acid residue number for the first in-frame residue of the amplicon. +first_amplicon_residue: 27 + +# Wildtype DNA sequence of the amplicon. +wt_seq: gccagccacgtttctgcgaaaacgcgggaaaaagtggaagcggcgatggcggagctgaattacattcccaaccgcgtggcacaacaactggcgggcaaacagtcgttgctgattggcgttgccacctccagtctggccctgcacgcgccgtcgcaaattgtcgcggcgattaaatctcgcgccgatcaactgggtgccagcgtggtggtgtcgatg + +# Degenerate codon pattern for library positions. '.' = wildtype; 'nnt' = NNT +# codon (encodes all 20 amino acids + stop with reduced synonymous redundancy). +# Must be the same length as wt_seq. +degen_sites: | + .............................................nnt.............................................................................................nnt...........................nnt.......................................... +# ......nntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnnt......nntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnntnnt + + +# sub_libraries groups contiguous degenerate positions that were cloned together. +# '.' = wildtype; any other character defines a sub-library block. Blocks must +# be contiguous (......111... ok; ....11.1... not ok). +sub_libraries: | + ......111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111......222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 + +# Flanking sequences immediately upstream and downstream of the amplicon. +expected_5p: cgcgtggtgaaccag +expected_3p: gtagaacgaagcggc + +# Which genotype classes to include in the library. 'single-x' = all +# single-mutation variants in sub-library x. 'double-x-y' = all pairwise +# combinations between sub-libraries x and y. 'double-x-x' = internal +# doubles within sub-library x. +library_combos: ["single-1", "single-2", "double-1-2"] + +# Control sequences (e.g. known variants) added as spikes. Must be the same +# length as wt_seq. +spiked_seqs: +- ........................................................................................................................................................................................................................ # wt +- .............................................ata........................................................................................................................................................................ # m42i +- .............................................................................................................................................gcg........................................................................ # h74a +- ...........................................................................................................................................................................ctg.......................................... # k84l +- .............................................ata.............................................................................................gcg........................................................................ # m42i/h74a +- .............................................ata...........................................................................................................................ctg.......................................... # m42i/k84l +- .............................................................................................................................................gcg...........................ctg.......................................... # h74a/k84l +- .............................................ata.............................................................................................gcg...........................ctg.......................................... # m42i/h74a/k84l +- ........................................................................................................................................................................................cc.............................. # d88a + +# --------------------------------------------------------------------------- +# Phenotype calculation +# --------------------------------------------------------------------------- + +# Registered theta component name. Any key in model_registry["theta"] is +# valid except "_simple" (calibration-only). Examples: "hill_geno", "thermo.O2_C12_K5_U0_a.PK". +theta_component: "hill_mut" + +# Pairwise epistasis (regularized horseshoe; used when library has doubles): +theta_sim_priors: + epi_tau_scale: 0.1 # HalfCauchy scale for global sparsity τ + +# Optional: path to structural/thermodynamic data file required by PnnC and +# PddG theta components. Omit (or set to null) for all other models. +# .h5 / .hdf5 → HDF5 structural ensemble (PnnC) +# .csv → per-mutation per-structure ddG prior means (PddG) +# thermo_data: null + +# Optional: overrides for the theta component's default hyperparameters. +# Any key returned by the component's get_hyperparameters() can be overridden. +# Omit this block to use all component defaults. +# theta_priors: +# theta_tf_total_M: 3.3e-7 + +# --------------------------------------------------------------------------- +# Conditions +# --------------------------------------------------------------------------- + +# Each block defines one set of growth conditions. The Cartesian product of +# titrant_conc × t_sel gives one sample per entry. condition_pre and +# condition_sel must also appear as keys in the 'growth' section below. +condition_blocks: + - library: "kanR" + titrant_name: "iptg" + titrant_conc: [0, 0.0001, 0.001, 0.003] + condition_pre: "kanR-kan" + t_pre: 30 + condition_sel: "kanR+kan" + t_sel: [160, 180, 200] + + - library: "kanR" + titrant_name: "iptg" + titrant_conc: [0.01, 0.03, 0.1, 1.0] + condition_pre: "kanR-kan" + t_pre: 30 + condition_sel: "kanR+kan" + t_sel: [145, 165, 180] + + - library: "kanR" + titrant_name: "iptg" + titrant_conc: [0, 1.0] + condition_pre: "kanR-kan" + t_pre: 30 + condition_sel: "kanR-kan" + t_sel: [160, 180, 200] + + - library: "pheS" + titrant_name: "iptg" + titrant_conc: [0, 0.0001, 0.001, 0.003, 0.01, 0.03, 0.1, 1.0] + condition_pre: "pheS-4CP" + t_pre: 30 + condition_sel: "pheS+4CP" + t_sel: [95, 110, 125] + + - library: "pheS" + titrant_name: "iptg" + titrant_conc: [0, 1.0] + condition_pre: "pheS-4CP" + t_pre: 30 + condition_sel: "pheS-4CP" + t_sel: [95, 110, 125] + +# --------------------------------------------------------------------------- +# Growth rate parameters +# --------------------------------------------------------------------------- + +# Linear mapping from operator occupancy (theta) to growth rate k for each +# condition: k = m * theta + b. Every condition_pre and condition_sel that +# appears in condition_blocks must have an entry here, and vice versa. +growth: + kanR+kan: {m: -0.009933, b: 0.010696} + kanR-kan: {m: 0.000808, b: 0.015366} + pheS+4CP: {m: 0.006226, b: 0.021437} + pheS-4CP: {m: -0.000344, b: 0.028558} + +# Per-genotype pleiotropic growth cost is sampled from a shifted lognormal: +# dk_geno = hyper_shift - exp(Normal(hyper_loc, hyper_scale)) +# Most variants are mildly deleterious; a short right tail (at most hyper_shift) +# allows beneficial mutations. Matches the hierarchical tfmodel inference prior. +dk_geno_hyper_loc: -3.5 +dk_geno_hyper_scale: 0.5 +dk_geno_hyper_shift: 0.02 +dk_geno_zero: false + +# Per-genotype TF activity (A) — a scalar that multiplies the theta contribution +# to growth: k = b + A * m * theta. The wild-type is fixed at activity_wt=1.0 +# by inference convention. Set activity_mut_scale > 0 to draw each mutant +# genotype's log(A) independently from Normal(log(activity_wt), activity_mut_scale). +# activity_mut_scale=0 (default) gives all genotypes identical activity, matching +# the 'fixed' activity inference component. +activity_wt: 1.0 +activity_mut_scale: 0.0 + +# --------------------------------------------------------------------------- +# Experimental simulation parameters +# --------------------------------------------------------------------------- + +# Number of transformants per sub-library. Keys must match the characters in +# library_combos (e.g. 'single-1', 'double-1-2') plus 'spiked'. +transform_sizes: + single-1: 100_000 + single-2: 100_000 + double-1-2: 300_000 + spiked: 1000 + +# Mixing ratio of sub-libraries in the pooled library. Only the ratio matters. +library_mixture: + single-1: 100 + single-2: 100 + double-1-2: 1000 + spiked: 100 + +# Sigma of the log-normal used to draw initial genotype frequencies. 0 = flat +# distribution; 1–2 is realistic for a typical transformation. +lib_assembly_skew_sigma: 1.5 + +# Each bacterium receives a number of plasmids drawn from a zero-truncated +# Poisson with this lambda. Set to 0 or null for exactly one plasmid per cell. +transformation_poisson_lambda: 0 + +# When a cell carries multiple plasmids, growth rates are combined via this +# function. Recommended: 'min' for repressors of antibiotic resistance (tightest +# binder dominates); 'max' for inducers. Default: 'mean' for all libraries. +multi_plasmid_combine_fcn: + kanR: "min" + pheS: "max" + +# Colony-forming units per mL at the start of pre-growth. +cfu0: 6.5e7 + +# Per-tube environmental growth-rate noise (hr⁻¹). Each physical tube gets its +# own delta_k ~ Normal(0, tube_noise_sigma); the kt contribution per tube is +# delta_k * (t_pre + t_sel), shared by all genotypes in that tube. A value of +# ~0.001–0.002 hr⁻¹ is typical for well-controlled liquid cultures. +# Set to 0 or null to disable. +tube_noise_sigma: 0.002 + +# --------------------------------------------------------------------------- +# Growth transition model (optional) +# --------------------------------------------------------------------------- + +# Models the lag phase when bacteria switch from pre-selection to selection medium. +# Every condition_pre in condition_blocks must have an entry. Use model: "instant" +# for conditions with no detectable lag. Omit this block to use instant everywhere. +# +# "memory" model: tau = tau0 + k1 / (theta + k2) +# tau0 = baseline lag (minutes) +# k1 = occupancy-dependence of lag (higher = more effect) +# k2 = curvature constant (> 0; prevents division by zero) +growth_transition: + - condition_pre: "pheS-4CP" + model: "instant" + + - condition_pre: "kanR-kan" + model: "instant" + +# --------------------------------------------------------------------------- +# Data collection parameters +# --------------------------------------------------------------------------- + +# Total sequencing reads across all samples. +total_num_reads: 100_000_000 + +# Fraction of reads re-assigned to a randomly chosen genotype (index hopping). +# Set to 0 or null to disable. +prob_index_hop: 0.00 + +# Random seed for reproducibility. Set to null for a random seed each run. +seed: 12345 + +# --------------------------------------------------------------------------- +# Binding data simulation (optional) +# --------------------------------------------------------------------------- + +# If present, tfs-simulate also writes a binding CSV with per-genotype theta +# curves. titrant_name must match e_name in observable_calc_kwargs. + +binding_data: + genotype_params_file: hill_params.csv + titrant_name: iptg + titrant_conc: [0, 0.0001, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0] + noise: 0.001 + +presplit_data: + noise: 0.00 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 52224a25..f9e68b49 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/tfmodel/scripts/extract_params_cli.py b/src/tfscreen/tfmodel/scripts/extract_params_cli.py index 241848d7..cc8154df 100644 --- a/src/tfscreen/tfmodel/scripts/extract_params_cli.py +++ b/src/tfscreen/tfmodel/scripts/extract_params_cli.py @@ -14,12 +14,13 @@ def extract_params(config_file, param_file, out_prefix="tfs_params"): If ``param_file`` ends with ``.pkl`` it is treated as a MAP (AutoDelta) checkpoint: parameters are converted to constrained space and written with - a single ``point_est`` column. + a single ``q0.5`` column. Otherwise it is treated as a posterior samples ``.h5`` or ``.npz`` file - produced by tfs-sample-posterior, and the default quantile set (min, - lower_95, lower_std, lower_quartile, median, upper_quartile, upper_std, - upper_95, max) is computed. + produced by tfs-sample-posterior, and 17 quantile columns are written: + ``q0.001``, ``q0.005``, ``q0.01``, ``q0.025``, ``q0.05``, ``q0.1``, + ``q0.159``, ``q0.25``, ``q0.5``, ``q0.75``, ``q0.841``, ``q0.9``, + ``q0.95``, ``q0.975``, ``q0.99``, ``q0.995``, ``q0.999``. Writes one CSV per parameter group named ``{out_prefix}_{param_name}.csv`` (e.g. tfs_params_activity.csv, tfs_params_theta.csv). From 406f60153bbace3752cbe563ecbbee430ec25673 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 22 Jun 2026 15:04:14 -0700 Subject: [PATCH 22/24] bumped version --- src/tfscreen/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tfscreen/__version__.py b/src/tfscreen/__version__.py index 663dd15d..ff3c18a2 100644 --- a/src/tfscreen/__version__.py +++ b/src/tfscreen/__version__.py @@ -5,5 +5,5 @@ Version string. """ -VERSION = (0, 4, 0) +VERSION = (0, 4, 1) __version__ = '.'.join(map(str, VERSION)) From db01fa8c536ea23b5021b9450b32add4fd15f7bc Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 22 Jun 2026 15:13:04 -0700 Subject: [PATCH 23/24] fixed typo causing test crash in gh workflow --- tests/tfscreen/plot/heatmap/test_heatmap_core.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/tfscreen/plot/heatmap/test_heatmap_core.py b/tests/tfscreen/plot/heatmap/test_heatmap_core.py index a7125ba7..511ee4c5 100644 --- a/tests/tfscreen/plot/heatmap/test_heatmap_core.py +++ b/tests/tfscreen/plot/heatmap/test_heatmap_core.py @@ -641,10 +641,16 @@ def mock_helpers(self, mocker) -> dict: "format_axis": mocker.patch("tfscreen.plot.heatmap.heatmap_core._format_axis"), "colorbar": mocker.patch("matplotlib.figure.Figure.colorbar") } - # The collection needs to be a mock that can be added to an ax + # The collection needs to be a mock that can be added to an ax. + # get_transform().contains_branch_separately() must return a 2-tuple so + # matplotlib's _update_collection_limits can unpack (x_is_data, y_is_data). + # The method was misspelled in matplotlib <3.10 as "seperately"; cover both. mock_p_collection = MagicMock(spec=PatchCollection) - mock_p_collection.get_transform().contains_branch_seperately.return_value = (True, True) - mock_p_collection.get_offset_transform().contains_branch_seperately.return_value = (True, True) + mock_transform = MagicMock() + mock_transform.contains_branch_separately.return_value = (True, True) + mock_transform.contains_branch_seperately.return_value = (True, True) + mock_p_collection.get_transform.return_value = mock_transform + mock_p_collection.get_offset_transform.return_value = mock_transform mocks["build_heatmap_collection"].return_value = mock_p_collection return mocks From 81c97078cf2df836138f73c64f06cb459f4f37f9 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 22 Jun 2026 15:24:12 -0700 Subject: [PATCH 24/24] fix mock that crashes in github workflow --- tests/tfscreen/plot/heatmap/test_heatmap_core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/tfscreen/plot/heatmap/test_heatmap_core.py b/tests/tfscreen/plot/heatmap/test_heatmap_core.py index 511ee4c5..bb350827 100644 --- a/tests/tfscreen/plot/heatmap/test_heatmap_core.py +++ b/tests/tfscreen/plot/heatmap/test_heatmap_core.py @@ -651,6 +651,14 @@ def mock_helpers(self, mocker) -> dict: mock_transform.contains_branch_seperately.return_value = (True, True) mock_p_collection.get_transform.return_value = mock_transform mock_p_collection.get_offset_transform.return_value = mock_transform + # _sticky_edges is an instance attribute (set in Artist.__init__) so it + # is absent from a class-spec'd mock; set it explicitly with the shape + # matplotlib expects: an object with .x and .y list attributes. + mock_sticky = MagicMock() + mock_sticky.x = [] + mock_sticky.y = [] + mock_p_collection._sticky_edges = mock_sticky + mock_p_collection.sticky_edges = mock_sticky mocks["build_heatmap_collection"].return_value = mock_p_collection return mocks