10#ifdef MOOSE_LIBTORCH_ENABLED
12#include "gtest/gtest.h"
30linearSequence(
const unsigned int size,
const Real offset,
const Real slope)
32 std::vector<Real>
values(size);
34 values[i] = offset + slope * i;
39trajectoryWithRewards(
const std::vector<Real> & rewards)
44 trajectory.
actions.assign(rewards.size(), {0.0});
50TEST(LibtorchRLCoreTest, ObservationHistoryStacksCurrentAndTrajectoryData)
54 const auto expanded_observation_factors = history.expandObservationFactors({0.5, 2.0});
55 EXPECT_EQ(expanded_observation_factors, std::vector<Real>({0.5, 2.0, 0.5, 2.0, 0.5, 2.0}));
57 std::vector<std::vector<Real>> old_observations;
58 history.initializeHistory({1.0, 6.0}, old_observations);
60 const auto stacked_current = history.stackCurrentObservation({3.0, 2.0}, old_observations);
61 EXPECT_EQ(stacked_current, std::vector<Real>({3.0, 2.0, 1.0, 6.0, 1.0, 6.0}));
63 std::vector<std::vector<Real>> component_trajectories = {{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}};
64 const auto stacked_trajectory = history.stackTrajectoryObservation(component_trajectories, 2);
65 EXPECT_EQ(stacked_trajectory, std::vector<Real>({3.0, 6.0, 2.0, 5.0, 1.0, 4.0}));
68TEST(LibtorchRLCoreTest, ObservationHistoryRejectsMalformedStoredHistory)
76 history.stackCurrentObservation({3.0, 2.0}, {{1.0, 6.0}});
78 catch (
const std::exception & e)
80 EXPECT_EQ(std::string(e.what()),
81 "Observation history must contain 2 stored entries, but got 1.");
88TEST(LibtorchRLCoreTest, ObservationHistoryRejectsZeroTimesteps)
95 static_cast<void>(history);
97 catch (
const std::exception & e)
99 EXPECT_EQ(std::string(e.what()),
100 "Observation history requires at least one input timestep.");
107TEST(LibtorchRLCoreTest, ValueEstimatorComputesGAETargets)
110 auto value_params = value_network.named_parameters();
111 value_params[0].value().data().fill_(1.0);
112 value_params[1].value().data().fill_(0.0);
117 trajectory.
rewards = {0.5, 1.0};
120 const auto targets = estimator.estimate(trajectory, value_network);
122 ASSERT_EQ(targets.advantages.size(), 2u);
123 ASSERT_EQ(targets.value_targets.size(), 2u);
124 EXPECT_NEAR(targets.advantages[0], 2.7535, 1e-12);
125 EXPECT_NEAR(targets.advantages[1], 1.7, 1e-12);
126 EXPECT_NEAR(targets.value_targets[0], 3.7535, 1e-12);
127 EXPECT_NEAR(targets.value_targets[1], 3.7, 1e-12);
130TEST(LibtorchRLCoreTest, ValueEstimatorMasksTerminalTransitions)
133 auto value_params = value_network.named_parameters();
134 value_params[0].value().data().fill_(1.0);
135 value_params[1].value().data().fill_(0.0);
140 trajectory.
rewards = {0.5, 1.0};
144 const auto targets = estimator.estimate(trajectory, value_network);
146 ASSERT_EQ(targets.advantages.size(), 2u);
147 ASSERT_EQ(targets.value_targets.size(), 2u);
148 EXPECT_NEAR(targets.advantages[0], 0.445, 1e-12);
149 EXPECT_NEAR(targets.advantages[1], -1.0, 1e-12);
150 EXPECT_NEAR(targets.value_targets[0], 1.445, 1e-12);
151 EXPECT_NEAR(targets.value_targets[1], 1.0, 1e-12);
154TEST(LibtorchRLCoreTest, PPOLossUsesStoredLogProbabilityAndValueTarget)
158 policy_network.gaussianActionDistribution().meanModule()->bias.data().fill_(0.0);
159 policy_network.gaussianActionDistribution().logStd().data().fill_(0.0);
162 auto value_params = value_network.named_parameters();
163 value_params[0].value().data().fill_(0.0);
164 value_params[1].value().data().fill_(1.0);
167 batch.
observations = torch::zeros({1, 1}, torch::TensorOptions().dtype(torch::kDouble));
168 batch.
actions = torch::zeros({1, 1}, torch::TensorOptions().dtype(torch::kDouble));
170 torch::tensor({{-0.5 * std::log(2.0 *
libMesh::pi)}},
171 torch::TensorOptions().dtype(torch::kDouble));
172 batch.
value_targets = torch::tensor({{1.5}}, torch::TensorOptions().dtype(torch::kDouble));
173 batch.
advantages = torch::tensor({{2.0}}, torch::TensorOptions().dtype(torch::kDouble));
176 const auto loss_values = loss.compute(policy_network, value_network, batch);
179 const Real expected_actor_loss = -(2.0 + 0.01 * expected_entropy);
180 const Real expected_critic_loss = 0.25;
182 EXPECT_NEAR(loss_values.entropy.item<Real>(), expected_entropy, 1e-12);
183 EXPECT_NEAR(loss_values.actor_loss.item<Real>(), expected_actor_loss, 1e-12);
184 EXPECT_NEAR(loss_values.critic_loss.item<Real>(), expected_critic_loss, 1e-12);
187TEST(LibtorchRLCoreTest, MiniBatchSamplerStandardizesAdvantagesPerBatch)
191 torch::tensor({{0.0}, {1.0}, {2.0}, {3.0}}, torch::TensorOptions().dtype(torch::kDouble));
193 torch::tensor({{0.1}, {0.2}, {0.3}, {0.4}}, torch::TensorOptions().dtype(torch::kDouble));
195 torch::tensor({{-1.0}, {-1.1}, {-1.2}, {-1.3}}, torch::TensorOptions().dtype(torch::kDouble));
197 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
199 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
202 const auto mini_batches = sampler.
sample(batch, 2,
true);
204 ASSERT_EQ(mini_batches.size(), 2u);
205 for (
const auto & mini_batch : mini_batches)
207 ASSERT_EQ(mini_batch.size(), 2);
208 EXPECT_NEAR(mini_batch.advantages.mean().item<Real>(), 0.0, 1e-12);
212TEST(LibtorchRLCoreTest, MiniBatchSamplerUsesExplicitGeneratorForDeterministicShuffling)
216 torch::tensor({{0.0}, {1.0}, {2.0}, {3.0}}, torch::TensorOptions().dtype(torch::kDouble));
218 torch::tensor({{0.1}, {0.2}, {0.3}, {0.4}}, torch::TensorOptions().dtype(torch::kDouble));
220 torch::tensor({{-1.0}, {-1.1}, {-1.2}, {-1.3}}, torch::TensorOptions().dtype(torch::kDouble));
222 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
224 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
228 const auto second_batches =
231 ASSERT_EQ(first_batches.size(), second_batches.size());
234 EXPECT_TRUE(torch::allclose(first_batches[i].observations,
235 second_batches[i].observations,
238 EXPECT_TRUE(torch::allclose(first_batches[i].actions,
239 second_batches[i].actions,
245TEST(LibtorchRLCoreTest, TrajectoryAssemblerDownsamplesWithWindowStride)
249 const std::vector<std::vector<Real>> observations = {linearSequence(16, 0.0, 1.0)};
250 const std::vector<std::vector<Real>> actions = {linearSequence(16, 100.0, 1.0)};
251 const std::vector<std::vector<Real>> log_probabilities = {linearSequence(16, 0.0, -1.0)};
252 const std::vector<std::vector<Real>> rewards = {linearSequence(16, 0.0, 10.0)};
255 const auto trajectories =
256 assembler.assemble({&observations}, {&actions}, {&log_probabilities}, rewards);
258 ASSERT_EQ(trajectories.size(), 1u);
259 const auto & trajectory = trajectories[0];
262 std::vector<std::vector<Real>>({{0.0, 0.0}, {5.0, 0.0}, {10.0, 5.0}}));
264 std::vector<std::vector<Real>>({{5.0, 0.0}, {10.0, 5.0}, {15.0, 10.0}}));
265 EXPECT_EQ(trajectory.
actions, std::vector<std::vector<Real>>({{101.0}, {106.0}, {111.0}}));
267 std::vector<std::vector<Real>>({{-1.0}, {-6.0}, {-11.0}}));
268 EXPECT_EQ(trajectory.
rewards, std::vector<Real>({50.0, 100.0, 150.0}));
269 EXPECT_EQ(trajectory.
terminals, std::vector<bool>({false, false, true}));
272TEST(LibtorchRLCoreTest, TrajectoryAssemblerAveragesRewardsWithoutShift)
274 const std::vector<std::vector<Real>> observations = {linearSequence(16, 0.0, 1.0)};
275 const std::vector<std::vector<Real>> actions = {linearSequence(16, 100.0, 1.0)};
276 const std::vector<std::vector<Real>> log_probabilities = {linearSequence(16, 0.0, -1.0)};
277 const std::vector<std::vector<Real>> rewards = {linearSequence(16, 0.0, 10.0)};
280 const auto trajectories =
281 assembler.assemble({&observations}, {&actions}, {&log_probabilities}, rewards);
283 ASSERT_EQ(trajectories.size(), 1u);
284 const auto & trajectory = trajectories[0];
285 EXPECT_EQ(trajectory.
observations, std::vector<std::vector<Real>>({{0.0}, {5.0}, {10.0}}));
286 EXPECT_EQ(trajectory.
actions, std::vector<std::vector<Real>>({{100.0}, {105.0}, {110.0}}));
287 EXPECT_EQ(trajectory.
log_probabilities, std::vector<std::vector<Real>>({{0.0}, {-5.0}, {-10.0}}));
289 EXPECT_EQ(trajectory.
rewards, std::vector<Real>({30.0, 80.0, 130.0}));
292TEST(LibtorchRLCoreTest, TrajectoryAssemblerCountsCompleteWindows)
295 EXPECT_EQ(assembler.numTransitions(0), 0u);
296 EXPECT_EQ(assembler.numTransitions(1), 0u);
297 EXPECT_EQ(assembler.numTransitions(5), 0u);
298 EXPECT_EQ(assembler.numTransitions(6), 1u);
299 EXPECT_EQ(assembler.numTransitions(16), 3u);
302 const std::vector<std::vector<Real>> observations = {linearSequence(16, 0.0, 1.0),
303 linearSequence(5, 0.0, 1.0)};
304 const std::vector<std::vector<Real>> rewards = {linearSequence(16, 0.0, 1.0),
305 linearSequence(5, 0.0, 1.0)};
306 const auto trajectories = assembler.assemble({&observations}, {}, {}, rewards);
307 ASSERT_EQ(trajectories.size(), 1u);
308 EXPECT_EQ(trajectories[0].rewards.size(), 3u);
311TEST(LibtorchRLCoreTest, TrajectoryAssemblerRejectsMismatchedSampleCounts)
313 const std::vector<std::vector<Real>> first_observations = {linearSequence(3, 0.0, 1.0),
314 linearSequence(3, 0.0, 1.0)};
315 const std::vector<std::vector<Real>> second_observations = {linearSequence(3, 0.0, 1.0)};
316 const std::vector<std::vector<Real>> rewards = {linearSequence(3, 0.0, 1.0),
317 linearSequence(3, 0.0, 1.0)};
324 assembler.assemble({&first_observations, &second_observations}, {}, {}, rewards);
326 catch (
const std::exception & e)
328 EXPECT_EQ(std::string(e.what()),
329 "Entry 1 of the observation reporters holds 1 samples, but the reward "
330 "reporter holds 2 samples.");
337TEST(LibtorchRLCoreTest, TrajectoryAssemblerRejectsZeroTimestepWindow)
344 static_cast<void>(assembler);
346 catch (
const std::exception & e)
348 EXPECT_EQ(std::string(e.what()),
"The RL trajectory timestep window must be at least 1.");
355TEST(LibtorchRLCoreTest, TrajectoryBufferRewardStatisticsIncludeSpreadBetweenTrajectories)
364 EXPECT_NEAR(statistics.mean, 3.0, 1e-12);
365 EXPECT_NEAR(statistics.std, std::sqrt(8.0 / 3.0), 1e-12);
366 EXPECT_EQ(statistics.trajectory_means, std::vector<Real>({2.0, 5.0}));
367 EXPECT_EQ(statistics.trajectory_stds, std::vector<Real>({1.0, 0.0}));
371 EXPECT_EQ(empty_statistics.mean, 0.0);
372 EXPECT_EQ(empty_statistics.std, 0.0);
373 EXPECT_TRUE(empty_statistics.trajectory_means.empty());
376TEST(LibtorchRLCoreTest, TrajectoryBufferFlattensTrajectoriesInOrder)
378 const auto options = torch::TensorOptions().dtype(torch::kDouble);
382 auto first = trajectoryWithRewards({10.0, 11.0});
383 first.observations = {{10.0, 10.5}, {11.0, 11.5}};
384 first.actions = {{-10.0}, {-11.0}};
385 first.value_targets = {0.10, 0.11};
386 first.advantages = {1.0, 1.1};
387 auto second = trajectoryWithRewards({20.0});
388 second.observations = {{20.0, 20.5}};
389 second.actions = {{-20.0}};
390 second.value_targets = {0.20};
391 second.advantages = {2.0};
399 const auto batch = buffer.
flatten();
400 ASSERT_EQ(batch.
size(), 3);
402 torch::tensor({{10.0, 10.5}, {11.0, 11.5}, {20.0, 20.5}}, options)));
403 EXPECT_TRUE(torch::equal(batch.
next_observations, torch::zeros({3, 1}, options)));
404 EXPECT_TRUE(torch::equal(batch.
actions, torch::tensor({{-10.0}, {-11.0}, {-20.0}}, options)));
405 EXPECT_TRUE(torch::equal(batch.
log_probabilities, torch::zeros({3, 1}, options)));
406 EXPECT_TRUE(torch::equal(batch.
rewards, torch::tensor({{10.0}, {11.0}, {20.0}}, options)));
408 torch::equal(batch.
value_targets, torch::tensor({{0.10}, {0.11}, {0.20}}, options)));
409 EXPECT_TRUE(torch::equal(batch.
advantages, torch::tensor({{1.0}, {1.1}, {2.0}}, options)));
415TEST(LibtorchRLCoreTest, TrajectoryBufferRejectsMismatchedTrajectoryLengths)
417 const auto expect_rejected =
427 catch (
const std::exception & e)
429 EXPECT_EQ(std::string(e.what()), message);
434 EXPECT_TRUE(buffer.
empty());
438 auto short_actions = trajectoryWithRewards({1.0, 2.0});
439 short_actions.actions.pop_back();
440 expect_rejected(short_actions,
"RL trajectory actions must match the reward sequence length.");
443 auto short_value_targets = trajectoryWithRewards({1.0, 2.0});
444 short_value_targets.value_targets = {1.0};
445 expect_rejected(short_value_targets,
446 "RL trajectory value targets must match the reward sequence length.");
TEST(EquilibriumConstantFitTest, constructor)
std::array< Real, 2 > values
Samples shuffled mini-batches from a flattened on-policy trajectory batch.
std::vector< LibtorchRLMiniBatch > sample(const LibtorchRLTrajectoryBuffer::TensorBatch &batch, unsigned int batch_size, bool standardize_advantage, c10::optional< at::Generator > generator=c10::nullopt) const
Shuffle a flattened rollout batch into PPO-sized chunks.
PPO clipped surrogate loss on top of the reusable RL buffer/value-estimation core.
Builds fixed-horizon RL trajectories from raw per-sample reporter sequences.
On-policy trajectory storage for fixed-horizon RL training.
void addTrajectory(Trajectory trajectory)
Append one trajectory to the on-policy buffer.
TensorBatch flatten() const
Flatten every stored trajectory into one tensor batch.
std::size_t numTrajectories() const
Return the number of stored trajectories.
RewardStatistics rewardStatistics() const
Compute reward statistics over the stored trajectories.
bool empty() const
Return true if the buffer contains no trajectories.
void clear()
Clear every stored trajectory.
std::size_t numTransitions() const
Count the total number of transitions stored across every trajectory.
Computes generalized-advantage estimates and value targets for an on-policy trajectory buffer,...
Feed-forward actor network coupled to a Gaussian or Beta action distribution.
const LibtorchGaussianActionDistribution & gaussianActionDistribution() const
Return the Gaussian action distribution reference.
torch::nn::Linear & meanModule()
Return the Gaussian mean head.
at::Generator makeLibtorchCPUGenerator()
auto index_range(const T &sizable)
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
IntRange< T > make_range(T beg, T end)
Tensor data for one PPO mini-batch.
torch::Tensor actions
Action rows that match the sampled observations.
torch::Tensor value_targets
Critic targets aligned with the sampled observations.
torch::Tensor old_log_probabilities
Behavior-policy log probabilities for the sampled actions.
torch::Tensor advantages
Advantage estimates aligned with the sampled observations.
torch::Tensor observations
Flattened observation rows for the mini-batch.
Flattened tensor data assembled from the stored trajectories.
torch::Tensor actions
Flattened action matrix.
torch::Tensor value_targets
Flattened critic targets.
torch::Tensor next_observations
Flattened next-observation matrix.
torch::Tensor observations
Flattened observation matrix.
torch::Tensor advantages
Flattened advantages.
torch::Tensor log_probabilities
Flattened action log-probabilities.
torch::Tensor rewards
Flattened rewards.
std::int64_t size() const
Return the number of transitions represented by the batch.
Vector data for one on-policy trajectory.
std::vector< Real > rewards
Scalar rewards for each transition.
std::vector< std::vector< Real > > observations
Observations for each transition.
std::vector< std::vector< Real > > actions
Actions applied at each transition.
std::vector< std::vector< Real > > next_observations
Next observations for each transition.
std::vector< std::vector< Real > > log_probabilities
Action log-probabilities recorded during rollout.
std::vector< bool > terminals
Terminal flags for each transition. A true value cuts value bootstrapping at that step.