diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3e6f06817..15eee6de8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,7 @@ cycamore Change Log **Added:** * GitHub workflows for building/testing on a PR and push to `main` (#549, #564) -* Add functionality for random behavior on the size of a sink (#550) +* Add functionality for random behavior on the size (#550) and frequency (#565) of a sink * GitHub workflow to check that the CHANGELOG has been updated (#562) **Changed:** diff --git a/src/sink.cc b/src/sink.cc index d1fe861e0..05fdc3311 100644 --- a/src/sink.cc +++ b/src/sink.cc @@ -56,7 +56,20 @@ void Sink::EnterNotify() { /// Create first requestAmt. Only used in testing, as a simulation will /// overwrite this on Tick() SetRequestAmt(); + SetNextBuyTime(); + if (random_size_type != "None") { + LOG(cyclus::LEV_INFO4, "SnkFac") << "Sink " << this->id() + << " is using random behavior " + << random_size_type + << " for determining request size."; + } + if (random_frequency_type != "None") { + LOG(cyclus::LEV_INFO4, "SnkFac") << "Sink " << this->id() + << " is using random behavior " + << random_frequency_type + << " for determining request frequency."; + } RecordPosition(); } @@ -170,21 +183,32 @@ void Sink::AcceptGenRsrcTrades( void Sink::Tick() { using std::string; using std::vector; - LOG(cyclus::LEV_INFO3, "SnkFac") << prototype() << " is ticking {"; + LOG(cyclus::LEV_INFO3, "SnkFac") << "Sink " << this->id() << " is ticking {"; - SetRequestAmt(); + if (nextBuyTime == -1) { + SetRequestAmt(); + } + else if (nextBuyTime == context()->time()) { + SetRequestAmt(); + SetNextBuyTime(); - LOG(cyclus::LEV_INFO3, "SnkFac") << prototype() << " has default request amount " << requestAmt; + LOG(cyclus::LEV_INFO4, "SnkFac") << "Sink " << this->id() + << " has reached buying time. The next buy time will be time step " << nextBuyTime; + } + else { + requestAmt = 0; + } // inform the simulation about what the sink facility will be requesting if (requestAmt > cyclus::eps()) { - LOG(cyclus::LEV_INFO4, "SnkFac") << prototype() - << " has request amount " << requestAmt - << " kg of " << in_commods[0] << "."; + LOG(cyclus::LEV_INFO4, "SnkFac") << "Sink " << this->id() + << " has request amount " << requestAmt + << " kg of " << in_commods[0] << "."; for (vector::iterator commod = in_commods.begin(); commod != in_commods.end(); commod++) { - LOG(cyclus::LEV_INFO4, "SnkFac") << " will request " << requestAmt + LOG(cyclus::LEV_INFO4, "SnkFac") << "Sink " << this->id() + << " will request " << requestAmt << " kg of " << *commod << "."; cyclus::toolkit::RecordTimeSeries("demand"+*commod, this, requestAmt); @@ -242,6 +266,22 @@ void Sink::SetRequestAmt() { return; } +void Sink::SetNextBuyTime() { + if (random_frequency_type == "None") { + nextBuyTime = -1; + } + else if (random_frequency_type == "UniformInt") { + nextBuyTime = context()->time() + context()->random_uniform_int(random_frequency_min, random_frequency_max); + } + else if (random_frequency_type == "NormalInt") { + nextBuyTime = context()->time() + context()->random_normal_int(random_frequency_mean, random_frequency_stddev, random_frequency_min, random_frequency_max); + } + else { + nextBuyTime = -1; + } + return; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - extern "C" cyclus::Agent* ConstructSink(cyclus::Context* ctx) { return new Sink(ctx); diff --git a/src/sink.h b/src/sink.h index e9e4de0d2..8de16975d 100644 --- a/src/sink.h +++ b/src/sink.h @@ -72,6 +72,9 @@ class Sink /// @brief SinkFacilities update request amount using random behavior virtual void SetRequestAmt(); + /// @brief SinkFacilities update request time using random behavior + virtual void SetNextBuyTime(); + /// add a commodity to the set of input commodities /// @param name the commodity name inline void AddCommodity(std::string name) { in_commods.push_back(name); } @@ -111,6 +114,7 @@ class Sink private: double requestAmt; + int nextBuyTime; /// all facilities must have at least one input commodity #pragma cyclus var {"tooltip": "input commodities", \ "doc": "commodities that the sink facility accepts", \ @@ -195,8 +199,61 @@ class Sink "space to use as the standard deviation. Default 0.1"} double random_size_stddev; - // random status (frequencing/timing of request) + #pragma cyclus var {"default": "None", \ + "tooltip": "type of random behavior when setting the " \ + "timing of the request", \ + "uitype": "combobox", \ + "uilabel": "Random Timing", \ + "categorical": ["None", "UniformInt", "NormalInt"], \ + "doc": "type of random behavior to use. Default None, " \ + "other options are, 'UniformInt', and 'NormalInt'. " \ + "When using 'UniformInt', also set "\ + "'random_frequency_min' and 'random_frequency_max'. " \ + "For 'NormalInt', set 'random_frequency_mean' and " \ + "'random_fequency_stddev', min and max values are " \ + "optional. "} + std::string random_frequency_type; + + // random frequency mean + #pragma cyclus var {"default": 1, \ + "tooltip": "mean of the random frequency", \ + "uilabel": "Random Frequency Mean", \ + "uitype": "range", \ + "range": [0.0, 1e299], \ + "doc": "When a normal distribution is used to determine the " \ + "frequency of the request, this is the mean. Default 1"} + double random_frequency_mean; + + // random frequency std dev + #pragma cyclus var {"default": 1, \ + "tooltip": "std dev of the random frequency", \ + "uilabel": "Random Frequency Std Dev", \ + "uitype": "range", \ + "range": [0.0, 1e299], \ + "doc": "When a normal distribution is used to determine the " \ + "frequency of the request, this is the standard deviation. Default 1"} + double random_frequency_stddev; + + // random frequency lower bound + #pragma cyclus var {"default": 1, \ + "tooltip": "lower bound of the random frequency", \ + "uilabel": "Random Frequency Lower Bound", \ + "uitype": "range", \ + "range": [1, 1e299], \ + "doc": "When a random distribution is used to determine the " \ + "frequency of the request, this is the lower bound. Default 1"} + int random_frequency_min; + + // random frequency upper bound + #pragma cyclus var {"default": 1e299, \ + "tooltip": "upper bound of the random frequency", \ + "uilabel": "Random Frequency Upper Bound", \ + "uitype": "range", \ + "range": [1, 1e299], \ + "doc": "When a random distribution is used to determine the " \ + "frequency of the request, this is the upper bound. Default 1e299"} + int random_frequency_max; #pragma cyclus var { \ "default": 0.0, \ diff --git a/src/sink_tests.cc b/src/sink_tests.cc index 630db4f5f..2d727a30f 100644 --- a/src/sink_tests.cc +++ b/src/sink_tests.cc @@ -342,7 +342,9 @@ TEST_F(SinkTest, PositionInitialize2) { } -TEST_F(SinkTest, RandomUniform) { +// A random number pulled from a uniform integer distribution can be +// implemented as the request size +TEST_F(SinkTest, RandomUniformSize) { using cyclus::QueryResult; using cyclus::Cond; @@ -361,10 +363,13 @@ TEST_F(SinkTest, RandomUniform) { QueryResult qr = sim.db().Query("Resources", NULL); EXPECT_EQ(qr.rows.size(), 1); + // Given the PRNG with default seed, the resource should have mass 9.41273 EXPECT_NEAR(qr.GetVal("Quantity"), 9.41273, 0.0001); } -TEST_F(SinkTest, RandomNormal) { +// A random number pulled from a normal int distribution with default mean and +// stddev can be implemented as the request size +TEST_F(SinkTest, RandomNormalSize) { using cyclus::QueryResult; using cyclus::Cond; @@ -383,10 +388,13 @@ TEST_F(SinkTest, RandomNormal) { QueryResult qr = sim.db().Query("Resources", NULL); EXPECT_EQ(qr.rows.size(), 1); + // Given the PRNG with default seed, the resource should have mass 9.60929 EXPECT_NEAR(qr.GetVal("Quantity"), 9.60929, 0.0001); } -TEST_F(SinkTest, RandomNormalWithMeanSttdev) { +// A random number pulled from a normal int distribution with user-defined mean +// and stddev can be implemented as the request size +TEST_F(SinkTest, RandomNormalSizeWithMeanSttdev) { using cyclus::QueryResult; using cyclus::Cond; @@ -407,9 +415,168 @@ TEST_F(SinkTest, RandomNormalWithMeanSttdev) { QueryResult qr = sim.db().Query("Resources", NULL); EXPECT_EQ(qr.rows.size(), 1); + // Given the PRNG with default seed, the resource should have mass 1.52979 EXPECT_NEAR(qr.GetVal("Quantity"), 1.52979, 0.0001); } +// A random number pulled from a uniform integer distribution can be +// implemented as the buying frequency +TEST_F(SinkTest, RandomUniformFreq) { + using cyclus::QueryResult; + using cyclus::Cond; + + std::string config = + " " + " commods_1" + " " + " 10" + " UniformInt " + " 2 " + " 4 "; + + int simdur = 3; + cyclus::MockSim sim(cyclus::AgentSpec + (":cycamore:Sink"), config, simdur); + sim.AddSource("commods_1").capacity(10).Finalize(); + int id = sim.Run(); + + QueryResult qr = sim.db().Query("Transactions", NULL); + // only one transaction has occurred + EXPECT_EQ(qr.rows.size(), 1); + // Get the time from the first transaction in the database (0th entry) + int trans_time = qr.GetVal("Time", 0); + // Given the PRNG with default seed , this time should be time step 2 + EXPECT_EQ(trans_time, 2); +} + +// A random number pulled from a normal int distribution with default mean and +// stddev can be implemented as the buying frequency +TEST_F(SinkTest, RandomNormalFreq) { + using cyclus::QueryResult; + using cyclus::Cond; + + std::string config = + " " + " commods_1" + " " + " 10" + " NormalInt "; + + int simdur = 3; + cyclus::MockSim sim(cyclus::AgentSpec + (":cycamore:Sink"), config, simdur); + sim.AddSource("commods_1").capacity(10).Finalize(); + int id = sim.Run(); + + QueryResult qr = sim.db().Query("Transactions", NULL); + // only one transaction has occurred + EXPECT_EQ(qr.rows.size(), 1); + // Get the time from the first transaction in the database (0th entry) + int trans_time = qr.GetVal("Time", 0); + // Given the PRNG with default seed , this time should be time step 2 + EXPECT_EQ(trans_time, 2); +} + +// A random number pulled from a normal int distribution with user-defined mean +// and stddev can be implemented as the buying frequency +TEST_F(SinkTest, RandomNormalFreqWithMeanSttdev) { + using cyclus::QueryResult; + using cyclus::Cond; + + std::string config = + " " + " commods_1" + " " + " 10" + " NormalInt " + " 2 " + " 0.2 "; + + int simdur = 3; + cyclus::MockSim sim(cyclus::AgentSpec + (":cycamore:Sink"), config, simdur); + sim.AddSource("commods_1").capacity(10).Finalize(); + int id = sim.Run(); + + QueryResult qr = sim.db().Query("Transactions", NULL); + // only one transaction has occurred + EXPECT_EQ(qr.rows.size(), 1); + // Get the time from the first transaction in the database (0th entry) + int trans_time = qr.GetVal("Time", 0); + // Given the PRNG with default seed, this time should be time step 2 + EXPECT_EQ(trans_time, 2); +} + +// Check that multiple buying cycles set by random number execute as expected +TEST_F(SinkTest, RandomNormalFreqMultipleCycles) { + using cyclus::QueryResult; + using cyclus::Cond; + + std::string config = + " " + " commods_1" + " " + " 10" + " NormalInt " + " 4 " + " 1 "; + + int simdur = 12; + cyclus::MockSim sim(cyclus::AgentSpec + (":cycamore:Sink"), config, simdur); + sim.AddSource("commods_1").capacity(10).Finalize(); + int id = sim.Run(); + + QueryResult qr = sim.db().Query("Transactions", NULL); + // three transaction should have occurred + EXPECT_EQ(3, qr.rows.size()); + // check multiple cycles execute at the expected time + // Get the time from the first, second, and third transactions in the + // database (0th, 1st, and 2nd entry) + // Given the PRNG with default seed, buy times on time step 5, 7, and 10 + int first_trans_time = qr.GetVal("Time", 0); + EXPECT_EQ(5, first_trans_time); + int second_trans_time = qr.GetVal("Time", 1); + EXPECT_EQ(7, second_trans_time); + int third_trans_time = qr.GetVal("Time", 2); + EXPECT_EQ(10, third_trans_time); +} + +// Check that randomness can be implemented in both size of request and +// request frequency at the same time +TEST_F(SinkTest, RandomNormalSizeUniformFreq) { + using cyclus::QueryResult; + using cyclus::Cond; + + std::string config = + " " + " commods_1" + " " + " 10" + " NormalReal" + " 0.8" + " 0.2" + " UniformInt " + " 2 " + " 4 "; + + int simdur = 6; + cyclus::MockSim sim(cyclus::AgentSpec + (":cycamore:Sink"), config, simdur); + sim.AddSource("commods_1").capacity(20).Finalize(); + int id = sim.Run(); + + QueryResult tqr = sim.db().Query("Transactions", NULL); + // two transactions should have occurred + EXPECT_EQ(2, tqr.rows.size()); + // check multiple cycles execute at the expected time + int trans_time = tqr.GetVal("Time", 0); + EXPECT_EQ(3, trans_time); + int res_id = tqr.GetVal("ResourceId", 0); + QueryResult rqr = sim.db().Query("Resources", NULL); + double quantity = rqr.GetVal("Quantity", 0); + EXPECT_NEAR(6.54143, quantity, 0.00001); +} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - cyclus::Agent* SinkConstructor(cyclus::Context* ctx) { return new cycamore::Sink(ctx);