From cc6b4aee621f3959152043c4863896db2bf97950 Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Sun, 16 Aug 2026 08:36:55 -0400 Subject: [PATCH 1/3] persist attack through attacker transformation Invert the decision made earlier on this branch. Transformation now leaves an in-progress attack running instead of ending it. `pAttackTarget` lives in `phenotype.cell_interactions`, so `phenotype = cd.phenotype` would drop it; hold it across the assignment and put it back. The other two halves of the attack link -- the target's `attacked_by` and the shared spring -- live in `state`, which transformation already leaves alone, so restoring `pAttackTarget` keeps all three consistent without touching the spring. This also makes transformation symmetric: attacks *against* a transforming cell already persisted, while attacks *by* it were ended. --- core/PhysiCell_cell.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/PhysiCell_cell.cpp b/core/PhysiCell_cell.cpp index cbeb4e239..364464bf7 100644 --- a/core/PhysiCell_cell.cpp +++ b/core/PhysiCell_cell.cpp @@ -1158,12 +1158,11 @@ void Cell::convert_to_cell_definition( Cell_Definition& cd ) Molecular cell_molecular = phenotype.molecular; Custom_Cell_Data cell_custom_data = custom_data; - // pAttackTarget is phenotype state, and the assignment below replaces it, so - // end this cell's own attack. attacked_by and the spring are in state, which - // transformation preserves, so attacks against this cell continue. - remove_self_from_attacked(); - // should we also remove all attackers? That would be a change in behavior, so for now we don't. - // remove_all_attackers(); + // pAttackTarget is phenotype state, and the assignment below replaces it, so hold + // on to it and put it back afterwards. attacked_by and the spring are in state, + // which transformation preserves, so restoring pAttackTarget keeps the whole + // attack link intact -- matching attacks against this cell, which already persist. + Cell* cell_attack_target = phenotype.cell_interactions.pAttackTarget; // use the cell defaults; type = cd.type; @@ -1184,6 +1183,7 @@ void Cell::convert_to_cell_definition( Cell_Definition& cd ) phenotype.geometry = cell_geometry; // leave the geometry alone phenotype.molecular.internalized_total_substrates = cell_molecular.internalized_total_substrates; + phenotype.cell_interactions.pAttackTarget = cell_attack_target; // leave any ongoing attack alone for( int nn = 0 ; nn < custom_data.variables.size() ; nn++ ) { From 1855646c7c418fb9f35825847237edd1a1590e69 Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Sun, 16 Aug 2026 08:53:38 -0400 Subject: [PATCH 2/3] carry total_damage_delivered across transformation It lives in phenotype.cell_interactions too, so the transformation reset the attacker's lifetime damage tally to zero -- now visibly wrong when the attack it was counting is still running afterwards. --- core/PhysiCell_cell.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/PhysiCell_cell.cpp b/core/PhysiCell_cell.cpp index 364464bf7..340734a88 100644 --- a/core/PhysiCell_cell.cpp +++ b/core/PhysiCell_cell.cpp @@ -1163,6 +1163,9 @@ void Cell::convert_to_cell_definition( Cell_Definition& cd ) // which transformation preserves, so restoring pAttackTarget keeps the whole // attack link intact -- matching attacks against this cell, which already persist. Cell* cell_attack_target = phenotype.cell_interactions.pAttackTarget; + // a lifetime tally of this cell's own doing, so carry it across the transformation + // rather than restarting it partway through an attack that is still running + double cell_total_damage_delivered = phenotype.cell_interactions.total_damage_delivered; // use the cell defaults; type = cd.type; @@ -1184,6 +1187,7 @@ void Cell::convert_to_cell_definition( Cell_Definition& cd ) phenotype.geometry = cell_geometry; // leave the geometry alone phenotype.molecular.internalized_total_substrates = cell_molecular.internalized_total_substrates; phenotype.cell_interactions.pAttackTarget = cell_attack_target; // leave any ongoing attack alone + phenotype.cell_interactions.total_damage_delivered = cell_total_damage_delivered; for( int nn = 0 ; nn < custom_data.variables.size() ; nn++ ) { From 6c6449e2da1155553b3925458f64570a2898b8b7 Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Sun, 16 Aug 2026 09:01:29 -0400 Subject: [PATCH 3/3] warn when a transformation hands an attack to a non-attacking type Transformation now keeps an in-progress attack running. That attack can land on a cell definition that would never have started it: attack_rates default to zero, so a type the user never wrote attack parameters for inherits the attack anyway, and the attack_damage_rate (1.0) and attack_duration (30 min) defaults then decide how much damage it does. Whether the attack should continue is a modelling question, not one PhysiCell can answer, so nothing is changed -- but the case is easy to reach by accident, so warn when the new type has a zero attack rate against the target's type. Reported once per combination of (old type, new type, target type), since the same transformation recurs throughout a run. --- core/PhysiCell_cell.cpp | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/core/PhysiCell_cell.cpp b/core/PhysiCell_cell.cpp index 340734a88..31716a9a1 100644 --- a/core/PhysiCell_cell.cpp +++ b/core/PhysiCell_cell.cpp @@ -1151,6 +1151,63 @@ Cell* create_cell( Cell_Definition& cd ) return pNew; } +// A transformation hands any in-progress attack to the new cell definition, which may be +// one that would never have started it: attack_rates default to zero, so a type the user +// never wrote attack parameters for inherits the attack anyway, and it is then the +// attack_damage_rate (1.0) and attack_duration (30 min) defaults that decide how much +// damage gets done. Whether such an attack should continue is a modelling question we +// cannot answer for the user, so nothing is changed here -- but this is easy to reach by +// accident, so say it out loud. +static void warn_if_attack_inherited_by_nonattacking_type( Cell_Definition& cd, + const std::string& old_type_name , Cell* pTarget ) +{ + if( pTarget == NULL ) + { return; } + + // go through the target's type (an int) rather than its type_name: a target + // transforming on another thread in this same step would be rewriting that string + // underneath us. find_cell_definition_index only reads its map, so it is safe here. + int nTarget = find_cell_definition_index( pTarget->type ); + if( nTarget < 0 || nTarget >= (int) cd.phenotype.cell_interactions.attack_rates.size() ) + { return; } + + // the new type does attack this type, so inheriting the attack is self-consistent + if( cd.phenotype.cell_interactions.attack_rates[nTarget] > 0.0 ) + { return; } + + std::string target_type_name = cell_definitions_by_index[nTarget]->name; + + // the same transformation recurs throughout a run, so report each combination of + // (old type, new type, target type) once instead of once per cell per time step. + static std::vector already_reported; + + // a named critical: detach_cells_as_spring() and the attacked_by helpers take the + // unnamed one, and OpenMP criticals are not reentrant. + #pragma omp critical( attack_transformation_warning ) + { + std::string key = old_type_name + ">" + cd.name + ">" + target_type_name; + if( std::find( already_reported.begin() , already_reported.end() , key ) + == already_reported.end() ) + { + already_reported.push_back( key ); + std::cout << "Warning: a '" << old_type_name << "' transformed into a '" << cd.name + << "' while attacking a '" << target_type_name << "'," << std::endl + << "\tbut '" << cd.name << "' has an attack rate of 0 against '" + << target_type_name << "', i.e. it would never start this attack itself." + << std::endl + << "\tThe attack continues regardless, for up to attack_duration = " + << cd.phenotype.cell_interactions.attack_duration + << " min at attack_damage_rate = " + << cd.phenotype.cell_interactions.attack_damage_rate << "." << std::endl + << "\tIf that is not what you intend, set the attack parameters for '" << cd.name + << "' explicitly, or end the attack in a custom function." << std::endl + << "\t(reported once per combination of transformation and target type)" + << std::endl; + } + } + return; +} + void Cell::convert_to_cell_definition( Cell_Definition& cd ) { Volume cell_volume = phenotype.volume; @@ -1166,6 +1223,8 @@ void Cell::convert_to_cell_definition( Cell_Definition& cd ) // a lifetime tally of this cell's own doing, so carry it across the transformation // rather than restarting it partway through an attack that is still running double cell_total_damage_delivered = phenotype.cell_interactions.total_damage_delivered; + // type_name is still this cell's old one until it is overwritten below + warn_if_attack_inherited_by_nonattacking_type( cd , type_name , cell_attack_target ); // use the cell defaults; type = cd.type;