diff --git a/InverseOfLife.csproj b/InverseOfLife.csproj
index b55ac43..b1b52e4 100644
--- a/InverseOfLife.csproj
+++ b/InverseOfLife.csproj
@@ -2,10 +2,18 @@
net8.0
+ linux-x64
enable
disable
true
0.0.2
+ true
+
+
+
+
+
+
diff --git a/src/Board.cs b/src/Board.cs
index d245af4..5779c15 100644
--- a/src/Board.cs
+++ b/src/Board.cs
@@ -4,6 +4,7 @@ namespace InverseOfLife;
public class Board
{
+
[SetsRequiredMembers]
public Board(int w, int h, bool qx = false, bool qy = false, bool useTracer = false)
{
@@ -127,8 +128,26 @@ public class Board
return builder.ToString();
}
-
-
+
+ public HashSet<(int, int)>[] Frames(int steps)
+ {
+ HashSet<(int, int)>[] res = new HashSet<(int, int)>[steps];
+ for (int i = 0; i < steps; i++)
+ {
+ Evaluate();
+ res[i] = CopyLives();
+ }
+ return res;
+ }
+
+ private HashSet<(int, int)> CopyLives()
+ {
+ HashSet<(int, int)> res = new();
+ foreach ((int, int) cell in Lives)
+ res.Add(cell);
+ return res;
+ }
+
public void Play(int generations, int delay = 200)
{
for (int i = 0; i < generations; i++)
diff --git a/src/Generator.cs b/src/Generator.cs
index ccd5cc2..985997d 100644
--- a/src/Generator.cs
+++ b/src/Generator.cs
@@ -32,7 +32,7 @@ public class Generator
{
if (idx > layers * split)
break;
- (Gene g, double x) = s.Solve(mode: mode);
+ (Gene g, double x) = s.Solve(2, 10, 20, 0.2f, mode: mode);
res[idx] = g.Restore(target.Width, target.Height, target.QuotientX, target.QuotientY);
idx += 1;
Console.WriteLine($"Progress: {idx}/{layers * split}");
@@ -45,13 +45,13 @@ public class Generator
result[i] = new Dictionary<(int, int), float>();
foreach (Board b in res)
{
+ b.Evaluate();
foreach ((int, int) cell in b.Lives)
{
if(!result[i].Keys.Contains(cell))
result[i][cell] = 0f;
result[i][cell] += 1f/(layers * split);
}
- b.Evaluate();
}
}
diff --git a/src/NeuralSolver.cs b/src/NeuralSolver.cs
new file mode 100644
index 0000000..5944012
--- /dev/null
+++ b/src/NeuralSolver.cs
@@ -0,0 +1,198 @@
+using Tensorflow;
+using Tensorflow.Gradients;
+using Tensorflow.Keras.Engine;
+using Tensorflow.NumPy;
+using static Tensorflow.Binding;
+using static Tensorflow.KerasApi;
+namespace InverseOfLife;
+
+public class NeuralSolver
+{
+ private int Width { get; set; }
+ private int Height { get; set; }
+ private int Steps { get; set; }
+ private bool QuotientX { get; set; }
+ private bool QuotientY { get; set; }
+ private IOptimizer Optimizer { get; set; }
+
+ private IModel ForwardModel { get; set; }
+ private IModel ReverseModel { get; set; }
+
+ private void BuildForwardModel()
+ {
+ Tensors inputs = keras.Input(shape: new Shape(Height, Width, 1), name: "InitialState");
+ Tensors hidden = keras.layers.Conv2D(32, kernel_size: 3, padding:"same", activation: keras.activations.Relu).Apply(inputs);
+ hidden = keras.layers.Conv2D(32, kernel_size: 3, padding: "same", activation: keras.activations.Relu).Apply(hidden);
+ Tensors outputs = keras.layers.Conv2D(1, kernel_size: 1, padding: "same", activation: keras.activations.Sigmoid).Apply(hidden);
+ ForwardModel = keras.Model(inputs, outputs, name: "ForwardModel");
+ }
+
+ private void BuildReverseModel()
+ {
+ Tensors inputs = keras.Input(shape: new Shape(Height, Width, 1), name: "FinalState");
+ Tensors hidden = keras.layers.Conv2D(32, kernel_size: 3, padding: "same", activation: keras.activations.Relu).Apply(inputs);
+ hidden = keras.layers.Conv2D(32, kernel_size:3, padding:"same", activation: keras.activations.Relu).Apply(hidden);
+ Tensors outputs = keras.layers.Conv2D(1, kernel_size: 1, padding:"same", activation: keras.activations.Sigmoid).Apply(hidden);
+ ReverseModel = keras.Model(inputs, outputs, name: "ReverseModel");
+ }
+
+ public NeuralSolver(int width, int height, int steps, bool quotientX, bool quotientY)
+ {
+ Width = width;
+ Height = height;
+ Steps = steps;
+ QuotientX = quotientX;
+ QuotientY = quotientY;
+ BuildForwardModel();
+ BuildReverseModel();
+ }
+
+ public void SaveModel(string basePath)
+ {
+ ForwardModel.save($"{basePath}/FM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}");
+ ReverseModel.save($"{basePath}/RM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}");
+ }
+ public void LoadModel(string basePath)
+ {
+ ForwardModel = keras.models.load_model($"{basePath}/FM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}");
+ ReverseModel = keras.models.load_model($"{basePath}/RM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}");
+ }
+ public (NDArray, NDArray) GenerateTrainingData(int datasetSize)
+ {
+ Random rnd = new Random();
+
+ float[] inputsData = new float[datasetSize * Height * Width];
+ float[] labelsData = new float[datasetSize * Height * Width];
+
+ for (int idx = 0; idx < datasetSize; idx++)
+ {
+ Board board = new Board(Width, Height, QuotientX, QuotientY);
+ int randomCells = rnd.Next(1, Width * Height / 4);
+ for (int c = 0; c < randomCells; c++)
+ {
+ int x = rnd.Next(0, Width);
+ int y = rnd.Next(0, Height);
+ board.Toggle(x, y, true);
+ }
+
+ int offsetLabel = idx * Width * Height;
+ foreach ( (int x, int y) in board.Lives)
+ {
+ int pos = y * Width + x;
+ labelsData[offsetLabel + pos] = 1f;
+ }
+
+ board.Evaluate(Steps);
+
+ int offsetInput = idx * Width * Height;
+ foreach (var (x, y) in board.Lives)
+ inputsData[offsetInput + (y * Width + x)] = 1f;
+ }
+ NDArray inputsTensor = np.array(inputsData).reshape(new Shape(datasetSize, Height, Width, 1));
+ NDArray labelsTensor = np.array(labelsData).reshape(new Shape(datasetSize, Height, Width, 1));
+ return (inputsTensor, labelsTensor);
+ }
+
+ public void Train(int datasetSize = 1000, int batchSize = 8, int epochs = 10)
+ {
+ (NDArray trainFinal, NDArray trainInitial) = GenerateTrainingData(datasetSize);
+ Optimizer = keras.optimizers.Adam(learning_rate: 0.001f);
+ for (int epoch = 0; epoch < epochs; epoch++)
+ {
+ for (int i = 0; i < datasetSize; i += batchSize)
+ {
+ NDArray initialBatch = trainInitial[$"{i}:{i + batchSize}"];
+ NDArray finalBatch = trainFinal[$"{i}:{i + batchSize}"];
+ using (GradientTape tape = tf.GradientTape())
+ {
+ Tensors predictedFinal = ForwardModel.Apply(initialBatch);
+ Tensors predictedInitial = ReverseModel.Apply(finalBatch);
+ Tensors reconstructedFinal = ForwardModel.Apply(predictedInitial);
+
+ Tensor forwardLoss = keras.losses.BinaryCrossentropy().Call(finalBatch, predictedFinal);
+ Tensor reverseLoss = keras.losses.BinaryCrossentropy().Call(initialBatch, predictedInitial);
+ Tensor cycleLoss = keras.losses.BinaryCrossentropy().Call(finalBatch, reconstructedFinal);
+ Tensor totalLoss = forwardLoss + reverseLoss + cycleLoss;
+ Tensor[] gradients = tape.gradient(totalLoss, ForwardModel.TrainableVariables.Concat(ReverseModel.TrainableVariables));
+ Optimizer.apply_gradients(zip(gradients, ForwardModel.TrainableVariables.Concat(ReverseModel.TrainableVariables)));
+ Console.WriteLine($"Epoch {epoch + 1}, Batch {i / batchSize + 1}, Loss: {totalLoss.numpy()}");
+ }
+
+ }
+ }
+
+
+ }
+
+ public Board Predict(Board target)
+ {
+ float[] inputData = new float[Height * Width];
+ foreach (var (x, y) in target.Lives)
+ inputData[y * Width + x] = 1f;
+ NDArray input = np.array(inputData).reshape(new Shape(1, Height, Width, 1));
+ Tensors pred = ReverseModel.predict(input);
+ float[] predData = pred.ToArray();
+ Board res = new Board(Width, Height);
+ for (int i = 0; i < predData.Length; i++)
+ {
+ int x = i % Width;
+ int y = i / Width;
+ if (predData[i] > 0.5f)
+ res.Lives.Add((x, y));
+ }
+ return res;
+ }
+
+ public static IEnumerable<(int, int)> Circle()
+ {
+ int centerX = 10;
+ int centerY = 10;
+ int radius = 7;
+ int x = 0;
+ int y = radius;
+
+ int d = 1 - radius;
+
+ IEnumerable<(int, int)> PlotCirclePoints(int cx, int cy, int px, int py)
+ {
+ yield return (cx + px, cy + py);
+ yield return (cx - px, cy + py);
+ yield return (cx + px, cy - py);
+ yield return (cx - px, cy - py);
+ yield return (cx + py, cy + px);
+ yield return (cx - py, cy + px);
+ yield return (cx + py, cy - px);
+ yield return (cx - py, cy - px);
+ }
+
+ foreach (var point in PlotCirclePoints(centerX, centerY, x, y))
+ yield return point;
+
+ while (x < y)
+ {
+ x++;
+ if (d < 0)
+ d += 2 * x + 1;
+ else
+ {
+ y--;
+ d += 2 * (x - y) + 1;
+ }
+
+ foreach (var point in PlotCirclePoints(centerX, centerY, x, y))
+ yield return point;
+ }
+ }
+ public static void Run()
+ {
+ NeuralSolver solver = new NeuralSolver(20, 20, 10, false, false);
+ solver.Train(1000,8,20);
+ Board b = new Board(20, 20);
+ foreach ((int, int) cell in Circle())
+ b.Toggle(cell);
+ b.Evaluate(10);
+ Board z = solver.Predict(b);
+ Console.WriteLine(z.ToString());
+
+ }
+}
diff --git a/src/ResultData.cs b/src/ResultData.cs
new file mode 100644
index 0000000..aa08185
--- /dev/null
+++ b/src/ResultData.cs
@@ -0,0 +1,123 @@
+namespace InverseOfLife;
+
+public class ResultData
+{
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public bool QuotientX { get; set; }
+ public bool QuotientY { get; set; }
+ public byte[] TargetSignature { get; set; }
+ public HashSet<(int x, int y)>[] Frames { get; set; }
+ public double Score { get; set; }
+
+ public override string ToString()
+ {
+
+ return $"""
+ {Width}x{Height}
+ {QuotientX} {QuotientY}
+ {BytesToString(TargetSignature)}
+ {FramesToString(Frames)}
+ {Score}
+ """;
+ }
+ public static ResultData Restore(string save)
+ {
+
+ var rawLines = save.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
+
+ var linesList = new List();
+ foreach (var line in rawLines)
+ {
+ var trimmed = line.Trim();
+ if (trimmed.Length > 0)
+ linesList.Add(trimmed);
+ }
+ var lines = linesList.ToArray();
+
+ if (lines.Length < 4)
+ throw new FormatException("Input string not in expected format (less than 4 non-empty lines).");
+
+ var line1 = lines[0];
+ var wh = line1.Split('x');
+ if (wh.Length != 2)
+ throw new FormatException($"Line1 format invalid: {line1}");
+ int width = int.Parse(wh[0]);
+ int height = int.Parse(wh[1]);
+
+ var line2 = lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (line2.Length != 2)
+ throw new FormatException($"Line2 format invalid: {lines[1]}");
+ bool qx = bool.Parse(line2[0]);
+ bool qy = bool.Parse(line2[1]);
+
+ var line3 = lines[2];
+ byte[] signature;
+ if (string.IsNullOrWhiteSpace(line3))
+ signature = Array.Empty();
+ else
+ {
+ var hexes = line3.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ signature = new byte[hexes.Length];
+ for (int i = 0; i < hexes.Length; i++)
+ signature[i] = Convert.ToByte(hexes[i], 16);
+ }
+
+
+ var scoreLine = lines[lines.Length - 1];
+ double score = double.Parse(scoreLine);
+
+ var framesList = new List>();
+ for (int i = 3; i < lines.Length - 1; i++)
+ {
+ var frameLine = lines[i].Trim();
+ if (string.IsNullOrEmpty(frameLine))
+ {
+ framesList.Add(new HashSet<(int,int)>());
+ continue;
+ }
+
+ var tokens = frameLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+
+ var frameSet = new HashSet<(int, int)>();
+ foreach (var token in tokens)
+ {
+ var trimmed = token.Trim('(', ')');
+ var xy = trimmed.Split(',');
+ if (xy.Length != 2)
+ throw new FormatException($"Frame cell format invalid: {token}");
+ int cellX = int.Parse(xy[0]);
+ int cellY = int.Parse(xy[1]);
+ frameSet.Add((cellX, cellY));
+ }
+ framesList.Add(frameSet);
+ }
+
+ return new ResultData
+ {
+ Width = width,
+ Height = height,
+ QuotientX = qx,
+ QuotientY = qy,
+ TargetSignature = signature,
+ Frames = framesList.ToArray(),
+ Score = score
+ };
+ }
+
+ private static string BytesToString(byte[] bytes)
+ => String.Join(" ", bytes.Select(b => b.ToString("X2")));
+
+ private static string FramesToString(HashSet<(int, int)>[] frames)
+ {
+ string res = "";
+ foreach (HashSet<(int, int)> frame in frames)
+ {
+ res += String.Join(" ", frame.Select(cell => $"({cell.Item1},{cell.Item2})"));
+ res += "\n";
+ }
+
+ return res;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Solver.cs b/src/Solver.cs
index c1e07dc..a4350d6 100644
--- a/src/Solver.cs
+++ b/src/Solver.cs
@@ -36,7 +36,22 @@ public class Solver
return res;
}
- [SuppressMessage("ReSharper.DPA", "DPA0002: Excessive memory allocations in SOH", MessageId = "type: Entry[System.Int32,System.Double][]; size: 14391MB")]
+ public ResultData SolveToData(int resolution, int maxGeneration, int topN, float mutationRate, string mode)
+ {
+ (Gene res, double score) = Solve(resolution, maxGeneration, topN, mutationRate, mode);
+ ResultData result = new ResultData
+ {
+ Width = Target.Width,
+ Height = Target.Height,
+ QuotientX = Target.QuotientX,
+ QuotientY = Target.QuotientY,
+ TargetSignature = new Gene(1, Target).RiboseSequence,
+ Frames = res.Restore(Target.Width, Target.Height, Target.QuotientX, Target.QuotientY).Frames(Steps),
+ Score = score,
+ };
+ return result;
+ }
+
public (Gene, double) Solve(int withResolution=1, int maxGenerations=50, int topN=10, float mutationRate=0.1f, string mode="tp_only")
{
List<(Board, Gene)> currentGeneration = GetInitialGeneration()
@@ -104,4 +119,4 @@ public class Solver
yield return b;
}
}
-}
\ No newline at end of file
+}
diff --git a/summerizer.py b/summerizer.py
new file mode 100644
index 0000000..056ae6b
--- /dev/null
+++ b/summerizer.py
@@ -0,0 +1,30 @@
+import os
+
+ignores = [
+ 'bin',
+ 'obj'
+]
+def find_all_proj_files(base_path):
+ res = []
+ for root, dirs, files in os.walk(base_path):
+ dirs[:] = [d for d in dirs if not d.startswith('.') and not d in ignores]
+ for file in files:
+ if file not in ignores:
+ res.append(os.path.join(root, file))
+ return res
+
+
+def summerizer():
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+
+
+ fs = find_all_proj_files(current_dir)
+ res = ""
+ for file in fs:
+ with open(file) as f:
+ res += f"---------------------{file}-------------------------\n"
+ res += f.read()
+ res += "\n"
+ print(res)
+
+summerizer()
\ No newline at end of file