Files

250 lines
9.2 KiB
C++

#include "agent/Agent.h"
#include "agent/Codec.h"
#include "common/Capability.h"
#include "zeroerr/unittest.h"
#include <cmath>
#include <limits>
using namespace eve::agent;
TEST_CASE("agent.gpuUnavailableAndTrainingFailure") {
struct DeviceFailure final : IGpuPolicyBackend {
bool ready = false;
int trainingCalls = 0;
std::string name() const override { return "injected-device"; }
eve::Result<void> available() const override {
if (ready) return eve::Result<void>::success();
return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "no device"));
}
eve::Result<std::vector<double>> evaluate(const Policy& p, const Observation& o) override {
return infer(p, o);
}
eve::Result<Policy> train(const Policy&, const Observation&, std::uint32_t, double) override {
++trainingCalls;
return eve::Result<Policy>::failure(eve::Diagnostic::error(eve::DiagnosticCode::Cancelled, "device lost"));
}
} provider;
struct Environment final : IEnvironment {
int resets = 0;
eve::Result<Observation> reset(std::uint64_t) override {
++resets;
Observation o;
o.features = {0};
o.legalActions = {0, 1};
return eve::Result<Observation>::success(std::move(o));
}
eve::Result<Observation> step(std::uint32_t, double) override {
Observation o;
o.features = {1};
o.outcome = Outcome::Success;
return eve::Result<Observation>::success(std::move(o));
}
} environment;
Config config;
config.backend = Backend::Gpu;
config.population = 1;
config.elites = 1;
config.generations = 1;
REQUIRE(!run(config, environment).ok());
REQUIRE_EQ(environment.resets, 0);
struct Registration {
IGpuPolicyBackend* provider;
~Registration() { eve::cap::revoke<IGpuPolicyBackend>(provider); }
} registration{&provider};
eve::cap::provide<IGpuPolicyBackend>(&provider);
REQUIRE(!run(config, environment).ok());
REQUIRE_EQ(environment.resets, 0);
provider.ready = true;
auto result = run(config, environment);
REQUIRE(!result.ok());
REQUIRE(result.status().code() == eve::StatusCode::Cancelled);
REQUIRE_EQ(provider.trainingCalls, 1);
}
namespace {
class Domain final : public IEnvironment {
public:
int resets = 0;
int tick = 0;
bool failReset = false;
bool failStep = false;
bool invalidState = false;
bool drift = false;
eve::Result<Observation> reset(std::uint64_t) override {
++resets;
tick = 0;
if (failReset)
return eve::Result<Observation>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "reset unavailable"));
return eve::Result<Observation>::success(state(0));
}
eve::Result<Observation> step(std::uint32_t action, double) override {
if (failStep)
return eve::Result<Observation>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Cancelled, "cancelled"));
++tick;
auto o = state(action == 1 ? 1 : 0);
if (tick == 4 && action == 1) {
o.outcome = Outcome::Failure;
o.finding = "injected.invariant";
}
return eve::Result<Observation>::success(std::move(o));
}
private:
Observation state(double reward) const {
Observation o;
o.features = {float(tick) / 4};
if (invalidState) o.features[0] = std::numeric_limits<float>::quiet_NaN();
if (drift) o.features[0] += 0.1f;
o.legalActions = {0, 1};
o.reward = reward;
o.coverage = {std::to_string(tick)};
return o;
}
};
Config smallConfig() {
Config c;
c.horizon = 4;
c.population = 16;
c.elites = 4;
c.generations = 3;
return c;
}
} // namespace
TEST_CASE("agent.findingsReplayAndDeterminism") {
Domain a, b;
auto first = run(smallConfig(), a);
auto second = run(smallConfig(), b);
REQUIRE(first.ok());
REQUIRE(second.ok());
REQUIRE(first.value().failures > 0);
REQUIRE_EQ(first.value().findings.size(), 1u);
REQUIRE_EQ(first.value().episodes, 48u);
REQUIRE_EQ(first.value().bestScore, second.value().bestScore);
REQUIRE(first.value().policy.weights == second.value().policy.weights);
REQUIRE(replay(first.value().findings.front(), b).ok());
b.drift = true;
auto divergent = replay(first.value().findings.front(), b);
REQUIRE(!divergent.ok());
REQUIRE(divergent.status().code() == eve::StatusCode::Conflict);
}
TEST_CASE("agent.invalidBeforeMutationAndErrorPropagation") {
Domain environment;
auto c = smallConfig();
c.dt = std::numeric_limits<double>::quiet_NaN();
REQUIRE(!run(c, environment).ok());
REQUIRE_EQ(environment.resets, 0);
c = smallConfig();
environment.failReset = true;
auto missing = run(c, environment);
REQUIRE(!missing.ok());
REQUIRE(missing.status().code() == eve::StatusCode::Unsupported);
environment.failReset = false;
environment.failStep = true;
auto cancelled = run(c, environment);
REQUIRE(!cancelled.ok());
REQUIRE(cancelled.status().code() == eve::StatusCode::Cancelled);
environment.failStep = false;
environment.invalidState = true;
REQUIRE(!run(c, environment).ok());
}
TEST_CASE("agent.policyTrainingMaskAndImport") {
Domain environment;
auto c = smallConfig();
auto result = run(c, environment);
REQUIRE(result.ok());
REQUIRE(result.value().trainingSamples > 0);
Observation state;
state.features = {0};
state.legalActions = {1};
auto masked = infer(result.value().policy, state);
REQUIRE(masked.ok());
REQUIRE_EQ(masked.value()[0], 0.0);
REQUIRE_EQ(masked.value()[1], 1.0);
auto policy = result.value().policy;
policy.schemaVersion = 2;
REQUIRE(!infer(policy, state).ok());
policy = result.value().policy;
policy.weights[0] = std::numeric_limits<double>::infinity();
REQUIRE(!infer(policy, state).ok());
auto badTrace = result.value().best;
badTrace.schemaVersion = 2;
const auto resets = environment.resets;
REQUIRE(!replay(badTrace, environment).ok());
REQUIRE_EQ(environment.resets, resets);
badTrace = result.value().best;
badTrace.steps[0].action = 55;
REQUIRE(!replay(badTrace, environment).ok());
REQUIRE_EQ(environment.resets, resets);
}
TEST_CASE("agent.randomBaselineAndBoundedFindings") {
Domain environment;
auto c = smallConfig();
c.strategy = Strategy::Random;
c.maxFindings = 0;
auto result = run(c, environment);
REQUIRE(result.ok());
REQUIRE_EQ(result.value().trainingSamples, 0u);
REQUIRE(result.value().failures > 0);
REQUIRE(result.value().findings.empty());
REQUIRE_EQ(result.value().episodes, 48u);
REQUIRE_EQ(result.value().steps, 192u);
}
TEST_CASE("agent.learningImprovesRewardedAction") {
Domain environment;
auto c = smallConfig();
c.generations = 8;
c.trainingEpochs = 12;
c.failureWeight = 0;
c.coverageWeight = 0;
c.strategy = Strategy::Random;
auto baseline = run(c, environment);
REQUIRE(baseline.ok());
c.strategy = Strategy::EvolutionLearning;
auto trained = run(c, environment);
REQUIRE(trained.ok());
auto observation = environment.reset(c.environmentSeed);
REQUIRE(observation.ok());
auto before = infer(baseline.value().policy, observation.value());
auto after = infer(trained.value().policy, observation.value());
REQUIRE(before.ok());
REQUIRE(after.ok());
REQUIRE(after.value()[1] > before.value()[1] + 0.2);
REQUIRE(after.value()[1] > 0.8);
}
TEST_CASE("agent.codecRoundTripAndUnknownFields") {
Domain environment;
auto c = smallConfig();
c.environmentSeed = std::numeric_limits<std::uint64_t>::max();
auto result = run(c, environment);
REQUIRE(result.ok());
auto json = encodePolicy(result.value().policy).toJson();
REQUIRE(json.ok());
auto value = eve::Value::fromJson(json.value());
REQUIRE(value.ok());
auto policy = decodePolicy(value.value());
REQUIRE(policy.ok());
REQUIRE(policy.value().weights == result.value().policy.weights);
auto traceJson = encodeTrace(result.value().best).toJson();
REQUIRE(traceJson.ok());
auto traceValue = eve::Value::fromJson(traceJson.value());
REQUIRE(traceValue.ok());
auto trace = decodeTrace(traceValue.value());
REQUIRE(trace.ok());
REQUIRE_EQ(trace.value().environmentSeed, c.environmentSeed);
REQUIRE(replay(trace.value(), environment).ok());
value.value().getIf<eve::Value::Object>()->emplace("unknown", 1);
REQUIRE(!decodePolicy(value.value()).ok());
REQUIRE(!decodeConfig(eve::Value::object({{"population", 1.5}})).ok());
REQUIRE(!decodeConfig(eve::Value::object({{"strategy", "typo"}})).ok());
}