Skip to content

PyPI readiness: Finalization of patchsim#3

Merged
snehasaisneha merged 55 commits intodevfrom
feature
Apr 7, 2026
Merged

PyPI readiness: Finalization of patchsim#3
snehasaisneha merged 55 commits intodevfrom
feature

Conversation

@shreyamukherji
Copy link
Copy Markdown
Collaborator

@shreyamukherji shreyamukherji commented Mar 31, 2026

This PR prepares patchsim for PyPI publication by addressing code hygiene, packaging, SDK usability, CLI structure, setting up CI/CD through github actions and documentation improvements.
It brings patchsim to a state ready for PyPI publication and external usage.

Summary by CodeRabbit

  • New Features

    • Subcommand-based CLI (init, run, validate, list-models, version), SDK exports, project scaffolding, per‑patch parameter overrides, and arrow-notation transitions.
  • Documentation

    • Major docs expansion, README enhancements, and revamped site navigation with getting-started and reference guides.
  • Bug Fixes / Validation

    • Stricter config/seed validation, improved simulation setup checks, plotting safeguards, and clearer patch titles when parameters present.
  • Tests

    • Added unit, integration, and smoke test suites.
  • Chores

    • CI/publish workflows, pre-commit, packaging/metadata, .gitignore, and template updates.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds GitHub Actions CI and PyPI publish workflows, pre-commit config, packaging metadata, extensive docs and templates, a public SDK entrypoint and subcommand CLI, refactors simulation core for per‑patch parameters and evaluated transition expressions with a Model wrapper, updates visualization/logging, and adds a broad pytest suite.

Changes

Cohort / File(s) Summary
CI / Publish Workflows
​.github/workflows/ci.yml, ​.github/workflows/publish.yml
Add CI workflow with two jobs (test-and-lintbuild) triggered on pushes to main/feature/dev/develop and PRs; add release-triggered/manual Publish workflow that runs tests, lint, builds, and uploads to PyPI.
Pre-commit
​.pre-commit-config.yaml
Introduce pre-commit hooks for EOF/trailing-whitespace fixes, YAML/TOML checks, and Ruff fix/format hooks.
Packaging & Build
pyproject.toml
Switch to Hatchling build-system; add authors/classifiers/urls; add rich dependency; replace dependency-groups with project.optional-dependencies (dev, notebook); update Ruff config and hatch build targets (wheel/sdist include lists).
Docs & Site
docs/*, mkdocs.yml, README.md
Add and expand documentation pages (architecture, configuration, getting-started, mathematical model, network design, workflow, results, references); upgrade MkDocs Material config; update README with SDK/CLI examples and PyPI install instruction.
Config Samples & Templates
configs/sample-sir-ode.yaml, src/patchsim/templates/project/config.yaml
Standardize Transitions to arrow-map syntax ("S -> I": "beta"), adjust sample beta, add project config template including PatchParameters and transition examples.
Core Simulation Engine
src/patchsim/core/model.py, src/patchsim/core/simulation.py, src/patchsim/core/model_runner.py
Refactor transitions to arrow labels; evaluate string rate expressions in a guarded scope; compute per-capita, network-weighted FOI; apply per-patch parameter overrides; add Model wrapper that builds RHS, solves with odeint, and visualizes.
Public SDK & CLI
src/patchsim/__init__.py, src/patchsim/cli.py
Add top-level SDK exports and __version__; refactor CLI to argparse subcommands (init, run, validate, list-models) with logging, project scaffolding, and safer file ops.
Utilities
src/patchsim/utils/logger.py, src/patchsim/utils/loader.py, src/patchsim/utils/viz.py
Logger now logs per-patch override entries; loader uses stdlib logger name; viz validates non-empty patches and accepts optional patch_parameters to annotate subplot titles.
Models & Examples
src/patchsim/models/ka_fmd_sirsv_discrete.py
Stylistic and formatting updates (quoting, spacing, multiline expressions) without behavioral changes.
Templates / Git
​.gitignore, src/patchsim/templates/project/...
Add explicit negation rules to keep template CSV assets tracked and add project template files (config and data placeholders).
Tests & Fixtures
tests/*, test.py
Add pytest fixtures and tests covering config loading, rate computation, derivatives, simulation setup, CLI smoke/integration tests; add repository-root test.py placeholder docstring.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    participant User
    participant CLI_SDK
    end
    rect rgba(200,255,200,0.5)
    participant Simulation
    participant Model
    end
    rect rgba(255,200,200,0.5)
    participant ODE_Solver
    participant Visualization
    end

    User->>CLI_SDK: run command / load_config()
    CLI_SDK->>Simulation: setup_simulation(config)
    Simulation->>Simulation: parse Transitions, build patches and parameters
    Simulation->>Model: instantiate Model(network_model, compartments)
    Model->>Model: construct_ode() (build rhs using per-patch FOI)
    CLI_SDK->>Model: solve(y0, t_range)
    Model->>ODE_Solver: odeint(rhs, y0, t_range)
    ODE_Solver-->>Model: solution arrays
    Model->>Visualization: visualize(results, patches, patch_parameters)
    Visualization-->>User: write plots / outputs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble YAML, arrows bright and clear,

Patches hum and parameters appear.
CI queues tests that hop in line,
ODEs tumble, networks intertwine.
PatchSim bounds forward — carrot-fueled, refined!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'PyPI readiness: Finalization of patchsim' accurately summarizes the main objective of this comprehensive PR, which is preparing the project for PyPI publication through packaging setup, CI/CD, SDK refactoring, and documentation improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/patchsim/core/model.py (1)

118-132: ⚠️ Potential issue | 🟠 Major

These derivatives are still patch-local, so network_matrix has no effect.

At this point the method only applies rates computed from each patch’s local state; neither self.network nor compute_force_of_infection() feeds into those rates. simulate_ode() and simulate_discrete() therefore produce the same trajectories for connected and disconnected multi-patch models, so cross-patch transmission is missing from this public API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/model.py` around lines 118 - 132, The derivatives are
computed only from local rates so cross-patch transmission is ignored; update
the loop that applies rates (the block using rates =
self.base_model.compute_rates(patch_state) and iterating
self.base_model.transitions) to incorporate network-mediated force of infection
by calling or computing per-patch force via compute_force_of_infection (and/or
using self.network/network_matrix) and adjusting any infection-type transition
rates before updating derivatives. Concretely: for each patch i, compute foI_i =
self.compute_force_of_infection(i, full_state_or_patch_states) (or derive it
from self.network), then modify the relevant transition rate(s) in
rates[transition_label] (e.g., multiply or replace infection transition rates)
prior to applying the decrement/increment to derivatives[f"{source}_{i}"] and
derivatives[f"{target}_{i}"]; keep restoring self.base_model.parameters
(old_params) as before. Ensure compute_force_of_infection and self.network are
fed the appropriate state vector so simulate_ode()/simulate_discrete() reflect
connected dynamics.
🧹 Nitpick comments (5)
test.py (1)

1-4: Consider removing this unused file.

An empty deprecated file at the repository root serves no purpose and may confuse developers or tooling. If it's genuinely required for compatibility (as the docstring claims), please clarify what requires it. Otherwise, remove this file entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test.py` around lines 1 - 4, test.py is an empty deprecated compatibility
placeholder that should either be removed or clarified; if it's not required,
delete test.py to avoid confusion, and remove any references to it in
packaging/CI; if it must remain, replace the current vague docstring with a
clear statement naming what requires the file (e.g., "Required by X
tooling/version Y for compatibility"), add a TODO with a removal timeframe, and
optionally add a unit test or comment linking to the external requirement so
future maintainers can understand why test.py exists.
docs/getting-started.md (1)

5-13: Consider adding PyPI installation instructions.

Since the PR prepares patchsim for PyPI publication, the "Getting Started" page should include PyPI installation as the primary method:

pip install patchsim

The current git clone + editable install is more appropriate for contributors/developers. Consider restructuring this section to show PyPI installation first, with development installation in a separate "For Contributors" or "Development Setup" subsection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/getting-started.md` around lines 5 - 13, Update the "Installation"
section to present PyPI as the primary install method by placing a single-line
instruction "pip install patchsim" first under the "Installation" heading, then
move the current git clone + pip install -e . block into a new subsection titled
"For Contributors" or "Development Setup" (or similar) and keep the editable
install instructions there; ensure headings "Installation" and the new
contributor subsection are updated accordingly so readers see PyPI install first
and development install separately.
docs/mathematical-model.md (1)

10-15: Add one explicit index-mapping sentence for W_ij.

To avoid source/target confusion, explicitly state that j is source and i is target (matching source,target CSV). This reduces implementation ambiguity when building the matrix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/mathematical-model.md` around lines 10 - 15, Clarify the index mapping
for W_ij by adding a sentence stating that in the weight matrix W, the first
index i is the target (receiving) patch and the second index j is the source
(originating) patch, so W_ij denotes the weight from source j to target i;
reference this explicitly alongside the force-of-infection formula λ_i(t) = Σ_j
W_ij * (I_j / N_j) to match the source, target ordering used in the CSV and
avoid confusion between I_j and N_j as source quantities.
configs/sample-sir-ode.yaml (1)

22-22: Block-style Transitions with quoted keys improves consistency and readability.

Current inline flow mapping parses correctly but block-style with quoted keys is a better convention for complex mappings with special characters. This improves clarity for developers and ensures compatibility across different YAML consumers.

♻️ Proposed refactor
-Transitions: {S -> I: "beta",  I -> R: "gamma * I"}
+Transitions:
+  "S -> I": "beta"
+  "I -> R": "gamma * I"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@configs/sample-sir-ode.yaml` at line 22, The Transitions mapping is currently
an inline flow mapping which is harder to read and may break with special
characters; change the inline "Transitions: {S -> I: \"beta\",  I -> R: \"gamma
* I\"}" to a block-style mapping with quoted keys and values (e.g., use a
top-level Transitions: followed by indented lines quoting "S -> I": "beta" and
"I -> R": "gamma * I") so the mapping keys like "S -> I" and "I -> R" and their
expressions "beta" and "gamma * I" are clear and YAML-safe.
.github/workflows/ci.yml (1)

5-5: Align push triggers with active default integration branch.

Given this PR targets dev, consider adding dev to the push trigger list (or replacing develop) so direct pushes to the integration branch always run CI.

♻️ Proposed tweak
-    branches: [main, feature, develop]
+    branches: [main, feature, dev, develop]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 5, Update the workflow push trigger
branches array so it includes the active integration branch `dev` instead of or
in addition to `develop`; locate the `branches: [main, feature, develop]` entry
in the CI YAML and modify it to include `dev` (e.g., add `dev` to the list or
replace `develop`), ensuring pushes to the current integration branch will run
CI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.pre-commit-config.yaml:
- Around line 1-15: Update the pinned revs for the two repos: change the
pre-commit-hooks rev from v5.0.0 to v6.0.0 and the ruff-pre-commit rev from
v0.11.13 to v0.15.8 by editing the rev values for repo:
https://github.com/pre-commit/pre-commit-hooks and repo:
https://github.com/astral-sh/ruff-pre-commit in .pre-commit-config.yaml; before
committing, confirm your project Python requirement (>=3.11) satisfies
pre-commit-hooks v6.0.0, ensure you are not relying on removed hooks
(check-byte-order-marker, fix-encoding-pragma) since only ids present are
end-of-file-fixer, trailing-whitespace, check-yaml, check-toml and
ruff/ruff-format, then run pre-commit autoupdate or run pre-commit hooks
(pre-commit run --all-files) and fix any issues reported.

In `@docs/configuration.md`:
- Around line 5-23: The example YAML under "Model definition" is missing
required runtime input-file keys and will not run as-is; update the snippet to
include the input file keys (PatchFile, NetworkFile, SeedFile) before the
ModelName or explicitly mark the block as a partial example. Specifically add
entries like PatchFile, NetworkFile, and SeedFile (referenced by those exact
keys) above or alongside ModelName in the example so users can copy-paste a
runnable config, or add a short comment/header saying "Partial example — input
files omitted" if you don't want to include real paths.

In `@docs/index.md`:
- Line 15: Fix the two typos in the feature bullets: change "models that
supports" to "models that support" in the Flexible Compartment Models bullet
(the phrase containing "SIR, SEIR") and correct the misspelling "Extensiblity"
to "Extensibility" in the Extensibility/Extensiblity bullet so both grammar and
spelling are accurate.

In `@docs/network-design.md`:
- Around line 18-19: Clarify that normalization of outgoing weights is performed
per day and per source patch: update the sentence that currently reads "Rows are
normalized so that outgoing weights from each patch sum to 1." to explicitly
state normalization is done "for each day and for each source patch (i.e.,
normalize outgoing weights per day, per source patch)" so users with a day
column won't mistakenly normalize across all days and distort force-of-infection
calculations.

In `@docs/reference/models.md`:
- Around line 50-54: Update the docs to reference the installed CLI command
instead of the module name: replace the "Run via `patchsim.cli`" instruction
with a concrete example using the exposed CLI (e.g., "Run via `patchsim run -c
<model_script>`" or another appropriate subcommand), ensuring you mention the
`patchsim` command and the `run -c` option so readers know how to invoke the new
CLI interface.

In `@mkdocs.yml`:
- Around line 41-44: The mkdocstrings Python handler path is incorrectly pointed
at ["src/patchsim"], causing imports like patchsim.utils.loader and
patchsim.core.model to be resolved to non-existent nested directories; update
the handlers.python.paths setting in mkdocs.yml to ["src"] so mkdocstrings can
find the top-level package patchsim at src/patchsim and correctly resolve its
submodules.

In `@pyproject.toml`:
- Around line 87-92: The sdist include list under
[tool.hatch.build.targets.sdist] currently restricts files to the listed
patterns and omits the package source; update the include array to add the
package directory pattern (e.g. add "/src/patchsim/**") so the importable code
is packaged in the source distribution and source installs won't be broken,
making the change in the include list in the same section of pyproject.toml.

In `@src/patchsim/cli.py`:
- Around line 53-58: The current logic may rmtree the current working directory
or try to remove a file; before calling shutil.rmtree(project_dir) add guards:
ensure project_dir.exists() and project_dir.is_dir() and project_dir.resolve()
!= Path.cwd().resolve() and that the caller explicitly passed force; if
project_dir exists but is a file raise a clear FileExistsError, and if
project_dir.resolve() == Path.cwd().resolve() raise a ValueError (or similar)
refusing to operate on '.'/CWD even with --force; only call
shutil.rmtree(project_dir) when all these checks pass (references: project_dir,
force, Path(name), shutil.rmtree).

In `@src/patchsim/core/model_runner.py`:
- Around line 47-50: In solve(self, y0, t_range) the list comprehension y0_vec =
[y0[v] for v in self.all_vars] can raise a raw KeyError; first validate that all
variables in self.all_vars are present in the y0 mapping by computing missing =
[v for v in self.all_vars if v not in y0] and if missing is non-empty raise a
ValueError listing the missing variable names (e.g. "Missing initial values for:
..."); then proceed to build y0_vec and call odeint as before.
- Around line 33-37: The FOI scaling is only applied when src == "S" and tgt ==
"I", which misses other infection transitions (e.g., S -> E) — change the
condition so FOI multiplies any susceptible-to-infection transition rather than
only S->I: detect infection transitions by checking tgt against the set of
infection compartments (e.g., {"E","I"} or a model-provided infection target
list) or by using a transition metadata flag if available, then compute
adjusted_rate = beta * state[f"S_{i}"] * lambdas[i] for those transitions (use
the existing variables src, tgt, beta, state[f"S_{i}"], lambdas to locate and
update the logic).

In `@src/patchsim/core/model.py`:
- Around line 105-110: The code currently selects patch parameters using
patch_names = list(self.patch_parameters.keys()) and index i, which relies on
dict insertion order and can misalign with PatchFile ordering; change the lookup
to use the canonical ordering provided by self.patch_names (plumbed from
setup_simulation()) so patch_name = self.patch_names[i] (with a safe fallback if
self.patch_names is missing or i out of range) and then merge parameters via
patch_params = {**self.base_model.parameters,
**self.patch_parameters.get(patch_name, {})}; update references in the function
that uses patch_parameters/patch_name to rely on self.patch_names instead of
list(self.patch_parameters.keys()) to preserve correct patch-to-parameter
mapping.
- Around line 29-35: The current string-substitution-and-eval flow in the branch
handling rate_expr (when isinstance(rate_expr, str)) is unsafe and brittle;
replace it by parsing/evaluating the expression with a safe evaluator that
receives a bindings dict built from self.parameters and state (e.g., variables =
{**self.parameters, **state}) instead of doing text replacements. Concretely, in
the block around rate_expr / rate_val, remove the for-loop replacements and
eval(), and call a safe evaluator such as sympy.sympify(expression,
locals=variables) or an asteval/numexpr evaluator, ensuring no access to Python
builtins/globals, catching and logging parse/eval errors, and assigning the
numeric result to rate_val; keep references to the same symbols (rate_expr,
self.parameters, state, rate_val) so the change is localized.

In `@src/patchsim/core/simulation.py`:
- Around line 59-63: The code forces num_patches = 1 when NetworkFile is
missing, which breaks shape consistency with patches and y0; instead, derive
num_patches from the built patches (or the rows in PatchFile) and construct
network_matrix to match that size (e.g., an all-zero or identity NxN matrix
where N = len(patches)), so NetworkModel and initial state y0 have consistent
dimensions; update the branch that checks "NetworkFile" to set num_patches =
len(patches) (or equivalent) and build network_matrix of shape (num_patches,
num_patches).

In `@src/patchsim/utils/logger.py`:
- Around line 36-41: The current loop over config["PatchParameters"] can crash
if items are not dicts or missing "patch"; update the logging in the block that
checks "PatchParameters" to defensively handle malformed entries: ensure each
entry is a dict (use isinstance check on entry), retrieve patch via
entry.get("patch") and parameters via entry.get("parameters", {}) rather than
indexing, skip or continue when patch is None or invalid, and catch unexpected
exceptions around the per-entry logging (log a warning via logger.warning
including the problematic entry) so setup does not raise from logger.info for
malformed PatchParameters.

In `@src/patchsim/utils/viz.py`:
- Around line 11-14: Add an early guard in the function using the patches list
so empty input won't compute ncols==0: check if not patches and
return/early-exit (or create an empty figure) before computing n, ncols, nrows
and calling plt.subplots; then replace any cleanup logic that relies on a loop
index i with index-independent iteration over axes (e.g., iterate axes.flat or
use enumerate over axes.flatten()) so removal/clearing of unused axes doesn't
reference a non-existent i; update references to n, ncols, nrows, fig, and axes
accordingly.

In `@tests/test_smoke.py`:
- Around line 72-76: The test currently calls
load_config("configs/sample-sir-ode.yaml") which assumes the working directory;
update test_yaml_model_loading to resolve the sample config path relative to the
test file before calling load_config (use the test module's file location via
__file__ and Path/Pathlib to build an absolute path to
"configs/sample-sir-ode.yaml"), then pass that resolved path to load_config so
the test works regardless of the current working directory; locate this change
in the test function test_yaml_model_loading that imports load_config from
patchsim.core.simulation.

---

Outside diff comments:
In `@src/patchsim/core/model.py`:
- Around line 118-132: The derivatives are computed only from local rates so
cross-patch transmission is ignored; update the loop that applies rates (the
block using rates = self.base_model.compute_rates(patch_state) and iterating
self.base_model.transitions) to incorporate network-mediated force of infection
by calling or computing per-patch force via compute_force_of_infection (and/or
using self.network/network_matrix) and adjusting any infection-type transition
rates before updating derivatives. Concretely: for each patch i, compute foI_i =
self.compute_force_of_infection(i, full_state_or_patch_states) (or derive it
from self.network), then modify the relevant transition rate(s) in
rates[transition_label] (e.g., multiply or replace infection transition rates)
prior to applying the decrement/increment to derivatives[f"{source}_{i}"] and
derivatives[f"{target}_{i}"]; keep restoring self.base_model.parameters
(old_params) as before. Ensure compute_force_of_infection and self.network are
fed the appropriate state vector so simulate_ode()/simulate_discrete() reflect
connected dynamics.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Line 5: Update the workflow push trigger branches array so it includes the
active integration branch `dev` instead of or in addition to `develop`; locate
the `branches: [main, feature, develop]` entry in the CI YAML and modify it to
include `dev` (e.g., add `dev` to the list or replace `develop`), ensuring
pushes to the current integration branch will run CI.

In `@configs/sample-sir-ode.yaml`:
- Line 22: The Transitions mapping is currently an inline flow mapping which is
harder to read and may break with special characters; change the inline
"Transitions: {S -> I: \"beta\",  I -> R: \"gamma * I\"}" to a block-style
mapping with quoted keys and values (e.g., use a top-level Transitions: followed
by indented lines quoting "S -> I": "beta" and "I -> R": "gamma * I") so the
mapping keys like "S -> I" and "I -> R" and their expressions "beta" and "gamma
* I" are clear and YAML-safe.

In `@docs/getting-started.md`:
- Around line 5-13: Update the "Installation" section to present PyPI as the
primary install method by placing a single-line instruction "pip install
patchsim" first under the "Installation" heading, then move the current git
clone + pip install -e . block into a new subsection titled "For Contributors"
or "Development Setup" (or similar) and keep the editable install instructions
there; ensure headings "Installation" and the new contributor subsection are
updated accordingly so readers see PyPI install first and development install
separately.

In `@docs/mathematical-model.md`:
- Around line 10-15: Clarify the index mapping for W_ij by adding a sentence
stating that in the weight matrix W, the first index i is the target (receiving)
patch and the second index j is the source (originating) patch, so W_ij denotes
the weight from source j to target i; reference this explicitly alongside the
force-of-infection formula λ_i(t) = Σ_j W_ij * (I_j / N_j) to match the source,
target ordering used in the CSV and avoid confusion between I_j and N_j as
source quantities.

In `@test.py`:
- Around line 1-4: test.py is an empty deprecated compatibility placeholder that
should either be removed or clarified; if it's not required, delete test.py to
avoid confusion, and remove any references to it in packaging/CI; if it must
remain, replace the current vague docstring with a clear statement naming what
requires the file (e.g., "Required by X tooling/version Y for compatibility"),
add a TODO with a removal timeframe, and optionally add a unit test or comment
linking to the external requirement so future maintainers can understand why
test.py exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ed6398a-10b4-4b5b-8199-883f0cb2ff09

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2bb83 and 5fc702c.

⛔ Files ignored due to path filters (7)
  • data/networks/sample-network-static.csv is excluded by !**/*.csv
  • data/patch/sample-sir-ode-patch-population.csv is excluded by !**/*.csv
  • data/seeds/sample-sir-ode-patchA-2.csv is excluded by !**/*.csv
  • src/patchsim/templates/project/data/networks/network-static.csv is excluded by !**/*.csv
  • src/patchsim/templates/project/data/patch/patch-population.csv is excluded by !**/*.csv
  • src/patchsim/templates/project/data/seeds/seed-initial.csv is excluded by !**/*.csv
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • .gitignore
  • .pre-commit-config.yaml
  • CHANGELOG.md
  • README.md
  • configs/sample-sir-ode.yaml
  • docs/architecture.md
  • docs/configuration.md
  • docs/getting-started.md
  • docs/index.md
  • docs/mathematical-model.md
  • docs/network-design.md
  • docs/reference/models.md
  • docs/reference/utils.md
  • docs/results.md
  • docs/simulation-workflow.md
  • mkdocs.yml
  • pyproject.toml
  • src/patchsim/__init__.py
  • src/patchsim/cli.py
  • src/patchsim/core/model.py
  • src/patchsim/core/model_runner.py
  • src/patchsim/core/simulation.py
  • src/patchsim/models/ka_fmd_sirsv_discrete.py
  • src/patchsim/templates/project/config.yaml
  • src/patchsim/templates/project/output/.gitkeep
  • src/patchsim/utils/loader.py
  • src/patchsim/utils/logger.py
  • src/patchsim/utils/viz.py
  • test.py
  • tests/conftest.py
  • tests/test_config.py
  • tests/test_model.py
  • tests/test_simulation_setup.py
  • tests/test_smoke.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/patchsim/core/simulation.py (1)

49-55: ⚠️ Potential issue | 🟡 Minor

Unknown seed patches should raise a clear validation error.

populations[patch] currently throws a raw KeyError when a seed row references a patch missing from PatchFile. Make this an explicit config validation error.

♻️ Suggested fix
     for _, row in seed_df.iterrows():
         patch = row["patch"]
+        if patch not in populations:
+            raise ValueError(f"SeedFile contains unknown patch '{patch}' not present in PatchFile")
         total = sum(row[c] for c in compartments)
         if not all(row[c] >= 0 for c in compartments):
             raise ValueError(f"Seed values must be non-negative for patch {patch}")
         if abs(total - populations[patch]) >= EPSILON:
             raise ValueError(f"Seed values do not sum to population for patch {patch}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/simulation.py` around lines 49 - 55, The loop that reads
seed rows uses populations[patch] which raises a raw KeyError for unknown patch
names; instead, explicitly validate the patch exists before using it and raise a
clear config validation error. In the seed_df.iterrows() loop, check "if patch
not in populations:" and raise a descriptive exception (e.g., ValueError or the
project’s ConfigValidationError if one exists) like "Unknown seed patch
'{patch}' not listed in PatchFile" before computing total or comparing to
populations[patch]; keep the existing non-negative and sum checks (using
EPSILON) after this validation.
♻️ Duplicate comments (1)
src/patchsim/cli.py (1)

55-66: ⚠️ Potential issue | 🔴 Critical

Broaden the --force guard to block root and parent paths.

Line 59 only rejects the exact current working directory. patchsim init .. --force from a subdirectory, or patchsim init / --force, still falls through to shutil.rmtree(project_dir) and can wipe the active tree or filesystem root.

🛡️ Suggested guardrail
 def _cmd_init(name: str, force: bool = False) -> None:
     project_dir = Path(name)
 
     # Safety checks: prevent deleting cwd or non-directories
     resolved = project_dir.resolve()
+    cwd = Path.cwd().resolve()
     if project_dir.exists() and not project_dir.is_dir():
         raise NotADirectoryError(f"Target exists and is not a directory: {project_dir}")
-    if force and resolved == Path.cwd().resolve():
-        raise ValueError(f"Refusing to overwrite current working directory: {resolved}")
+    if force and (resolved == Path(resolved.anchor) or resolved == cwd or resolved in cwd.parents):
+        raise ValueError(f"Refusing to overwrite unsafe target: {resolved}")
 
     if project_dir.exists() and any(project_dir.iterdir()) and not force:
         raise FileExistsError(f"Refusing to overwrite existing directory: {project_dir}. Use --force to overwrite.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/cli.py` around lines 55 - 66, The current force-guard only
rejects exact matches with Path.cwd(); update the check around
project_dir/resolved/force before calling shutil.rmtree to also block parents
and root: if resolved is the current cwd or is an ancestor of the current cwd
(e.g. Path.cwd().resolve().relative_to(resolved) succeeds) or resolved is the
filesystem root/anchor, raise a ValueError (or similar) instead of allowing
shutil.rmtree to run; reference the variables and helpers project_dir, resolved,
Path.cwd(), force, and shutil.rmtree so you locate and replace the existing
guard logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/patchsim/cli.py`:
- Around line 68-73: The package is missing non-Python template files in the
wheel so runtime calls to resources.files("patchsim").joinpath("templates",
"project") (used by template_root and _copy_template_tree when initializing
project_dir and writing config.yaml) fail; modify the build config under
[tool.hatch.build.targets.wheel] to explicitly include the templates directory
(e.g., add an include/artifacts entry that bundles patchsim/templates/** or the
templates/project tree) so that resources.files can find the files at runtime
after installation.

In `@src/patchsim/core/model.py`:
- Around line 101-107: The two conditional branches around computing lambdas are
redundant because both call self.compute_force_of_infection(state); simplify by
removing the if/else and directly assign lambdas =
self.compute_force_of_infection(state), eliminating the unused conditional
(references: variable lambdas and method compute_force_of_infection, and
attributes self.num_patches/self.network).
- Around line 127-131: The code temporarily mutates shared state by assigning
patch-specific values to self.base_model.parameters before calling
compute_rates, which can race with concurrent calls; change compute_rates on the
base model to accept an optional parameters override (e.g., def
compute_rates(state, parameters=None)) and update compute_derivatives to call
self.base_model.compute_rates(patch_state, parameters=patch_params) instead of
mutating self.base_model.parameters; remove the old temporary assignment/restore
and update any other callers of compute_rates to preserve current behavior when
no override is provided.
- Around line 145-157: The check `"beta" in str(rate)` is wrong because rate is
a float; instead inspect the original transition rate expression from the
transition definitions and test that for the substring "beta". Replace the
condition with a lookup into the transition definition (e.g., use the original
expression stored under a variable like transitions[transition_label],
transition_defs[transition_label], or
model.transitions[transition_label]["rate"] depending on the codebase) and do
`"beta" in str(original_rate_expr)`; keep the rest of the logic updating
adjusted_rate (using patch_params, patch_state, lambdas, etc.) the same.
- Around line 115-125: The branch checking self.patch_names is dead because
patch_names is never set; either initialize patch_names where patch_parameters
is configured or derive a stable ordering elsewhere. Update setup_simulation in
simulation.py to set net.patch_names (e.g., list of patch keys in the canonical
order) alongside net.patch_parameters so the branch in model.py (referencing
self.patch_names and self.patch_parameters) becomes reachable and uses the
stable ordering, or alternatively modify model.py to obtain ordering from the
same canonical source that setup_simulation uses instead of relying on dict key
order.

In `@src/patchsim/core/simulation.py`:
- Around line 140-142: The code silently truncates config["TMax"] via int(...)
when constructing t_range and can allow non-positive or fractional horizons;
validate config["TMax"] before building t_range: check that config["TMax"] is
numeric and > 0 and either an integer or explicitly convert with a documented
rule (e.g., use math.ceil if you want to round up), and raise a ValueError with
a clear message if validation fails; then compute the number of steps (use a
validated n_steps variable instead of raw int(config["TMax"])) and build t_range
and call Model/net and model.solve as before (referencing config["TMax"],
t_range, Model, and model.solve to locate the change).
- Around line 93-104: The code currently assigns entry["parameters"] directly to
patch_params[patch_name], dropping global defaults; change the logic that builds
patch_params from config["PatchParameters"] so each entry's parameters are
merged on top of global_params (e.g., patch_params[patch_name] =
{**global_params, **entry.get("parameters", {})}) to preserve global keys, and
add a validation step after parsing config["PatchParameters"] to warn or error
if a configured patch_name is not present in patches (to surface typos) rather
than silently ignoring them.

---

Outside diff comments:
In `@src/patchsim/core/simulation.py`:
- Around line 49-55: The loop that reads seed rows uses populations[patch] which
raises a raw KeyError for unknown patch names; instead, explicitly validate the
patch exists before using it and raise a clear config validation error. In the
seed_df.iterrows() loop, check "if patch not in populations:" and raise a
descriptive exception (e.g., ValueError or the project’s ConfigValidationError
if one exists) like "Unknown seed patch '{patch}' not listed in PatchFile"
before computing total or comparing to populations[patch]; keep the existing
non-negative and sum checks (using EPSILON) after this validation.

---

Duplicate comments:
In `@src/patchsim/cli.py`:
- Around line 55-66: The current force-guard only rejects exact matches with
Path.cwd(); update the check around project_dir/resolved/force before calling
shutil.rmtree to also block parents and root: if resolved is the current cwd or
is an ancestor of the current cwd (e.g.
Path.cwd().resolve().relative_to(resolved) succeeds) or resolved is the
filesystem root/anchor, raise a ValueError (or similar) instead of allowing
shutil.rmtree to run; reference the variables and helpers project_dir, resolved,
Path.cwd(), force, and shutil.rmtree so you locate and replace the existing
guard logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 128b0c0f-575f-44d1-b7d8-c8d567b520a2

📥 Commits

Reviewing files that changed from the base of the PR and between 5fc702c and d305ee6.

📒 Files selected for processing (19)
  • .github/workflows/ci.yml
  • .pre-commit-config.yaml
  • configs/sample-sir-ode.yaml
  • docs/configuration.md
  • docs/getting-started.md
  • docs/index.md
  • docs/mathematical-model.md
  • docs/network-design.md
  • docs/reference/models.md
  • mkdocs.yml
  • pyproject.toml
  • src/patchsim/cli.py
  • src/patchsim/core/model.py
  • src/patchsim/core/model_runner.py
  • src/patchsim/core/simulation.py
  • src/patchsim/utils/logger.py
  • src/patchsim/utils/viz.py
  • test.py
  • tests/test_smoke.py
✅ Files skipped from review due to trivial changes (10)
  • .pre-commit-config.yaml
  • docs/getting-started.md
  • docs/network-design.md
  • test.py
  • docs/index.md
  • docs/configuration.md
  • .github/workflows/ci.yml
  • docs/mathematical-model.md
  • tests/test_smoke.py
  • src/patchsim/core/model_runner.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • configs/sample-sir-ode.yaml
  • src/patchsim/utils/viz.py
  • docs/reference/models.md
  • src/patchsim/utils/logger.py
  • pyproject.toml

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/patchsim/core/model.py (1)

115-121: ⚠️ Potential issue | 🟠 Major

Dict key ordering still unreliable for patch parameter lookup.

The code uses list(self.patch_parameters.keys())[i] to match patches by index, which depends on dict insertion order. While Python 3.7+ guarantees insertion order, this assumes patch_parameters keys were inserted in the same order as patches in PatchFile. If the YAML PatchParameters section lists patches in a different order, parameters will be silently misapplied.

Per the relevant code snippet from simulation.py:93-107, patch_params is built by iterating config["PatchParameters"] (user-defined order), then augmented for remaining patches. This order may not match the patches list from PatchFile.

🐛 Proposed fix using patch index directly
         if hasattr(self, "patch_parameters"):
-            patch_names = list(self.patch_parameters.keys())
-            patch_name = patch_names[i] if i < len(patch_names) else None
-            patch_params = {**self.base_model.parameters, **self.patch_parameters.get(patch_name, {})}
+            # Use patch index to derive canonical patch name from patch_parameters
+            # Requires simulation.py to attach patch_names alongside patch_parameters
+            if hasattr(self, "patch_names") and i < len(self.patch_names):
+                patch_name = self.patch_names[i]
+            else:
+                patch_name = list(self.patch_parameters.keys())[i] if i < len(self.patch_parameters) else None
+            patch_params = {**self.base_model.parameters, **self.patch_parameters.get(patch_name, {})}
         else:
             patch_params = self.base_model.parameters

A cleaner solution: have simulation.py also attach net.patch_names = patches so the canonical order is always available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/model.py` around lines 115 - 121, The patch parameter
lookup currently uses list(self.patch_parameters.keys()) which can mismatch the
canonical patch order; update the codeflow so simulation.py sets the canonical
order by attaching the patches list to the network (e.g., net.patch_names =
patches), then change model.py to use that canonical list (self.patch_names)
when resolving patch_params (fall back to base_model.parameters or
self.patch_parameters.get(patch_name) as before) instead of relying on dict key
order; reference the symbols patch_parameters, base_model.parameters,
PatchFile/paches list, and add/consume net.patch_names to ensure correct
mapping.
🧹 Nitpick comments (3)
src/patchsim/core/simulation.py (2)

163-164: Remove commented-out code.

Line 163 contains a commented-out call to plot_patch_subplots. Since model.visualize() now handles plotting, this dead code should be removed.

♻️ Proposed cleanup
-    # plot_patch_subplots(t_range, out_ode, patches, plots_dir, model_name)
     model.visualize(t_range, out_ode, patches, plots_dir, model_name)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/simulation.py` around lines 163 - 164, Remove the dead
commented-out call to plot_patch_subplots: delete the commented line containing
"plot_patch_subplots(t_range, out_ode, patches, plots_dir, model_name)" so only
the active model.visualize(t_range, out_ode, patches, plots_dir, model_name)
call remains; this cleans up the code now that model.visualize handles plotting
and avoids confusion about duplicate plotting functions.

25-29: Consider adding Transitions and TMax to required fields.

Transitions and TMax are validated later with specific error messages, but including them in required_fields provides early, consistent feedback. Currently, missing Transitions triggers line 84's error, and missing TMax triggers line 146's error—both work, but the error messages differ in style from the "Missing required field" pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/simulation.py` around lines 25 - 29, The required_fields
list currently contains ["PatchFile", "SeedFile", "OutputDir"] but omits
"Transitions" and "TMax", causing inconsistent error styles; update the
required_fields variable to include "Transitions" and "TMax" so missing config
keys raise the same "Missing required field" ValueError; after adding them,
ensure the later specific checks for Transitions and TMax (the validation logic
referencing those keys) either remain as more detailed validation (not duplicate
the missing-key error) or are adjusted to assume presence and only validate
content.
src/patchsim/core/model.py (1)

134-135: Hardcoded infection compartments limit model flexibility.

The set {"I", "E"} assumes specific compartment naming conventions. Models using different naming (e.g., "Exposed", "Infected", "I1", "I2" for staged infections) won't have network FOI applied correctly.

Consider making this configurable via the model or transition metadata.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/model.py` around lines 134 - 135, The check for infection
transitions uses a hardcoded set infection_compartments = {"I", "E"} and the
boolean is_infection_transition = source == "S" and target in
infection_compartments; change this to consult a configurable source (e.g., a
model-level attribute like model.infection_compartments or a transition metadata
key such as
transition.metadata.get("infection_targets")/transition.metadata.get("is_infection"))
so the code uses the provided set/list of infected compartment names (falling
back to a sensible default if absent). Update the logic around
is_infection_transition to reference that configurable value (and allow staged
names like "I1","I2" or full names like "Exposed") so network FOI is applied
according to model/transition metadata rather than hardcoded names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/patchsim/core/model.py`:
- Around line 38-42: The eval of rate_expr inside the try/except around rate_val
is currently only catching KeyError, NameError, and ValueError; extend the
exception handling in that block to also catch SyntaxError, TypeError,
ZeroDivisionError (and any other common eval runtime errors you deem necessary)
so malformed or invalid expressions get the same contextual error; when catching
these (e.g., in the except tuple alongside KeyError/NameError/ValueError) build
the same msg using rate_expr and transition_label and raise a ValueError(msg)
from the original exception so the original traceback is preserved; keep the
eval call using the restricted globals {"__builtins__": {}} and scope as before.

In `@src/patchsim/core/simulation.py`:
- Around line 71-76: The loop that builds network_matrix currently assumes every
network row's source/target exist in patch_idx, which will raise an unhelpful
KeyError; before using patch_idx in the loop over net_df.iterrows(), validate
that source and target (after .strip('"')) are present in patch_idx and if not
raise a clear ValueError naming the missing patch(es) and the offending row (use
the row's source/target values), otherwise proceed to check weight >= 0 and
assign network_matrix[i, j]; reference the symbols net_df.iterrows(), patch_idx,
network_matrix and the row["source"]/row["target"] lookups when locating where
to add this validation.
- Around line 119-124: The NetworkModel is not receiving canonical patch
ordering, so set net.patch_names to the ordered list used when building
patch_params; specifically, after creating net =
NetworkModel(base_model=base_model, num_patches=num_patches,
network_matrix=network_matrix) assign net.patch_parameters = patch_params and
then set net.patch_names = <the same ordered list of patch identifiers used to
construct patch_params> so compute_derivatives sees self.patch_names instead of
relying on dict ordering; reference NetworkModel, patch_parameters, and
patch_names in the change to ensure canonical ordering matches PatchFile input.

---

Duplicate comments:
In `@src/patchsim/core/model.py`:
- Around line 115-121: The patch parameter lookup currently uses
list(self.patch_parameters.keys()) which can mismatch the canonical patch order;
update the codeflow so simulation.py sets the canonical order by attaching the
patches list to the network (e.g., net.patch_names = patches), then change
model.py to use that canonical list (self.patch_names) when resolving
patch_params (fall back to base_model.parameters or
self.patch_parameters.get(patch_name) as before) instead of relying on dict key
order; reference the symbols patch_parameters, base_model.parameters,
PatchFile/paches list, and add/consume net.patch_names to ensure correct
mapping.

---

Nitpick comments:
In `@src/patchsim/core/model.py`:
- Around line 134-135: The check for infection transitions uses a hardcoded set
infection_compartments = {"I", "E"} and the boolean is_infection_transition =
source == "S" and target in infection_compartments; change this to consult a
configurable source (e.g., a model-level attribute like
model.infection_compartments or a transition metadata key such as
transition.metadata.get("infection_targets")/transition.metadata.get("is_infection"))
so the code uses the provided set/list of infected compartment names (falling
back to a sensible default if absent). Update the logic around
is_infection_transition to reference that configurable value (and allow staged
names like "I1","I2" or full names like "Exposed") so network FOI is applied
according to model/transition metadata rather than hardcoded names.

In `@src/patchsim/core/simulation.py`:
- Around line 163-164: Remove the dead commented-out call to
plot_patch_subplots: delete the commented line containing
"plot_patch_subplots(t_range, out_ode, patches, plots_dir, model_name)" so only
the active model.visualize(t_range, out_ode, patches, plots_dir, model_name)
call remains; this cleans up the code now that model.visualize handles plotting
and avoids confusion about duplicate plotting functions.
- Around line 25-29: The required_fields list currently contains ["PatchFile",
"SeedFile", "OutputDir"] but omits "Transitions" and "TMax", causing
inconsistent error styles; update the required_fields variable to include
"Transitions" and "TMax" so missing config keys raise the same "Missing required
field" ValueError; after adding them, ensure the later specific checks for
Transitions and TMax (the validation logic referencing those keys) either remain
as more detailed validation (not duplicate the missing-key error) or are
adjusted to assume presence and only validate content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0bc81ce-03e7-4c94-a412-73b32c0d0529

📥 Commits

Reviewing files that changed from the base of the PR and between d305ee6 and cc94719.

📒 Files selected for processing (4)
  • pyproject.toml
  • src/patchsim/cli.py
  • src/patchsim/core/model.py
  • src/patchsim/core/simulation.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/patchsim/cli.py
  • pyproject.toml

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/patchsim/core/model.py (2)

139-139: Redundant hasattr(self, "network") check.

self.network is always set in __init__ (line 63), so hasattr(self, "network") is always True for any NetworkModel instance. The self.network is not None check alone is sufficient.

♻️ Suggested simplification
-                has_network = self.num_patches > 1 and hasattr(self, "network") and self.network is not None
+                has_network = self.num_patches > 1 and self.network is not None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/model.py` at line 139, The condition computing has_network
in NetworkModel is redundant; remove the hasattr(self, "network") check and set
has_network = self.num_patches > 1 and self.network is not None instead. Locate
the assignment to has_network (in method/constructor using self.num_patches and
self.network) and simplify the boolean expression to rely only on
self.num_patches > 1 and self.network is not None.

141-153: Consider consolidating FOI adjustment logic with model_runner.py.

The FOI adjustment logic here (lines 141-153) is also implemented in model_runner.py (lines 33-40 in context snippet). Both use the same formula beta * S * lambdas[i] for infection transitions. Having two implementations increases the risk of divergence during future updates.

Consider either:

  1. Having model_runner.py delegate to NetworkModel.compute_derivatives, or
  2. Extracting the FOI adjustment logic into a shared helper method.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patchsim/core/model.py` around lines 141 - 153, The FOI adjustment logic
is duplicated between NetworkModel.compute_derivatives (the snippet in model.py
that uses beta * S * lambdas[i] / FOI scaling) and the code in model_runner.py;
extract that logic into a single shared helper (e.g., compute_foi_adjusted_rate
or adjust_infection_rate) that accepts (patch_params, original_rate_expr, rate,
patch_state, lambdas, i, is_infection_transition, has_network) and returns
adjusted_rate, then replace the inline blocks in
NetworkModel.compute_derivatives and the model_runner FOI block to call this
helper; alternatively, have model_runner call NetworkModel.compute_derivatives
directly so only one implementation remains — update both call sites to use the
new helper or the single source-of-truth method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/patchsim/core/model.py`:
- Around line 145-150: The substring check using "beta" on original_rate_expr is
too broad and can match names like "beta_max"; update the conditional in the
block that sets adjusted_rate (referencing original_rate_expr, adjusted_rate,
beta, lambdas, patch_state) to use a word-boundary regex match instead (e.g.,
use re.search with re.escape for "beta") consistent with the earlier pattern
used for source matching; ensure you import re if not already and preserve the
existing branches so only exact whole-word "beta" triggers the network FOI
correction.
- Around line 115-122: The code silently ignores patch-specific settings when
self.patch_parameters exists but self.patch_names is missing (patch_name ends up
None and self.patch_parameters.get(None, {}) is empty); update the logic in the
method that builds patch_params (where patch_name, patch_params and the hasattr
checks are used, e.g., in compute_derivatives or the surrounding block) to
detect this misconfiguration and either raise a clear ValueError or emit a
warning (choose project convention) stating that patch_parameters were provided
but patch_names is not set, and avoid silently falling back to base parameters;
ensure the message references self.patch_parameters and self.patch_names so
callers can quickly locate and fix the configuration.

---

Nitpick comments:
In `@src/patchsim/core/model.py`:
- Line 139: The condition computing has_network in NetworkModel is redundant;
remove the hasattr(self, "network") check and set has_network = self.num_patches
> 1 and self.network is not None instead. Locate the assignment to has_network
(in method/constructor using self.num_patches and self.network) and simplify the
boolean expression to rely only on self.num_patches > 1 and self.network is not
None.
- Around line 141-153: The FOI adjustment logic is duplicated between
NetworkModel.compute_derivatives (the snippet in model.py that uses beta * S *
lambdas[i] / FOI scaling) and the code in model_runner.py; extract that logic
into a single shared helper (e.g., compute_foi_adjusted_rate or
adjust_infection_rate) that accepts (patch_params, original_rate_expr, rate,
patch_state, lambdas, i, is_infection_transition, has_network) and returns
adjusted_rate, then replace the inline blocks in
NetworkModel.compute_derivatives and the model_runner FOI block to call this
helper; alternatively, have model_runner call NetworkModel.compute_derivatives
directly so only one implementation remains — update both call sites to use the
new helper or the single source-of-truth method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57757273-54ce-4c38-a460-8c681d1cc7c0

📥 Commits

Reviewing files that changed from the base of the PR and between cc94719 and 2534755.

📒 Files selected for processing (3)
  • src/patchsim/core/model.py
  • src/patchsim/core/simulation.py
  • tests/test_config.py
✅ Files skipped from review due to trivial changes (2)
  • tests/test_config.py
  • src/patchsim/core/simulation.py

Copy link
Copy Markdown
Member

@snehasaisneha snehasaisneha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @shreyamukherji - I've flagged a few specific issues with the CI flow, please resolve those.

I won't be able to go through the details of the business logic (as there are too many files and lots of changes), and so can only comment on the code's sanity and the CI/packaging aspects. Please make these revisions and I can review further.

strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
Copy link
Copy Markdown
Member

@snehasaisneha snehasaisneha Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python version should be picked up from the .python-version file in the root.

This is present in the other file as well.

python-version: ${{ matrix.python-version }}

- name: Sync dependencies (including dev)
run: uv sync --extra dev
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv sync --extra dev allows uv to re-sync the requirements in the root, which allows updates to creep in and can potentially alter functionality. Run uv sync locally before your commits. In CI, always use the following pattern:

# Step 1: Sync dependencies — fails if lockfile is out of date. This is a separate steps.
uv sync --extra dev --frozen

# Step 2: Run tools without re-syncing
uv run --no-sync --frozen ruff check .

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document expects users to be running patchsim directly in their root. The recommended pattern should be uv run patchsim --help to let the user utilise the right dependencies.

@snehasaisneha snehasaisneha self-requested a review April 6, 2026 05:12
Copy link
Copy Markdown
Member

@snehasaisneha snehasaisneha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this one change. Aside from that, LGTM.

Will approve the merge after this change.

run: uv sync --frozen

- name: Build package
run: uv build
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be uv build --no-sync --frozen

@snehasaisneha snehasaisneha merged commit 75537d8 into dev Apr 7, 2026
5 checks passed
@snehasaisneha snehasaisneha deleted the feature branch April 7, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants