https://mooseframework.inl.gov
Loading...
Searching...
No Matches
TestLibtorchRLCore.C
Go to the documentation of this file.
1//* This file is part of the MOOSE framework
2//* https://mooseframework.inl.gov
3//*
4//* All rights reserved, see COPYRIGHT for full restrictions
5//* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6//*
7//* Licensed under LGPL 2.1, please see LICENSE for details
8//* https://www.gnu.org/licenses/lgpl-2.1.html
9
10#ifdef MOOSE_LIBTORCH_ENABLED
11
12#include "gtest/gtest.h"
13
17#include "LibtorchRandomUtils.h"
19#include "LibtorchRLPPOLoss.h"
23
24#include <cmath>
25
26namespace
27{
28
29std::vector<Real>
30linearSequence(const unsigned int size, const Real offset, const Real slope)
31{
32 std::vector<Real> values(size);
33 for (const auto i : make_range(size))
34 values[i] = offset + slope * i;
35 return values;
36}
37
39trajectoryWithRewards(const std::vector<Real> & rewards)
40{
42 trajectory.observations.assign(rewards.size(), {0.0});
43 trajectory.next_observations.assign(rewards.size(), {0.0});
44 trajectory.actions.assign(rewards.size(), {0.0});
45 trajectory.log_probabilities.assign(rewards.size(), {0.0});
46 trajectory.rewards = rewards;
47 return trajectory;
48}
49
50TEST(LibtorchRLCoreTest, ObservationHistoryStacksCurrentAndTrajectoryData)
51{
53
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}));
56
57 std::vector<std::vector<Real>> old_observations;
58 history.initializeHistory({1.0, 6.0}, old_observations);
59
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}));
62
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}));
66}
67
68TEST(LibtorchRLCoreTest, ObservationHistoryRejectsMalformedStoredHistory)
69{
71
72 EXPECT_THROW(
73 {
74 try
75 {
76 history.stackCurrentObservation({3.0, 2.0}, {{1.0, 6.0}});
77 }
78 catch (const std::exception & e)
79 {
80 EXPECT_EQ(std::string(e.what()),
81 "Observation history must contain 2 stored entries, but got 1.");
82 throw;
83 }
84 },
85 std::exception);
86}
87
88TEST(LibtorchRLCoreTest, ObservationHistoryRejectsZeroTimesteps)
89{
90 EXPECT_THROW(
91 {
92 try
93 {
95 static_cast<void>(history);
96 }
97 catch (const std::exception & e)
98 {
99 EXPECT_EQ(std::string(e.what()),
100 "Observation history requires at least one input timestep.");
101 throw;
102 }
103 },
104 std::exception);
105}
106
107TEST(LibtorchRLCoreTest, ValueEstimatorComputesGAETargets)
108{
109 Moose::LibtorchArtificialNeuralNet value_network("value", 1, 1, {}, {"linear"});
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);
113
115 trajectory.observations = {{1.0}, {2.0}};
116 trajectory.next_observations = {{2.0}, {3.0}};
117 trajectory.rewards = {0.5, 1.0};
118
119 LibtorchRLValueEstimator estimator(0.9, 0.95);
120 const auto targets = estimator.estimate(trajectory, value_network);
121
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);
128}
129
130TEST(LibtorchRLCoreTest, ValueEstimatorMasksTerminalTransitions)
131{
132 Moose::LibtorchArtificialNeuralNet value_network("value", 1, 1, {}, {"linear"});
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);
136
138 trajectory.observations = {{1.0}, {2.0}};
139 trajectory.next_observations = {{2.0}, {3.0}};
140 trajectory.rewards = {0.5, 1.0};
141 trajectory.terminals = {false, true};
142
143 LibtorchRLValueEstimator estimator(0.9, 0.95);
144 const auto targets = estimator.estimate(trajectory, value_network);
145
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);
152}
153
154TEST(LibtorchRLCoreTest, PPOLossUsesStoredLogProbabilityAndValueTarget)
155{
156 Moose::LibtorchActorNeuralNet policy_network("policy", 1, 1, {}, {"linear"});
157 policy_network.gaussianActionDistribution().meanModule()->weight.data().fill_(0.0);
158 policy_network.gaussianActionDistribution().meanModule()->bias.data().fill_(0.0);
159 policy_network.gaussianActionDistribution().logStd().data().fill_(0.0);
160
161 Moose::LibtorchArtificialNeuralNet value_network("value", 1, 1, {}, {"linear"});
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);
165
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));
174
175 LibtorchRLPPOLoss loss(0.2, 0.01);
176 const auto loss_values = loss.compute(policy_network, value_network, batch);
177
178 const Real expected_entropy = 0.5 * std::log(2.0 * libMesh::pi) + 0.5;
179 const Real expected_actor_loss = -(2.0 + 0.01 * expected_entropy);
180 const Real expected_critic_loss = 0.25;
181
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);
185}
186
187TEST(LibtorchRLCoreTest, MiniBatchSamplerStandardizesAdvantagesPerBatch)
188{
190 batch.observations =
191 torch::tensor({{0.0}, {1.0}, {2.0}, {3.0}}, torch::TensorOptions().dtype(torch::kDouble));
192 batch.actions =
193 torch::tensor({{0.1}, {0.2}, {0.3}, {0.4}}, torch::TensorOptions().dtype(torch::kDouble));
194 batch.log_probabilities =
195 torch::tensor({{-1.0}, {-1.1}, {-1.2}, {-1.3}}, torch::TensorOptions().dtype(torch::kDouble));
196 batch.value_targets =
197 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
198 batch.advantages =
199 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
200
202 const auto mini_batches = sampler.sample(batch, 2, true);
203
204 ASSERT_EQ(mini_batches.size(), 2u);
205 for (const auto & mini_batch : mini_batches)
206 {
207 ASSERT_EQ(mini_batch.size(), 2);
208 EXPECT_NEAR(mini_batch.advantages.mean().item<Real>(), 0.0, 1e-12);
209 }
210}
211
212TEST(LibtorchRLCoreTest, MiniBatchSamplerUsesExplicitGeneratorForDeterministicShuffling)
213{
215 batch.observations =
216 torch::tensor({{0.0}, {1.0}, {2.0}, {3.0}}, torch::TensorOptions().dtype(torch::kDouble));
217 batch.actions =
218 torch::tensor({{0.1}, {0.2}, {0.3}, {0.4}}, torch::TensorOptions().dtype(torch::kDouble));
219 batch.log_probabilities =
220 torch::tensor({{-1.0}, {-1.1}, {-1.2}, {-1.3}}, torch::TensorOptions().dtype(torch::kDouble));
221 batch.value_targets =
222 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
223 batch.advantages =
224 torch::tensor({{1.0}, {2.0}, {3.0}, {4.0}}, torch::TensorOptions().dtype(torch::kDouble));
225
227 const auto first_batches = sampler.sample(batch, 2, false, Moose::makeLibtorchCPUGenerator(9876));
228 const auto second_batches =
229 sampler.sample(batch, 2, false, Moose::makeLibtorchCPUGenerator(9876));
230
231 ASSERT_EQ(first_batches.size(), second_batches.size());
232 for (const auto i : index_range(first_batches))
233 {
234 EXPECT_TRUE(torch::allclose(first_batches[i].observations,
235 second_batches[i].observations,
236 /* rtol = */ 0.0,
237 /* atol = */ 0.0));
238 EXPECT_TRUE(torch::allclose(first_batches[i].actions,
239 second_batches[i].actions,
240 /* rtol = */ 0.0,
241 /* atol = */ 0.0));
242 }
243}
244
245TEST(LibtorchRLCoreTest, TrajectoryAssemblerDownsamplesWithWindowStride)
246{
247 // Raw index i holds 0 + i for observations, 100 + i for actions, -i for log probabilities, and
248 // 10 * i for rewards, so each assembled value identifies the raw index it came from.
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)};
253
254 LibtorchRLTrajectoryAssembler assembler(2, 5, true, false);
255 const auto trajectories =
256 assembler.assemble({&observations}, {&actions}, {&log_probabilities}, rewards);
257
258 ASSERT_EQ(trajectories.size(), 1u);
259 const auto & trajectory = trajectories[0];
260 // History lags span one window, and lags before the initial entry repeat raw index 0.
261 EXPECT_EQ(trajectory.observations,
262 std::vector<std::vector<Real>>({{0.0, 0.0}, {5.0, 0.0}, {10.0, 5.0}}));
263 EXPECT_EQ(trajectory.next_observations,
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}}));
266 EXPECT_EQ(trajectory.log_probabilities,
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}));
270}
271
272TEST(LibtorchRLCoreTest, TrajectoryAssemblerAveragesRewardsWithoutShift)
273{
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)};
278
279 LibtorchRLTrajectoryAssembler assembler(1, 5, false, true);
280 const auto trajectories =
281 assembler.assemble({&observations}, {&actions}, {&log_probabilities}, rewards);
282
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}}));
288 // Means of raw indices 1-5, 6-10, and 11-15.
289 EXPECT_EQ(trajectory.rewards, std::vector<Real>({30.0, 80.0, 130.0}));
290}
291
292TEST(LibtorchRLCoreTest, TrajectoryAssemblerCountsCompleteWindows)
293{
294 LibtorchRLTrajectoryAssembler assembler(1, 5, true, false);
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);
300
301 // A sample without a complete window produces no trajectory.
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);
309}
310
311TEST(LibtorchRLCoreTest, TrajectoryAssemblerRejectsMismatchedSampleCounts)
312{
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)};
318
319 LibtorchRLTrajectoryAssembler assembler(1, 1, true, false);
320 EXPECT_THROW(
321 {
322 try
323 {
324 assembler.assemble({&first_observations, &second_observations}, {}, {}, rewards);
325 }
326 catch (const std::exception & e)
327 {
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.");
331 throw;
332 }
333 },
334 std::exception);
335}
336
337TEST(LibtorchRLCoreTest, TrajectoryAssemblerRejectsZeroTimestepWindow)
338{
339 EXPECT_THROW(
340 {
341 try
342 {
343 LibtorchRLTrajectoryAssembler assembler(1, 0, true, false);
344 static_cast<void>(assembler);
345 }
346 catch (const std::exception & e)
347 {
348 EXPECT_EQ(std::string(e.what()), "The RL trajectory timestep window must be at least 1.");
349 throw;
350 }
351 },
352 std::exception);
353}
354
355TEST(LibtorchRLCoreTest, TrajectoryBufferRewardStatisticsIncludeSpreadBetweenTrajectories)
356{
358 buffer.addTrajectory(trajectoryWithRewards({1.0, 3.0}));
359 buffer.addTrajectory(trajectoryWithRewards({5.0}));
360
361 const auto statistics = buffer.rewardStatistics();
362
363 // The overall mean is 3, so the squared deviations are 4, 0, and 4.
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}));
368
369 buffer.clear();
370 const auto empty_statistics = buffer.rewardStatistics();
371 EXPECT_EQ(empty_statistics.mean, 0.0);
372 EXPECT_EQ(empty_statistics.std, 0.0);
373 EXPECT_TRUE(empty_statistics.trajectory_means.empty());
374}
375
376TEST(LibtorchRLCoreTest, TrajectoryBufferFlattensTrajectoriesInOrder)
377{
378 const auto options = torch::TensorOptions().dtype(torch::kDouble);
379
380 // Values in the tens come from the first trajectory and values in the twenties from the second,
381 // so the flattened rows show that trajectories of different lengths are stacked in order.
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};
392
394 buffer.addTrajectory(first);
395 buffer.addTrajectory(second);
396 EXPECT_EQ(buffer.numTrajectories(), 2u);
397 EXPECT_EQ(buffer.numTransitions(), 3u);
398
399 const auto batch = buffer.flatten();
400 ASSERT_EQ(batch.size(), 3);
401 EXPECT_TRUE(torch::equal(batch.observations,
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)));
407 EXPECT_TRUE(
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)));
410
411 buffer.clear();
412 EXPECT_EQ(buffer.flatten().size(), 0);
413}
414
415TEST(LibtorchRLCoreTest, TrajectoryBufferRejectsMismatchedTrajectoryLengths)
416{
417 const auto expect_rejected =
418 [](const LibtorchRLTrajectoryBuffer::Trajectory & trajectory, const std::string & message)
419 {
421 EXPECT_THROW(
422 {
423 try
424 {
425 buffer.addTrajectory(trajectory);
426 }
427 catch (const std::exception & e)
428 {
429 EXPECT_EQ(std::string(e.what()), message);
430 throw;
431 }
432 },
433 std::exception);
434 EXPECT_TRUE(buffer.empty());
435 };
436
437 // Required fields must match the reward length.
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.");
441
442 // Optional fields may be empty, but must match the reward length when given.
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.");
447}
448
449} // namespace
450
451#endif
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)
const Real pi
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.
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.