Skip to content

Commit

Permalink
Merge pull request #42 from pavel-zhur/feature/model-of-everything
Browse files Browse the repository at this point in the history
Feature/model of everything
  • Loading branch information
pavel-zhur authored Jul 21, 2024
2 parents 63c3906 + afeab7c commit 66bb8af
Show file tree
Hide file tree
Showing 51 changed files with 2,131 additions and 90 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/unit tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

name: unit tests

# Triggers the workflow on push or pull request events
on: [push, pull_request]
# Triggers the workflow on push to any branch, but not a tag
# https://stackoverflow.com/questions/68573888/how-can-i-not-execute-a-github-action-when-i-push-a-new-tag
on:
push:
branches:
- '**'

jobs:
build:
Expand Down
18 changes: 17 additions & 1 deletion HarmonyDB and OneShelf.sln
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HarmonyDB.Index.LongBalance
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HarmonyDB.Index.Analysis.Em", "HarmonyDB.Index\HarmonyDB.Index.Analysis.Em\HarmonyDB.Index.Analysis.Em.csproj", "{33A0E860-62DF-41C6-8E25-442FA2BC625F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneShelf.Common.Api.Common", "OneShelf.Common\OneShelf.Common.Api.Common\OneShelf.Common.Api.Common.csproj", "{C918C6DF-8C9B-4EB9-B509-58E0CC774771}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneShelf.Common.Api.Common", "OneShelf.Common\OneShelf.Common.Api.Common\OneShelf.Common.Api.Common.csproj", "{C918C6DF-8C9B-4EB9-B509-58E0CC774771}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HarmonyDB.Experimental", "HarmonyDB.Experimental", "{9D5C2940-DDAC-4598-8DE1-E731C8BC03A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HarmonyDB.Experimental.ShortestStructureFinder", "HarmonyDB.Experimental\HarmonyDB.Experimental.ShortestStructureFinder\HarmonyDB.Experimental.ShortestStructureFinder.csproj", "{AB7421D9-7C2C-4AA8-83E3-C066F3EBCE4B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HarmonyDB.Experimental.ExpectationMaximization", "HarmonyDB.Experimental\HarmonyDB.Experimental.ExpectationMaximization\HarmonyDB.Experimental.ExpectationMaximization.csproj", "{8D028303-370C-432D-BCBB-2DD13D88F49A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -445,6 +451,14 @@ Global
{C918C6DF-8C9B-4EB9-B509-58E0CC774771}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C918C6DF-8C9B-4EB9-B509-58E0CC774771}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C918C6DF-8C9B-4EB9-B509-58E0CC774771}.Release|Any CPU.Build.0 = Release|Any CPU
{AB7421D9-7C2C-4AA8-83E3-C066F3EBCE4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB7421D9-7C2C-4AA8-83E3-C066F3EBCE4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB7421D9-7C2C-4AA8-83E3-C066F3EBCE4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB7421D9-7C2C-4AA8-83E3-C066F3EBCE4B}.Release|Any CPU.Build.0 = Release|Any CPU
{8D028303-370C-432D-BCBB-2DD13D88F49A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D028303-370C-432D-BCBB-2DD13D88F49A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D028303-370C-432D-BCBB-2DD13D88F49A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D028303-370C-432D-BCBB-2DD13D88F49A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -516,6 +530,8 @@ Global
{59DD2309-74F4-4DBC-B98C-BB4E93AB08C2} = {7548FF8E-E1B9-4939-96E9-03DBD5236709}
{33A0E860-62DF-41C6-8E25-442FA2BC625F} = {7548FF8E-E1B9-4939-96E9-03DBD5236709}
{C918C6DF-8C9B-4EB9-B509-58E0CC774771} = {2AD8DB5A-A1C3-4116-B8C5-D8DA505B1CC5}
{AB7421D9-7C2C-4AA8-83E3-C066F3EBCE4B} = {9D5C2940-DDAC-4598-8DE1-E731C8BC03A8}
{8D028303-370C-432D-BCBB-2DD13D88F49A} = {9D5C2940-DDAC-4598-8DE1-E731C8BC03A8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {59093261-FDDA-411A-852D-EA21AEF83E07}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
public static class Constants
{
public const int TonicCount = 12; // Number of different tonics
public const int ScaleCount = 2; // Major and Minor

public static Scale GetParallelScale(Scale scale)
{
return scale == Scale.Major ? Scale.Minor : Scale.Major;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public enum Scale
{
Major,
Minor
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
public interface ISource
{
string Id { get; set; }
double[,] TonalityProbabilities { get; set; } // [TonicCount, ScaleCount]
(double TonicScore, double ScaleScore) Score { get; set; }
}

public interface ISong : ISource
{
bool IsTonalityKnown { get; set; }
(int Tonic, Scale Scale) KnownTonality { get; set; }
}

public interface ILoop : ISource
{
int SongCount { get; set; }
}

public class Song : ISong
{
public string Id { get; set; }
public double[,] TonalityProbabilities { get; set; } = new double[Constants.TonicCount, Constants.ScaleCount];
public (double TonicScore, double ScaleScore) Score { get; set; } = (1.0, 1.0);
public bool IsTonalityKnown { get; set; }
public (int Tonic, Scale Scale) KnownTonality { get; set; }
public (int Tonic, Scale Scale)[] SecretTonalities { get; set; }
public bool IsKnownTonalityIncorrect { get; set; }
}

public class Loop : ILoop
{
public string Id { get; set; }
public double[,] TonalityProbabilities { get; set; } = new double[Constants.TonicCount, Constants.ScaleCount];
public (double TonicScore, double ScaleScore) Score { get; set; } = (1.0, 1.0);
public int SongCount { get; set; } = 0;
public (int Tonic, Scale Scale)[] SecretTonalities { get; set; }
}

public class LoopLink
{
public string SongId { get; set; }
public string LoopId { get; set; }
public int Shift { get; set; }
public int Count { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
public class MusicAnalyzer
{
private readonly Dictionary<string, ISong> _songs;
private readonly Dictionary<string, ILoop> _loops;
private readonly List<LoopLink> _loopLinks;

public MusicAnalyzer(Dictionary<string, ISong> songs, Dictionary<string, ILoop> loops, List<LoopLink> loopLinks)
{
_songs = songs;
_loops = loops;
_loopLinks = loopLinks;
InitializeProbabilities();
}

private void InitializeProbabilities()
{
foreach (var song in _songs.Values)
{
if (song.IsTonalityKnown)
{
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
song.TonalityProbabilities[i, j] = (i == song.KnownTonality.Item1 && j == (int)song.KnownTonality.Item2) ? 1.0 : 0.0;
}
}
}
else
{
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
song.TonalityProbabilities[i, j] = 1.0 / (Constants.TonicCount * Constants.ScaleCount);
}
}
}
}

foreach (var loop in _loops.Values)
{
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
loop.TonalityProbabilities[i, j] = 1.0 / (Constants.TonicCount * Constants.ScaleCount);
}
}
}

// Calculate the number of distinct songs for each loop
foreach (var loop in _loops.Values)
{
loop.SongCount = _loopLinks.Where(l => l.LoopId == loop.Id).Select(l => l.SongId).Distinct().Count();
}
}

public void UpdateProbabilities()
{
const double tolerance = 0.01;
const int stabilityCheckWindow = 5; // Number of iterations to check for stability
const double stabilityThreshold = 1e-6; // Threshold for considering changes as stable
var recentMaxChanges = new List<double>();
var hasConverged = false;
var iterationCount = 0;

while (!hasConverged)
{
iterationCount++;
var maxChange = 0.0;

Parallel.ForEach(_songs.Values, song =>
{
if (!song.IsTonalityKnown)
{
var newProbabilities = CalculateProbabilities(song.Id, true);
var change = CalculateMaxChange(song.TonalityProbabilities, newProbabilities);
lock (song)
{
maxChange = Math.Max(maxChange, change);
song.TonalityProbabilities = newProbabilities;
}
}
song.Score = CalculateEntropy(song.Id, true);
});

Parallel.ForEach(_loops.Values, loop =>
{
var newProbabilities = CalculateProbabilities(loop.Id, false);
var change = CalculateMaxChange(loop.TonalityProbabilities, newProbabilities);
lock (loop)
{
maxChange = Math.Max(maxChange, change);
loop.TonalityProbabilities = newProbabilities;
}
loop.Score = CalculateEntropy(loop.Id, false);
});

recentMaxChanges.Add(maxChange);
if (recentMaxChanges.Count > stabilityCheckWindow)
{
recentMaxChanges.RemoveAt(0);
}

if (recentMaxChanges.Count == stabilityCheckWindow)
{
var minChange = recentMaxChanges.Min();
var maxInWindow = recentMaxChanges.Max();

if (maxInWindow - minChange < stabilityThreshold)
{
throw new("Algorithm is oscillating without converging.");
}
}

if (maxChange < tolerance)
{
hasConverged = true;
}

Console.WriteLine($"Iteration {iterationCount}, Max Change: {maxChange:F6}");
}

Console.WriteLine("Converged after " + iterationCount + " iterations.");
}

public double[,] CalculateProbabilities(string id, bool isSong)
{
var newProbabilities = new double[Constants.TonicCount, Constants.ScaleCount];
var relevantLinks = isSong ? _loopLinks.Where(l => l.SongId == id) : _loopLinks.Where(l => l.LoopId == id);

foreach (var link in relevantLinks)
{
var adjustedTonic = (link.Shift + Constants.TonicCount) % Constants.TonicCount;
var sourceProbabilities = isSong ? _loops[link.LoopId].TonalityProbabilities : _songs[link.SongId].TonalityProbabilities;
var sourceScore = isSong ? _loops[link.LoopId].Score : _songs[link.SongId].Score;

for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
var targetTonic = isSong ? (i - adjustedTonic + Constants.TonicCount) % Constants.TonicCount : (adjustedTonic + i) % Constants.TonicCount;
newProbabilities[targetTonic, j] += sourceProbabilities[i, j] * sourceScore.TonicScore * link.Count;
}
}
}

NormalizeProbabilities(newProbabilities);
return newProbabilities;
}

private (double TonicScore, double ScaleScore) CalculateEntropy(string id, bool isSong)
{
var relevantLinks = isSong ? _loopLinks.Where(l => l.SongId == id) : _loopLinks.Where(l => l.LoopId == id);
var shiftCounts = relevantLinks.GroupBy(l => GetRelativeShift(l, isSong)).ToDictionary(g => g.Key, g => g.Sum(l => l.Count));
double totalLinks = relevantLinks.Sum(l => l.Count);

var tonicEntropy = shiftCounts.Values.Select(count => (count / totalLinks) * Math.Log(count / totalLinks)).Sum() * -1;

var scaleProbabilities = new double[Constants.TonicCount, Constants.ScaleCount];
foreach (var link in relevantLinks)
{
var adjustedTonic = (link.Shift + Constants.TonicCount) % Constants.TonicCount;
var sourceProbabilities = isSong ? _loops[link.LoopId].TonalityProbabilities : _songs[link.SongId].TonalityProbabilities;
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
var targetTonic = isSong ? (i - adjustedTonic + Constants.TonicCount) % Constants.TonicCount : (adjustedTonic + i) % Constants.TonicCount;
scaleProbabilities[targetTonic, j] += sourceProbabilities[i, j];
}
}
}

var totalScaleLinks = scaleProbabilities.Cast<double>().Sum();
var scaleEntropy = scaleProbabilities.Cast<double>().Where(p => p > 0).Select(p => (p / totalScaleLinks) * Math.Log(p / totalScaleLinks)).Sum() * -1;

return (Math.Exp(-tonicEntropy), Math.Exp(-scaleEntropy));
}

private int GetRelativeShift(LoopLink loopLink, bool isSong)
{
var shift = loopLink.Shift;

if (isSong)
{
var loopTonic = GetPredictedTonality(_loops[loopLink.LoopId].TonalityProbabilities).Item1;
return (loopTonic - shift + Constants.TonicCount) % Constants.TonicCount;
}
else
{
var songTonic = GetPredictedTonality(_songs[loopLink.SongId].TonalityProbabilities).Item1;
return (songTonic + shift) % Constants.TonicCount;
}
}

private double CalculateMaxChange(double[,] oldProbabilities, double[,] newProbabilities)
{
var maxChange = 0.0;
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
maxChange = Math.Max(maxChange, Math.Abs(oldProbabilities[i, j] - newProbabilities[i, j]));
}
}
return maxChange;
}

private void NormalizeProbabilities(double[,] probabilities)
{
double sum = 0;
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
sum += probabilities[i, j];
}
}
if (sum == 0) return;

for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
probabilities[i, j] /= sum;
}
}
}

public static (int, Scale) GetPredictedTonality(double[,] probabilities)
{
var maxProbability = double.MinValue;
var maxIndices = new List<(int, Scale)>();
for (var i = 0; i < Constants.TonicCount; i++)
{
for (var j = 0; j < Constants.ScaleCount; j++)
{
if (probabilities[i, j] > maxProbability)
{
maxProbability = probabilities[i, j];
maxIndices.Clear();
maxIndices.Add((i, (Scale)j));
}
else if (probabilities[i, j] == maxProbability)
{
maxIndices.Add((i, (Scale)j));
}
}
}
var random = new Random();
return maxIndices[random.Next(maxIndices.Count)];
}
}
Loading

0 comments on commit 66bb8af

Please sign in to comment.