44TEST(LibtorchActorNeuralNetTest, artificialNetAppliesAffineInputAndOutputScaling)
46 TestableLibtorchArtificialNeuralNet network(
"test_ann",
58 ASSERT_EQ(network._weights.size(), 1u);
60 network._weights[0]->weight.data().fill_(0.0);
61 network._weights[0]->weight.data()[0][0] = 1.0;
62 network._weights[0]->weight.data()[0][1] = -1.0;
63 network._weights[0]->bias.data().fill_(0.0);
65 auto input = torch::tensor({{2.0, 6.0}}, at::kDouble);
66 const Real actual = network.forward(input).item<Real>();
68 EXPECT_NEAR(actual, -100.0, 1e-12);
71TEST(LibtorchActorNeuralNetTest, boundedBetaLogProbability)
73 constexpr Real min_value = -2.0;
74 constexpr Real max_value = 4.0;
75 constexpr Real alpha_target = 2.3;
76 constexpr Real beta_target = 3.7;
77 constexpr Real action_value = 1.2;
79 TestableLibtorchActorNeuralNet network(
80 "test_beta", 1, 1, {1}, {
"linear"}, {min_value}, {max_value});
82 ASSERT_EQ(network._weights.size(), 1u);
84 network._weights[0]->weight.data().fill_(0.0);
85 network._weights[0]->bias.data().fill_(1.0);
86 network.betaActionDistribution().alphaModule()->weight.data().fill_(
87 inverseSoftplusPlusOne(alpha_target));
88 network.betaActionDistribution().betaModule()->weight.data().fill_(
89 inverseSoftplusPlusOne(beta_target));
91 auto input = torch::zeros({1, 1}, at::kDouble);
92 network.evaluate(input,
false);
94 const Real alpha = network.betaActionDistribution().alphaTensor().item<Real>();
95 const Real beta = network.betaActionDistribution().betaTensor().item<Real>();
96 const Real normalized = (action_value - min_value) / (max_value - min_value);
97 const Real log_norm = std::lgamma(alpha) + std::lgamma(beta) - std::lgamma(alpha + beta);
98 const Real expected = (alpha - 1.0) * std::log(normalized) +
99 (beta - 1.0) * std::log1p(-normalized) - log_norm -
100 std::log(max_value - min_value);
102 auto action = torch::tensor({{action_value}}, at::kDouble);
103 const Real actual = network.logProbability(action).item<Real>();
105 EXPECT_NEAR(actual, expected, 1e-12);
108TEST(LibtorchActorNeuralNetTest, boundedBetaStaysFiniteForLargeLogits)
110 constexpr Real min_value = -2.0;
111 constexpr Real max_value = 4.0;
113 constexpr Real large_logit = 800.0;
114 constexpr Real action_value = 3.9;
116 TestableLibtorchActorNeuralNet network(
117 "test_beta", 1, 1, {1}, {
"linear"}, {min_value}, {max_value});
119 ASSERT_EQ(network._weights.size(), 1u);
121 network._weights[0]->weight.data().fill_(0.0);
122 network._weights[0]->bias.data().fill_(1.0);
123 auto & distribution = network.betaActionDistribution();
124 distribution.alphaModule()->weight.data().fill_(large_logit);
125 distribution.alphaModule()->bias.data().fill_(0.0);
126 distribution.betaModule()->weight.data().fill_(0.0);
127 distribution.betaModule()->bias.data().fill_(0.0);
129 auto input = torch::zeros({1, 1}, at::kDouble);
130 const Real deterministic_action = network.evaluate(input,
false).item<Real>();
132 EXPECT_NEAR(distribution.alphaTensor().item<Real>(), large_logit + 1.0, 1e-12);
133 EXPECT_NEAR(distribution.betaTensor().item<Real>(), std::log(2.0) + 1.0, 1e-12);
134 EXPECT_TRUE(std::isfinite(deterministic_action));
136 auto log_probability = network.logProbability(torch::tensor({{action_value}}, at::kDouble));
137 EXPECT_TRUE(std::isfinite(log_probability.item<Real>()));
139 log_probability.backward();
140 EXPECT_TRUE(torch::isfinite(distribution.alphaModule()->weight.grad()).all().item<
bool>());
143TEST(LibtorchActorNeuralNetTest, gaussianActorUsesPhysicalActionScalingAndStateIndependentStd)
145 constexpr Real input_shift = 1.0;
146 constexpr Real input_scale = 2.0;
147 constexpr Real action_scale = 5.0;
148 const Real log_std = std::log(2.0);
149 constexpr Real physical_action = 20.0;
150 constexpr Real expected_deterministic_action = 15.0;
152 TestableLibtorchActorNeuralNet network(
"test_gaussian",
166 network.gaussianActionDistribution().meanModule()->weight.data().fill_(1.5);
167 network.gaussianActionDistribution().meanModule()->bias.data().fill_(0.0);
168 network.gaussianActionDistribution().logStd().data().fill_(log_std);
170 auto input = torch::tensor({{2.0}}, at::kDouble);
171 const Real deterministic_action = network.evaluate(input,
false).item<Real>();
172 EXPECT_NEAR(deterministic_action, expected_deterministic_action, 1e-12);
174 network.gaussianActionDistribution().stdTensor().item<Real>(), std::exp(log_std), 1e-12);
176 const Real unscaled_mean = expected_deterministic_action / action_scale;
177 const Real unscaled_action = physical_action / action_scale;
178 const Real expected_log_probability =
179 -std::pow(unscaled_action - unscaled_mean, 2) / (2.0 * 4.0) - log_std -
180 0.5 * std::log(2.0 *
libMesh::pi) - std::log(action_scale);
182 auto action = torch::tensor({{physical_action}}, at::kDouble);
183 const Real actual_log_probability = network.logProbability(action).item<Real>();
185 EXPECT_NEAR(actual_log_probability, expected_log_probability, 1e-12);
187 auto second_input = torch::tensor({{4.0}}, at::kDouble);
188 network.evaluate(second_input,
false);
190 network.gaussianActionDistribution().stdTensor().item<Real>(), std::exp(log_std), 1e-12);
193TEST(LibtorchActorNeuralNetTest, gaussianActorCanUseStateDependentStdWhenRequested)
195 TestableLibtorchActorNeuralNet network(
"test_state_dependent_gaussian",
210 network.gaussianActionDistribution().meanModule()->weight.data().fill_(0.0);
211 network.gaussianActionDistribution().meanModule()->bias.data().fill_(0.0);
212 network.gaussianActionDistribution().stdModule()->weight.data().fill_(0.5);
213 network.gaussianActionDistribution().stdModule()->bias.data().fill_(0.0);
215 auto first_input = torch::tensor({{2.0}}, at::kDouble);
216 network.evaluate(first_input,
false);
217 const Real first_std = network.gaussianActionDistribution().stdTensor().item<Real>();
219 auto second_input = torch::tensor({{4.0}}, at::kDouble);
220 network.evaluate(second_input,
false);
221 const Real second_std = network.gaussianActionDistribution().stdTensor().item<Real>();
223 EXPECT_NEAR(first_std, std::exp(1.0), 1e-12);
224 EXPECT_NEAR(second_std, std::exp(3.0), 1e-12);
225 EXPECT_GT(second_std, first_std);
228TEST(LibtorchActorNeuralNetTest, explicitGeneratorKeepsGaussianSamplingStableAcrossCopies)
230 TestableLibtorchActorNeuralNet original(
"test_gaussian",
244 original.gaussianActionDistribution().meanModule()->weight.data().fill_(0.75);
245 original.gaussianActionDistribution().meanModule()->bias.data().fill_(1.25);
246 original.gaussianActionDistribution().logStd().data().fill_(std::log(0.5));
248 TestableLibtorchActorNeuralNet copied(original);
250 auto original_input = torch::tensor({{2.0}}, at::kDouble);
251 auto copied_input = torch::tensor({{2.0}}, at::kDouble);
255 const auto original_action = original.evaluate(original_input,
true, original_generator);
256 const auto copied_action = copied.evaluate(copied_input,
true, copied_generator);
258 EXPECT_TRUE(torch::allclose(original_action, copied_action, 0.0, 0.0));
261TEST(LibtorchActorNeuralNetTest, explicitGeneratorMakesInitializationIndependentOfConstructionOrder)
263 TestableLibtorchActorNeuralNet first(
"first_actor",
276 TestableLibtorchActorNeuralNet second(
"second_actor",
293 const auto first_parameters = first.named_parameters();
294 const auto second_parameters = second.named_parameters();
295 ASSERT_EQ(first_parameters.size(), second_parameters.size());
296 for (
const auto i : index_range(first_parameters))
298 EXPECT_EQ(first_parameters[i].key(), second_parameters[i].key());
299 EXPECT_TRUE(torch::allclose(first_parameters[i].value(),
300 second_parameters[i].value(),
306TEST(LibtorchActorNeuralNetTest, loadActorStateAcceptsTorchSaveArchive)
308 TestableLibtorchActorNeuralNet saved(
"saved_actor",
322 saved._weights[0]->weight.data() = torch::tensor({{1.0, 2.0}, {3.0, 4.0}}, at::kDouble);
323 saved._weights[0]->bias.data() = torch::tensor({5.0, 6.0}, at::kDouble);
324 saved.gaussianActionDistribution().meanModule()->weight.data() =
325 torch::tensor({{7.0, 8.0}}, at::kDouble);
326 saved.gaussianActionDistribution().meanModule()->bias.data() = torch::tensor({9.0}, at::kDouble);
327 saved.gaussianActionDistribution().logStd().data() = torch::tensor({-3.5}, at::kDouble);
330 torch::save(std::make_shared<Moose::LibtorchActorNeuralNet>(saved), archive.path().string());
332 TestableLibtorchActorNeuralNet restored(
"restored_actor",
348 const auto saved_parameters = saved.named_parameters();
349 const auto restored_parameters = restored.named_parameters();
350 ASSERT_EQ(saved_parameters.size(), restored_parameters.size());
351 for (std::size_t i = 0; i < saved_parameters.size(); ++i)
353 EXPECT_EQ(saved_parameters[i].key(), restored_parameters[i].key());
354 EXPECT_TRUE(torch::allclose(saved_parameters[i].value(),
355 restored_parameters[i].value(),
360 const auto saved_buffers = saved.named_buffers();
361 const auto restored_buffers = restored.named_buffers();
362 ASSERT_EQ(saved_buffers.size(), restored_buffers.size());
363 for (std::size_t i = 0; i < saved_buffers.size(); ++i)
365 EXPECT_EQ(saved_buffers[i].key(), restored_buffers[i].key());
366 EXPECT_TRUE(torch::allclose(saved_buffers[i].value(),
367 restored_buffers[i].value(),
372 auto saved_input = torch::tensor({{3.0, -1.0}}, at::kDouble);
373 auto restored_input = saved_input.clone();
374 EXPECT_TRUE(torch::allclose(saved.evaluate(saved_input,
false),
375 restored.evaluate(restored_input,
false),
380TEST(LibtorchActorNeuralNetTest, loadActorStateRejectsHiddenLayerMismatch)
382 TestableLibtorchActorNeuralNet saved(
"saved_actor",
397 torch::save(std::make_shared<Moose::LibtorchActorNeuralNet>(saved), archive.path().string());
399 TestableLibtorchActorNeuralNet restored(
"restored_actor",
416TEST(LibtorchActorNeuralNetTest, loadActorStateRejectsBoundednessMismatch)
418 TestableLibtorchActorNeuralNet saved(
"saved_actor",
433 torch::save(std::make_shared<Moose::LibtorchActorNeuralNet>(saved), archive.path().string());
435 TestableLibtorchActorNeuralNet restored(
"restored_actor",
LibtorchActorNeuralNet(const std::string name, const unsigned int num_inputs, const unsigned int num_outputs, const std::vector< unsigned int > &num_neurons_per_layer, const std::vector< std::string > &activation_function={"relu"}, const std::vector< Real > &minimum_values={}, const std::vector< Real > &maximum_values={}, const torch::DeviceType device_type=torch::kCPU, const torch::ScalarType scalar_type=torch::kDouble, const bool build_on_construct=true, const std::vector< Real > &input_shift_factors={}, const std::vector< Real > &input_scaling_factors={}, const std::vector< Real > &output_scaling_factors={}, const bool state_independent_std=true)
Build an actor network with either a Gaussian or Beta action distribution.
LibtorchArtificialNeuralNet(const std::string name, const unsigned int num_inputs, const unsigned int num_outputs, const std::vector< unsigned int > &num_neurons_per_layer, const std::vector< std::string > &activation_function={"relu"}, const torch::DeviceType device_type=torch::kCPU, const torch::ScalarType scalar_type=torch::kDouble, const bool build_on_construct=true, const std::vector< Real > &input_shift_factors={}, const std::vector< Real > &input_scaling_factors={}, const std::vector< Real > &output_scaling_factors={})