Replaced the near similar filter constraint by a duplicate recording mbid one

This commit is contained in:
emeric
2026-06-13 23:28:07 +02:00
parent af018a9166
commit 845c437364
6 changed files with 505 additions and 369 deletions
@@ -50,10 +50,10 @@
#include "InterpolationFitConstraint.hpp" #include "InterpolationFitConstraint.hpp"
#include "MaxDistanceConstraint.hpp" #include "MaxDistanceConstraint.hpp"
#include "NearDuplicateEmbeddingConstraint.hpp"
#include "SmoothTransitionConstraint.hpp" #include "SmoothTransitionConstraint.hpp"
#include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp"
#include "track-selection-constraints/SameArtistConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp"
#include "track-selection-constraints/SameRecordingMBIDConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "Types.hpp" #include "Types.hpp"
@@ -121,11 +121,9 @@ namespace lms::recommendation
constexpr float smoothTransitionWeight{ 0.2F }; constexpr float smoothTransitionWeight{ 0.2F };
constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameReleaseWeight{ 0.5F };
constexpr float sameArtistWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F };
constexpr float nearDuplicateThreshold{ 0.01F };
_similarityEvaluator = {}; _similarityEvaluator = {};
_similarityEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>()); _similarityEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
_similarityEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold)); _similarityEvaluator.addHardConstraint(std::make_unique<SameRecordingMBIDConstraint>(_trackMetadata));
_similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint<ReducedDimCount>>(_trackVectors, _trackDistanceThreshold)); _similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint<ReducedDimCount>>(_trackVectors, _trackDistanceThreshold));
_similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight); _similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
_similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight); _similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
@@ -134,7 +132,7 @@ namespace lms::recommendation
_pathEvaluator = {}; _pathEvaluator = {};
_pathEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>()); _pathEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
_pathEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold)); _pathEvaluator.addHardConstraint(std::make_unique<SameRecordingMBIDConstraint>(_trackMetadata));
_pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight); _pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight); _pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight); _pathEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
@@ -595,28 +593,29 @@ namespace lms::recommendation
_trackVectors.try_emplace(trackId, &reducedVector); _trackVectors.try_emplace(trackId, &reducedVector);
}); });
db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) {
std::vector<std::reference_wrapper<const ReducedVector>> releaseTrackFeatures; const auto itVec{ _trackVectors.find(track->getId()) };
if (itVec == _trackVectors.cend())
return;
db::Track::FindParameters params; auto& meta{ _trackMetadata[track->getId()] };
params.setRelease(release->getId()); const db::ReleaseId releaseId{ track->getReleaseId() };
meta.releaseId = releaseId;
meta.recordingMBID = track->getRecordingMBID();
const auto trackIds{ db::Track::findIds(session, params) }; if (releaseId.isValid())
for (const db::TrackId trackId : trackIds.results) _releaseVectors[releaseId].emplace_back(*itVec->second);
{
const auto itFeatures{ _trackVectors.find(trackId) };
if (itFeatures != std::cend(_trackVectors))
{
assert(itFeatures->second);
releaseTrackFeatures.emplace_back(*itFeatures->second);
_trackMetadata[trackId].releaseId = release->getId();
}
}
if (!releaseTrackFeatures.empty())
_releaseVectors.try_emplace(release->getId(), std::move(releaseTrackFeatures));
}); });
math::MedoidCalculator<ReducedVector> calc;
for (const auto& [id, vecs] : _releaseVectors)
{
calc.clear();
for (const auto& v : vecs)
calc.add(v.get());
_releaseMedoids.try_emplace(id, calc.finalize());
}
db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) {
const auto mbid{ artist->getMBID() }; const auto mbid{ artist->getMBID() };
// skip "Various Artists" to avoid false artist matches // skip "Various Artists" to avoid false artist matches
@@ -658,15 +657,6 @@ namespace lms::recommendation
_artistVectors.try_emplace(artist->getId(), std::move(artistTrackVectors)); _artistVectors.try_emplace(artist->getId(), std::move(artistTrackVectors));
}); });
math::MedoidCalculator<ReducedVector> calc;
for (const auto& [id, vecs] : _releaseVectors)
{
calc.clear();
for (const auto& v : vecs)
calc.add(v.get());
_releaseMedoids.try_emplace(id, calc.finalize());
}
for (const auto& [id, vecs] : _artistVectors) for (const auto& [id, vecs] : _artistVectors)
{ {
calc.clear(); calc.clear();
@@ -23,7 +23,6 @@
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <random>
#include <unordered_set> #include <unordered_set>
#include "database/IDb.hpp" #include "database/IDb.hpp"
@@ -39,6 +38,7 @@
#include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp"
#include "track-selection-constraints/SameArtistConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp"
#include "track-selection-constraints/SameRecordingMBIDConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "track-selection-constraints/TrackCandidateContext.hpp" #include "track-selection-constraints/TrackCandidateContext.hpp"
@@ -103,6 +103,7 @@ namespace lms::recommendation
constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameReleaseWeight{ 0.5F };
constexpr float sameArtistWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F };
_trackEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>()); _trackEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
_trackEvaluator.addHardConstraint(std::make_unique<SameRecordingMBIDConstraint>(_trackMetadata));
_trackEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight); _trackEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
_trackEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight); _trackEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
} }
@@ -122,8 +123,8 @@ namespace lms::recommendation
db::Session& session{ _db.getTLSSession() }; db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
buildTrackMetadata(session);
buildTrackClusters(session); buildTrackClusters(session);
buildTrackMetadata(session);
buildReleaseClusters(); buildReleaseClusters();
buildArtistClusters(); buildArtistClusters();
@@ -134,11 +135,17 @@ namespace lms::recommendation
{ {
LOG(DEBUG, "building track metadata..."); LOG(DEBUG, "building track metadata...");
db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { // Ensure cluster tracks with no release/artist have an entry
db::Track::FindParameters params; for (const auto& [trackId, _] : _trackClusters)
params.setRelease(release->getId()); _trackMetadata.try_emplace(trackId);
for (const db::TrackId trackId : db::Track::findIds(session, params).results)
_trackMetadata[trackId].releaseId = release->getId(); db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) {
const auto it{ _trackMetadata.find(track->getId()) };
if (it != _trackMetadata.cend())
{
it->second.releaseId = track->getReleaseId();
it->second.recordingMBID = track->getRecordingMBID();
}
}); });
db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) {
@@ -54,8 +54,8 @@ namespace lms::recommendation
ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override; ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
TrackResults greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const; TrackResults greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const;
void buildTrackMetadata(db::Session& session);
void buildTrackClusters(db::Session& session); void buildTrackClusters(db::Session& session);
void buildTrackMetadata(db::Session& session);
void buildReleaseClusters(); void buildReleaseClusters();
void buildArtistClusters(); void buildArtistClusters();
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "ITrackCandidateHardConstraint.hpp"
#include "TrackMetadata.hpp"
namespace lms::recommendation
{
class SameRecordingMBIDConstraint : public ITrackCandidateHardConstraint
{
public:
SameRecordingMBIDConstraint(const TrackMetadataMap& trackMetadata)
: _trackMetadata{ trackMetadata }
{
}
bool rejects(const TrackCandidateContext& context) const override
{
const auto it{ _trackMetadata.find(context.candidateTrackId) };
if (it == _trackMetadata.cend() || !it->second.recordingMBID)
return false;
for (const db::TrackId selectedId : context.selectedTracks)
{
const auto itSel{ _trackMetadata.find(selectedId) };
if (itSel != _trackMetadata.cend() && itSel->second.recordingMBID == it->second.recordingMBID)
return true;
}
return false;
}
private:
const TrackMetadataMap& _trackMetadata;
};
} // namespace lms::recommendation
@@ -19,9 +19,11 @@
#pragma once #pragma once
#include <optional>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "core/UUID.hpp"
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
@@ -31,8 +33,8 @@ namespace lms::recommendation
struct TrackMetadata struct TrackMetadata
{ {
db::ReleaseId releaseId; // invalid if track has no release db::ReleaseId releaseId; // invalid if track has no release
std::vector<db::ArtistId> artistIds; // sorted; album artists only std::vector<db::ArtistId> artistIds; // sorted, album artists only
// Future: db::MediaLibraryId mediaLibraryId; std::optional<core::UUID> recordingMBID; // absent if track has no recording MBID
}; };
using TrackMetadataMap = std::unordered_map<db::TrackId, TrackMetadata>; using TrackMetadataMap = std::unordered_map<db::TrackId, TrackMetadata>;
@@ -24,22 +24,24 @@
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
#include "math/Vector.hpp" #include "math/Vector.hpp"
#include "core/UUID.hpp"
#include "audio-similarity/InterpolationFitConstraint.hpp" #include "audio-similarity/InterpolationFitConstraint.hpp"
#include "audio-similarity/MaxDistanceConstraint.hpp" #include "audio-similarity/MaxDistanceConstraint.hpp"
#include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp" #include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp"
#include "audio-similarity/SmoothTransitionConstraint.hpp" #include "audio-similarity/SmoothTransitionConstraint.hpp"
#include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp"
#include "track-selection-constraints/SameArtistConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp"
#include "track-selection-constraints/SameRecordingMBIDConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "track-selection-constraints/TrackCandidateContext.hpp" #include "track-selection-constraints/TrackCandidateContext.hpp"
#include "track-selection-constraints/TrackCandidateEvaluator.hpp" #include "track-selection-constraints/TrackCandidateEvaluator.hpp"
#include "track-selection-constraints/TrackMetadata.hpp" #include "track-selection-constraints/TrackMetadata.hpp"
using namespace lms; namespace lms::recommendation::tests
using namespace lms::recommendation;
namespace
{ {
namespace
{
const db::TrackId T1{ 1 }; const db::TrackId T1{ 1 };
const db::TrackId T2{ 2 }; const db::TrackId T2{ 2 };
const db::TrackId T3{ 3 }; const db::TrackId T3{ 3 };
@@ -50,30 +52,30 @@ namespace
const db::ReleaseId R1{ 100 }; const db::ReleaseId R1{ 100 };
const db::ReleaseId R2{ 200 }; const db::ReleaseId R2{ 200 };
} // namespace } // namespace
TEST(DuplicateTrackConstraint, acceptsNewCandidate) TEST(DuplicateTrackConstraint, acceptsNewCandidate)
{ {
const std::vector<db::TrackId> selected{ T1, T2 }; const std::vector<db::TrackId> selected{ T1, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
} }
TEST(DuplicateTrackConstraint, rejectsAlreadySelected) TEST(DuplicateTrackConstraint, rejectsAlreadySelected)
{ {
const std::vector<db::TrackId> selected{ T1, T2, T3 }; const std::vector<db::TrackId> selected{ T1, T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx)); EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx));
} }
TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty) TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty)
{ {
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
} }
TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist) TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist)
{ {
const TrackMetadataMap meta{ const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = { A1 } } }, { T1, { .releaseId = {}, .artistIds = { A1 } } },
{ T2, { .releaseId = {}, .artistIds = { A2 } } }, { T2, { .releaseId = {}, .artistIds = { A2 } } },
@@ -82,10 +84,10 @@ TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist)
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist) TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = { A1 } } }, { T1, { .releaseId = {}, .artistIds = { A1 } } },
{ T2, { .releaseId = {}, .artistIds = { A1 } } }, { T2, { .releaseId = {}, .artistIds = { A1 } } },
@@ -95,10 +97,10 @@ TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist)
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
} }
TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist) TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = { A1 } } }, { T1, { .releaseId = {}, .artistIds = { A1 } } },
{ T2, { .releaseId = {}, .artistIds = { A2 } } }, { T2, { .releaseId = {}, .artistIds = { A2 } } },
@@ -110,10 +112,10 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist)
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
} }
TEST(SameArtistConstraint, trackOutsideWindowIsIgnored) TEST(SameArtistConstraint, trackOutsideWindowIsIgnored)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = { A1 } } }, { T1, { .releaseId = {}, .artistIds = { A1 } } },
{ T2, { .releaseId = {}, .artistIds = { A2 } } }, { T2, { .releaseId = {}, .artistIds = { A2 } } },
@@ -124,19 +126,19 @@ TEST(SameArtistConstraint, trackOutsideWindowIsIgnored)
const std::vector<db::TrackId> selected{ T3, T2 }; const std::vector<db::TrackId> selected{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap) TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap)
{ {
const TrackMetadataMap meta{}; const TrackMetadataMap meta{};
const SameArtistConstraint constraint{ meta }; const SameArtistConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease) TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = R1, .artistIds = {} } }, { T1, { .releaseId = R1, .artistIds = {} } },
{ T2, { .releaseId = R2, .artistIds = {} } }, { T2, { .releaseId = R2, .artistIds = {} } },
@@ -145,10 +147,10 @@ TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease)
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease) TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = R1, .artistIds = {} } }, { T1, { .releaseId = R1, .artistIds = {} } },
{ T2, { .releaseId = R1, .artistIds = {} } }, { T2, { .releaseId = R1, .artistIds = {} } },
@@ -157,10 +159,10 @@ TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease)
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
} }
TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease) TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {} } }, { T1, { .releaseId = {}, .artistIds = {} } },
{ T2, { .releaseId = R1, .artistIds = {} } }, { T2, { .releaseId = R1, .artistIds = {} } },
@@ -169,27 +171,27 @@ TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease)
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(TrackCandidateEvaluator, hardConstraintRejects) TEST(TrackCandidateEvaluator, hardConstraintRejects)
{ {
TrackCandidateEvaluator evaluator; TrackCandidateEvaluator evaluator;
evaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>()); evaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
const std::vector<db::TrackId> selected{ T1 }; const std::vector<db::TrackId> selected{ T1 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(evaluator.rejects(ctx)); EXPECT_TRUE(evaluator.rejects(ctx));
} }
TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject) TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject)
{ {
TrackCandidateEvaluator evaluator; TrackCandidateEvaluator evaluator;
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(evaluator.rejects(ctx)); EXPECT_FALSE(evaluator.rejects(ctx));
} }
TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted) TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = R1, .artistIds = {} } }, { T1, { .releaseId = R1, .artistIds = {} } },
{ T2, { .releaseId = R1, .artistIds = {} } }, { T2, { .releaseId = R1, .artistIds = {} } },
@@ -201,10 +203,10 @@ TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted)
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F); EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
} }
TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated) TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = R1, .artistIds = { A1 } } }, { T1, { .releaseId = R1, .artistIds = { A1 } } },
{ T2, { .releaseId = R1, .artistIds = { A1 } } }, { T2, { .releaseId = R1, .artistIds = { A1 } } },
@@ -217,10 +219,10 @@ TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated)
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F); EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
} }
TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints) TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints)
{ {
TrackMetadataMap meta{ TrackMetadataMap meta{
{ T1, { .releaseId = R1, .artistIds = {} } }, { T1, { .releaseId = R1, .artistIds = {} } },
{ T2, { .releaseId = R1, .artistIds = {} } }, { T2, { .releaseId = R1, .artistIds = {} } },
@@ -233,181 +235,263 @@ TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints)
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(evaluator.rejects(ctx)); EXPECT_FALSE(evaluator.rejects(ctx));
EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F); EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F);
} }
namespace namespace
{ {
using TestVec = math::Vector<2, float>; using TestVec = math::Vector<2, float>;
const TestVec v_x{ 1.F, 0.F }; const TestVec v_x{ 1.F, 0.F };
const TestVec v_y{ 0.F, 1.F }; const TestVec v_y{ 0.F, 1.F };
using TestConstraint = NearDuplicateEmbeddingConstraint<2>; using TestConstraint = NearDuplicateEmbeddingConstraint<2>;
using TestVectorMap = TestConstraint::TrackVectorMap; using TestVectorMap = TestConstraint::TrackVectorMap;
} // namespace } // namespace
TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap) TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap)
{ {
const TestVectorMap trackVectors{ { T1, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T1 }; const std::vector<db::TrackId> selected{ T1 };
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty) TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty)
{ {
const TestVectorMap trackVectors{ { T1, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap) TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap)
{ {
const TestVectorMap trackVectors{ { T1, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold) TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(constraint.rejects(ctx)); EXPECT_TRUE(constraint.rejects(ctx));
} }
TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold) TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(NearDuplicateEmbeddingConstraint, rejectsWhenOneOfManySelectedIsNearDuplicate) TEST(NearDuplicateEmbeddingConstraint, rejectsWhenOneOfManySelectedIsNearDuplicate)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T3, T4 }; const std::vector<db::TrackId> selected{ T3, T4 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(constraint.rejects(ctx)); EXPECT_TRUE(constraint.rejects(ctx));
} }
TEST(NearDuplicateEmbeddingConstraint, acceptsWhenAllSelectedAreFarEnough) TEST(NearDuplicateEmbeddingConstraint, acceptsWhenAllSelectedAreFarEnough)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } };
const TestConstraint constraint{ trackVectors, 0.1F }; const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2, T3 }; const std::vector<db::TrackId> selected{ T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(InterpolationFitConstraint, zeroScoreWhenNoSeeds) TEST(InterpolationFitConstraint, zeroScoreWhenNoSeeds)
{ {
const TestVectorMap trackVectors{ { T1, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x } };
const InterpolationFitConstraint<2> constraint{ trackVectors }; const InterpolationFitConstraint<2> constraint{ trackVectors };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(InterpolationFitConstraint, zeroScoreWhenCandidateMatchesSeed) TEST(InterpolationFitConstraint, zeroScoreWhenCandidateMatchesSeed)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const InterpolationFitConstraint<2> constraint{ trackVectors }; const InterpolationFitConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> seeds{ T2 }; const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(InterpolationFitConstraint, halfScoreWhenCandidateOrthogonalToSeed) TEST(InterpolationFitConstraint, halfScoreWhenCandidateOrthogonalToSeed)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const InterpolationFitConstraint<2> constraint{ trackVectors }; const InterpolationFitConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> seeds{ T2 }; const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
} }
TEST(InterpolationFitConstraint, usesClosestSeed) TEST(InterpolationFitConstraint, usesClosestSeed)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } };
const InterpolationFitConstraint<2> constraint{ trackVectors }; const InterpolationFitConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> seeds{ T3, T2 }; const std::vector<db::TrackId> seeds{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(MaxDistanceConstraint, acceptsWhenNoSeeds) TEST(MaxDistanceConstraint, acceptsWhenNoSeeds)
{ {
const TestVectorMap trackVectors{ { T1, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(MaxDistanceConstraint, acceptsWhenWithinThresholdOfOneSeed) TEST(MaxDistanceConstraint, acceptsWhenWithinThresholdOfOneSeed)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
const std::vector<db::TrackId> seeds{ T2 }; const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(MaxDistanceConstraint, rejectsWhenBeyondThresholdFromAllSeeds) TEST(MaxDistanceConstraint, rejectsWhenBeyondThresholdFromAllSeeds)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.1F }; const MaxDistanceConstraint<2> constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> seeds{ T2 }; const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_TRUE(constraint.rejects(ctx)); EXPECT_TRUE(constraint.rejects(ctx));
} }
TEST(MaxDistanceConstraint, acceptsWhenAnyOfManySeedsIsCloseEnough) TEST(MaxDistanceConstraint, acceptsWhenAnyOfManySeedsIsCloseEnough)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_x } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
const std::vector<db::TrackId> seeds{ T2, T3 }; const std::vector<db::TrackId> seeds{ T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FALSE(constraint.rejects(ctx)); EXPECT_FALSE(constraint.rejects(ctx));
} }
TEST(SmoothTransitionConstraint, zeroScoreWhenNoSelectedTracks) TEST(SmoothTransitionConstraint, zeroScoreWhenNoSelectedTracks)
{ {
const TestVectorMap trackVectors{ { T1, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x } };
const SmoothTransitionConstraint<2> constraint{ trackVectors }; const SmoothTransitionConstraint<2> constraint{ trackVectors };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(SmoothTransitionConstraint, zeroScoreWhenPreviousMatchesCandidate) TEST(SmoothTransitionConstraint, zeroScoreWhenPreviousMatchesCandidate)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const SmoothTransitionConstraint<2> constraint{ trackVectors }; const SmoothTransitionConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
TEST(SmoothTransitionConstraint, halfScoreWhenPreviousOrthogonalToCandidate) TEST(SmoothTransitionConstraint, halfScoreWhenPreviousOrthogonalToCandidate)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const SmoothTransitionConstraint<2> constraint{ trackVectors }; const SmoothTransitionConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> selected{ T2 }; const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
} }
TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack) TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack)
{ {
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } }; const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } };
const SmoothTransitionConstraint<2> constraint{ trackVectors }; const SmoothTransitionConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> selected{ T3, T2 }; const std::vector<db::TrackId> selected{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
namespace
{
const core::UUID MBID1{ *core::UUID::fromString("3d2508f7-3a7e-4f25-895c-fca079dd71ce") };
const core::UUID MBID2{ *core::UUID::fromString("8e44e4ea-9bce-47c2-a48c-8aa338242fb2") };
} // namespace
TEST(SameRecordingMBIDConstraint, acceptsWhenCandidateNotInMap)
{
const TrackMetadataMap meta{ { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } };
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenCandidateHasNoMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = std::nullopt } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenSelectionEmpty)
{
const TrackMetadataMap meta{ { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } };
const SameRecordingMBIDConstraint constraint{ meta };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenNoneOfSelectedHasSameMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID2 } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, rejectsWhenOneOfSelectedHasSameMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID2 } },
{ T3, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenSelectedTrackNotInMap)
{
const TrackMetadataMap meta{ { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } };
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenSelectedTrackHasNoMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = std::nullopt } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
} // namespace lms::recommendation::tests