From acb85824a7f4eaefe8cb3ded3c0cf92cfb23ec01 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 2 Oct 2025 15:40:12 -0500 Subject: [PATCH 01/15] force both materials to decay on absorption --- CHANGELOG.rst | 1 + src/material.cc | 20 +++++++------------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5e72d37b77..c03faada2b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -53,6 +53,7 @@ Since last release * Modified Doxygen homepage (#1815) * Pin ``python<3.13`` in CI workflows (#1826) * Changed the way Position is added to the archetypes (#1510) (#1872) +* Changed interaction between ``mat::Absorb()`` and ``mat::Decay()`` to ensure all materials being absorbed are decayed first **Removed:** diff --git a/src/material.cc b/src/material.cc index a7136ed10e..47b99df44b 100644 --- a/src/material.cc +++ b/src/material.cc @@ -103,6 +103,11 @@ Material::Ptr Material::ExtractComp(double qty, Composition::Ptr c, } void Material::Absorb(Material::Ptr mat) { + + // force both mateiral objects to decay prior to absorption + this->Decay(); + mat->Decay(); + // these calls force lazy evaluation if in lazy decay mode Composition::Ptr c0 = comp(); Composition::Ptr c1 = mat->comp(); @@ -114,19 +119,8 @@ void Material::Absorb(Material::Ptr mat) { compmath::Normalize(&otherv, mat->qty_); comp_ = Composition::CreateFromMass(compmath::Add(v, otherv)); } - - // Set the decay time to the value of the material that had the larger - // quantity. This helps avoid inheriting erroneous prev decay times if, for - // example, you absorb a material into a zero-quantity material that had a - // prev decay time prior to the current simulation time step. - if (qty_ < mat->qty_) { - prev_decay_time_ = mat->prev_decay_time_; - } - double tot_mass = qty_ + mat->quantity(); - double avg_unit_value = - (qty_ * UnitValue() + mat->quantity() * mat->UnitValue()) / tot_mass; - SetUnitValue(avg_unit_value); - qty_ = tot_mass; + + qty_ += mat->qty_; mat->qty_ = 0; tracker_.Absorb(&mat->tracker_); } From 488077492c1d7469aef40fe817e473a5c8c76d46 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 2 Oct 2025 22:21:19 -0500 Subject: [PATCH 02/15] select the common decay time for absorption more carefully --- src/material.cc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/material.cc b/src/material.cc index 47b99df44b..3dd7b50676 100644 --- a/src/material.cc +++ b/src/material.cc @@ -104,9 +104,13 @@ Material::Ptr Material::ExtractComp(double qty, Composition::Ptr c, void Material::Absorb(Material::Ptr mat) { - // force both mateiral objects to decay prior to absorption - this->Decay(); - mat->Decay(); + // force both mateiral objects to advance to the same decay time + int common_decay_time = std::max(this->prev_decay_time_, mat->prev_decay_time_); + if (ctx_ != NULL && ctx_->sim_info().decay != "never") { + common_decay_time = ctx_->time(); + } + this->Decay(common_decay_time); + mat->Decay(common_decay_time); // these calls force lazy evaluation if in lazy decay mode Composition::Ptr c0 = comp(); From d9bef8f6fefaf44e9222ab092e5e887882c5c199 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 2 Oct 2025 22:42:27 -0500 Subject: [PATCH 03/15] force update of prev_decay_time --- src/material.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/material.cc b/src/material.cc index 3dd7b50676..74405274da 100644 --- a/src/material.cc +++ b/src/material.cc @@ -109,8 +109,12 @@ void Material::Absorb(Material::Ptr mat) { if (ctx_ != NULL && ctx_->sim_info().decay != "never") { common_decay_time = ctx_->time(); } - this->Decay(common_decay_time); mat->Decay(common_decay_time); + this->Decay(common_decay_time); + + // manually update decay time in case the change was so small + // that no decay was invoked + this->prev_decay_time_ = common_decay_time; // these calls force lazy evaluation if in lazy decay mode Composition::Ptr c0 = comp(); From 1a0fa7b6187b22ebae7b520be917b9a33838177b Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Thu, 4 Dec 2025 16:46:27 -0600 Subject: [PATCH 04/15] changed the AbsorbPrevDecay test from material_tests to fit new decay behavior --- tests/material_tests.cc | 29 ++++++++++++++++++++--------- tests/material_tests.h | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/tests/material_tests.cc b/tests/material_tests.cc index a0566102f8..89b05c5141 100644 --- a/tests/material_tests.cc +++ b/tests/material_tests.cc @@ -408,23 +408,34 @@ TEST_F(MaterialTest, TransmutePrevDecay) { // as coded. We may decide to change the behavior in the future breaking // this test; the test will need to be modified accordingly. // -// This test checks to see that, when materials are absorbed together, the -// previous decay time for the larger quantity material is used as the value -// for the new, combined material. +// This test checks to see that, when materials are absorbed together, both +// materials are decayed prior to the absorption. + TEST_F(MaterialTest, AbsorbPrevDecay) { - Material::Ptr m1 = Material::Create(fac, 1, diff_comp_); - Material::Ptr m2 = Material::Create(fac, 1, diff_comp_); - Material::Ptr m3 = Material::Create(fac, 1000, diff_comp_); - m3->Decay(10); + FakeContext* fake_ctx = new FakeContext(&ti, &rec); + TestFacility* fake_fac = new TestFacility(fake_ctx); + + Material::Ptr m1 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m2 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m3 = Material::Create(fake_fac, 1000, diff_comp_); + + // Set context time to 10 to match the decay time + fake_ctx->time(10); + m3->Decay(); // decay m3 to time 10 EXPECT_EQ(0, m1->prev_decay_time()); EXPECT_EQ(0, m2->prev_decay_time()); EXPECT_EQ(10, m3->prev_decay_time()); + fake_ctx->time(11); + m1->Absorb(m3); - EXPECT_EQ(10, m1->prev_decay_time()); + EXPECT_EQ(11, m1->prev_decay_time()); m1->Absorb(m2); - EXPECT_EQ(10, m1->prev_decay_time()); + EXPECT_EQ(11, m1->prev_decay_time()); + + delete fake_fac; + delete fake_ctx; } TEST_F(MaterialTest, DecayHeatTest) { diff --git a/tests/material_tests.h b/tests/material_tests.h index a562f21b3d..e49b4c35f5 100644 --- a/tests/material_tests.h +++ b/tests/material_tests.h @@ -11,6 +11,7 @@ #include "test_agents/test_facility.h" #include "recorder.h" #include "timer.h" +#include "test_context.h" namespace cyclus { From 5163ac3fa7128b7e5f5cb07aeb217a9e5fcff791 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Thu, 4 Dec 2025 16:53:42 -0600 Subject: [PATCH 05/15] added unitvalue averaging back to absorb to restore tested behavior --- src/material.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/material.cc b/src/material.cc index 74405274da..68be084017 100644 --- a/src/material.cc +++ b/src/material.cc @@ -128,7 +128,11 @@ void Material::Absorb(Material::Ptr mat) { comp_ = Composition::CreateFromMass(compmath::Add(v, otherv)); } - qty_ += mat->qty_; + double tot_mass = qty_ + mat->quantity(); + double avg_unit_value = + (qty_ * UnitValue() + mat->quantity() * mat->UnitValue()) / tot_mass; + SetUnitValue(avg_unit_value); + qty_ = tot_mass; mat->qty_ = 0; tracker_.Absorb(&mat->tracker_); } From fd6c6541c01b44eb14a955b6a3926b79c3d89c14 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Tue, 12 May 2026 09:07:14 -0500 Subject: [PATCH 06/15] added blocks for backwards decay, and checks for both matls having context before setting common_decay_time to the context time --- src/material.cc | 10 ++++++---- src/material.h | 3 +++ tests/material_tests.cc | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/material.cc b/src/material.cc index 68be084017..601addf35a 100644 --- a/src/material.cc +++ b/src/material.cc @@ -106,8 +106,8 @@ void Material::Absorb(Material::Ptr mat) { // force both mateiral objects to advance to the same decay time int common_decay_time = std::max(this->prev_decay_time_, mat->prev_decay_time_); - if (ctx_ != NULL && ctx_->sim_info().decay != "never") { - common_decay_time = ctx_->time(); + if (HasContext() && mat->HasContext() && ctx_->sim_info().decay != "never") { + common_decay_time = std::max(ctx_->time(), common_decay_time); } mat->Decay(common_decay_time); this->Decay(common_decay_time); @@ -208,7 +208,8 @@ void Material::Decay(int curr_time) { curr_time = ctx_->time(); } - int dt = curr_time - prev_decay_time_; + // Block backwards decay with std::max + int dt = std::max(curr_time - prev_decay_time_, 0); if (dt == 0) { return; } @@ -246,7 +247,8 @@ void Material::Decay(int curr_time) { } } - prev_decay_time_ = curr_time; // this must go before Transmute call + // Need to set prev_decay_time before Transmute. Block setting it backwards. + prev_decay_time_ = std::max(curr_time, prev_decay_time_); Composition::Ptr decayed = comp_->Decay(dt, secs_per_timestep); Transmute(decayed); } diff --git a/src/material.h b/src/material.h index 9ed29950b4..2e7635069a 100644 --- a/src/material.h +++ b/src/material.h @@ -108,6 +108,9 @@ class Material : public Resource { /// Returns the mass of this material in kg. virtual double quantity() const; + /// Returns true if the material has a context, false otherwise. + virtual bool HasContext() const {return ctx_ != NULL;} + virtual Resource::Ptr ExtractRes(double qty); /// Same as ExtractComp with c = this->comp(). diff --git a/tests/material_tests.cc b/tests/material_tests.cc index 89b05c5141..341c5c0e63 100644 --- a/tests/material_tests.cc +++ b/tests/material_tests.cc @@ -134,6 +134,38 @@ TEST_F(MaterialTest, AbsorbIntoZeroMaterial) { EXPECT_FLOAT_EQ(test_size_, same_as_test_mat->quantity()); } +TEST_F(MaterialTest, AbsorbMatlWithoutContext) { + Material::Ptr no_ctx_mat = Material::CreateUntracked(0, test_comp_); + EXPECT_FALSE(no_ctx_mat->HasContext()); + EXPECT_NO_THROW(test_mat_->Absorb(no_ctx_mat)); +} + +TEST_F(MaterialTest, BackwardsDecay) { + FakeContext* fake_ctx = new FakeContext(&ti, &rec); + TestFacility* fake_fac = new TestFacility(fake_ctx); + + Material::Ptr m1 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m2 = Material::Create(fake_fac, 1, diff_comp_); + + // Set context time to 10 to match the decay time + fake_ctx->time(10); + + m2->Decay(); // decay m2 to time 10 + EXPECT_EQ(0, m1->prev_decay_time()); + EXPECT_EQ(10, m2->prev_decay_time()); + + fake_ctx->time(9); + + // After manually setting the ctx time backwards, common_decay_time is 10, + // and ctx->time() is 9, meaning decay will set dt = 0 with std::max, and + // prev_decay_time for the combined mat should remain at 10. + m1->Absorb(m2); + EXPECT_EQ(10, m1->prev_decay_time()); + + delete fake_fac; + delete fake_ctx; +} + TEST_F(MaterialTest, ExtractMass) { double amt = test_size_ / 3; double diff = test_size_ - amt; From aba1ca376d1d82d6d20ea218421def4ad8867347 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 2 Oct 2025 15:40:12 -0500 Subject: [PATCH 07/15] force both materials to decay on absorption --- CHANGELOG.rst | 1 + src/material.cc | 20 +++++++------------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1036e80165..124576d127 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -60,6 +60,7 @@ Since last release * Modified Doxygen homepage (#1815) * Pin ``python<3.13`` in CI workflows (#1826) * Changed the way Position is added to the archetypes (#1510) (#1872) +* Changed interaction between ``mat::Absorb()`` and ``mat::Decay()`` to ensure all materials being absorbed are decayed first **Removed:** diff --git a/src/material.cc b/src/material.cc index 33fe216f46..25baa51ce4 100644 --- a/src/material.cc +++ b/src/material.cc @@ -103,6 +103,11 @@ Material::Ptr Material::ExtractComp(double qty, Composition::Ptr c, } void Material::Absorb(Material::Ptr mat) { + + // force both mateiral objects to decay prior to absorption + this->Decay(); + mat->Decay(); + // these calls force lazy evaluation if in lazy decay mode Composition::Ptr c0 = comp(); Composition::Ptr c1 = mat->comp(); @@ -114,19 +119,8 @@ void Material::Absorb(Material::Ptr mat) { compmath::Normalize(&otherv, mat->qty_); comp_ = Composition::CreateFromMass(compmath::Add(v, otherv)); } - - // Set the decay time to the value of the material that had the larger - // quantity. This helps avoid inheriting erroneous prev decay times if, for - // example, you absorb a material into a zero-quantity material that had a - // prev decay time prior to the current simulation time step. - if (qty_ < mat->qty_) { - prev_decay_time_ = mat->prev_decay_time_; - } - double tot_mass = qty_ + mat->quantity(); - double avg_unit_value = - (qty_ * UnitValue() + mat->quantity() * mat->UnitValue()) / tot_mass; - SetUnitValue(avg_unit_value); - qty_ = tot_mass; + + qty_ += mat->qty_; mat->qty_ = 0; tracker_.Absorb(&mat->tracker_); } From 8e2897c2ec31cc4d13d9e037c276313badecc711 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 2 Oct 2025 22:21:19 -0500 Subject: [PATCH 08/15] select the common decay time for absorption more carefully --- src/material.cc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/material.cc b/src/material.cc index 25baa51ce4..cb9d9ca4df 100644 --- a/src/material.cc +++ b/src/material.cc @@ -104,9 +104,13 @@ Material::Ptr Material::ExtractComp(double qty, Composition::Ptr c, void Material::Absorb(Material::Ptr mat) { - // force both mateiral objects to decay prior to absorption - this->Decay(); - mat->Decay(); + // force both mateiral objects to advance to the same decay time + int common_decay_time = std::max(this->prev_decay_time_, mat->prev_decay_time_); + if (ctx_ != NULL && ctx_->sim_info().decay != "never") { + common_decay_time = ctx_->time(); + } + this->Decay(common_decay_time); + mat->Decay(common_decay_time); // these calls force lazy evaluation if in lazy decay mode Composition::Ptr c0 = comp(); From 6dd02f69d8d9940dd38b61933512cd6bf7aef017 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 2 Oct 2025 22:42:27 -0500 Subject: [PATCH 09/15] force update of prev_decay_time --- src/material.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/material.cc b/src/material.cc index cb9d9ca4df..113c26d25b 100644 --- a/src/material.cc +++ b/src/material.cc @@ -109,8 +109,12 @@ void Material::Absorb(Material::Ptr mat) { if (ctx_ != NULL && ctx_->sim_info().decay != "never") { common_decay_time = ctx_->time(); } - this->Decay(common_decay_time); mat->Decay(common_decay_time); + this->Decay(common_decay_time); + + // manually update decay time in case the change was so small + // that no decay was invoked + this->prev_decay_time_ = common_decay_time; // these calls force lazy evaluation if in lazy decay mode Composition::Ptr c0 = comp(); From 7a467e3467d81859a0090cc8bb0d93372c057f52 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Thu, 4 Dec 2025 16:46:27 -0600 Subject: [PATCH 10/15] changed the AbsorbPrevDecay test from material_tests to fit new decay behavior --- tests/material_tests.cc | 29 ++++++++++++++++++++--------- tests/material_tests.h | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/tests/material_tests.cc b/tests/material_tests.cc index a75cc22ee2..9a54dc661d 100644 --- a/tests/material_tests.cc +++ b/tests/material_tests.cc @@ -408,23 +408,34 @@ TEST_F(MaterialTest, TransmutePrevDecay) { // as coded. We may decide to change the behavior in the future breaking // this test; the test will need to be modified accordingly. // -// This test checks to see that, when materials are absorbed together, the -// previous decay time for the larger quantity material is used as the value -// for the new, combined material. +// This test checks to see that, when materials are absorbed together, both +// materials are decayed prior to the absorption. + TEST_F(MaterialTest, AbsorbPrevDecay) { - Material::Ptr m1 = Material::Create(fac, 1, diff_comp_); - Material::Ptr m2 = Material::Create(fac, 1, diff_comp_); - Material::Ptr m3 = Material::Create(fac, 1000, diff_comp_); - m3->Decay(10); + FakeContext* fake_ctx = new FakeContext(&ti, &rec); + TestFacility* fake_fac = new TestFacility(fake_ctx); + + Material::Ptr m1 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m2 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m3 = Material::Create(fake_fac, 1000, diff_comp_); + + // Set context time to 10 to match the decay time + fake_ctx->time(10); + m3->Decay(); // decay m3 to time 10 EXPECT_EQ(0, m1->prev_decay_time()); EXPECT_EQ(0, m2->prev_decay_time()); EXPECT_EQ(10, m3->prev_decay_time()); + fake_ctx->time(11); + m1->Absorb(m3); - EXPECT_EQ(10, m1->prev_decay_time()); + EXPECT_EQ(11, m1->prev_decay_time()); m1->Absorb(m2); - EXPECT_EQ(10, m1->prev_decay_time()); + EXPECT_EQ(11, m1->prev_decay_time()); + + delete fake_fac; + delete fake_ctx; } TEST_F(MaterialTest, DecayHeatTest) { diff --git a/tests/material_tests.h b/tests/material_tests.h index bad25aabfa..2b25e1ac6a 100644 --- a/tests/material_tests.h +++ b/tests/material_tests.h @@ -11,6 +11,7 @@ #include "test_agents/test_facility.h" #include "recorder.h" #include "timer.h" +#include "test_context.h" namespace cyclus { From dadcbd7dcd565abc523686af1d056fe4f8bf53a8 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Thu, 4 Dec 2025 16:53:42 -0600 Subject: [PATCH 11/15] added unitvalue averaging back to absorb to restore tested behavior --- src/material.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/material.cc b/src/material.cc index 113c26d25b..7b84947351 100644 --- a/src/material.cc +++ b/src/material.cc @@ -128,7 +128,11 @@ void Material::Absorb(Material::Ptr mat) { comp_ = Composition::CreateFromMass(compmath::Add(v, otherv)); } - qty_ += mat->qty_; + double tot_mass = qty_ + mat->quantity(); + double avg_unit_value = + (qty_ * UnitValue() + mat->quantity() * mat->UnitValue()) / tot_mass; + SetUnitValue(avg_unit_value); + qty_ = tot_mass; mat->qty_ = 0; tracker_.Absorb(&mat->tracker_); } From 6e5f2cb8cddd2cc565952abf49d37d5d1dedf3cb Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Tue, 12 May 2026 09:07:14 -0500 Subject: [PATCH 12/15] added blocks for backwards decay, and checks for both matls having context before setting common_decay_time to the context time --- src/material.cc | 10 ++++++---- src/material.h | 3 +++ tests/material_tests.cc | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/material.cc b/src/material.cc index 7b84947351..664b500570 100644 --- a/src/material.cc +++ b/src/material.cc @@ -106,8 +106,8 @@ void Material::Absorb(Material::Ptr mat) { // force both mateiral objects to advance to the same decay time int common_decay_time = std::max(this->prev_decay_time_, mat->prev_decay_time_); - if (ctx_ != NULL && ctx_->sim_info().decay != "never") { - common_decay_time = ctx_->time(); + if (HasContext() && mat->HasContext() && ctx_->sim_info().decay != "never") { + common_decay_time = std::max(ctx_->time(), common_decay_time); } mat->Decay(common_decay_time); this->Decay(common_decay_time); @@ -208,7 +208,8 @@ void Material::Decay(int curr_time) { curr_time = ctx_->time(); } - int dt = curr_time - prev_decay_time_; + // Block backwards decay with std::max + int dt = std::max(curr_time - prev_decay_time_, 0); if (dt == 0) { return; } @@ -247,7 +248,8 @@ void Material::Decay(int curr_time) { } } - prev_decay_time_ = curr_time; // this must go before Transmute call + // Need to set prev_decay_time before Transmute. Block setting it backwards. + prev_decay_time_ = std::max(curr_time, prev_decay_time_); Composition::Ptr decayed = comp_->Decay(dt, secs_per_timestep); Transmute(decayed); } diff --git a/src/material.h b/src/material.h index 9ed29950b4..2e7635069a 100644 --- a/src/material.h +++ b/src/material.h @@ -108,6 +108,9 @@ class Material : public Resource { /// Returns the mass of this material in kg. virtual double quantity() const; + /// Returns true if the material has a context, false otherwise. + virtual bool HasContext() const {return ctx_ != NULL;} + virtual Resource::Ptr ExtractRes(double qty); /// Same as ExtractComp with c = this->comp(). diff --git a/tests/material_tests.cc b/tests/material_tests.cc index 9a54dc661d..83a534522b 100644 --- a/tests/material_tests.cc +++ b/tests/material_tests.cc @@ -134,6 +134,38 @@ TEST_F(MaterialTest, AbsorbIntoZeroMaterial) { EXPECT_FLOAT_EQ(test_size_, same_as_test_mat->quantity()); } +TEST_F(MaterialTest, AbsorbMatlWithoutContext) { + Material::Ptr no_ctx_mat = Material::CreateUntracked(0, test_comp_); + EXPECT_FALSE(no_ctx_mat->HasContext()); + EXPECT_NO_THROW(test_mat_->Absorb(no_ctx_mat)); +} + +TEST_F(MaterialTest, BackwardsDecay) { + FakeContext* fake_ctx = new FakeContext(&ti, &rec); + TestFacility* fake_fac = new TestFacility(fake_ctx); + + Material::Ptr m1 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m2 = Material::Create(fake_fac, 1, diff_comp_); + + // Set context time to 10 to match the decay time + fake_ctx->time(10); + + m2->Decay(); // decay m2 to time 10 + EXPECT_EQ(0, m1->prev_decay_time()); + EXPECT_EQ(10, m2->prev_decay_time()); + + fake_ctx->time(9); + + // After manually setting the ctx time backwards, common_decay_time is 10, + // and ctx->time() is 9, meaning decay will set dt = 0 with std::max, and + // prev_decay_time for the combined mat should remain at 10. + m1->Absorb(m2); + EXPECT_EQ(10, m1->prev_decay_time()); + + delete fake_fac; + delete fake_ctx; +} + TEST_F(MaterialTest, ExtractMass) { double amt = test_size_ / 3; double diff = test_size_ - amt; From 191545ffa7d343a442c87f967caf5ecf31a9c315 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Mon, 8 Jun 2026 13:42:55 -0500 Subject: [PATCH 13/15] tracked materials can no longer backwards decay or decay past current sim time, untracked materials can only absorb other untracked materials which are not decayed ahead of them, added checks to confirm all this --- src/material.cc | 28 ++++++++++----- tests/material_tests.cc | 78 ++++++++++++++++++++++++++--------------- tests/material_tests.h | 2 ++ 3 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/material.cc b/src/material.cc index 664b500570..761fe21f13 100644 --- a/src/material.cc +++ b/src/material.cc @@ -105,9 +105,16 @@ Material::Ptr Material::ExtractComp(double qty, Composition::Ptr c, void Material::Absorb(Material::Ptr mat) { // force both mateiral objects to advance to the same decay time - int common_decay_time = std::max(this->prev_decay_time_, mat->prev_decay_time_); - if (HasContext() && mat->HasContext() && ctx_->sim_info().decay != "never") { - common_decay_time = std::max(ctx_->time(), common_decay_time); + int common_decay_time; + if (HasContext() && mat->HasContext()) { + common_decay_time = ctx_->time(); + } else if (!HasContext() && !mat->HasContext() + && mat->prev_decay_time_ > prev_decay_time_) { + throw ValueError("Cannot absorb a material that is more decayed than this one"); + } else if ((HasContext() && !mat->HasContext()) || (!HasContext() && mat->HasContext())) { + throw cyclus::Error("Cannot combine a tracked and untracked material!"); + } else { + common_decay_time = prev_decay_time_; } mat->Decay(common_decay_time); this->Decay(common_decay_time); @@ -208,10 +215,13 @@ void Material::Decay(int curr_time) { curr_time = ctx_->time(); } - // Block backwards decay with std::max - int dt = std::max(curr_time - prev_decay_time_, 0); - if (dt == 0) { - return; + int dt = curr_time - prev_decay_time_; + + // Block decay backwards and past sim time for materials in a context + if (ctx_ && (dt < 0 || curr_time > ctx_->time())) { + std::string msg = "Materials in a context cannot decay backwards or " + "past the current simulation time!"; + throw cyclus::Error(msg); } // eps_decay defined such that tritium (12.32 yr half life) decays over 1 day @@ -248,8 +258,8 @@ void Material::Decay(int curr_time) { } } - // Need to set prev_decay_time before Transmute. Block setting it backwards. - prev_decay_time_ = std::max(curr_time, prev_decay_time_); + // Need to set prev_decay_time before Transmute. + prev_decay_time_ = curr_time; Composition::Ptr decayed = comp_->Decay(dt, secs_per_timestep); Transmute(decayed); } diff --git a/tests/material_tests.cc b/tests/material_tests.cc index 83a534522b..8b108de4aa 100644 --- a/tests/material_tests.cc +++ b/tests/material_tests.cc @@ -137,30 +137,33 @@ TEST_F(MaterialTest, AbsorbIntoZeroMaterial) { TEST_F(MaterialTest, AbsorbMatlWithoutContext) { Material::Ptr no_ctx_mat = Material::CreateUntracked(0, test_comp_); EXPECT_FALSE(no_ctx_mat->HasContext()); + // Since test_mat_ and no_ctx_mat both don't have context this is fine EXPECT_NO_THROW(test_mat_->Absorb(no_ctx_mat)); } -TEST_F(MaterialTest, BackwardsDecay) { +TEST_F(MaterialTest, DecayTiming) { FakeContext* fake_ctx = new FakeContext(&ti, &rec); TestFacility* fake_fac = new TestFacility(fake_ctx); + double untracked_qty = 1.0; + Material::Ptr m1 = Material::Create(fake_fac, 1, diff_comp_); - Material::Ptr m2 = Material::Create(fake_fac, 1, diff_comp_); + Material::Ptr m2 = Material::CreateUntracked(untracked_qty, diff_comp_); // Set context time to 10 to match the decay time fake_ctx->time(10); - m2->Decay(); // decay m2 to time 10 - EXPECT_EQ(0, m1->prev_decay_time()); - EXPECT_EQ(10, m2->prev_decay_time()); - - fake_ctx->time(9); - - // After manually setting the ctx time backwards, common_decay_time is 10, - // and ctx->time() is 9, meaning decay will set dt = 0 with std::max, and - // prev_decay_time for the combined mat should remain at 10. - m1->Absorb(m2); + m1->Decay(); // decay m1 to time 10 EXPECT_EQ(10, m1->prev_decay_time()); + EXPECT_ANY_THROW(m1->Decay(9)); // no backwards decay for tracked mats + EXPECT_ANY_THROW(m1->Decay(1000)); // tracked can't decay past sim time + + m2->Decay(10); // decay m2 to time 10 + EXPECT_EQ(10, m2->prev_decay_time()); + EXPECT_NO_THROW(m2->Decay(6)); // Backwards decay is fine if untracked + EXPECT_EQ(6, m2->prev_decay_time()); // correctly updates prev_decay_time_ + EXPECT_NO_THROW(m2->Decay(1000)); // Arbitrary decay fine on untracked + EXPECT_EQ(1000, m2->prev_decay_time()); delete fake_fac; delete fake_ctx; @@ -243,15 +246,15 @@ TEST_F(MaterialTest, ExtractInGrams) { TEST_F(MaterialTest, DecayResBuf) { // prequeries - cyclus::toolkit::MatQuery orig(tracked_mat_); + cyclus::toolkit::MatQuery orig(untracked_mat_); double u235_qty = orig.mass(u235_); double pb208_qty = orig.mass(pb208_); double am241_qty = orig.mass(am241_); double sr89_qty = orig.mass(sr89_); - double orig_mass = tracked_mat_->quantity(); + double orig_mass = untracked_mat_->quantity(); cyclus::toolkit::ResBuf res_buf; - res_buf.Push(tracked_mat_); + res_buf.Push(untracked_mat_); // use untracked mat so we can forward decay // decay for 2 months which is just over 1 Sr-89 half-life res_buf.Decay(2); cyclus::Material::Ptr pop_mat = res_buf.Pop(); @@ -268,17 +271,17 @@ TEST_F(MaterialTest, DecayResBuf) { TEST_F(MaterialTest, DecayManual) { // prequeries - cyclus::toolkit::MatQuery orig(tracked_mat_); + cyclus::toolkit::MatQuery orig(untracked_mat_); double u235_qty = orig.mass(u235_); double pb208_qty = orig.mass(pb208_); double am241_qty = orig.mass(am241_); double sr89_qty = orig.mass(sr89_); - double orig_mass = tracked_mat_->quantity(); + double orig_mass = untracked_mat_->quantity(); - tracked_mat_->Decay(100); + untracked_mat_->Decay(100); // postquery - cyclus::toolkit::MatQuery mq(tracked_mat_); + cyclus::toolkit::MatQuery mq(untracked_mat_); // postchecks EXPECT_NE(u235_qty, mq.mass(u235_)); @@ -385,20 +388,22 @@ TEST_F(MaterialTest, DecayCustomTimeStep) { cyclus::Env::SetNucDataPath(); std::string cs137 ("Cs137"); uint64_t custom_timestep = pyne::half_life(cs137); + double qty = 1.0; SimInfo si(10, 2015, 1, "", "manual"); si.dt = custom_timestep; - cyclus::Context ctx(&ti, &rec); - ctx.InitSim(si); - Agent* a = new TestFacility(&ctx); + FakeContext* fake_ctx = new FakeContext(&ti, &rec); + fake_ctx->InitSim(si); CompMap v; v[id("Cs137")] = 1; Composition::Ptr c = Composition::CreateFromAtom(v); CompMap tmp = c->atom(); - Material::Ptr m = Material::Create(a, 1.0, c); + TestFacility* fake_fac = new TestFacility(fake_ctx); + Material::Ptr m = Material::Create(fake_fac, qty, c); - m->Decay(1); + fake_ctx->time(1); + m->Decay(); Composition::Ptr newc = m->comp(); CompMap newv = newc->atom(); @@ -407,14 +412,17 @@ TEST_F(MaterialTest, DecayCustomTimeStep) { // one half of atoms should have decayed away double eps = cyclus::CY_NEAR_ZERO; EXPECT_NEAR(0.5, newv[id("Cs137")], eps) << "one Cs137 half-life duration time step did not decay half of Cs atoms"; + + delete fake_fac; + delete fake_ctx; } TEST_F(MaterialTest, ExtractPrevDecay) { - tracked_mat_->Decay(10); - double qty = tracked_mat_->quantity() / 2; - cyclus::Material::Ptr x = tracked_mat_->ExtractQty(qty); + untracked_mat_->Decay(10); + double qty = untracked_mat_->quantity() / 2; + cyclus::Material::Ptr x = untracked_mat_->ExtractQty(qty); - EXPECT_EQ(tracked_mat_->prev_decay_time(), x->prev_decay_time()); + EXPECT_EQ(untracked_mat_->prev_decay_time(), x->prev_decay_time()); } // Transmute should reset a material's prev_decay_time to the current @@ -447,9 +455,13 @@ TEST_F(MaterialTest, AbsorbPrevDecay) { FakeContext* fake_ctx = new FakeContext(&ti, &rec); TestFacility* fake_fac = new TestFacility(fake_ctx); + double untracked_qty = 1.0; + Material::Ptr m1 = Material::Create(fake_fac, 1, diff_comp_); Material::Ptr m2 = Material::Create(fake_fac, 1, diff_comp_); Material::Ptr m3 = Material::Create(fake_fac, 1000, diff_comp_); + Material::Ptr m4 = Material::CreateUntracked(untracked_qty, diff_comp_); + Material::Ptr m5 = Material::CreateUntracked(untracked_qty, diff_comp_); // Set context time to 10 to match the decay time fake_ctx->time(10); @@ -466,6 +478,16 @@ TEST_F(MaterialTest, AbsorbPrevDecay) { m1->Absorb(m2); EXPECT_EQ(11, m1->prev_decay_time()); + // We shouldn't be able to absorb a more-decayed untracked material + // but we should be able to absorb materials not in a context. + m4->Decay(11); + EXPECT_ANY_THROW(m5->Absorb(m4)); + EXPECT_NO_THROW(m4->Absorb(m5)); + + // Can't combine a tracked an untracked mat + EXPECT_ANY_THROW(m1->Absorb(m5)); + EXPECT_ANY_THROW(m5->Absorb(m1)); + delete fake_fac; delete fake_ctx; } diff --git a/tests/material_tests.h b/tests/material_tests.h index 2b25e1ac6a..d9dd335e21 100644 --- a/tests/material_tests.h +++ b/tests/material_tests.h @@ -27,6 +27,7 @@ class MaterialTest : public ::testing::Test { Material::Ptr diff_mat_; Material::Ptr default_mat_; Material::Ptr tracked_mat_; + Material::Ptr untracked_mat_; Material::Ptr tracked_mat_no_decay_; long int u235_halflife_; int th228_halflife_; @@ -89,6 +90,7 @@ class MaterialTest : public ::testing::Test { two_test_mat_ = Material::CreateUntracked(2 * test_size_, test_comp_); ten_test_mat_ = Material::CreateUntracked(10 * test_size_, test_comp_); diff_mat_ = Material::CreateUntracked(test_size_, diff_comp_); + untracked_mat_ = Material::CreateUntracked(1000, diff_comp_); // tracked material tracked_mat_ = Material::Create(fac, 1000, diff_comp_); From b0004a793f3b5cb4b98ec80ba8fc7b2e42274f06 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Wed, 10 Jun 2026 15:14:57 -0500 Subject: [PATCH 14/15] simplified logic in Absorb rules handling, changed leftover comment to be more descriptive --- src/material.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/material.cc b/src/material.cc index 17b6de022f..814cf9dae6 100644 --- a/src/material.cc +++ b/src/material.cc @@ -104,14 +104,14 @@ Material::Ptr Material::ExtractComp(double qty, Composition::Ptr c, void Material::Absorb(Material::Ptr mat) { - // force both mateiral objects to advance to the same decay time + // Handle absorb-specific decay rules int common_decay_time; if (HasContext() && mat->HasContext()) { common_decay_time = ctx_->time(); } else if (!HasContext() && !mat->HasContext() && mat->prev_decay_time_ > prev_decay_time_) { throw ValueError("Cannot absorb a material that is more decayed than this one"); - } else if ((HasContext() && !mat->HasContext()) || (!HasContext() && mat->HasContext())) { + } else if (HasContext() != !mat->HasContext()) { throw cyclus::Error("Cannot combine a tracked and untracked material!"); } else { common_decay_time = prev_decay_time_; From 7bf394480b25caa04c1306ccdf2e4038e93f1ca6 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Mon, 15 Jun 2026 16:35:02 -0500 Subject: [PATCH 15/15] fixed double NOT typo in Absorb decay logic, fixed remaining test --- src/material.cc | 2 +- tests/material_tests.cc | 14 ++++++++++++-- tests/material_tests.h | 12 +----------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/material.cc b/src/material.cc index f403ddbb5f..a995fd5aec 100644 --- a/src/material.cc +++ b/src/material.cc @@ -111,7 +111,7 @@ void Material::Absorb(Material::Ptr mat) { } else if (!HasContext() && !mat->HasContext() && mat->prev_decay_time_ > prev_decay_time_) { throw ValueError("Cannot absorb a material that is more decayed than this one"); - } else if (HasContext() != !mat->HasContext()) { + } else if (HasContext() != mat->HasContext()) { throw cyclus::Error("Cannot combine a tracked and untracked material!"); } else { common_decay_time = prev_decay_time_; diff --git a/tests/material_tests.cc b/tests/material_tests.cc index f65bf14bad..48442fa58b 100644 --- a/tests/material_tests.cc +++ b/tests/material_tests.cc @@ -515,14 +515,24 @@ TEST_F(MaterialTest, DecaySmallAmount) { v[tritium_id] = 1.0; Composition::Ptr tritium_comp = Composition::CreateFromMass(v); + // Set up the one day time step context + int one_day = 86400; + cyclus::Timer ti_day_timestep; + si_day_timestep = SimInfo(100, 2015, 1, "", "manual"); + si_day_timestep.dt = one_day; + FakeContext* ctx_day_timestep = new FakeContext(&ti_day_timestep, &rec); + ctx_day_timestep->InitSim(si_day_timestep); + TestFacility* fac_day_timestep = new TestFacility(ctx_day_timestep); + Material::Ptr tritium = Material::Create(fac_day_timestep, qty, tritium_comp); cyclus::toolkit::MatQuery mq(tritium); double start_qty = mq.mass(tritium_id); EXPECT_EQ(qty, start_qty); - // Decay forward by one day (we use the one day time step facility) - tritium->Decay(1); + // Decay forward by one day (we have to push the fake_ctx's time forward first) + ctx_day_timestep->time(1); + tritium->Decay(); double decayed_qty = mq.mass(tritium_id); // NOTE: we've already tested that Decay is decaying the correct amt, so we diff --git a/tests/material_tests.h b/tests/material_tests.h index d9dd335e21..efa826f3a9 100644 --- a/tests/material_tests.h +++ b/tests/material_tests.h @@ -34,14 +34,12 @@ class MaterialTest : public ::testing::Test { double u235_g_per_mol_; cyclus::Timer ti; - cyclus::Timer ti_day_timestep; cyclus::Recorder rec; cyclus::Context* ctx; TestFacility* fac; cyclus::Context* ctx_no_decay; TestFacility* fac_no_decay; - cyclus::Context* ctx_day_timestep; - TestFacility* fac_day_timestep; + // dur 100, y0 = 2015, m0=1, handle="", d="never" SimInfo si; SimInfo si_day_timestep; @@ -51,14 +49,6 @@ class MaterialTest : public ::testing::Test { ctx = new cyclus::Context(&ti, &rec); fac = new TestFacility(ctx); - // Set up the one day time step context - int one_day = 86400; - si_day_timestep = SimInfo(100, 2015, 1, "", "manual"); - si_day_timestep.dt = one_day; - ctx_day_timestep = new cyclus::Context(&ti_day_timestep, &rec); - ctx_day_timestep->InitSim(si_day_timestep); - fac_day_timestep = new TestFacility(ctx_day_timestep); - si = SimInfo(100, 2015, 1, "", "never"); ctx_no_decay = new cyclus::Context(&ti, &rec); ctx_no_decay->InitSim(si);