Skip to content

Commit

Permalink
Verify solutions using libverify (from omsim)
Browse files Browse the repository at this point in the history
  • Loading branch information
gtw123 committed May 17, 2024
1 parent fd44354 commit d0fe549
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 7 deletions.
23 changes: 22 additions & 1 deletion OpusSolver/IO/SolutionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void WriteSolution()
m_writer.Write(7); // Solution format
m_writer.Write(m_solution.Puzzle.FileName);
m_writer.Write(m_solution.Name);
m_writer.Write(0); // TODO: Write metrics
WriteMetrics(m_solution.Metrics);

IEnumerable<GameObject> realObjects = m_solution.GetObjects<Glyph>();
realObjects = realObjects.Concat(m_solution.GetObjects<Mechanism>())
Expand All @@ -56,6 +56,27 @@ public void WriteSolution()
}
}

private void WriteMetrics(Metrics metrics)
{
if (metrics == null)
{
m_writer.Write(0);
return;
}

// Number of metrics
m_writer.Write(4);

m_writer.Write(0);
m_writer.Write(metrics.Cycles);
m_writer.Write(1);
m_writer.Write(metrics.Cost);
m_writer.Write(2);
m_writer.Write(metrics.Area);
m_writer.Write(3);
m_writer.Write(metrics.Instructions);
}

private void WriteObject(GameObject obj)
{
m_writer.Write(GetObjectName(obj));
Expand Down
4 changes: 4 additions & 0 deletions OpusSolver/OpusSolver.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<Compile Include="IO\SolutionWriter.cs" />
<Compile Include="IO\PuzzleReader.cs" />
<Compile Include="IO\ParseException.cs" />
<Compile Include="Solution\Metrics.cs" />
<Compile Include="Solver\AtomGenerator.cs" />
<Compile Include="Solver\AtomGenerators\Input\SimpleInputArea.cs" />
<Compile Include="Solver\AtomGenerators\Input\TrivialInputArea.cs" />
Expand Down Expand Up @@ -132,6 +133,9 @@
<Compile Include="Utils\Vector2.cs" />
<Compile Include="ProgramMain.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Verifier\NativeMethods.cs" />
<Compile Include="Verifier\VerifierException.cs" />
<Compile Include="Verifier\SolutionVerifier.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config">
Expand Down
41 changes: 35 additions & 6 deletions OpusSolver/ProgramMain.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using OpusSolver.IO;
using OpusSolver.Solver;
using OpusSolver.Verifier;
using System;
using System.Collections.Generic;
using System.IO;
Expand All @@ -16,10 +17,13 @@ private class CommandLineArguments
{
public List<string> PuzzleFiles = new();
public string OutputDir;
public bool SkipVerification = false;
}

public static int Main(string[] args)
{
sm_log.Debug($"Starting up");

CommandLineArguments commandArgs;
try
{
Expand All @@ -38,13 +42,14 @@ public static int Main(string[] args)
foreach (var puzzleFile in commandArgs.PuzzleFiles)
{
string solutionFile = Path.Combine(commandArgs.OutputDir, Path.GetFileNameWithoutExtension(puzzleFile) + ".solution");
if (SolvePuzzle(puzzleFile, solutionFile))
if (SolvePuzzle(puzzleFile, solutionFile, commandArgs.SkipVerification))
{
totalPuzzlesSolved++;
}
}

Console.WriteLine($"Generated solutions for {totalPuzzlesSolved}/{commandArgs.PuzzleFiles.Count} puzzles.");
string verifyMessage = commandArgs.SkipVerification ? "" : "and verified ";
sm_log.Info($"Successfully generated {verifyMessage}solutions for {totalPuzzlesSolved}/{commandArgs.PuzzleFiles.Count} puzzles.");
}
catch (Exception e)
{
Expand Down Expand Up @@ -86,6 +91,9 @@ private static CommandLineArguments ParseArguments(string[] args)
excludedFiles.Add(args[++i]);
break;
}
case "--noverify":
commandArgs.SkipVerification = true;
break;
default:
puzzlePaths.Add(args[i]);
break;
Expand Down Expand Up @@ -127,11 +135,12 @@ private static void ShowUsage()
sm_log.Error("Usage: OpusSolver.exe [<options>] <puzzle file/dir>...");
sm_log.Error("");
sm_log.Error("Options:");
sm_log.Error(" --output Directory to write solutions to (default is current dir)");
sm_log.Error(" --exclude Name of a puzzle file to skip");
sm_log.Error(" --output Directory to write solutions to (default is current dir)");
sm_log.Error(" --exclude Name of a puzzle file to skip");
sm_log.Error(" --noverify Skip solution verification (useful if you don't have a copy of libverify)");
}

private static bool SolvePuzzle(string puzzleFile, string solutionFile)
private static bool SolvePuzzle(string puzzleFile, string solutionFile, bool skipVerification)
{
try
{
Expand All @@ -144,8 +153,23 @@ private static bool SolvePuzzle(string puzzleFile, string solutionFile)
var solution = solver.Solve();

sm_log.Info($"Writing solution to \"{solutionFile}\"");
// It might be more efficient to write the solution to a byte array first and pass that to the verifier,
// rather than writing it to disk twice. But it's also convenient having a copy on disk even if the
// verification fails so that we can debug it in the game.
SolutionWriter.WriteSolution(solution, solutionFile);

if (!skipVerification)
{
sm_log.Debug("Verifying solution");
using var verifier = new SolutionVerifier(puzzleFile, solutionFile);
var metrics = verifier.CalculateMetrics();
sm_log.Info($"Cost/cycles/area/instructions: {metrics.Cost}/{metrics.Cycles}/{metrics.Area}/{metrics.Instructions}");

sm_log.Debug($"Writing metrics to solution file");
solution.Metrics = metrics;
SolutionWriter.WriteSolution(solution, solutionFile);
}

return true;
}
catch (ParseException e)
Expand All @@ -155,7 +179,12 @@ private static bool SolvePuzzle(string puzzleFile, string solutionFile)
}
catch (SolverException e)
{
sm_log.Error($"Unable to solve puzzle \"{puzzleFile}\": {e.Message}");
sm_log.Error($"Error solving puzzle \"{puzzleFile}\": {e.Message}");
return false;
}
catch (VerifierException e)
{
sm_log.Error($"Error verifying solution to puzzle \"{puzzleFile}\": {e.Message}");
return false;
}
catch (Exception e)
Expand Down
10 changes: 10 additions & 0 deletions OpusSolver/Solution/Metrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace OpusSolver
{
public class Metrics
{
public int Cost { get; set; }
public int Cycles { get; set; }
public int Area { get; set; }
public int Instructions { get; set; }
}
}
2 changes: 2 additions & 0 deletions OpusSolver/Solution/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class Solution
public List<GameObject> Objects { get; private set; }
public Program Program { get; private set; }

public Metrics Metrics { get; set; }

public Solution(Puzzle puzzle, IEnumerable<GameObject> objects, Program program)
{
Puzzle = puzzle;
Expand Down
42 changes: 42 additions & 0 deletions OpusSolver/Verifier/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Runtime.InteropServices;

namespace OpusSolver.Verifier
{
internal static class NativeMethods
{
[DllImport("libverify")]
public static extern IntPtr verifier_create(string puzzle_filename, string solution_filename);

[DllImport("libverify")]
public static extern void verifier_destroy(IntPtr verifier);

[DllImport("libverify")]
private static extern IntPtr verifier_error(IntPtr verifier);

/// <summary>
/// Wrapper function for calling verifier_error safely. This is required because verifier_error
/// returns a const char* which should not be freed. If we simply declare it as returning a string
/// then the default .NET marshalling will attempt to free the returned pointer, crashing the program.
/// </summary>
public static string GetVerifierError(IntPtr verifier)
{
return Marshal.PtrToStringAnsi(verifier_error(verifier));
}

[DllImport("libverify")]
public static extern int verifier_error_cycle(IntPtr verifier);

[DllImport("libverify")]
public static extern int verifier_error_location_u(IntPtr verifier);

[DllImport("libverify")]
public static extern int verifier_error_location_v(IntPtr verifier);

[DllImport("libverify")]
public static extern void verifier_error_clear(IntPtr verifier);

[DllImport("libverify")]
public static extern int verifier_evaluate_metric(IntPtr verifier, string metric);
}
}
47 changes: 47 additions & 0 deletions OpusSolver/Verifier/SolutionVerifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;

namespace OpusSolver.Verifier
{
public class SolutionVerifier : IDisposable
{
private IntPtr m_verifier;

public SolutionVerifier(string puzzleFile, string solutionFile)
{
m_verifier = NativeMethods.verifier_create(puzzleFile, solutionFile);
CheckForError();
}

private int GetMetric(string metricName)
{
int metric = NativeMethods.verifier_evaluate_metric(m_verifier, metricName);
CheckForError(includeCycleAndLocation: true);
return metric;
}

public Metrics CalculateMetrics() => new Metrics
{
Cost = GetMetric("cost"),
Cycles = GetMetric("cycles"),
Area = GetMetric("area"),
Instructions = GetMetric("instructions"),
};

public void Dispose()
{
if (m_verifier != IntPtr.Zero)
{
NativeMethods.verifier_destroy(m_verifier);
m_verifier = IntPtr.Zero;
}
}

private void CheckForError(bool includeCycleAndLocation = false)
{
if (NativeMethods.GetVerifierError(m_verifier) != null)
{
throw new VerifierException(m_verifier, includeCycleAndLocation);
}
}
}
}
43 changes: 43 additions & 0 deletions OpusSolver/Verifier/VerifierException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Text;

namespace OpusSolver.Verifier
{
public class VerifierException : Exception
{
public int? Cycle { get; set; }
public Vector2? Location { get; set; }

public VerifierException(IntPtr verifier, bool includeCycleAndLocation)
: base(NativeMethods.GetVerifierError(verifier))
{
// These don't make sense for some types of errors (e.g. failing to load a puzzle) so only
// get them if requested
if (includeCycleAndLocation)
{
Cycle = NativeMethods.verifier_error_cycle(verifier);
Location = new Vector2(NativeMethods.verifier_error_location_u(verifier), NativeMethods.verifier_error_location_v(verifier));
}

NativeMethods.verifier_error_clear(verifier);
}

public override string Message
{
get
{
var message = new StringBuilder(base.Message);
if (Cycle.HasValue)
{
message.Append($" on cycle {Cycle}");
}
if (Location.HasValue)
{
message.Append($" at {Location}");
}

return message.ToString();
}
}
}
}

0 comments on commit d0fe549

Please sign in to comment.