Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
acb8582
force both materials to decay on absorption
gonuke Oct 2, 2025
4880774
select the common decay time for absorption more carefully
gonuke Oct 3, 2025
d9bef8f
force update of prev_decay_time
gonuke Oct 3, 2025
1a0fa7b
changed the AbsorbPrevDecay test from material_tests to fit new decay…
dean-krueger Dec 4, 2025
5163ac3
added unitvalue averaging back to absorb to restore tested behavior
dean-krueger Dec 4, 2025
fd6c654
added blocks for backwards decay, and checks for both matls having co…
dean-krueger May 12, 2026
aba1ca3
force both materials to decay on absorption
gonuke Oct 2, 2025
8e2897c
select the common decay time for absorption more carefully
gonuke Oct 3, 2025
6dd02f6
force update of prev_decay_time
gonuke Oct 3, 2025
7a467e3
changed the AbsorbPrevDecay test from material_tests to fit new decay…
dean-krueger Dec 4, 2025
dadcbd7
added unitvalue averaging back to absorb to restore tested behavior
dean-krueger Dec 4, 2025
6e5f2cb
added blocks for backwards decay, and checks for both matls having co…
dean-krueger May 12, 2026
191545f
tracked materials can no longer backwards decay or decay past current…
dean-krueger Jun 8, 2026
510354b
merged local branch with origin after dk and pw rebase. I think this …
dean-krueger Jun 10, 2026
b0004a7
simplified logic in Absorb rules handling, changed leftover comment t…
dean-krueger Jun 10, 2026
4f88172
Merge branch 'decay_absorb' into decay-absorb
dean-krueger Jun 10, 2026
7bf3944
fixed double NOT typo in Absorb decay logic, fixed remaining test
dean-krueger Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions src/material.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,17 @@ 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 (ctx_ != NULL && ctx_->sim_info().decay != "never") {
// 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()) {
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);
Expand All @@ -128,7 +135,12 @@ 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_);
}
Expand Down Expand Up @@ -204,9 +216,14 @@ void Material::Decay(int curr_time) {
curr_time = ctx_->time();
}


int dt = curr_time - prev_decay_time_;
if (dt == 0) {
return;

// 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
Expand Down Expand Up @@ -243,7 +260,9 @@ 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.
prev_decay_time_ = curr_time;

Composition::Ptr decayed = comp_->Decay(dt, secs_per_timestep);
Transmute(decayed);
}
Expand Down
3 changes: 3 additions & 0 deletions src/material.h
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
130 changes: 103 additions & 27 deletions tests/material_tests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,42 @@ 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());

// 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, 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::CreateUntracked(untracked_qty, diff_comp_);

// Set context time to 10 to match the decay time
fake_ctx->time(10);

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;
}

TEST_F(MaterialTest, ExtractMass) {
double amt = test_size_ / 3;
double diff = test_size_ - amt;
Expand Down Expand Up @@ -211,15 +247,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<cyclus::Material> 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();
Expand All @@ -236,17 +272,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_));
Expand Down Expand Up @@ -353,20 +389,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();
Expand All @@ -375,14 +413,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
Expand All @@ -408,23 +449,48 @@ 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);

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);

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());

// 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;
}

TEST_F(MaterialTest, DecayHeatTest) {
Expand All @@ -449,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
Expand Down
15 changes: 4 additions & 11 deletions tests/material_tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "test_agents/test_facility.h"
#include "recorder.h"
#include "timer.h"
#include "test_context.h"


namespace cyclus {
Expand All @@ -26,20 +27,19 @@ 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_;
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;
Expand All @@ -49,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);
Expand Down Expand Up @@ -88,6 +80,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_);
Expand Down
Loading