Skip to content

Fix bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile, and ProfileThreshold models#265

Draft
Copilot wants to merge 2 commits into
masterfrom
copilot/fix-bugs-in-models
Draft

Fix bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile, and ProfileThreshold models#265
Copilot wants to merge 2 commits into
masterfrom
copilot/fix-bugs-in-models

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 12, 2026

Seven diffusion models contained logic bugs affecting probabilistic correctness, state tracking, and update semantics. Fixes span time-tracking resets, misused random samples, an indentation bug that silently dropped all-but-one update, inverted probability conditions, and asynchronous-vs-synchronous opinion update inconsistencies.

Changes

SEIR_ct_Model.py & SEIS_ct_Model.py

  • self.progress_I was reinitialised to 0 on every iteration, clobbering the recorded infection timestamp for all currently-infected nodes. Now initialised only at actual_iteration == 0.
  • In SEISctModel, newly exposed nodes recorded self.progress[u] = 0 regardless of when exposure occurred. Changed to self.progress[u] = self.actual_iteration.

SWIRModel.py

  • A single eventp drawn once per node was reused across all its neighbours' independent probability checks. Moved eventp = np.random.random_sample() to just before each individual condition.

HKModel.py

  • Critical: actual_status[n1] = new_op was outside the for loop — only the last randomly-selected node's opinion was ever written. Fixed indentation.
  • Neighbour opinion reads used actual_status (in-flight values), producing unintended within-step cascades. Changed to self.status for synchronous (Jacobi-style) updates.
  • Return value changed from delta (only changed nodes) to self.status.copy() (full opinion vector), consistent with continuous-state model conventions.

WHKModel.py

  • Same actual_statusself.status fix for synchronous neighbour reads; the indentation and return-value issues were already correct here.

ProfileModel.py & ProfileThresholdModel.py

  • Blocked nodes (status −1) were summed into infected, producing negative counts and corrupting the infected > 0 guard. Now excluded: infected += status[v] if status[v] != -1 else 0.
  • Adoption/blocking probability conditions were inverted relative to the documented semantics:
# Before: adopts with probability (1 - profile)
if eventp >= self.params["nodes"]["profile"][u]:
    actual_status[u] = 1

# After: adopts with probability profile
if eventp < self.params["nodes"]["profile"][u]:
    actual_status[u] = 1

Same inversion fixed for the blocked parameter.

Original prompt

This section details on the original issue you should resolve

<issue_title>Bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile and ProfileThreshold models</issue_title>
<issue_description>Describe the bug

Bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile and ProfileThreshold models

To Reproduce

Steps to reproduce the behavior:

  • NDlib version: 5.1.1
  • Operating System: Any
  • Python version: Any
  • Version(s) of NDlib required libraries: Any

Expected behavior

  1. In the file ndlib/models/epidemics/SEIR_ct_Model.py,
    the function iteration update the self.progress_I of all Inf nodes as 0:

    # start from line 45
    def iteration(self, node_status=True):
        self.clean_initial_status(self.available_statuses.values())
    
        actual_status = {
            node: nstatus for node, nstatus in future.utils.iteritems(self.status)
        }
        self.progress_I = {
            node: 0 for node in actual_status if actual_status[node] == 1
        }
    
        if self.actual_iteration == 0:
            self.actual_iteration += 1
            delta, node_count, status_delta = self.status_delta(actual_status)
            # ...

    I think self.progress_I only can be initiated when self.actual_iteration == 0

    def iteration(self, node_status=True):
        self.clean_initial_status(self.available_statuses.values())
    
        actual_status = {
            node: nstatus for node, nstatus in future.utils.iteritems(self.status)
        }
        
        if self.actual_iteration == 0:
            self.progress_I = {
                node: 0 for node in actual_status if actual_status[node] == 1
            }
            self.actual_iteration += 1
            delta, node_count, status_delta = self.status_delta(actual_status)
            # ...
  2. The first problem of ndlib/models/epidemics/SEIS_ct_Model.py is the same as above.
    The second problem is that self.progress of new Exposed nodes always be 0.

    def iteration(self, node_status=True):
        # ...
                # start form line 99
                if self.params["model"]["tp_rate"] == 1:
                    if eventp < 1 - (1 - self.params["model"]["beta"]) ** len(
                        infected_neighbors
                    ):
                        actual_status[u] = 2  # Exposed
                        self.progress[u] = 0
                else:
                    if eventp < self.params["model"]["beta"] * triggered:
                        actual_status[u] = 2  # Exposed
                        self.progress[u] = 0

    I think self.progress of new Exposed nodes should be updated by self.actual_iteration:

    def iteration(self, node_status=True):
        # ...
                # start form line 99
                if self.params["model"]["tp_rate"] == 1:
                    if eventp < 1 - (1 - self.params["model"]["beta"]) ** len(
                        infected_neighbors
                    ):
                        actual_status[u] = 2  # Exposed
                        self.progress[u] = self.actual_iteration
                else:
                    if eventp < self.params["model"]["beta"] * triggered:
                        actual_status[u] = 2  # Exposed
                        self.progress[u] = self.actual_iteration
  3. In the file ndlib/models/epidemics/SWIRModel.py, the random value eventp is not updated in time

    def iteration(self, node_status=True):
        # ...
        # start from line 65 
        for u in self.graph.nodes:
    
            u_status = self.status[u]
            eventp = np.random.random_sample()
            neighbors = self.graph.neighbors(u)
            if self.graph.directed:
                neighbors = self.graph.predecessors(u)
    
            if u_status == 1:  # Infected
                for neighbor in neighbors:
                    if self.status[neighbor] == 0:  # Susceptible
                        if eventp < self.params["model"]["kappa"]:
                            actual_status[neighbor] = 1  # Infected
                        else:
                            eventp = np.random.random_sample()
                            if eventp < self.params["model"]["mu"]:
                                actual_status[neighbor] = 2  # Weakened
                    elif self.status[neighbor] == 2:  # Weakened
                        if eventp < self.params["model"]["nu"]:
                            actual_status[neighbor] = 1  # Infected
                actual_status[u] = 3  # Removed

    eventp should be updated for each judgment

    def iteration(self, node_status=True):
        # ...
        for u in self.graph.nodes:
    
            u_status = self.status[u]
            neighbors = self.graph.neighbors(u)
            if self.graph.directed:
                neighbors = self.graph.predecessors(u)
    
           ...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…ld models

Co-authored-by: GiulioRossetti <13123607+GiulioRossetti@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile and ProfileThreshold models Fix bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile, and ProfileThreshold models Mar 12, 2026
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.

Bugs in SEIRct, SEISct, SWIR, HK, WHK, Profile and ProfileThreshold models

2 participants