Relaxed constraints on play queue auto filling + cleaned audio similarity constraints

This commit is contained in:
emeric
2026-06-05 23:42:12 +02:00
parent 710c53b4b4
commit 833fc708dc
10 changed files with 369 additions and 200 deletions
@@ -49,13 +49,13 @@
#include "math/PrincipalComponents.hpp"
#include "math/StatsAccumulator.hpp"
#include "InterpolationFitConstraint.hpp"
#include "MaxDistanceConstraint.hpp"
#include "NearDuplicateEmbeddingConstraint.hpp"
#include "SmoothTransitionConstraint.hpp"
#include "track-selection-constraints/DuplicateTrackConstraint.hpp"
#include "track-selection-constraints/InterpolationFitConstraint.hpp"
#include "track-selection-constraints/MaxDistanceConstraint.hpp"
#include "track-selection-constraints/SameArtistConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "track-selection-constraints/SmoothTransitionConstraint.hpp"
#include "Types.hpp"
@@ -121,17 +121,17 @@ namespace lms::recommendation
_similarityEvaluator = {};
_similarityEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
_similarityEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold));
_similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint>(_trackDistanceThreshold));
_similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint>(), interpolationFitWeight);
_similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint>(), smoothTransitionWeight);
_similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint<ReducedDimCount>>(_trackVectors, _trackDistanceThreshold));
_similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
_similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
_similarityEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
_similarityEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
_pathEvaluator = {};
_pathEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
_pathEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold));
_pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint>(), interpolationFitWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint>(), smoothTransitionWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
}
@@ -210,8 +210,6 @@ namespace lms::recommendation
rankedTracks.resize(candidateCount);
// Greedy selection: at each step pick the candidate with the lowest penalized score.
// distanceToPrevious is the cosine distance to the last selected track, so that
// SmoothTransitionConstraint penalises large acoustic jumps between consecutive results.
// Pre-seed selectedTracks with the input tracks so that soft constraints (same release,
// same artist) treat them as already taken, preventing the first results from being
// from the same release/artist as the inputs.
@@ -219,8 +217,6 @@ namespace lms::recommendation
selectedTracks.reserve(selectedTracks.size() + maxCount);
res.reserve(maxCount);
const ReducedVector* previousVector{};
while (res.size() < maxCount && !rankedTracks.empty())
{
std::optional<std::size_t> bestIdx;
@@ -228,15 +224,12 @@ namespace lms::recommendation
for (std::size_t i{}; i < rankedTracks.size(); ++i)
{
const auto& [candidateId, distanceToQuery]{ rankedTracks[i] };
const ReducedVector* candidateVector{ _trackVectors.at(candidateId) };
const float distanceToPrevious{ previousVector ? math::NormalizedCosineDistance{ *previousVector }(*candidateVector) : 0.F };
const db::TrackId candidateId{ rankedTracks[i].first };
const TrackCandidateContext context{
.candidateTrackId = candidateId,
.selectedTracks = selectedTracks,
.distanceToQuery = distanceToQuery,
.distanceToPrevious = distanceToPrevious,
.seedTrackIds = tracksId,
};
if (_similarityEvaluator.rejects(context))
@@ -256,7 +249,6 @@ namespace lms::recommendation
const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] };
res.push_back({ .id = selectedId, .distance = distanceToQuery });
selectedTracks.push_back(selectedId);
previousVector = _trackVectors.at(selectedId);
rankedTracks.erase(std::begin(rankedTracks) + static_cast<std::ptrdiff_t>(*bestIdx));
}
@@ -284,26 +276,28 @@ namespace lms::recommendation
path.reserve(maxCount);
path.push_back(startTrackId);
const ReducedVector* previousVector{ itStart->second };
static constexpr std::size_t DefaultNeighborCount{ 16 };
static constexpr std::size_t BroadNeighborCount{ 64 };
std::size_t neighborCount{ DefaultNeighborCount };
static constexpr std::size_t NeighborCount{ 32 };
const std::size_t interiorCount{ (maxCount > 2) ? (maxCount - 2) : 0 };
auto evaluateCandidates = [&](const TrackResults& neighborList) -> std::optional<db::TrackId> {
for (std::size_t i{}; i < interiorCount; ++i)
{
const float t{ static_cast<float>(i + 1) / static_cast<float>(interiorCount + 1) };
auto queryPoint{ startVector + direction * t };
queryPoint.normalizeL2();
const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, NeighborCount, endTrackId) };
const db::TrackId stepSeedTrackId{ neighbors.empty() ? startTrackId : neighbors[0].id };
const std::array<db::TrackId, 1> stepSeedTrackIds{ stepSeedTrackId };
std::optional<db::TrackId> best;
float bestScore{ std::numeric_limits<float>::max() };
for (const auto& [candidateId, candidateDistance] : neighborList)
for (const auto& [candidateTrackId, candidateDistance] : neighbors)
{
const auto* candidateVector{ _trackVectors.at(candidateId) };
const float transitionDistance{ math::NormalizedCosineDistance{ *previousVector }(*candidateVector) };
const TrackCandidateContext context{
.candidateTrackId = candidateId,
.candidateTrackId = candidateTrackId,
.selectedTracks = path,
.distanceToQuery = candidateDistance,
.distanceToPrevious = transitionDistance,
.seedTrackIds = stepSeedTrackIds,
};
if (_pathEvaluator.rejects(context))
@@ -313,34 +307,14 @@ namespace lms::recommendation
if (score < bestScore)
{
bestScore = score;
best = candidateId;
best = candidateTrackId;
}
}
return best;
};
for (std::size_t i{}; i < interiorCount; ++i)
{
const float t{ static_cast<float>(i + 1) / static_cast<float>(interiorCount + 1) };
auto queryPoint{ startVector + direction * t };
queryPoint.normalizeL2();
const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) };
std::optional<db::TrackId> bestCandidate{ evaluateCandidates(neighbors) };
if (!bestCandidate && neighborCount < BroadNeighborCount)
{
neighborCount = BroadNeighborCount;
const auto broaderNeighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) };
bestCandidate = evaluateCandidates(broaderNeighbors);
}
if (!bestCandidate)
if (!best)
continue;
path.push_back(*bestCandidate);
previousVector = _trackVectors.at(*bestCandidate);
path.push_back(*best);
}
if (maxCount > 1)
@@ -0,0 +1,69 @@
/*
* 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 <limits>
#include <unordered_map>
#include "database/objects/TrackId.hpp"
#include "math/NormalizedCosineDistance.hpp"
#include "math/Vector.hpp"
#include "Types.hpp"
#include "track-selection-constraints/ITrackCandidateSoftConstraint.hpp"
namespace lms::recommendation
{
template<std::size_t ReducedDimCount>
class InterpolationFitConstraint : public ITrackCandidateSoftConstraint
{
public:
using ReducedVector = math::Vector<ReducedDimCount, FloatType>;
using TrackVectorMap = std::unordered_map<db::TrackId, const ReducedVector*>;
explicit InterpolationFitConstraint(const TrackVectorMap& trackVectors)
: _trackVectors{ trackVectors }
{
}
~InterpolationFitConstraint() override = default;
InterpolationFitConstraint(const InterpolationFitConstraint&) = delete;
InterpolationFitConstraint& operator=(const InterpolationFitConstraint&) = delete;
float computeScore(const TrackCandidateContext& context) const override
{
const auto itCand{ _trackVectors.find(context.candidateTrackId) };
if (itCand == _trackVectors.cend())
return {};
float best{ std::numeric_limits<float>::max() };
for (const db::TrackId seedId : context.seedTrackIds)
{
const auto it{ _trackVectors.find(seedId) };
if (it != _trackVectors.cend())
best = std::min(best, math::computeNormalizedCosineDistance(*it->second, *itCand->second));
}
return (best == std::numeric_limits<float>::max()) ? 0.F : best;
}
private:
const TrackVectorMap& _trackVectors;
};
} // namespace lms::recommendation
@@ -0,0 +1,70 @@
/*
* 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 <unordered_map>
#include "database/objects/TrackId.hpp"
#include "math/NormalizedCosineDistance.hpp"
#include "math/Vector.hpp"
#include "Types.hpp"
#include "track-selection-constraints/ITrackCandidateHardConstraint.hpp"
namespace lms::recommendation
{
// Rejects any candidate whose distance to every seed track exceeds the threshold
template<std::size_t ReducedDimCount>
class MaxDistanceConstraint : public ITrackCandidateHardConstraint
{
public:
using ReducedVector = math::Vector<ReducedDimCount, FloatType>;
using TrackVectorMap = std::unordered_map<db::TrackId, const ReducedVector*>;
MaxDistanceConstraint(const TrackVectorMap& trackVectors, float threshold)
: _trackVectors{ trackVectors }
, _threshold{ threshold }
{
}
~MaxDistanceConstraint() override = default;
MaxDistanceConstraint(const MaxDistanceConstraint&) = delete;
MaxDistanceConstraint& operator=(const MaxDistanceConstraint&) = delete;
bool rejects(const TrackCandidateContext& context) const override
{
const auto itCand{ _trackVectors.find(context.candidateTrackId) };
if (itCand == _trackVectors.cend())
return false;
for (const db::TrackId seedId : context.seedTrackIds)
{
const auto it{ _trackVectors.find(seedId) };
if (it != _trackVectors.cend() && math::computeNormalizedCosineDistance(*it->second, *itCand->second) <= _threshold)
return false;
}
return !context.seedTrackIds.empty();
}
private:
const TrackVectorMap& _trackVectors;
float _threshold;
};
} // namespace lms::recommendation
@@ -0,0 +1,68 @@
/*
* 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 <unordered_map>
#include "database/objects/TrackId.hpp"
#include "math/NormalizedCosineDistance.hpp"
#include "math/Vector.hpp"
#include "Types.hpp"
#include "track-selection-constraints/ITrackCandidateSoftConstraint.hpp"
namespace lms::recommendation
{
template<std::size_t ReducedDimCount>
class SmoothTransitionConstraint : public ITrackCandidateSoftConstraint
{
public:
using ReducedVector = math::Vector<ReducedDimCount, FloatType>;
using TrackVectorMap = std::unordered_map<db::TrackId, const ReducedVector*>;
explicit SmoothTransitionConstraint(const TrackVectorMap& trackVectors)
: _trackVectors{ trackVectors }
{
}
~SmoothTransitionConstraint() override = default;
SmoothTransitionConstraint(const SmoothTransitionConstraint&) = delete;
SmoothTransitionConstraint& operator=(const SmoothTransitionConstraint&) = delete;
float computeScore(const TrackCandidateContext& context) const override
{
if (context.selectedTracks.empty())
return {};
const auto itCand{ _trackVectors.find(context.candidateTrackId) };
if (itCand == _trackVectors.cend())
return {};
const auto it{ _trackVectors.find(context.selectedTracks.back()) };
if (it == _trackVectors.cend())
return {};
return math::computeNormalizedCosineDistance(*it->second, *itCand->second);
}
private:
const TrackVectorMap& _trackVectors;
};
} // namespace lms::recommendation
@@ -278,6 +278,7 @@ namespace lms::recommendation
const TrackCandidateContext context{
.candidateTrackId = candidates[i],
.selectedTracks = selectedTracks,
.seedTrackIds = {},
};
if (_trackEvaluator.rejects(context))
@@ -1,34 +0,0 @@
/*
* 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 "ITrackCandidateSoftConstraint.hpp"
namespace lms::recommendation
{
class InterpolationFitConstraint : public ITrackCandidateSoftConstraint
{
public:
float computeScore(const TrackCandidateContext& context) const override
{
return context.distanceToQuery;
}
};
} // namespace lms::recommendation
@@ -1,43 +0,0 @@
/*
* 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"
namespace lms::recommendation
{
// Rejects any candidate whose distance to the query exceeds a given threshold
class MaxDistanceConstraint : public ITrackCandidateHardConstraint
{
public:
explicit MaxDistanceConstraint(float threshold)
: _threshold{ threshold }
{
}
bool rejects(const TrackCandidateContext& context) const override
{
return context.distanceToQuery > _threshold;
}
private:
float _threshold;
};
} // namespace lms::recommendation
@@ -1,34 +0,0 @@
/*
* 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 "ITrackCandidateSoftConstraint.hpp"
namespace lms::recommendation
{
class SmoothTransitionConstraint : public ITrackCandidateSoftConstraint
{
public:
float computeScore(const TrackCandidateContext& context) const override
{
return context.distanceToPrevious;
}
};
} // namespace lms::recommendation
@@ -29,7 +29,6 @@ namespace lms::recommendation
{
db::TrackId candidateTrackId;
std::span<const db::TrackId> selectedTracks;
float distanceToQuery{};
float distanceToPrevious{};
std::span<const db::TrackId> seedTrackIds;
};
} // namespace lms::recommendation
@@ -24,7 +24,10 @@
#include "database/objects/TrackId.hpp"
#include "math/Vector.hpp"
#include "audio-similarity/InterpolationFitConstraint.hpp"
#include "audio-similarity/MaxDistanceConstraint.hpp"
#include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp"
#include "audio-similarity/SmoothTransitionConstraint.hpp"
#include "track-selection-constraints/DuplicateTrackConstraint.hpp"
#include "track-selection-constraints/SameArtistConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp"
@@ -41,7 +44,6 @@ namespace
const db::TrackId T2{ 2 };
const db::TrackId T3{ 3 };
const db::TrackId T4{ 4 };
const db::TrackId T5{ 5 };
const db::ArtistId A1{ 10 };
const db::ArtistId A2{ 20 };
@@ -53,20 +55,20 @@ namespace
TEST(DuplicateTrackConstraint, acceptsNewCandidate)
{
const std::vector<db::TrackId> selected{ T1, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
}
TEST(DuplicateTrackConstraint, rejectsAlreadySelected)
{
const std::vector<db::TrackId> selected{ T1, T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx));
}
TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty)
{
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
}
@@ -78,7 +80,7 @@ TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist)
};
const SameArtistConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
@@ -90,7 +92,7 @@ TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist)
};
const SameArtistConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
}
@@ -105,7 +107,7 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist)
const SameArtistConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
}
@@ -113,23 +115,15 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist)
TEST(SameArtistConstraint, trackOutsideWindowIsIgnored)
{
TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = { A1 } } },
{ T2, { .releaseId = {}, .artistIds = { A2 } } },
{ T3, { .releaseId = {}, .artistIds = { A2 } } },
{ T4, { .releaseId = {}, .artistIds = { A2 } } },
{ T5, { .releaseId = {}, .artistIds = { A1 } } }, // outside window=4
};
const SameArtistConstraint constraint{ meta, /*window=*/4 };
TrackMetadataMap meta2{
{ T1, { .releaseId = {}, .artistIds = { A1 } } },
{ T2, { .releaseId = {}, .artistIds = { A2 } } },
{ T3, { .releaseId = {}, .artistIds = { A1 } } },
};
const SameArtistConstraint constraint2{ meta2, /*window=*/1 };
const SameArtistConstraint constraint{ meta, /*window=*/1 };
const std::vector<db::TrackId> selected{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
EXPECT_FLOAT_EQ(constraint2.computeScore(ctx), 0.F);
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap)
@@ -137,7 +131,7 @@ TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap)
const TrackMetadataMap meta{};
const SameArtistConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
@@ -149,7 +143,7 @@ TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease)
};
const SameReleaseConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
@@ -161,7 +155,7 @@ TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease)
};
const SameReleaseConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
}
@@ -173,7 +167,7 @@ TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease)
};
const SameReleaseConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
@@ -183,14 +177,14 @@ TEST(TrackCandidateEvaluator, hardConstraintRejects)
evaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
const std::vector<db::TrackId> selected{ T1 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(evaluator.rejects(ctx));
}
TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject)
{
TrackCandidateEvaluator evaluator;
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(evaluator.rejects(ctx));
}
@@ -204,7 +198,7 @@ TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted)
evaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(meta), 2.F);
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
}
@@ -220,7 +214,7 @@ TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated)
evaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(meta), 1.F);
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
}
@@ -236,7 +230,7 @@ TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints)
evaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(meta), 1.F);
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(evaluator.rejects(ctx));
EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F);
}
@@ -256,7 +250,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap)
const TestVectorMap trackVectors{ { T1, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T1 };
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
@@ -264,7 +258,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty)
{
const TestVectorMap trackVectors{ { T1, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
@@ -273,7 +267,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap)
const TestVectorMap trackVectors{ { T1, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
@@ -282,7 +276,7 @@ TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold)
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(constraint.rejects(ctx));
}
@@ -291,7 +285,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold)
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
@@ -300,7 +294,7 @@ TEST(NearDuplicateEmbeddingConstraint, rejectsWhenOneOfManySelectedIsNearDuplica
const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } };
const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T3, T4 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(constraint.rejects(ctx));
}
@@ -309,6 +303,111 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenAllSelectedAreFarEnough)
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } };
const TestConstraint constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> selected{ T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(InterpolationFitConstraint, zeroScoreWhenNoSeeds)
{
const TestVectorMap trackVectors{ { T1, &v_x } };
const InterpolationFitConstraint<2> constraint{ trackVectors };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
TEST(InterpolationFitConstraint, zeroScoreWhenCandidateMatchesSeed)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const InterpolationFitConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
TEST(InterpolationFitConstraint, halfScoreWhenCandidateOrthogonalToSeed)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const InterpolationFitConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
}
TEST(InterpolationFitConstraint, usesClosestSeed)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } };
const InterpolationFitConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> seeds{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
TEST(MaxDistanceConstraint, acceptsWhenNoSeeds)
{
const TestVectorMap trackVectors{ { T1, &v_x } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(MaxDistanceConstraint, acceptsWhenWithinThresholdOfOneSeed)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(MaxDistanceConstraint, rejectsWhenBeyondThresholdFromAllSeeds)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.1F };
const std::vector<db::TrackId> seeds{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_TRUE(constraint.rejects(ctx));
}
TEST(MaxDistanceConstraint, acceptsWhenAnyOfManySeedsIsCloseEnough)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_x } };
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
const std::vector<db::TrackId> seeds{ T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SmoothTransitionConstraint, zeroScoreWhenNoSelectedTracks)
{
const TestVectorMap trackVectors{ { T1, &v_x } };
const SmoothTransitionConstraint<2> constraint{ trackVectors };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
TEST(SmoothTransitionConstraint, zeroScoreWhenPreviousMatchesCandidate)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
const SmoothTransitionConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}
TEST(SmoothTransitionConstraint, halfScoreWhenPreviousOrthogonalToCandidate)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
const SmoothTransitionConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
}
TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack)
{
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } };
const SmoothTransitionConstraint<2> constraint{ trackVectors };
const std::vector<db::TrackId> selected{ T3, T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
}