hpvsim 2.2.7__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. {hpvsim-2.2.7 → hpvsim-2.3.0}/.gitignore +3 -0
  2. {hpvsim-2.2.7 → hpvsim-2.3.0}/CHANGELOG.md +18 -1
  3. {hpvsim-2.2.7/hpvsim.egg-info → hpvsim-2.3.0}/PKG-INFO +1 -1
  4. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/analysis.py +0 -1
  5. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/calibration.py +41 -11
  6. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/defaults.py +1 -0
  7. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/interventions.py +34 -4
  8. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/parameters.py +17 -19
  9. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/people.py +30 -34
  10. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/population.py +12 -1
  11. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/sim.py +4 -0
  12. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/version.py +2 -2
  13. {hpvsim-2.2.7 → hpvsim-2.3.0/hpvsim.egg-info}/PKG-INFO +1 -1
  14. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim.egg-info/SOURCES.txt +7 -0
  15. hpvsim-2.3.0/tests/baseline.json +61 -0
  16. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/benchmark.json +3 -3
  17. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_dt.py +1 -1
  18. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_multiscale.py +6 -6
  19. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_multiscale_pars.py +2 -3
  20. hpvsim-2.3.0/tests/generate_v2_baselines.py +446 -0
  21. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_analysis.py +92 -1
  22. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_calibration.py +11 -5
  23. hpvsim-2.3.0/tests/test_immunity.py +219 -0
  24. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_interventions.py +98 -1
  25. hpvsim-2.3.0/tests/test_misc.py +172 -0
  26. hpvsim-2.3.0/tests/test_parameters.py +74 -0
  27. hpvsim-2.3.0/tests/test_people.py +31 -0
  28. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_sim.py +40 -4
  29. hpvsim-2.3.0/tests/test_utils.py +367 -0
  30. hpvsim-2.3.0/tests/test_v2_regression.py +230 -0
  31. hpvsim-2.2.7/tests/baseline.json +0 -60
  32. {hpvsim-2.2.7 → hpvsim-2.3.0}/.github/workflows/pypi_release.yaml +0 -0
  33. {hpvsim-2.2.7 → hpvsim-2.3.0}/.github/workflows/tests.yaml +0 -0
  34. {hpvsim-2.2.7 → hpvsim-2.3.0}/CODE_OF_CONDUCT.md +0 -0
  35. {hpvsim-2.2.7 → hpvsim-2.3.0}/LICENSE +0 -0
  36. {hpvsim-2.2.7 → hpvsim-2.3.0}/MANIFEST.in +0 -0
  37. {hpvsim-2.2.7 → hpvsim-2.3.0}/README.md +0 -0
  38. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/.gitignore +0 -0
  39. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/CNAME +0 -0
  40. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/README.md +0 -0
  41. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/_quarto.yml +0 -0
  42. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/_variables.yml +0 -0
  43. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/assets/favicon.ico +0 -0
  44. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/assets/starsim-logo-dark.png +0 -0
  45. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/assets/starsim-logo.png +0 -0
  46. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/assets/styles-dark.scss +0 -0
  47. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/assets/styles-light.scss +0 -0
  48. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/assets/styles.css +0 -0
  49. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/conduct.md +0 -0
  50. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/index.md +0 -0
  51. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/overview.md +0 -0
  52. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/preview +0 -0
  53. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/publish +0 -0
  54. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/quarto_utils.py +0 -0
  55. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/render +0 -0
  56. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/requirements.txt +0 -0
  57. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/nigeria_cancer_cases.csv +0 -0
  58. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/nigeria_cancer_types.csv +0 -0
  59. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/south_africa_age_pyramid.csv +0 -0
  60. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_analyzers.qmd +0 -0
  61. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_calibration.qmd +0 -0
  62. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_interventions.qmd +0 -0
  63. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_intro.qmd +0 -0
  64. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_people.qmd +0 -0
  65. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_plotting.qmd +0 -0
  66. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials/tut_running.qmd +0 -0
  67. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/tutorials.md +0 -0
  68. {hpvsim-2.2.7 → hpvsim-2.3.0}/docs/whats-new.md +0 -0
  69. {hpvsim-2.2.7 → hpvsim-2.3.0}/examples/t05_screen_algorithms.py +0 -0
  70. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/__init__.py +0 -0
  71. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/base.py +0 -0
  72. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/__init__.py +0 -0
  73. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/downloaders.py +0 -0
  74. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/loaders.py +0 -0
  75. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/products_dx.csv +0 -0
  76. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/products_tx.csv +0 -0
  77. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/products_txvx.csv +0 -0
  78. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/products_vx.csv +0 -0
  79. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/test_downloaders.py +0 -0
  80. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/data/test_loaders.py +0 -0
  81. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/hiv.py +0 -0
  82. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/immunity.py +0 -0
  83. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/misc.py +0 -0
  84. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/plotting.py +0 -0
  85. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/regression/pars_v0.2.6.json +0 -0
  86. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/regression/pars_v0.2.9.json +0 -0
  87. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/regression/pars_v0.3.0.json +0 -0
  88. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/regression/pars_v0.3.1.json +0 -0
  89. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/run.py +0 -0
  90. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/settings.py +0 -0
  91. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim/utils.py +0 -0
  92. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim.egg-info/dependency_links.txt +0 -0
  93. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim.egg-info/entry_points.txt +0 -0
  94. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim.egg-info/requires.txt +0 -0
  95. {hpvsim-2.2.7 → hpvsim-2.3.0}/hpvsim.egg-info/top_level.txt +0 -0
  96. {hpvsim-2.2.7 → hpvsim-2.3.0}/pyproject.toml +0 -0
  97. {hpvsim-2.2.7 → hpvsim-2.3.0}/setup.cfg +0 -0
  98. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/.coveragerc +0 -0
  99. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/README.md +0 -0
  100. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/benchmark_profile.py +0 -0
  101. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/check_coverage +0 -0
  102. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/check_hiv_data.py +0 -0
  103. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/hpv_test_pars.csv +0 -0
  104. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/multiscale_concept1.py +0 -0
  105. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/multiscale_concept2.py +0 -0
  106. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/multiscale_concept3.py +0 -0
  107. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/multiscale_test.df +0 -0
  108. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/mysql_hpvsim_test.py +0 -0
  109. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/mysql_test.py +0 -0
  110. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/network_dx.py +0 -0
  111. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/plot_nathx.py +0 -0
  112. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/setup_mysql +0 -0
  113. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_cancer_dysp.py +0 -0
  114. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_hiv.py +0 -0
  115. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_latency.py +0 -0
  116. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_logistic.py +0 -0
  117. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_merge_scens.py +0 -0
  118. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_networks.py +0 -0
  119. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_new_progs.py +0 -0
  120. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_parameter_exploration.py +0 -0
  121. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_popgrowth.py +0 -0
  122. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_popscale.py +0 -0
  123. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_sampler.py +0 -0
  124. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/test_txvx.py +0 -0
  125. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/devtests/tut_parameter_exploration.ipynb +0 -0
  126. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/hpvsim_v1.2.2.yml +0 -0
  127. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/pytest.ini +0 -0
  128. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/requirements.txt +0 -0
  129. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/run_tests +0 -0
  130. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/simple.py +0 -0
  131. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_baselines.py +0 -0
  132. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/RSA_data.csv +0 -0
  133. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/art_coverage_south_africa.csv +0 -0
  134. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/australia_age_pyramid.csv +0 -0
  135. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/hiv_incidence_south_africa.csv +0 -0
  136. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_cancer_cases.csv +0 -0
  137. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_cancer_deaths.csv +0 -0
  138. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_cancer_types.csv +0 -0
  139. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_cin_types.csv +0 -0
  140. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_data.csv +0 -0
  141. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_hpv_data.csv +0 -0
  142. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/india_hpv_prevalence.csv +0 -0
  143. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/kenya_age_pyramid.csv +0 -0
  144. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/kenya_cancer_incidence.csv +0 -0
  145. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/kenya_cancer_mortality.csv +0 -0
  146. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/kenya_data.csv +0 -0
  147. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/param_space.csv +0 -0
  148. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/param_space_filled.csv +0 -0
  149. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_age_pyramid.csv +0 -0
  150. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_art_coverage_by_age_females.csv +0 -0
  151. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_art_coverage_by_age_males.csv +0 -0
  152. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_cancer_data_2020.csv +0 -0
  153. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_cancer_data_hiv_2020.csv +0 -0
  154. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_cancer_incidence_by_age_no_hiv.csv +0 -0
  155. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_cancer_incidence_by_age_with_hiv.csv +0 -0
  156. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_female_hiv_mortality.csv +0 -0
  157. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_hpv_data.csv +0 -0
  158. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_male_hiv_mortality.csv +0 -0
  159. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_target_data.csv +0 -0
  160. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_type_distribution_cancer.csv +0 -0
  161. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/south_africa_type_distribution_high_grade_lesion.csv +0 -0
  162. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/tanzania_age_pyramid.csv +0 -0
  163. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/tanzania_data.csv +0 -0
  164. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/test_tx_assigner.csv +0 -0
  165. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/test_via.csv +0 -0
  166. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data/test_via_triage.csv +0 -0
  167. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_data.py +0 -0
  168. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_event_schedule.py +0 -0
  169. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/test_run.py +0 -0
  170. {hpvsim-2.2.7 → hpvsim-2.3.0}/tests/update_baseline +0 -0
@@ -8,6 +8,9 @@ _inv/
8
8
  api/
9
9
  .jupyter_cache/
10
10
 
11
+ # Regression baselines (regenerate via tests/generate_v2_baselines.py)
12
+ tests/regression_baselines/
13
+
11
14
  # Other files
12
15
  .idea
13
16
  .vscode
@@ -3,10 +3,27 @@ that may result in differences in model output, or are required in order
3
3
  to run an old parameter set with the current version, are flagged with
4
4
  the term "Regression information".
5
5
 
6
+ ## Version 2.3.0 (2026-04-20)
7
+
8
+ - Fixes dt-dependent results by scaling partnership formation rates to
9
+ per-timestep probabilities; `layer_probs` and cross-layer defaults
10
+ converted to annual probabilities.
11
+ - *Regression information*: If workflows from v2.2.6 or earlier override default `layer_probs`, `f_cross_layer`, or `m_cross_layer` values
12
+ and have timesteps not equal to 1 year, then the probabilities must be converted to annual probabilities instead of per-timestep probabilities using this formula: `1 - (1 - prob) ** dt`
13
+ - *Regression information:* baseline model outputs change; baselines have been regenerated.
14
+ - Fixes `precins` flow never being incremented; removes redundant `dysplasias` flow (was an alias of `cins`).
15
+ - Adds test coverage for previously untested code paths.
16
+ - Vaccine immunity is now sterilizing (all-or-nothing) rather than leaky (per-contact). `imm_init` sets the probability of sterilizing immunity; non-sterilizing recipients get leaky protection at the `imm_init` level. Default is 0.95.
17
+ - Adds per-timestep transmission logging (`sim._transmission_log`) for downstream analysis of transmission chains.
18
+ - Calibration now supports resuming from an existing database via `keep_db=True`, running only the remaining trials.
19
+ - Calibration workers catch exceptions instead of crashing the entire run.
20
+ - Fixes `res_to_plot` indexing bug in `Calibration.plot()`.
21
+ - *Regression information*: vaccine efficacy will differ from previous versions due to the immunity model change.
22
+
6
23
  ## Version 2.2.7 (2026-04-22)
7
24
 
8
25
  - Fix cancer treatment results always being blank: `BaseTreatment.check_eligibility` was excluding cancer patients (preventing radiation from running), `BaseTreatment.apply` was writing to CIN fields for cancer treatment, and `cum_cancer_treated` cumsum used the wrong source array
9
- - *Github info* PR [92](https://github.com/starsimhub/hpvsim/pull/92), issue [91](https://github.com/starsimhub/hpvsim/issues/91)
26
+ - *Github info* PR [94](https://github.com/starsimhub/hpvsim/pull/94), issue [91](https://github.com/starsimhub/hpvsim/issues/91)
10
27
 
11
28
  ## Version 2.2.6 (2026-04-17)
12
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hpvsim
3
- Version: 2.2.7
3
+ Version: 2.3.0
4
4
  Summary: HPVsim: Human Papillomavirus Simulator
5
5
  Author: Robyn Stuart, Jamie Cohen, Cliff Kerr, Romesh Abeysuriya, Mariah Boudreau, Daniel Klein, Hao Hu
6
6
  Maintainer-email: HPVsim Team <info@hpvsim.org>
@@ -742,7 +742,6 @@ class age_results(Analyzer):
742
742
  mapping = {
743
743
  'infections': ['date_exposed', 'infectious'],
744
744
  'cin': ['date_cin', 'cin'],
745
- 'dysplasias': ['date_cin', 'cin'],
746
745
  'cins': ['date_cin', 'cin'],
747
746
  'cancers': ['date_cancerous', 'cancerous'],
748
747
  'cancer': ['date_cancerous', 'cancerous'],
@@ -89,8 +89,10 @@ class Calibration(sc.prettyobj):
89
89
  if keep_db is None: keep_db = False
90
90
  if storage is None: storage = f'sqlite:///{db_name}'
91
91
  if total_trials is not None: n_trials = int(np.ceil(total_trials/n_workers))
92
- self.run_args = sc.objdict(n_trials=int(n_trials), n_workers=int(n_workers), name=name, db_name=db_name,
93
- keep_db=keep_db, storage=storage, rand_seed=rand_seed, sampler=sampler)
92
+ total_trials = int(n_trials * n_workers)
93
+ self.run_args = sc.objdict(n_trials=int(n_trials), n_workers=int(n_workers), total_trials=total_trials,
94
+ name=name, db_name=db_name, keep_db=keep_db, storage=storage,
95
+ rand_seed=rand_seed, sampler=sampler)
94
96
 
95
97
  # Handle other inputs
96
98
  self.label = label
@@ -412,7 +414,11 @@ class Calibration(sc.prettyobj):
412
414
  else:
413
415
  op.logging.set_verbosity(op.logging.ERROR)
414
416
  study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name, sampler = self.run_args.sampler)
415
- output = study.optimize(self.run_trial, n_trials=self.run_args.n_trials, callbacks=None)
417
+ try:
418
+ output = study.optimize(self.run_trial, n_trials=self.run_args.n_trials, callbacks=None)
419
+ except Exception as E:
420
+ print(f'Worker failed with error: {E}')
421
+ output = None
416
422
  return output
417
423
 
418
424
 
@@ -438,9 +444,12 @@ class Calibration(sc.prettyobj):
438
444
  print('Could not delete study, skipping...')
439
445
  print(str(E))
440
446
  if os.path.exists(self.run_args.db_name):
441
- os.remove(self.run_args.db_name)
442
- if self.verbose:
443
- print(f'Removed existing calibration {self.run_args.db_name}')
447
+ try:
448
+ os.remove(self.run_args.db_name)
449
+ if self.verbose:
450
+ print(f'Removed existing calibration {self.run_args.db_name}')
451
+ except PermissionError:
452
+ print(f'Could not remove {self.run_args.db_name} (file in use), skipping...')
444
453
  return
445
454
 
446
455
 
@@ -455,7 +464,8 @@ class Calibration(sc.prettyobj):
455
464
  raise NotImplementedError('Implemented but does not work')
456
465
  else:
457
466
  sampler = None
458
- output = op.create_study(storage=self.run_args.storage, study_name=self.run_args.name, sampler=sampler)
467
+ output = op.create_study(storage=self.run_args.storage, study_name=self.run_args.name, sampler=sampler,
468
+ load_if_exists=self.run_args.keep_db)
459
469
  return output
460
470
 
461
471
 
@@ -485,7 +495,23 @@ class Calibration(sc.prettyobj):
485
495
  # Run the optimization
486
496
  t0 = sc.tic()
487
497
  self.make_study()
488
- self.run_workers()
498
+
499
+ # If resuming with keep_db, check for existing trials and only run the remainder
500
+ if self.run_args.keep_db:
501
+ study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name)
502
+ n_existing = len([t for t in study.trials if t.state == op.trial.TrialState.COMPLETE])
503
+ if n_existing > 0:
504
+ n_remaining = max(0, self.run_args.total_trials - n_existing)
505
+ if n_remaining == 0:
506
+ print(f'Calibration already has {n_existing} completed trials, skipping workers')
507
+ else:
508
+ self.run_args.n_trials = int(np.ceil(n_remaining / self.run_args.n_workers))
509
+ print(f'Resuming calibration: {n_existing} trials complete, running ~{n_remaining} more')
510
+ self.run_workers()
511
+ else:
512
+ self.run_workers()
513
+ else:
514
+ self.run_workers()
489
515
  study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name, sampler = self.run_args.sampler)
490
516
  self.best_pars = sc.objdict(study.best_params)
491
517
  self.elapsed = sc.toc(t0, output=True)
@@ -500,6 +526,7 @@ class Calibration(sc.prettyobj):
500
526
  self.analyzer_results = []
501
527
  self.sim_results = []
502
528
  self.extra_sim_results = []
529
+ loaded_trials = set()
503
530
  if load:
504
531
  print('Loading saved results...')
505
532
  for trial in study.trials:
@@ -510,6 +537,7 @@ class Calibration(sc.prettyobj):
510
537
  self.sim_results.append(results['sim'])
511
538
  self.analyzer_results.append(results['analyzer'])
512
539
  self.extra_sim_results.append(results['extra_sim_results'])
540
+ loaded_trials.add(n)
513
541
  if tidyup:
514
542
  try:
515
543
  os.remove(filename)
@@ -524,7 +552,7 @@ class Calibration(sc.prettyobj):
524
552
 
525
553
  # Compare the results
526
554
  self.initial_pars, self.par_bounds = self.sim_to_sample_pars()
527
- self.parse_study(study)
555
+ self.parse_study(study, loaded_trials=loaded_trials)
528
556
 
529
557
  # Tidy up
530
558
  self.calibrated = True
@@ -534,7 +562,7 @@ class Calibration(sc.prettyobj):
534
562
  return self
535
563
 
536
564
 
537
- def parse_study(self, study):
565
+ def parse_study(self, study, loaded_trials=None):
538
566
  '''Parse the study into a data frame -- called automatically '''
539
567
  best = study.best_params
540
568
  self.best_pars = best
@@ -549,6 +577,8 @@ class Calibration(sc.prettyobj):
549
577
  data[key] = val
550
578
  if data['mismatch'] is None:
551
579
  failed_trials.append(data['index'])
580
+ elif loaded_trials is not None and trial.number not in loaded_trials:
581
+ failed_trials.append(data['index'])
552
582
  else:
553
583
  results.append(data)
554
584
  print(f'Processed {n_trials} trials; {len(failed_trials)} failed')
@@ -653,7 +683,7 @@ class Calibration(sc.prettyobj):
653
683
 
654
684
  # determine how many results to plot
655
685
  if res_to_plot is not None:
656
- index_to_plot = self.df.iloc[0:res_to_plot, 0].values
686
+ index_to_plot = self.df.index[0:res_to_plot].values
657
687
  analyzer_results = [analyzer_results[i] for i in index_to_plot]
658
688
  sim_results = [sim_results[i] for i in index_to_plot]
659
689
 
@@ -271,6 +271,7 @@ class Flow():
271
271
 
272
272
  flows = [
273
273
  Flow('infections', color='#c78f65', label='Infections'),
274
+ Flow('precins', color='#c1ad71', label='Pre-CINs'),
274
275
  Flow('cins', color='#b86113', label='CINs'),
275
276
  Flow('cancers', color='#5f5cd2', label='Cancers'),
276
277
  Flow('cancer_deaths', color='#000000', label='Cancer deaths', by_genotype=False),
@@ -1414,22 +1414,52 @@ class tx(Product):
1414
1414
 
1415
1415
 
1416
1416
  class vx(Product):
1417
- ''' Vaccine product '''
1417
+ '''
1418
+ Vaccine product.
1419
+
1420
+ imm_init controls the probability that a vaccinated person gets sterilizing
1421
+ (all-or-nothing) immunity. People who don't get sterilizing immunity still
1422
+ receive leaky (per-contact) protection equal to imm_init.
1423
+
1424
+ imm_init can be:
1425
+ - a float (e.g. 0.95): used directly as the sterilizing probability
1426
+ - a dict with dist/par keys (legacy beta-distribution format): the
1427
+ mean of the distribution is used as the sterilizing probability
1428
+ '''
1418
1429
  def __init__(self, genotype_pars=None, imm_init=None, imm_boost=None):
1419
1430
  self.genotype_pars = genotype_pars
1420
- self.imm_init = imm_init
1421
1431
  self.imm_boost = imm_boost
1422
1432
  self.imm_source = None # Set during immunity initialization. Warning, fragile!!!
1423
1433
  if (imm_init is None and imm_boost is None) or (imm_init is not None and imm_boost is not None):
1424
1434
  errormsg = 'Must provide either an initial immune effect (for first doses) or an immune boosting effect (for subsequent doses), not both/neither.'
1425
1435
  raise ValueError(errormsg)
1426
1436
 
1437
+ # Convert imm_init to a float (sterilizing probability)
1438
+ if imm_init is not None:
1439
+ if isinstance(imm_init, dict):
1440
+ if imm_init.get('dist') == 'beta':
1441
+ a, b = imm_init['par1'], imm_init['par2']
1442
+ self.imm_init = a / (a + b)
1443
+ elif imm_init.get('dist') == 'beta_mean':
1444
+ self.imm_init = imm_init['par1']
1445
+ else:
1446
+ # Estimate the mean of an unknown distribution via Monte Carlo;
1447
+ # 10k samples is just for accuracy, not related to agent count
1448
+ self.imm_init = float(np.mean(hpu.sample(**imm_init, size=10000)))
1449
+ else:
1450
+ self.imm_init = float(imm_init)
1451
+ else:
1452
+ self.imm_init = None
1453
+
1427
1454
 
1428
1455
  def administer(self, people, inds):
1429
1456
  ''' Apply the vaccine to the requested people indices. '''
1430
1457
  inds = inds[people.alive[inds]] # Skip anyone that is dead
1431
1458
  if self.imm_init is not None:
1432
- people.peak_imm[self.imm_source, inds] = hpu.sample(**self.imm_init, size=len(inds))* people.rel_imm[inds]
1459
+ n = len(inds)
1460
+ sterilizing = np.random.random(n) < self.imm_init
1461
+ peak = np.where(sterilizing, 1.0, self.imm_init)
1462
+ people.peak_imm[self.imm_source, inds] = peak * people.rel_imm[inds]
1433
1463
  elif self.imm_boost is not None:
1434
1464
  people.peak_imm[self.imm_source, inds] *= self.imm_boost
1435
1465
  people.t_imm_event[self.imm_source, inds] = people.t
@@ -1515,7 +1545,7 @@ def default_vx(prod_name=None):
1515
1545
  dfvx = pd.read_csv(datafiles.vx) # Read in dataframe with parameters
1516
1546
  vxprods = dict()
1517
1547
  for name in dfvx.name.unique():
1518
- vxprods[name] = vx(genotype_pars=dfvx[dfvx.name==name], imm_init=dict(dist='beta', par1=30, par2=2))
1548
+ vxprods[name] = vx(genotype_pars=dfvx[dfvx.name==name], imm_init=0.95)
1519
1549
  vxprods[name+'2'] = vx(genotype_pars=dfvx[dfvx.name==name], imm_boost=1.2) # 2nd dose
1520
1550
  vxprods[name+'3'] = vx(genotype_pars=dfvx[dfvx.name==name], imm_boost=1.1) # 3rd dose
1521
1551
  if prod_name is not None: return vxprods[prod_name]
@@ -73,8 +73,8 @@ def make_pars(**kwargs):
73
73
  pars['add_mixing'] = None # Mixing matrix between clusters
74
74
  pars['debut'] = dict(f=dict(dist='normal', par1=15.0, par2=2.1), # Location-specific data should be used here if possible
75
75
  m=dict(dist='normal', par1=17.6, par2=1.8))
76
- pars['f_cross_layer'] = 0.05 # Proportion of females who have concurrent cross-layer relationships - by layer
77
- pars['m_cross_layer'] = 0.30 # Proportion of males who have concurrent cross-layer relationships - by layer
76
+ pars['f_cross_layer'] = 0.185 # Annual probability of females having concurrent cross-layer relationships
77
+ pars['m_cross_layer'] = 0.760 # Annual probability of males having concurrent cross-layer relationships
78
78
  pars['f_partners'] = None # Distribution of preferred number of concurrent sexual partners, females
79
79
  pars['m_partners'] = None # Distribution of preferred number of concurrent sexual partners, males
80
80
  pars['acts'] = None # The number of sexual acts for each partnership type per year
@@ -578,16 +578,14 @@ def get_mixing(network=None):
578
578
 
579
579
  layer_probs = dict(
580
580
  m=np.array([
581
- [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75],
582
- [ 0, 0, 0.01, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.3, 0.2, 0.1, 0.05, 0.01], # Share of females of each age who are actively seeking marriage if underpartnered
583
- [ 0, 0, 0.01, 0.2, 0.3, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.3, 0.2, 0.1, 0.05, 0.01]] # Share of males of each age who are actively seeking marriage if underpartnered
584
- # [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] # Share of males of each age who are actively seeking marriage if underpartnered
581
+ [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75],
582
+ [ 0, 0, 0.0394, 0.938, 0.938, 0.938, 0.938, 0.938, 0.938, 0.938, 0.938, 0.760, 0.590, 0.344, 0.185, 0.0394], # Annual prob of females seeking marriage if underpartnered
583
+ [ 0, 0, 0.0394, 0.590, 0.760, 0.938, 0.938, 0.938, 0.938, 0.938, 0.938, 0.760, 0.590, 0.344, 0.185, 0.0394]] # Annual prob of males seeking marriage if underpartnered
585
584
  ),
586
585
  c=np.array([
587
- [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75],
588
- [ 0, 0, 0.2, 0.6, 0.8, 0.6, 0.4, 0.4, 0.4, 0.1, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], # Share of females of each age actively seeking casual relationships if underpartnered
589
- [ 0, 0, 0.2, 0.4, 0.4, 0.4, 0.4, 0.6, 0.8, 0.6, 0.2, 0.1, 0.05, 0.02, 0.02, 0.02]] # Share of males of each age actively seeking casual relationships if underpartnered
590
- # [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] # Share of males of each age actively seeking casual relationships if underpartnered
586
+ [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75],
587
+ [ 0, 0, 0.590, 0.974, 0.998, 0.974, 0.870, 0.870, 0.870, 0.344, 0.0776, 0.0776, 0.0776, 0.0776, 0.0776, 0.0776], # Annual prob of females seeking casual relationships if underpartnered
588
+ [ 0, 0, 0.590, 0.870, 0.870, 0.870, 0.870, 0.974, 0.998, 0.974, 0.590, 0.344, 0.185, 0.0776, 0.0776, 0.0776]] # Annual prob of males seeking casual relationships if underpartnered
591
589
  )
592
590
  )
593
591
 
@@ -615,9 +613,9 @@ def get_mixing(network=None):
615
613
  )
616
614
  layer_probs = dict(
617
615
  a=np.array([
618
- [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75],
619
- [ 0, 0, 0.04, 0.2, 0.6, 0.8, 0.8, 0.8, 0.75, 0.65, 0.55, 0.4, 0.4, 0.4, 0.4, 0.4], # Share of females of each age who are married
620
- [ 0, 0, 0.01, 0.01, 0.2, 0.6, 0.8, 0.9, 0.90, 0.90, 0.90, 0.8, 0.7, 0.6, 0.5, 0.6]] # Share of males of each age who are married
616
+ [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75],
617
+ [ 0, 0, 0.151, 0.590, 0.974, 0.998, 0.998, 0.998, 0.996, 0.985, 0.959, 0.870, 0.870, 0.870, 0.870, 0.870], # Annual prob of females seeking partnership if underpartnered
618
+ [ 0, 0, 0.0394, 0.0394, 0.590, 0.974, 0.998, 0.999, 0.999, 0.999, 0.999, 0.998, 0.992, 0.974, 0.938, 0.974]] # Annual prob of males seeking partnership if underpartnered
621
619
  ))
622
620
 
623
621
  else:
@@ -636,42 +634,42 @@ def get_vaccine_dose_pars(default=False, vaccine=None):
636
634
  pars = dict(
637
635
 
638
636
  default = dict(
639
- imm_init = dict(dist='beta', par1=30, par2=2), # Initial distribution of immunity
637
+ imm_init = 0.95, # Probability of sterilizing immunity
640
638
  doses = 1, # Number of doses for this vaccine
641
639
  interval = None, # Interval between doses
642
640
  imm_boost=None, # For vaccines wiht >1 dose, the factor by which each additional boost increases immunity
643
641
  ),
644
642
 
645
643
  bivalent = dict(
646
- imm_init=dict(dist='beta', par1=30, par2=2), # Initial distribution of immunity
644
+ imm_init=0.95, # Probability of sterilizing immunity
647
645
  doses=1, # Number of doses for this vaccine
648
646
  interval=None, # Interval between doses
649
647
  imm_boost=None, # For vaccines wiht >1 dose, the factor by which each additional boost increases immunity
650
648
  ),
651
649
 
652
650
  bivalent_2dose = dict(
653
- imm_init=dict(dist='beta', par1=30, par2=2), # Initial distribution of immunity
651
+ imm_init=0.95, # Probability of sterilizing immunity
654
652
  doses=2, # Number of doses for this vaccine
655
653
  interval=0.5, # Interval between doses in years
656
654
  imm_boost=1.2, # For vaccines wiht >1 dose, the factor by which each additional boost increases immunity
657
655
  ),
658
656
 
659
657
  bivalent_3dose = dict(
660
- imm_init=dict(dist='beta', par1=30, par2=2), # Initial distribution of immunity
658
+ imm_init=0.95, # Probability of sterilizing immunity
661
659
  doses=3, # Number of doses for this vaccine
662
660
  interval=[0.2, 0.5], # Interval between doses in years
663
661
  imm_boost=[1.2, 1.1], # Factor by which each dose increases immunity
664
662
  ),
665
663
 
666
664
  quadrivalent = dict(
667
- imm_init=dict(dist='beta', par1=30, par2=2), # Initial distribution of immunity
665
+ imm_init=0.95, # Probability of sterilizing immunity
668
666
  doses=1, # Number of doses for this vaccine
669
667
  interval=None, # Interval between doses
670
668
  imm_boost=None, # For vaccines wiht >1 dose, the factor by which each additional boost increases immunity
671
669
  ),
672
670
 
673
671
  nonavalent = dict(
674
- imm_init=dict(dist='beta', par1=30, par2=2), # Initial distribution of immunity
672
+ imm_init=0.95, # Probability of sterilizing immunity
675
673
  doses=1, # Number of doses for this vaccine
676
674
  interval=None, # Interval between doses
677
675
  imm_boost=None, # For vaccines wiht >1 dose, the factor by which each additional boost increases immunity
@@ -458,6 +458,16 @@ class People(hpb.BasePeople):
458
458
  # Initialize
459
459
  new_pships = dict()
460
460
 
461
+ # Scale annual participation rates to per-timestep probabilities so that
462
+ # partnership formation rates are consistent regardless of dt (issue #13)
463
+ dt = self.dt
464
+ scaled_layer_probs = {lkey: lp.copy() for lkey, lp in layer_probs.items()}
465
+ for lp in scaled_layer_probs.values():
466
+ lp[1, :] = 1 - (1 - lp[1, :]) ** dt # Female participation
467
+ lp[2, :] = 1 - (1 - lp[2, :]) ** dt # Male participation
468
+ scaled_f_cross = 1 - (1 - f_cross_layer) ** dt
469
+ scaled_m_cross = 1 - (1 - m_cross_layer) ** dt
470
+
461
471
  # Loop over layers
462
472
  lno = 0
463
473
  for lkey in self.layer_keys():
@@ -471,9 +481,9 @@ class People(hpb.BasePeople):
471
481
  is_female=self.is_female,
472
482
  is_active=self.is_active,
473
483
  mixing=mixing[lkey],
474
- layer_probs=layer_probs[lkey],
475
- f_cross_layer=f_cross_layer,
476
- m_cross_layer=m_cross_layer,
484
+ layer_probs=scaled_layer_probs[lkey],
485
+ f_cross_layer=scaled_f_cross,
486
+ m_cross_layer=scaled_m_cross,
477
487
  durations=dur_pship[lkey],
478
488
  acts=acts[lkey],
479
489
  age_act_pars=age_act_pars[lkey],
@@ -942,31 +952,6 @@ class People(hpb.BasePeople):
942
952
  ) # These are not indices, so they scale differently
943
953
 
944
954
  # %% Methods to make events occur (death, infection, others TBC)
945
- def make_naive(self, inds):
946
- """
947
- Make a set of people naive. This is used during dynamic resampling.
948
-
949
- Args:
950
- inds (array): list of people to make naive
951
- """
952
- for key in self.meta.states:
953
- if key in ["susceptible"]:
954
- self[key][:, inds] = True
955
- elif key in ["other_dead"]:
956
- self[key][inds] = False
957
- else:
958
- self[key][:, inds] = False
959
-
960
- # Reset immunity
961
- for key in self.meta.imm_states:
962
- self[key][:, inds] = 0
963
-
964
- # Reset dates
965
- for key in self.meta.dates + self.meta.durs:
966
- self[key][:, inds] = np.nan
967
-
968
- return
969
-
970
955
  def infect(self, inds, g=None, layer=None):
971
956
  """
972
957
  Infect people and determine their eventual outcomes.
@@ -1049,6 +1034,14 @@ class People(hpb.BasePeople):
1049
1034
  f_inds = hpu.itruei(self.is_female, inds)
1050
1035
  m_inds = hpu.itruei(self.is_male, inds)
1051
1036
 
1037
+ # Count new precin cases (females entering the precin state)
1038
+ if layer != "seed_infection" and layer != "reactivation" and len(f_inds) > 0:
1039
+ self.flows["precins"] += self.scale_flows(f_inds)
1040
+ self.genotype_flows["precins"][g] += self.scale_flows(f_inds)
1041
+ self.age_flows["precins"] += np.histogram(
1042
+ self.age[f_inds], bins=self.age_bin_edges, weights=self.scale[f_inds]
1043
+ )[0]
1044
+
1052
1045
  # Compute disease progression for females
1053
1046
  if len(f_inds) > 0:
1054
1047
  gpars = self.pars["genotype_pars"][g]
@@ -1168,10 +1161,12 @@ class People(hpb.BasePeople):
1168
1161
 
1169
1162
  intro = f"\nThis is the story of {uid}, a {p.age:.0f} year old {sex}."
1170
1163
  intro += f"\n{uid} became sexually active at age {p.debut:.0f}."
1171
- if not p.susceptible:
1172
- if ~np.isnan(p.date_infectious):
1164
+ if not np.all(p.susceptible):
1165
+ dates_inf = p.date_infectious
1166
+ if np.any(~np.isnan(dates_inf)):
1167
+ earliest = np.nanmin(dates_inf)
1173
1168
  print(
1174
- f"{intro}\n{uid} contracted HPV on timestep {p.date_infectious} of the simulation."
1169
+ f"{intro}\n{uid} contracted HPV on timestep {earliest:.0f} of the simulation."
1175
1170
  )
1176
1171
  else:
1177
1172
  print(f"{intro}\n{uid} did not contract HPV during the simulation.")
@@ -1196,13 +1191,14 @@ class People(hpb.BasePeople):
1196
1191
  events = []
1197
1192
 
1198
1193
  dates = {
1199
- "date_HPV_clearance": "HPV cleared",
1194
+ "date_clearance": "HPV cleared",
1200
1195
  }
1201
1196
 
1202
1197
  for attribute, message in dates.items():
1203
1198
  date = getattr(p, attribute)
1204
- if not np.isnan(date):
1205
- events.append((date, message))
1199
+ if np.any(~np.isnan(date)):
1200
+ earliest = np.nanmin(date)
1201
+ events.append((earliest, message))
1206
1202
 
1207
1203
  if len(events):
1208
1204
  for timestep, event in sorted(events, key=lambda x: x[0]):
@@ -110,6 +110,17 @@ def make_people(sim, popdict=None, reset=False, verbose=None, use_age_data=True,
110
110
  # Create the contacts
111
111
  lkeys = sim['acts'].keys() # TODO: consider a more robust way to do this
112
112
  if microstructure in ['random', 'default']:
113
+ # Scale annual participation rates to per-timestep probabilities so
114
+ # that initial-network formation matches ongoing per-timestep rates
115
+ # (issue #13). Matches the scaling in People.create_partnerships.
116
+ dt = sim['dt']
117
+ scaled_layer_probs = {k: v.copy() for k, v in sim['layer_probs'].items()}
118
+ for lp in scaled_layer_probs.values():
119
+ lp[1, :] = 1 - (1 - lp[1, :]) ** dt
120
+ lp[2, :] = 1 - (1 - lp[2, :]) ** dt
121
+ scaled_f_cross = 1 - (1 - sim['f_cross_layer']) ** dt
122
+ scaled_m_cross = 1 - (1 - sim['m_cross_layer']) ** dt
123
+
113
124
  contacts = dict()
114
125
  current_partners = np.zeros((len(lkeys),n_agents))
115
126
  lno=0
@@ -117,7 +128,7 @@ def make_people(sim, popdict=None, reset=False, verbose=None, use_age_data=True,
117
128
  contacts[lkey], current_partners,_,_ = make_contacts(
118
129
  lno=lno, tind=0, partners=partners[lno,:], current_partners=current_partners, ages=ages,
119
130
  debuts=debuts, is_female=is_female, is_active=is_active, mixing=sim['mixing'][lkey],
120
- layer_probs=sim['layer_probs'][lkey], f_cross_layer=sim['f_cross_layer'], m_cross_layer=sim['m_cross_layer'],
131
+ layer_probs=scaled_layer_probs[lkey], f_cross_layer=scaled_f_cross, m_cross_layer=scaled_m_cross,
121
132
  durations=sim['dur_pship'][lkey], acts=sim['acts'][lkey], age_act_pars=sim['age_act_pars'][lkey],
122
133
  cluster=cluster, add_mixing=sim['add_mixing'], **kwargs
123
134
  )
@@ -794,6 +794,8 @@ class Sim(hpb.BaseSim):
794
794
  for lkey, layer in people.contacts.items():
795
795
 
796
796
  sus = people.susceptible.copy() # for each layer, update who's still susceptible
797
+ if not hasattr(self, '_transmission_log'):
798
+ self._transmission_log = []
797
799
 
798
800
  # Shorten variables
799
801
  f = layer['f']
@@ -822,6 +824,8 @@ class Sim(hpb.BaseSim):
822
824
  transmissions = (np.random.random(len(betas)) < betas).nonzero()[0] # Apply probabilities to determine partnerships in which transmission occurred
823
825
  target_inds = targets[transmissions] # Extract indices of those who got infected
824
826
  target_inds, unique_inds = np.unique(target_inds, return_index=True) # Due to multiple partnerships, some people will be counted twice; remove them
827
+ source_inds = sources[transmissions[unique_inds]]
828
+ self._transmission_log.append((source_inds, target_inds, lkey, g))
825
829
  people.infect(inds=target_inds, g=g, layer=lkey) # Infect people
826
830
 
827
831
  # Determine if there are any reactivated infections on this timestep
@@ -4,6 +4,6 @@ Version and license information.
4
4
 
5
5
  __all__ = ['__version__', '__versiondate__', '__license__']
6
6
 
7
- __version__ = '2.2.7'
8
- __versiondate__ = '2026-04-22'
7
+ __version__ = '2.3.0'
8
+ __versiondate__ = '2026-04-20'
9
9
  __license__ = f'HPVsim {__version__} ({__versiondate__}) — © 2023-2026 by the Gates Foundation'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hpvsim
3
- Version: 2.2.7
3
+ Version: 2.3.0
4
4
  Summary: HPVsim: Human Papillomavirus Simulator
5
5
  Author: Robyn Stuart, Jamie Cohen, Cliff Kerr, Romesh Abeysuriya, Mariah Boudreau, Daniel Klein, Hao Hu
6
6
  Maintainer-email: HPVsim Team <info@hpvsim.org>
@@ -82,6 +82,7 @@ tests/baseline.json
82
82
  tests/benchmark.json
83
83
  tests/benchmark_profile.py
84
84
  tests/check_coverage
85
+ tests/generate_v2_baselines.py
85
86
  tests/hpvsim_v1.2.2.yml
86
87
  tests/pytest.ini
87
88
  tests/requirements.txt
@@ -92,9 +93,15 @@ tests/test_baselines.py
92
93
  tests/test_calibration.py
93
94
  tests/test_data.py
94
95
  tests/test_event_schedule.py
96
+ tests/test_immunity.py
95
97
  tests/test_interventions.py
98
+ tests/test_misc.py
99
+ tests/test_parameters.py
100
+ tests/test_people.py
96
101
  tests/test_run.py
97
102
  tests/test_sim.py
103
+ tests/test_utils.py
104
+ tests/test_v2_regression.py
98
105
  tests/update_baseline
99
106
  tests/devtests/check_hiv_data.py
100
107
  tests/devtests/hpv_test_pars.csv
@@ -0,0 +1,61 @@
1
+ {
2
+ "summary": {
3
+ "infections": 25397265.0,
4
+ "precins": 8005294.0,
5
+ "cins": 1208087.46875,
6
+ "cancers": 21194.517578125,
7
+ "cancer_deaths": 21194.517578125,
8
+ "reinfections": 19518902.5,
9
+ "reactivations": 0.0,
10
+ "n_susceptible": 586788800.0,
11
+ "n_infectious": 32725578.0,
12
+ "n_inactive": 187010.4375,
13
+ "n_normal": 306995072.0,
14
+ "n_cin": 2937310.75,
15
+ "n_cancerous": 187010.4375,
16
+ "n_infected": 32912586.0,
17
+ "n_abnormal": 3124321.0,
18
+ "n_latent": 0.0,
19
+ "n_precin": 12149445.0,
20
+ "n_screened": 2515913.5,
21
+ "n_cin_treated": 0.0,
22
+ "n_cancer_treated": 0.0,
23
+ "n_vaccinated": 1284138.375,
24
+ "n_tx_vaccinated": 11888876.0,
25
+ "hpv_incidence": 0.021640891066768825,
26
+ "cin_incidence": 0.008250109718770863,
27
+ "cancer_incidence": 14.207040688510245,
28
+ "births": 7941710.1673,
29
+ "other_deaths": 3009621.5,
30
+ "migration": 299216.7096,
31
+ "asr_cancer_incidence": 18.621130304934443,
32
+ "asr_cancer_mortality": 19.734067713347496,
33
+ "new_vaccinated": 99738.90625,
34
+ "cum_vaccinated": 1371409.9609375,
35
+ "new_doses": 99738.90625,
36
+ "cum_doses": 1408812.05078125,
37
+ "new_txvx_doses": 12327728.0,
38
+ "new_tx_vaccinated": 11900097.25,
39
+ "cum_txvx_doses": 12327728.0,
40
+ "cum_tx_vaccinated": 11900097.25,
41
+ "new_screens": 2580744.5,
42
+ "new_screened": 2515914.125,
43
+ "new_cin_treatments": 0.0,
44
+ "new_cin_treated": 0.0,
45
+ "new_cancer_treatments": 0.0,
46
+ "new_cancer_treated": 0.0,
47
+ "cum_screens": 2580744.5,
48
+ "cum_screened": 2515914.125,
49
+ "cum_cin_treatments": 0.0,
50
+ "cum_cin_treated": 0.0,
51
+ "cum_cancer_treatments": 0.0,
52
+ "cum_cancer_treated": 0.0,
53
+ "cancer_mortality": 14.189253574665305,
54
+ "n_alive": 306995072.0,
55
+ "cdr": 0.00980348472824997,
56
+ "cbr": 0.025869178015013868,
57
+ "hpv_prevalence": 0.10659968509201347,
58
+ "precin_prevalence": 0.040668902998381044,
59
+ "cin_prevalence": 0.019664635869021486
60
+ }
61
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "time": {
3
- "initialize": 0.015,
4
- "run": 1.809
3
+ "initialize": 0.011,
4
+ "run": 1.078
5
5
  },
6
6
  "parameters": {
7
7
  "n_agents": 10000,
@@ -10,5 +10,5 @@
10
10
  "n_interventions": 7,
11
11
  "n_analyzers": 0
12
12
  },
13
- "cpu_performance": 0.3104645325856668
13
+ "cpu_performance": 0.5786863801131209
14
14
  }