Files
MapLibreParticleLab/test/particle/field_binding.test.cpp

314 lines
21 KiB
C++

#include <mln/particle/field/field_binding.hpp>
#include <mln/particle/field/field_projection_materialization.hpp>
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <vector>
using namespace mln;
namespace {
constexpr uint64_t Hour = 3'600'000;
constexpr uint64_t Base = 1'700'000'000'000ULL;
particle::DecodedFieldPlane makePlane(uint16_t semantic, uint16_t group, particle::FieldValueClass valueClass,
particle::FieldComponent component, uint16_t unit, std::string identity,
std::shared_ptr<const std::vector<uint8_t>> bytes) {
return {{semantic, group, component}, particle::FieldDType::UInt8, valueClass, unit, false, 4, 1.0f, 0.0f,
std::move(identity), std::move(bytes)};
}
std::shared_ptr<const particle::DecodedFieldFrame> makeFrame(std::string field, uint64_t time, uint64_t generation,
std::string suffix, bool vector = true,
bool scalar = true) {
const auto bytes = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{1, 2, 3, 4});
const auto bits = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{15});
particle::DecodedFieldFrame frame;
frame.frameContentIdentity = {std::move(field), generation, generation, time};
// All frames in these series represent one forecast reference cycle.
frame.referenceTimeMs = Base;
frame.grid = {.layout = 1, .projection = 1, .sampleOrder = 1, .gridRegistration = 1, .width = 2, .height = 2,
.west = 0, .south = 0, .east = 1, .north = 1};
frame.validityContentIdentity = "valid-" + suffix;
frame.validityBits = bits;
frame.validityPlane = makePlane(900, 0, particle::FieldValueClass::Validity, particle::FieldComponent::None,
particle::FieldUnitNone, frame.validityContentIdentity, bits);
frame.validityPlane.dtype = particle::FieldDType::Bitset;
frame.validityPlane.requiredFlag = true;
if (vector) {
frame.planes.push_back(makePlane(1, 7, particle::FieldValueClass::VelocityComponent, particle::FieldComponent::UEast,
particle::FieldUnitMetersPerSecond, "u-" + suffix, bytes));
frame.planes.push_back(makePlane(2, 7, particle::FieldValueClass::VelocityComponent, particle::FieldComponent::VNorth,
particle::FieldUnitMetersPerSecond, "v-" + suffix, bytes));
}
if (scalar) frame.planes.push_back(makePlane(50, 0, particle::FieldValueClass::Scalar, particle::FieldComponent::None,
42, "s-" + suffix, bytes));
return std::make_shared<const particle::DecodedFieldFrame>(std::move(frame));
}
void submit(particle::EncodedFieldStore& store, const std::string& key,
std::shared_ptr<const particle::DecodedFieldFrame> frame) {
ASSERT_EQ(store.submit(key, std::move(frame)), particle::FieldSeriesSubmitResult::Inserted);
}
particle::ColorBinding color(std::string key = "weather.wave") {
return {std::move(key), {50, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 42};
}
particle::ParticleBinding wind() {
particle::ParticleBinding binding;
binding.fieldKey = "weather.wind"; binding.uPlane = {1, 7, particle::FieldComponent::None};
binding.vPlane = {2, 7, particle::FieldComponent::None}; binding.expectedUnitID = particle::FieldUnitMetersPerSecond;
return binding;
}
} // namespace
TEST(ParticleFieldBinding, ResolvesIndependentFieldKeyCadence) {
particle::EncodedFieldStore store;
for (uint64_t hour : {0ULL, 1ULL, 2ULL}) submit(store, "weather.wind", makeFrame("wind", Base + hour * Hour, 1, "w" + std::to_string(hour)));
for (uint64_t hour : {0ULL, 3ULL, 6ULL}) submit(store, "weather.wave", makeFrame("wave", Base + hour * Hour, 1, "a" + std::to_string(hour)));
const auto c = particle::BindingProjectionResolver::resolve(store, color(), Base + Hour + Hour / 2);
const auto p = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour + Hour / 2);
ASSERT_TRUE(c.accepted()); ASSERT_TRUE(p.accepted());
EXPECT_FLOAT_EQ(c.projection->sampleState.mix, 0.5f);
EXPECT_FLOAT_EQ(p.projection->sampleState.mix, 0.5f);
EXPECT_EQ(c.projection->contentIdentity.t0Coordinate.validTimeMs, Base);
EXPECT_EQ(c.projection->contentIdentity.t1Coordinate.validTimeMs, Base + 3 * Hour);
EXPECT_EQ(p.projection->contentIdentity.t0Coordinate.validTimeMs, Base + Hour);
EXPECT_EQ(p.projection->contentIdentity.t1Coordinate.validTimeMs, Base + 2 * Hour);
EXPECT_NE(c.projection->t0Frame, p.projection->t0Frame);
EXPECT_NE(c.projection->t0ValidityStorage, p.projection->t0ValidityStorage);
}
TEST(ParticleFieldBinding, SingleFrameProjectsAtItsExactTimeAndUpgradesToInterpolation) {
particle::EncodedFieldStore store;
submit(store, "weather.wind", makeFrame("wind", Base, 1, "single"));
const auto colorAtFrame = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base);
const auto particleAtFrame = particle::BindingProjectionResolver::resolve(store, wind(), Base);
ASSERT_TRUE(colorAtFrame.accepted());
ASSERT_TRUE(particleAtFrame.accepted());
EXPECT_EQ(colorAtFrame.projection->t0Frame, colorAtFrame.projection->t1Frame);
EXPECT_EQ(particleAtFrame.projection->t0Frame, particleAtFrame.projection->t1Frame);
EXPECT_FLOAT_EQ(colorAtFrame.projection->sampleState.mix, 0.0f);
EXPECT_FLOAT_EQ(particleAtFrame.projection->sampleState.mix, 0.0f);
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + 1).error,
particle::BindingProjectionError::WindowUnavailable);
const auto colorSnapshot = particle::materializeColorProjection(*colorAtFrame.projection);
const auto particleSnapshot = particle::materializeParticleProjection(*particleAtFrame.projection);
ASSERT_TRUE(colorSnapshot);
ASSERT_TRUE(particleSnapshot);
EXPECT_EQ(colorSnapshot->t0, colorSnapshot->t1);
EXPECT_EQ(particleSnapshot->t0, particleSnapshot->t1);
EXPECT_EQ(colorSnapshot->t0->scalar, (std::vector<float>{1, 2, 3, 4}));
EXPECT_TRUE(particleSnapshot->t0->hasVector());
particle::EncodedFieldStore scalarOnly;
submit(scalarOnly, "weather.rain", makeFrame("rain", Base, 1, "scalar-only", false));
const auto scalarProjection = particle::BindingProjectionResolver::resolve(
scalarOnly, color("weather.rain"), Base);
ASSERT_TRUE(scalarProjection.accepted());
const auto scalarSnapshot = particle::materializeColorProjection(*scalarProjection.projection);
ASSERT_TRUE(scalarSnapshot);
EXPECT_TRUE(scalarSnapshot->t0->hasScalar());
EXPECT_FALSE(scalarSnapshot->t0->hasVector());
auto invalidParticleBinding = wind();
invalidParticleBinding.fieldKey = "weather.rain";
EXPECT_EQ(particle::BindingProjectionResolver::resolve(scalarOnly, invalidParticleBinding, Base).error,
particle::BindingProjectionError::PlaneMissing);
submit(store, "weather.wind", makeFrame("wind", Base + 4 * Hour, 1, "later"));
const auto interpolated = particle::BindingProjectionResolver::resolve(store, wind(), Base + 2 * Hour);
ASSERT_TRUE(interpolated.accepted());
EXPECT_NE(interpolated.projection->t0Frame, interpolated.projection->t1Frame);
EXPECT_FLOAT_EQ(interpolated.projection->sampleState.mix, 0.5f);
}
TEST(ParticleFieldBinding, MixChangesSampleStateButNotContentIdentityOrStorage) {
particle::EncodedFieldStore store;
submit(store, "weather.wave", makeFrame("wave", Base, 1, "0"));
submit(store, "weather.wave", makeFrame("wave", Base + 4 * Hour, 1, "4"));
const auto at0 = particle::BindingProjectionResolver::resolve(store, color(), Base);
const auto at1 = particle::BindingProjectionResolver::resolve(store, color(), Base + Hour);
const auto at2 = particle::BindingProjectionResolver::resolve(store, color(), Base + 2 * Hour);
const auto at3 = particle::BindingProjectionResolver::resolve(store, color(), Base + 14'256'000);
ASSERT_TRUE(at0.accepted()); ASSERT_TRUE(at1.accepted()); ASSERT_TRUE(at2.accepted()); ASSERT_TRUE(at3.accepted());
EXPECT_FLOAT_EQ(at0.projection->sampleState.mix, 0.0f); EXPECT_FLOAT_EQ(at1.projection->sampleState.mix, 0.25f);
EXPECT_FLOAT_EQ(at2.projection->sampleState.mix, 0.5f); EXPECT_FLOAT_EQ(at3.projection->sampleState.mix, 0.99f);
EXPECT_EQ(at0.projection->t0Frame, at0.projection->t1Frame);
EXPECT_EQ(at1.projection->contentIdentity, at2.projection->contentIdentity);
EXPECT_EQ(at1.projection->contentIdentity, at3.projection->contentIdentity);
EXPECT_EQ(at0.projection->t0PlaneStorage, at3.projection->t0PlaneStorage);
}
TEST(ParticleFieldBinding, ProjectionIdentityIsolatedBySelectedRevision) {
particle::EncodedFieldStore store;
submit(store, "weather.wind", makeFrame("wind", Base, 1, "0"));
submit(store, "weather.wind", makeFrame("wind", Base + Hour, 1, "1"));
const auto beforeColor = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + Hour / 2);
const auto beforeParticle = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour / 2);
ASSERT_TRUE(beforeColor.accepted()); ASSERT_TRUE(beforeParticle.accepted());
auto revision = *beforeParticle.projection->t0Frame;
revision.frameContentIdentity.generation = 2;
revision.frameContentIdentity.dataVersion = 2;
revision.planes.back().contentIdentity = "s-revised";
ASSERT_EQ(store.submit("weather.wind", std::make_shared<const particle::DecodedFieldFrame>(std::move(revision))),
particle::FieldSeriesSubmitResult::ReplacedRevision);
const auto afterColor = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + Hour / 2);
const auto afterParticle = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour / 2);
ASSERT_TRUE(afterColor.accepted()); ASSERT_TRUE(afterParticle.accepted());
EXPECT_NE(beforeColor.projection->contentIdentity, afterColor.projection->contentIdentity);
EXPECT_EQ(beforeParticle.projection->contentIdentity, afterParticle.projection->contentIdentity);
EXPECT_EQ(beforeParticle.projection->t0FirstPlaneStorage, afterParticle.projection->t0FirstPlaneStorage);
EXPECT_EQ(beforeParticle.projection->t0ValidityStorage, afterParticle.projection->t0ValidityStorage);
auto vectorRevision = *afterParticle.projection->t0Frame;
vectorRevision.frameContentIdentity.generation = 3;
vectorRevision.frameContentIdentity.dataVersion = 3;
vectorRevision.planes[0].contentIdentity = "u-revised";
vectorRevision.planes[1].contentIdentity = "v-revised";
ASSERT_EQ(store.submit("weather.wind", std::make_shared<const particle::DecodedFieldFrame>(std::move(vectorRevision))),
particle::FieldSeriesSubmitResult::ReplacedRevision);
const auto afterVectorColor = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + Hour / 2);
const auto afterVectorParticle = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour / 2);
ASSERT_TRUE(afterVectorColor.accepted()); ASSERT_TRUE(afterVectorParticle.accepted());
EXPECT_EQ(afterColor.projection->contentIdentity, afterVectorColor.projection->contentIdentity);
EXPECT_NE(afterParticle.projection->contentIdentity, afterVectorParticle.projection->contentIdentity);
auto validityRevision = *afterVectorParticle.projection->t0Frame;
validityRevision.frameContentIdentity.generation = 4;
validityRevision.frameContentIdentity.dataVersion = 4;
validityRevision.validityContentIdentity = "valid-revised";
validityRevision.validityBits = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{14});
validityRevision.validityPlane.contentIdentity = validityRevision.validityContentIdentity;
validityRevision.validityPlane.storage = validityRevision.validityBits;
ASSERT_EQ(store.submit("weather.wind", std::make_shared<const particle::DecodedFieldFrame>(std::move(validityRevision))),
particle::FieldSeriesSubmitResult::ReplacedRevision);
const auto afterValidityColor = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + Hour / 2);
const auto afterValidityParticle = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour / 2);
ASSERT_TRUE(afterValidityColor.accepted()); ASSERT_TRUE(afterValidityParticle.accepted());
EXPECT_NE(afterVectorColor.projection->contentIdentity, afterValidityColor.projection->contentIdentity);
EXPECT_NE(afterVectorParticle.projection->contentIdentity, afterValidityParticle.projection->contentIdentity);
}
TEST(ParticleFieldBinding, VectorComponentsIgnoresInactiveDirectionSelectorInIdentity) {
particle::EncodedFieldStore store;
submit(store, "weather.wind", makeFrame("wind", Base, 1, "0"));
submit(store, "weather.wind", makeFrame("wind", Base + Hour, 1, "1"));
const auto first = wind();
auto inactiveDirectionChanged = first;
inactiveDirectionChanged.directionPlane = {99, 4, particle::FieldComponent::WUp};
EXPECT_EQ(first, inactiveDirectionChanged);
const auto firstProjection = particle::BindingProjectionResolver::resolve(store, first, Base);
const auto inactiveDirectionProjection =
particle::BindingProjectionResolver::resolve(store, inactiveDirectionChanged, Base);
ASSERT_TRUE(firstProjection.accepted());
ASSERT_TRUE(inactiveDirectionProjection.accepted());
EXPECT_EQ(firstProjection.projection->contentIdentity, inactiveDirectionProjection.projection->contentIdentity);
auto activeUChanged = first;
activeUChanged.uPlane.semanticID = 99;
EXPECT_NE(first, activeUChanged);
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, activeUChanged, Base).error,
particle::BindingProjectionError::PlaneMissing);
}
TEST(ParticleFieldBinding, DirectionAngleIgnoresInactiveVectorSelectorsInIdentity) {
particle::EncodedFieldStore store;
auto d0 = *makeFrame("direction", Base, 1, "d0", false, false);
auto d1 = *makeFrame("direction", Base + Hour, 1, "d1", false, false);
const auto directionBytes = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{1, 2, 3, 4});
for (auto* frame : {&d0, &d1}) {
frame->planes.push_back(makePlane(10, 0, particle::FieldValueClass::DirectionAngle, particle::FieldComponent::None,
particle::FieldUnitDegrees, "direction", directionBytes));
}
submit(store, "weather.direction", std::make_shared<const particle::DecodedFieldFrame>(std::move(d0)));
submit(store, "weather.direction", std::make_shared<const particle::DecodedFieldFrame>(std::move(d1)));
particle::ParticleBinding first;
first.mode = particle::ParticleBindingMode::DirectionAngle;
first.fieldKey = "weather.direction";
first.directionPlane = {10, 0, particle::FieldComponent::None};
first.expectedUnitID = particle::FieldUnitDegrees;
auto inactiveVectorsChanged = first;
inactiveVectorsChanged.uPlane = {1, 7, particle::FieldComponent::UEast};
inactiveVectorsChanged.vPlane = {2, 7, particle::FieldComponent::VNorth};
EXPECT_EQ(first, inactiveVectorsChanged);
const auto firstProjection = particle::BindingProjectionResolver::resolve(store, first, Base);
const auto inactiveVectorsProjection = particle::BindingProjectionResolver::resolve(store, inactiveVectorsChanged, Base);
ASSERT_TRUE(firstProjection.accepted());
ASSERT_TRUE(inactiveVectorsProjection.accepted());
EXPECT_EQ(firstProjection.projection->contentIdentity, inactiveVectorsProjection.projection->contentIdentity);
auto activeDirectionChanged = first;
activeDirectionChanged.directionPlane.semanticID = 99;
EXPECT_NE(first, activeDirectionChanged);
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, activeDirectionChanged, Base).error,
particle::BindingProjectionError::PlaneMissing);
}
TEST(ParticleFieldBinding, FailsClosedAndSupportsDirectionAngle) {
particle::EncodedFieldStore store;
submit(store, "weather.wind", makeFrame("wind", Base, 1, "0"));
submit(store, "weather.wind", makeFrame("wind", Base + Hour, 1, "1"));
const particle::ColorBinding velocityComponent{"weather.wind", {1, 7, particle::FieldComponent::UEast},
particle::FieldValueClass::VelocityComponent,
particle::FieldUnitMetersPerSecond};
const auto velocityProjection = particle::BindingProjectionResolver::resolve(store, velocityComponent, Base);
ASSERT_TRUE(velocityProjection.accepted());
EXPECT_EQ(velocityProjection.projection->t0PlaneStorage,
velocityProjection.projection->t0Frame->planes.front().storage);
const particle::ColorBinding validity{"weather.wind", {900, 0, particle::FieldComponent::None},
particle::FieldValueClass::Validity, particle::FieldUnitNone};
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, validity, Base).error,
particle::BindingProjectionError::InvalidBinding);
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color(""), Base).error, particle::BindingProjectionError::InvalidBinding);
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color("missing"), Base).error, particle::BindingProjectionError::FieldSeriesMissing);
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + 2 * Hour).error, particle::BindingProjectionError::WindowUnavailable);
auto missing = color("weather.wind"); missing.plane.semanticID = 99;
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, missing, Base).error, particle::BindingProjectionError::PlaneMissing);
auto classMismatch = color("weather.wind"); classMismatch.plane = {1, 7, particle::FieldComponent::None};
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, classMismatch, Base).error, particle::BindingProjectionError::PlaneClassMismatch);
auto unitMismatch = wind(); unitMismatch.expectedUnitID = 99;
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, unitMismatch, Base).error, particle::BindingProjectionError::PlaneUnitMismatch);
auto componentMismatch = wind(); componentMismatch.uPlane.semanticID = 2; componentMismatch.vPlane.semanticID = 1;
EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, componentMismatch, Base).error, particle::BindingProjectionError::PlaneComponentMismatch);
particle::EncodedFieldStore unequalGroups;
auto g0 = *makeFrame("wind", Base, 1, "g0"), g1 = *makeFrame("wind", Base + Hour, 1, "g1");
g0.planes[1].key.groupID = 8; g1.planes[1].key.groupID = 8;
submit(unequalGroups, "weather.wind", std::make_shared<const particle::DecodedFieldFrame>(std::move(g0)));
submit(unequalGroups, "weather.wind", std::make_shared<const particle::DecodedFieldFrame>(std::move(g1)));
auto groupMismatch = wind(); groupMismatch.vPlane.groupID = 8;
EXPECT_EQ(particle::BindingProjectionResolver::resolve(unequalGroups, groupMismatch, Base).error,
particle::BindingProjectionError::PlaneGroupMismatch);
particle::EncodedFieldStore directions;
auto d0 = *makeFrame("direction", Base, 1, "d0", false, false);
auto d1 = *makeFrame("direction", Base + Hour, 1, "d1", false, false);
const auto directionBytes = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{1, 2, 3, 4});
for (auto* frame : {&d0, &d1}) {
frame->planes.push_back(makePlane(10, 0, particle::FieldValueClass::DirectionAngle, particle::FieldComponent::None,
particle::FieldUnitDegrees, "direction", directionBytes));
frame->planes.back().dtype = particle::FieldDType::UInt16;
frame->planes.back().scale = 360.0f / 65536.0f;
}
submit(directions, "weather.direction", std::make_shared<const particle::DecodedFieldFrame>(std::move(d0)));
submit(directions, "weather.direction", std::make_shared<const particle::DecodedFieldFrame>(std::move(d1)));
particle::ParticleBinding direction;
direction.mode = particle::ParticleBindingMode::DirectionAngle; direction.fieldKey = "weather.direction";
direction.directionPlane = {10, 0, particle::FieldComponent::None}; direction.expectedUnitID = particle::FieldUnitDegrees;
EXPECT_TRUE(particle::BindingProjectionResolver::resolve(directions, direction, Base).accepted());
direction.expectedUnitID = particle::FieldUnitMetersPerSecond;
EXPECT_EQ(particle::BindingProjectionResolver::resolve(directions, direction, Base).error,
particle::BindingProjectionError::PlaneUnitMismatch);
direction.expectedUnitID = particle::FieldUnitDegrees;
direction.directionPlane.semanticID = 99;
EXPECT_EQ(particle::BindingProjectionResolver::resolve(directions, direction, Base).error,
particle::BindingProjectionError::PlaneMissing);
}