diff --git a/cell2fire/Cell2FireC/Cell2Fire.cpp b/cell2fire/Cell2FireC/Cell2Fire.cpp index 951fb9b..5bdaa47 100644 --- a/cell2fire/Cell2FireC/Cell2Fire.cpp +++ b/cell2fire/Cell2FireC/Cell2Fire.cpp @@ -234,6 +234,7 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv this->nonBurnableCells.clear(); this->burningCells.clear(); this->burntCells.clear(); + this->ignitionCells.clear(); this->harvestCells.clear(); for (i=0; i < this->statusCells.size(); i++){ if(this->statusCells[i] < 3) this->availCells.insert (i+1); @@ -535,6 +536,7 @@ void Cell2Fire::reset(int rnumber, double rnumber2){ this->nonBurnableCells.clear(); this->burningCells.clear(); this->burntCells.clear(); + this->ignitionCells.clear(); this->harvestCells.clear(); // Harvest Cells @@ -697,6 +699,7 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ this->nIgnitions++; this->burningCells.insert(newId); this->burntCells.insert(newId); + this->ignitionCells.insert(newId); // forest root: excluded as a message destination this->availCells.erase(newId); // Print sets information @@ -1049,6 +1052,63 @@ void Cell2Fire::GetMessages(std::unordered_map> sendMessag } +/* + * Reduce the raw FSCell event buffer to the fire-propagation forest written to MessagesFileNN.csv. + * + * FSCell is an append-only stream of (source, destination, period, ROS) reach-to-center events. + * A destination may appear several times (several sources reaching it in one period, or repeated + * reaches across periods) and may even name a cell that never actually burned. The MessagesFile is + * meant to be a spanning forest rooted at the ignition cells, with exactly one parent edge per + * burned, non-ignition cell, so we select a single representative edge per burned destination. + * + * Selection rule (per destination): earliest fire period; ties broken by highest realized ROS, then + * by lowest source id. Output rows are sorted by destination id so the file is deterministic. + */ +std::vector Cell2Fire::buildMessageTree(){ + // destination -> winning edge {source, destination, period, ros} + std::unordered_map> best; + + for (size_t i = 0; i + 3 < this->FSCell.size(); i += 4){ + double src = this->FSCell[i]; + int dst = (int) this->FSCell[i + 1]; + double period = this->FSCell[i + 2]; + double ros = this->FSCell[i + 3]; + + // Keep only edges into cells that actually burned, and never into an ignition root. + if (this->burntCells.find(dst) == this->burntCells.end()) continue; + if (this->ignitionCells.find(dst) != this->ignitionCells.end()) continue; + + auto it = best.find(dst); + if (it == best.end()){ + best[dst] = {src, (double) dst, period, ros}; + continue; + } + + double bSrc = it->second[0], bPeriod = it->second[2], bRos = it->second[3]; + bool better = (period < bPeriod) + || (period == bPeriod && ros > bRos) + || (period == bPeriod && ros == bRos && src < bSrc); + if (better){ + it->second = {src, (double) dst, period, ros}; + } + } + + // Emit rows ordered by destination id for a reproducible file. + std::vector dsts; + dsts.reserve(best.size()); + for (auto & kv : best) dsts.push_back(kv.first); + std::sort(dsts.begin(), dsts.end()); + + std::vector tree; + tree.reserve(dsts.size() * 4); + for (int d : dsts){ + std::vector & e = best[d]; + tree.insert(tree.end(), e.begin(), e.end()); + } + return tree; +} + + // Display results void Cell2Fire::Results(){ /***************************************************************************** @@ -1151,7 +1211,9 @@ void Cell2Fire::Results(){ std::cout << "We are generating the network messages to a csv file " << messagesName << std::endl; } CSVWriter CSVPloter(messagesName, ","); - CSVPloter.printCSVDouble_V2(this->burntCells.size() - this->nIgnitions, 4, this->FSCell); + // Serialize one canonical parent edge per burned, non-ignition cell (see buildMessageTree). + std::vector messageTree = this->buildMessageTree(); + CSVPloter.printCSVDouble_V2(messageTree.size() / 4, 4, messageTree); } diff --git a/cell2fire/Cell2FireC/Cell2Fire.h b/cell2fire/Cell2FireC/Cell2Fire.h index eec1785..4c36ae9 100644 --- a/cell2fire/Cell2FireC/Cell2Fire.h +++ b/cell2fire/Cell2FireC/Cell2Fire.h @@ -88,6 +88,7 @@ class Cell2Fire { std::unordered_set nonBurnableCells; std::unordered_set burningCells; std::unordered_set burntCells; + std::unordered_set ignitionCells; // roots of the propagation forest (no parent edge) std::unordered_set harvestCells; // Cells Dictionary @@ -102,6 +103,7 @@ class Cell2Fire { bool RunIgnition(std::default_random_engine generator); std::unordered_map> SendMessages(); void GetMessages(std::unordered_map> sendMessageList); + std::vector buildMessageTree(); void Results(); void outputGrid(); void updateWeather(); diff --git a/cell2fire/utils/Heuristics.py b/cell2fire/utils/Heuristics.py index 970fcf7..bc1817f 100644 --- a/cell2fire/utils/Heuristics.py +++ b/cell2fire/utils/Heuristics.py @@ -1530,7 +1530,8 @@ def Global_FPVPlot(self, normalized=False, xticks=50, yticks=50): plt.title(self.Alg + " Heatmap $|R| = 100$", y=1.02) # Modify existing map to have white values - cmap = cm.get_cmap('RdBu_r') + # matplotlib.cm.get_cmap was removed in matplotlib 3.9; use the registry. + cmap = matplotlib.colormaps['RdBu_r'] lower = plt.cm.seismic(np.ones(2)*0.50) # Original is ones range = cmap(np.linspace(0.5, np.max((self._FPVMatrix / np.max(self._FPVMatrix))), 100)) colors = np.vstack((lower,range)) @@ -1616,7 +1617,8 @@ def Ind_FPVPlot(self, nSim, FPVMatrix, normalized=False): plt.title("FPV Heatmap") # Modify existing map to have white values - cmap = cm.get_cmap('RdBu_r') + # matplotlib.cm.get_cmap was removed in matplotlib 3.9; use the registry. + cmap = matplotlib.colormaps['RdBu_r'] lower = plt.cm.seismic(np.ones(100)*0.50) upper = cmap(np.linspace(1-0.5, 1, 90)) colors = np.vstack((lower, upper)) diff --git a/cell2fire/utils/Stats.py b/cell2fire/utils/Stats.py index 12e2530..2d28d7c 100644 --- a/cell2fire/utils/Stats.py +++ b/cell2fire/utils/Stats.py @@ -354,7 +354,8 @@ def ROSHeatmap(self, ROSM, Path=None, nscen=1, sq=True, namePlot="ROS_HeatMap", plt.title(Title) # Modify existing map to have white values - cmap = cm.get_cmap('RdBu_r') + # matplotlib.cm.get_cmap was removed in matplotlib 3.9; use the registry. + cmap = matplotlib.colormaps['RdBu_r'] lower = plt.cm.seismic(np.ones(1)*0.50) # Original is ones upper = cmap(np.linspace(0.5, 1, 100)) colors = np.vstack((lower,upper)) diff --git a/tests/test_ptree.py b/tests/test_ptree.py index 374f165..7ddd15b 100644 --- a/tests/test_ptree.py +++ b/tests/test_ptree.py @@ -65,10 +65,15 @@ def test_readme_cmd(self): csv_path = os.path.join(data_path, "..", "results", "Sub40x40", "Messages", "MessagesFile01.csv") df = pd.read_csv(csv_path, names = ["node1", "node2", "time", "ros"]) - self.assertAlmostEqual(df['node1'][10],935,0), "TEST ERROR" - self.assertAlmostEqual(df['node2'][10],896,0), "TEST ERROR" - self.assertAlmostEqual(df['time'][10], 139, 0), "TEST ERROR" - self.assertAlmostEqual(df['node2'][10], 14.1367, 2), "TEST ERROR" + # MessagesFile is the fire-propagation forest: exactly one parent edge per burned, + # non-ignition cell, with rows sorted by destination (node2) so the file is + # deterministic. See Cell2Fire::buildMessageTree in Cell2Fire.cpp. + self.assertEqual(df['node2'].nunique(), len(df)) # one parent per destination + self.assertEqual(list(df['node2']), sorted(df['node2'])) # deterministic ordering + self.assertAlmostEqual(df['node1'][10], 456, 0), "TEST ERROR" + self.assertAlmostEqual(df['node2'][10], 417, 0), "TEST ERROR" + self.assertAlmostEqual(df['time'][10], 402, 0), "TEST ERROR" + self.assertAlmostEqual(df['ros'][10], 27.2931, 2), "TEST ERROR" if __name__ == "__main__":