diff --git a/src/reactor.cc b/src/reactor.cc index 26b82a644b..e8d30ecb4d 100644 --- a/src/reactor.cc +++ b/src/reactor.cc @@ -57,7 +57,7 @@ void Reactor::InitFrom(cyclus::QueryableBackend* b) { void Reactor::EnterNotify() { cyclus::Facility::EnterNotify(); - + // If the user ommitted fuel_prefs, we set it to zeros for each fuel // type. Without this segfaults could occur - yuck. if (fuel_prefs.size() == 0) { @@ -98,6 +98,10 @@ void Reactor::EnterNotify() { } } +bool Reactor::CheckDecommissionCondition() { + return core.count() == 0 && spent.count() == 0; +} + void Reactor::Tick() { // The following code must go in the Tick so they fire on the time step // following the cycle_step update - allowing for the all reactor events to @@ -105,10 +109,32 @@ void Reactor::Tick() { // they // can't go at the beginnin of the Tock is so that resource exchange has a // chance to occur after the discharge on this same time step. + + if (retired()) { + Record("RETIRED", ""); + + if (context()->time() == exit_time()) { // only need to transmute once + Transmute(ceil(static_cast(n_assem_core) / 2.0)); + } + while (core.count() > 0) { + if (!Discharge()) { + break; + } + } + // in case a cycle lands exactly on our last time step, we will need to + // burn a batch from fresh inventory on this time step. When retired, + // this batch also needs to be discharged to spent fuel inventory. + while (fresh.count() > 0 && spent.space() >= assem_size) { + spent.Push(fresh.Pop()); + } + return; + } + if (cycle_step == cycle_time) { Transmute(); Record("CYCLE_END", ""); } + if (cycle_step >= cycle_time && !discharged) { discharged = Discharge(); } @@ -158,10 +184,26 @@ std::set::Ptr> Reactor::GetMatlRequests() { std::set::Ptr> ports; Material::Ptr m; - int n_assem_order = - n_assem_core - core.count() + n_assem_fresh - fresh.count(); + // second min expression reduces assembles to amount needed until + // retirement if it is near. + int n_assem_order = n_assem_core - core.count() + n_assem_fresh - fresh.count(); + + if (exit_time() != -1) { + // the +1 accounts for the fact that the reactor is alive and gets to + // operate during its exit_time time step. + int t_left = exit_time() - context()->time() + 1; + int t_left_cycle = cycle_time + refuel_time - cycle_step; + double n_cycles_left = static_cast(t_left - t_left_cycle) / + static_cast(cycle_time + refuel_time); + n_cycles_left = ceil(n_cycles_left); + int n_need = std::max(0.0, n_cycles_left * n_assem_batch - n_assem_fresh); + n_assem_order = std::min(n_assem_order, n_need); + } + if (n_assem_order == 0) { return ports; + } else if (retired()) { + return ports; } for (int i = 0; i < n_assem_order; i++) { @@ -189,7 +231,6 @@ void Reactor::GetMatlTrades( using cyclus::Trade; std::map mats = PopSpent(); - std::vector >::const_iterator it; for (int i = 0; i < trades.size(); i++) { std::string commod = trades[i].request->commodity(); Material::Ptr m = mats[commod].back(); @@ -233,8 +274,16 @@ std::set::Ptr> Reactor::GetMatlBids( bool gotmats = false; std::map all_mats; - for (int i = 0; i < fuel_outcommods.size(); i++) { - std::string commod = fuel_outcommods[i]; + + if (uniq_outcommods_.empty()) { + for (int i = 0; i < fuel_outcommods.size(); i++) { + uniq_outcommods_.insert(fuel_outcommods[i]); + } + } + + std::set::iterator it; + for (it = uniq_outcommods_.begin(); it != uniq_outcommods_.end(); ++it) { + std::string commod = *it; std::vector*>& reqs = commod_requests[commod]; if (reqs.size() == 0) { continue; @@ -275,6 +324,10 @@ std::set::Ptr> Reactor::GetMatlBids( } void Reactor::Tock() { + if (retired()) { + return; + } + if (cycle_step >= cycle_time + refuel_time && core.count() == n_assem_core) { discharged = false; cycle_step = 0; @@ -298,12 +351,15 @@ void Reactor::Tock() { } } -void Reactor::Transmute() { - // safe to assume full core. - MatVec old = core.PopN(n_assem_batch); - MatVec tail = core.PopN(core.count()); +void Reactor::Transmute() { Transmute(n_assem_batch); } + +void Reactor::Transmute(int n_assem) { + MatVec old = core.PopN(std::min(n_assem, core.count())); core.Push(old); - core.Push(tail); + if (core.count() > old.size()) { + // rotate untransmuted mats back to back of buffer + core.Push(core.PopN(core.count() - old.size())); + } std::stringstream ss; ss << old.size() << " assemblies"; @@ -326,13 +382,12 @@ std::map Reactor::PeekSpent() { } bool Reactor::Discharge() { - if (n_assem_spent - spent.count() < n_assem_batch) { + int npop = std::min(n_assem_batch, core.count()); + if (n_assem_spent - spent.count() < npop) { Record("DISCHARGE", "failed"); return false; // not enough room in spent buffer } - int npop = std::min(n_assem_batch, core.count()); - std::stringstream ss; ss << npop << " assemblies"; Record("DISCHARGE", ss.str()); diff --git a/src/reactor.h b/src/reactor.h index 8d9cfcecf4..a6aa645dbf 100644 --- a/src/reactor.h +++ b/src/reactor.h @@ -42,6 +42,15 @@ namespace cycamore { /// becomes full, the reactor will halt operation at the end of the next cycle /// until there is more room. Each time step, the reactor will try to trade /// away as much of its spent fuel inventory as possible. +/// +/// When the reactor reaches the end of its lifetime, it will discharge all +/// material from its core and trade away all its spent fuel as quickly as +/// possible. Full decommissioning will be delayed until all spent fuel is +/// gone. If the reactor has a full core when it is decommissioned (i.e. is +/// mid-cycle) when the reactor is decommissioned, half (rounded up to nearest +/// int) of its assemblies are transmuted to their respective burnt +/// compositions. + class Reactor : public cyclus::Facility, public cyclus::toolkit::CommodityProducer { #pragma cyclus note { \ @@ -83,7 +92,16 @@ class Reactor : public cyclus::Facility, " operational cycle before the next begins. If the spent fuel inventory" \ " becomes full, the reactor will halt operation at the end of the next cycle" \ " until there is more room. Each time step, the reactor will try to trade" \ - " away as much of its spent fuel inventory as possible.", \ + " away as much of its spent fuel inventory as possible." \ + "\n\n" \ + "When the reactor reaches the end of its lifetime, it will discharge all" \ + " material from its core and trade away all its spent fuel as quickly as" \ + " possible. Full decommissioning will be delayed until all spent fuel is" \ + " gone. If the reactor has a full core when it is decommissioned (i.e. is" \ + " mid-cycle) when the reactor is decommissioned, half (rounded up to nearest" \ + " int) of its assemblies are transmuted to their respective burnt" \ + " compositions." \ + "", \ } public: @@ -93,6 +111,7 @@ class Reactor : public cyclus::Facility, virtual void Tick(); virtual void Tock(); virtual void EnterNotify(); + virtual bool CheckDecommissionCondition(); virtual void AcceptMatlTrades(const std::vector, cyclus::Material::Ptr> >& responses); @@ -117,6 +136,10 @@ class Reactor : public cyclus::Facility, std::string fuel_outrecipe(cyclus::Material::Ptr m); double fuel_pref(cyclus::Material::Ptr m); + bool retired() { + return exit_time() != -1 && context()->time() >= exit_time(); + } + /// Store fuel info index for the given resource received on incommod. void index_res(cyclus::Resource::Ptr m, std::string incommod); @@ -131,6 +154,10 @@ class Reactor : public cyclus::Facility, /// fully burnt state as defined by its outrecipe. void Transmute(); + /// Transmute the specified number of assemblies in the core to their + /// fully burnt state as defined by their outrecipe. + void Transmute(int n_assem); + /// Records a reactor event to the output db with the given name and note val. void Record(std::string name, std::string val); @@ -335,6 +362,9 @@ class Reactor : public cyclus::Facility, "internal": True \ } std::map res_indexes; + + // populated lazily and no need to persist. + std::set uniq_outcommods_; }; } // namespace cycamore diff --git a/src/reactor_tests.cc b/src/reactor_tests.cc index 8c743e5cf4..d67d14d897 100644 --- a/src/reactor_tests.cc +++ b/src/reactor_tests.cc @@ -461,6 +461,52 @@ TEST(ReactorTests, RecipeChange) { EXPECT_TRUE(0 < mq.mass(id("H1"))); } +TEST(ReactorTests, Retire) { + std::string config = + " lwr_fresh " + " lwr_spent " + " enriched_u " + " waste " + "" + " 7 " + " 0 " + " 300 " + " 1 " + " 3 " + " 1 " + ""; + + int dur = 50; + int life = 36; + cyclus::MockSim sim(cyclus::AgentSpec(":cycamore:Reactor"), config, dur, life); + sim.AddSource("enriched_u").Finalize(); + sim.AddSink("waste").Finalize(); + sim.AddRecipe("lwr_fresh", c_uox()); + sim.AddRecipe("lwr_spent", c_spentuox()); + int id = sim.Run(); + + int ncore = 3; + int nbatch = 1; + + // reactor should stop requesting new fresh fuel as it approaches retirement + int nassem_recv = + static_cast(ceil(static_cast(life) / 7.0)) * nbatch + + (ncore - nbatch); + + std::vector conds; + conds.push_back(Cond("ReceiverId", "==", id)); + QueryResult qr = sim.db().Query("Transactions", &conds); + EXPECT_EQ(nassem_recv, qr.rows.size()) + << "failed to stop ordering near retirement"; + + // reactor should discharge all fuel before/by retirement + conds.clear(); + conds.push_back(Cond("SenderId", "==", id)); + qr = sim.db().Query("Transactions", &conds); + EXPECT_EQ(nassem_recv, qr.rows.size()) + << "failed to discharge all material by retirement time"; +} + } // namespace reactortests } // namespace cycamore