From 6ed23225080f1b968e72fd800351dd1247facfd9 Mon Sep 17 00:00:00 2001 From: Gregor Billing Date: Tue, 16 Aug 2022 14:45:56 +0900 Subject: [PATCH] Add `scrambleanalysis` subproject (#18) * Add project Scramble Analysis * Add CubeHelper * Add unit test for Scramble Analysis * MVP for histogram * Add parity check * Count edges -1 for orientation * WIP test for edge orientation * Complete test for edge distribution * Add test for random position of edges * Add test for corner position * Add test for corner orientation * Different test for EO * Actual calculation of the probability for i pairs misoriented * Test for corner position again * Complete the tests * Do some cleaning, add unit tests * Finish tests * Cleaning a bit * Remove test that mail fail in the future * Move `scrambleanalysis` build structure to Gradle (#3) * Remove legacy eclipse project files * Move scrambleanalysis POM to Gradle build kts * Add JUnit test library to Gradle * Fix JUnit5 configuration discrepancy * Move package org.thewca => org.worldcubeassociation.tnoodle * Fix broken imports from gnehzr-migration * Fix Gradle deprecations * Code cleanup and reuse * Run checks on CubeState instead of scramble strings * Run RandomMoves test on more samples to guarantee skewed EO Co-authored-by: Alexandre Campos --- gradle/libs.versions.toml | 1 + .../src/main/java/cs/min2phase/CubieCube.java | 12 +- .../src/main/java/cs/min2phase/Search.java | 4 +- scrambleanalysis/.gitignore | 1 + scrambleanalysis/build.gradle.kts | 26 ++ .../tnoodle/scrambleanalysis/App.java | 29 ++ .../tnoodle/scrambleanalysis/CubeHelper.java | 273 ++++++++++++++++++ .../tnoodle/scrambleanalysis/CubeTest.java | 107 +++++++ .../RepresentationException.java | 6 + .../scrambleanalysis/ScrambleProvider.java | 74 +++++ .../statistics/Distribution.java | 127 ++++++++ .../scrambleanalysis/utils/MathUtils.java | 10 + .../scrambleanalysis/utils/StringUtils.java | 28 ++ .../scrambleanalysis/CubeHelperTest.java | 130 +++++++++ .../scrambleanalysis/CubeTestTest.java | 40 +++ .../ScrambleProviderTest.java | 14 + .../statistics/DistributionTest.java | 13 + .../scrambleanalysis/utils/MathUtilsTest.java | 14 + .../utils/StringUtilsTest.java | 20 ++ .../tnoodle/puzzle/CubePuzzle.java | 9 + settings.gradle.kts | 1 + 21 files changed, 935 insertions(+), 4 deletions(-) create mode 100644 scrambleanalysis/.gitignore create mode 100644 scrambleanalysis/build.gradle.kts create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/App.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelper.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTest.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/RepresentationException.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProvider.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/Distribution.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtils.java create mode 100644 scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtils.java create mode 100644 scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelperTest.java create mode 100644 scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTestTest.java create mode 100644 scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProviderTest.java create mode 100644 scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/DistributionTest.java create mode 100644 scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtilsTest.java create mode 100644 scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtilsTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7fb729..345643c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version = "1.2.10 gwt-exporter = { module = "org.timepedia.exporter:gwtexporter", version = "2.5.1" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } +apache-commons-math3 = { module = "org.apache.commons:commons-math3", version = "3.6.1" } [plugins] shadow = { id = "com.github.johnrengelman.shadow", version = "7.1.2" } diff --git a/min2phase/src/main/java/cs/min2phase/CubieCube.java b/min2phase/src/main/java/cs/min2phase/CubieCube.java index 654dfbd..2ca52a4 100644 --- a/min2phase/src/main/java/cs/min2phase/CubieCube.java +++ b/min2phase/src/main/java/cs/min2phase/CubieCube.java @@ -2,7 +2,7 @@ import java.util.Arrays; -class CubieCube { +public class CubieCube { /** * 16 symmetries generated by S_F2, S_U4 and S_LR2 @@ -337,12 +337,20 @@ int verify() { if (sum % 3 != 0) { return -5;// twisted corner } - if ((Util.getNParity(Util.getNPerm(ea, 12, true), 12) ^ Util.getNParity(getCPerm(), 8)) != 0) { + if ((getEdgeParityBit() ^ getCornerParityBit()) != 0) { return -6;// parity error } return 0;// cube ok } + public int getEdgeParityBit() { + return Util.getNParity(Util.getNPerm(ea, 12, true), 12); + } + + public int getCornerParityBit() { + return Util.getNParity(getCPerm(), 8); + } + long selfSymmetry() { CubieCube c = new CubieCube(this); CubieCube d = new CubieCube(); diff --git a/min2phase/src/main/java/cs/min2phase/Search.java b/min2phase/src/main/java/cs/min2phase/Search.java index c3d9e37..e3e5fb6 100644 --- a/min2phase/src/main/java/cs/min2phase/Search.java +++ b/min2phase/src/main/java/cs/min2phase/Search.java @@ -60,7 +60,7 @@ public class Search { protected int verbose; protected int valid1; protected boolean allowShorter = false; - protected CubieCube cc = new CubieCube(); + public CubieCube cc = new CubieCube(); protected CubieCube[] urfCubieCube = new CubieCube[6]; protected CoordCube[] urfCoordCube = new CoordCube[6]; protected CubieCube[] phase1Cubie = new CubieCube[21]; @@ -240,7 +240,7 @@ public synchronized static void init() { } } - int verify(String facelets) { + public int verify(String facelets) { int count = 0x000000; byte[] f = new byte[54]; try { diff --git a/scrambleanalysis/.gitignore b/scrambleanalysis/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/scrambleanalysis/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/scrambleanalysis/build.gradle.kts b/scrambleanalysis/build.gradle.kts new file mode 100644 index 0000000..700bd5a --- /dev/null +++ b/scrambleanalysis/build.gradle.kts @@ -0,0 +1,26 @@ +import configurations.Languages.attachRemoteRepositories +import configurations.Frameworks.configureJUnit5 +import configurations.Languages.configureJava + +description = "Scramble quality checker that performs statistical analyses" + +attachRemoteRepositories() + +plugins { + java + application +} + +configureJava() + +dependencies { + implementation(project(":scrambles")) + implementation(project(":min2phase")) + implementation(libs.apache.commons.math3) +} + +configureJUnit5() + +application { + mainClass.set("org.thewca.scrambleanalysis.App") +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/App.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/App.java new file mode 100644 index 0000000..9ae0027 --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/App.java @@ -0,0 +1,29 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import org.worldcubeassociation.tnoodle.puzzle.CubePuzzle; +import org.worldcubeassociation.tnoodle.puzzle.ThreeByThreeCubePuzzle; +import org.worldcubeassociation.tnoodle.scrambles.InvalidScrambleException; + +import java.util.List; + +public class App { + + public static void main(String[] args) + throws InvalidScrambleException, RepresentationException { + + // to test your set of scrambles + // ArrayList scrambles = ScrambleProvider.getScrambles(fileName); + // boolean passed = testScrambles(scrambles); + + // Main test + int numberOfScrambles = 6500; + CubePuzzle puzzle = new ThreeByThreeCubePuzzle(); + + List scrambles = ScrambleProvider.generateWcaScrambles(puzzle, numberOfScrambles); + List representations = ScrambleProvider.convertToCubeStates(scrambles); + + boolean passed = CubeTest.testScrambles(representations); + System.out.println("\nMain test passed? " + passed); + + } +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelper.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelper.java new file mode 100644 index 0000000..966d768 --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelper.java @@ -0,0 +1,273 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import cs.min2phase.Search; +import cs.min2phase.SearchWCA; +import org.worldcubeassociation.tnoodle.puzzle.CubePuzzle; + +import static org.worldcubeassociation.tnoodle.scrambleanalysis.utils.StringUtils.stringCompareIgnoringOrder; + +public class CubeHelper { + // For 3x3 only. + + private static final int edges = 12; + private static final int central = 4; // Index 4 represents the central sticker; + private static final int stickersPerFace = 9; + + private static final int corners = 8; + + // Refer to toFaceCube representation. + // For FB edge orientation, we only care about edges on U/D, Equator F/B. + // Also, this sets an order to edges, which will be reused + // UB, UL, UR, UF + // DF, DL, DR, DB + // FL, FR + // BR, BL + private static final int[] edgesIndex = {1, 3, 5, 7, // U edges index + 28, 30, 32, 34, // D edges index + 21, 23, // Equator front + 48, 50 // Equator back + }; + + // Each edge has 2 stickers. This array represents, respectively, the index of + // the other attached sticker. + private static final int[] attachedEdgesIndex = {46, 37, 10, 19, // Attached to the U face. + 25, 43, 16, 52, // Attached to the D face + 41, 12, // Attached to Equator front + 14, 39 // Attached to Equator back + }; + + // Again, an order to corners + // UBL, UBR, UFL, UFR, + // DFL, DFR, DBL, DBR + private static final int[] cornersIndex = {0, 2, 6, 8, // U corners + 27, 29, 33, 35}; // D corners + private static final int[] cornersIndexClockWise = {36, 45, 18, 9, // U twist clockwise + 44, 26, 53, 17, // D stickers + }; + private static final int[] cornersIndexCounterClockWise = {47, 11, 38, 20, // U twists + 24, 15, 42, 51}; // D twists + + /** + * Count misoriented edges considering the FB axis. + * + * @param representation + * @return the number of misoriented edges in a cube. + * @throws RepresentationException + */ + public static int countMisorientedEdges(String representation) throws RepresentationException { + assert representation.length() == 54 : "Expected size: 54 = 6x9 stickers. Use cubeState.toFaceCube()."; + + int result = 0; + for (int i = 0; i < edges; i++) { + if (!isOrientedEdge(representation, i)) { + result++; + } + } + return result; + } + + public static int countMisorientedEdges(CubePuzzle.CubeState cubeState) throws RepresentationException { + String representation = cubeState.toFaceCube(); + return countMisorientedEdges(representation); + } + + public static boolean isOrientedEdge(String representation, int index) throws RepresentationException { + char color; + char attachedColor; + + char uColor = representation.charAt(central + 0 * stickersPerFace); + char rColor = representation.charAt(central + 1 * stickersPerFace); + // char fColor = representation.charAt(central + 2 * stickersPerFace); + char dColor = representation.charAt(central + 3 * stickersPerFace); + char lColor = representation.charAt(central + 4 * stickersPerFace); + // char bColor = representation.charAt(central + 5 * stickersPerFace); + + color = representation.charAt(edgesIndex[index]); + attachedColor = representation.charAt(attachedEdgesIndex[index]); + + if (color == uColor || color == dColor) { + return true; + } + if (color == rColor || color == lColor) { + return false; + } + // Now, we're left with f and b colors. + if (attachedColor == uColor || attachedColor == dColor) { + return false; + } else if (attachedColor == rColor || attachedColor == lColor) { + return true; + } + + throw new RepresentationException(); + } + + /** + * Given a representation, returns a number that represents the orientation of a + * corner at index cornerIndex + * + * @param representation a representation of a cube. + * @param cornerIndex 0 ≤ cornerIndex < 8 + * @return 0 if the corner is oriented. 1 if the corner is oriented clockwise. + * 2 if the corner is oriented counter clockwise. + * @throws RepresentationException + */ + public static int getCornerOrientationNumber(String representation, int cornerIndex) + throws RepresentationException { + char uColor = representation.charAt(central + 0 * stickersPerFace); + char dColor = representation.charAt(central + 3 * stickersPerFace); + + int index = cornersIndex[cornerIndex]; + int indexClockWise = cornersIndexClockWise[cornerIndex]; + int indexCounterClockWise = cornersIndexCounterClockWise[cornerIndex]; + + char sticker = representation.charAt(index); + char stickerClockWise = representation.charAt(indexClockWise); + char stickerCounterClockWise = representation.charAt(indexCounterClockWise); + + if (sticker == uColor || sticker == dColor) { + return 0; + } else if (stickerClockWise == uColor || stickerClockWise == dColor) { + return 1; + } else if (stickerCounterClockWise == uColor || stickerCounterClockWise == dColor) { + return 2; + } + throw new RepresentationException(); + } + + /** + * Sum of corner orientation. 0 for oriented, 1 for clockwise, 2 for counter + * clock wise. + * + * @param representation + * @return The sum of it. + * @throws RepresentationException + */ + public static int cornerOrientationSum(String representation) throws RepresentationException { + assert representation.length() == 54 : "Expected size: 54 = 6x9 stickers. Use cubeState.toFaceCube()."; + + int result = 0; + + for (int i = 0; i < corners; i++) { + result += getCornerOrientationNumber(representation, i); + } + + return result; + } + + // Parity is the oddness of the number of two-swaps. + // Right now, we don't consider cases where corner and edge parity are uneven. + public static boolean hasParity(String faceletRepresentation) { + Search search = new SearchWCA(); + int errors = search.verify(faceletRepresentation); + + if (errors != 0) { + throw new RuntimeException("min2phase cannot handle the cube: Error " + errors); + } + + int edgeParity = search.cc.getEdgeParityBit(); + int cornerParity = search.cc.getCornerParityBit(); + + return edgeParity == 1 || cornerParity == 1; + } + + // Actually, these next 2 methods did not need to be public, but it's for + // consistency with the + // getFinalLocationOfEdheSticker method. + public int[] getEdgesIndex() { + return edgesIndex; + } + + public int[] getAttachedEdgesIndex() { + return attachedEdgesIndex; + } + + /** + * Given a representation of a cube and the initial position of an edge (when + * solved), returns the final index position of that edge. The UB sticker is the + * first one on a toFaceCube representation, so call this 0 (index). Consider a + * U applied to a solved cube and let's call this repr. UB goes to UR, which is + * the 3rd edge in a representation (solved). + * getFinalLocationOfEdgeSticker(repr, 0) returns 2, which is 3rd sticker (0 + * based). + *

+ * UF is initially the 4th edge, so index 3. When an F is applied it goes to RF, + * which is the 6th edge on the toFaceCube representation, so this returns 5. + * + * @param representation: the final representation of a cube. + * @param i: the index, when solved, of a edge (0 for UB or BU, 1 + * for UL or LU, 3 for UR... + * @return If the final position of a sticker is in UB (either U or B), it + * returns 0 (which is the index of UB in edgesIndex or + * attachedEdgesIndex). If the final position of a sticker is in UL + * (either U or L), it returns 1 (which is the index of UL in + * edgesIndex). etc. + * @throws RepresentationException + */ + public static int getFinalPositionOfEdge(String representation, int i) throws RepresentationException { + // Here, we are reusing the position of edges mentioned above. + + if (representation.length() != 54) { + throw new IllegalArgumentException("Representation size must be 54."); + } + if (i < 0 || i >= edges) { + throw new IllegalArgumentException("Make sure 0 <= i <= 11."); + } + + int initialEdgeIndex = edgesIndex[i]; + int initialAttachedIndex = attachedEdgesIndex[i]; + + char initialColor = representation.charAt(central + initialEdgeIndex / stickersPerFace * stickersPerFace); + char initialAttachedColor = representation + .charAt(central + initialAttachedIndex / stickersPerFace * stickersPerFace); + + for (int j = 0; j < edges; j++) { + char color = representation.charAt(edgesIndex[j]); + char attachedColor = representation.charAt(attachedEdgesIndex[j]); + + if (color == initialColor && attachedColor == initialAttachedColor) { + return j; + } + if (color == initialAttachedColor && attachedColor == initialColor) { + return j; + } + } + + throw new RepresentationException(); + } + + public static int getFinalPositionOfCorner(String representation, int i) throws RepresentationException { + // Here, we are reusing the position of edges mentioned above. + + if (representation.length() != 54) { + throw new IllegalArgumentException("Representation size must be 54."); + } + if (i < 0 || i >= corners) { + throw new IllegalArgumentException("Make sure 0 <= i <= 7."); + } + + int initialIndex = cornersIndex[i]; + int initialClockWiseIndex = cornersIndexClockWise[i]; + int initialCounterClockWiseIndex = cornersIndexCounterClockWise[i]; + + char initialColor = representation.charAt(initialIndex - initialIndex % stickersPerFace + central); + char initialClockWiseColor = representation + .charAt(initialClockWiseIndex - initialClockWiseIndex % stickersPerFace + central); + char initialCounterClockWiseColor = representation + .charAt(initialCounterClockWiseIndex - initialCounterClockWiseIndex % stickersPerFace + central); + String initial = "" + initialColor + initialClockWiseColor + initialCounterClockWiseColor; + + for (int j = 0; j < corners; j++) { + char color = representation.charAt(cornersIndex[j]); + char clockWiseColor = representation.charAt(cornersIndexClockWise[j]); + char counterClockWiseColor = representation.charAt(cornersIndexCounterClockWise[j]); + + String current = "" + color + clockWiseColor + counterClockWiseColor; + + if (stringCompareIgnoringOrder(initial, current)) { + return j; + } + } + + throw new RepresentationException(); + } +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTest.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTest.java new file mode 100644 index 0000000..bb57daf --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTest.java @@ -0,0 +1,107 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import static org.worldcubeassociation.tnoodle.scrambleanalysis.CubeHelper.cornerOrientationSum; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.CubeHelper.countMisorientedEdges; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.CubeHelper.getFinalPositionOfCorner; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.CubeHelper.getFinalPositionOfEdge; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.CubeHelper.hasParity; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.statistics.Distribution.expectedCornersFinalPosition; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.statistics.Distribution.expectedCornersOrientationProbability; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.statistics.Distribution.expectedEdgesFinalPosition; +import static org.worldcubeassociation.tnoodle.scrambleanalysis.statistics.Distribution.expectedEdgesOrientationProbability; + +import java.util.List; + +import org.apache.commons.math3.stat.inference.AlternativeHypothesis; +import org.apache.commons.math3.stat.inference.BinomialTest; +import org.apache.commons.math3.stat.inference.ChiSquareTest; +import org.worldcubeassociation.tnoodle.puzzle.CubePuzzle; +import org.worldcubeassociation.tnoodle.scrambleanalysis.statistics.Distribution; + +public class CubeTest { + + private static final int edges = 12; + private static final int corners = 8; + + public static boolean testScrambles(List scrambles) + throws RepresentationException { + + int N = scrambles.size(); + + long minimumSampleSize = Distribution.minimumSampleSize(); + if (N < Distribution.minimumSampleSize()) { + throw new IllegalArgumentException("Minimum sample size is " + minimumSampleSize); + } + + long[] misorientedEdgesList = new long[7]; + long[][] finalEdgesPosition = new long[edges][edges]; + + long[] misorientedCornersList = new long[6]; // Sum is 0, 3, 6, ..., 15. + long[][] finalCornersPosition = new long[corners][corners]; + + int parity = 0; + + for (CubePuzzle.CubeState cubeState : scrambles) { + String representation = cubeState.toFaceCube(); + + int misorientedEdges = countMisorientedEdges(representation); + int cornerSum = cornerOrientationSum(representation); + + misorientedEdgesList[misorientedEdges / 2]++; + misorientedCornersList[cornerSum / 3]++; + + for (int j = 0; j < edges; j++) { + int finalPosition = getFinalPositionOfEdge(representation, j); + finalEdgesPosition[j][finalPosition]++; + } + + for (int j = 0; j < corners; j++) { + int finalPosition = getFinalPositionOfCorner(representation, j); + finalCornersPosition[j][finalPosition]++; + } + + if (hasParity(representation)) { + parity++; + } + } + + ChiSquareTest cst = new ChiSquareTest(); + double alpha = 0.01; + + double[] expectedEdges = expectedEdgesOrientationProbability(); + boolean randomEO = !cst.chiSquareTest(expectedEdges, misorientedEdgesList, alpha); + System.out.println("Random EO? " + randomEO); + + boolean edgesRandomPosition = true; + long[] expectedEdgesFinalPosition = expectedEdgesFinalPosition(N); + for (long[] item : finalEdgesPosition) { + if (cst.chiSquareTestDataSetsComparison(expectedEdgesFinalPosition, item, alpha)) { + edgesRandomPosition = false; + break; + } + } + System.out.println("Edges in random position? " + edgesRandomPosition); + + double[] expectedCorners = expectedCornersOrientationProbability(); + boolean randomCO = !cst.chiSquareTest(expectedCorners, misorientedCornersList, alpha); + System.out.println("Random CO? " + randomCO); + + boolean cornersRandomPosition = true; + long[] expectedCornersFinalPosition = expectedCornersFinalPosition(N); + for (long[] item : finalCornersPosition) { + if (cst.chiSquareTestDataSetsComparison(expectedCornersFinalPosition, item, alpha)) { + cornersRandomPosition = false; + break; + } + } + System.out.println("Corners in random position? " + cornersRandomPosition); + + BinomialTest bt = new BinomialTest(); + double probability = 0.5; + boolean randomParity = !bt.binomialTest(N, parity, probability, AlternativeHypothesis.TWO_SIDED, alpha); + System.out.println("Random parity? " + randomParity); + + return randomEO && edgesRandomPosition && randomCO && cornersRandomPosition && randomParity; + } + +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/RepresentationException.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/RepresentationException.java new file mode 100644 index 0000000..f7b8fdc --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/RepresentationException.java @@ -0,0 +1,6 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +public class RepresentationException extends Exception { + public RepresentationException() { + } +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProvider.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProvider.java new file mode 100644 index 0000000..3253926 --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProvider.java @@ -0,0 +1,74 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import org.worldcubeassociation.tnoodle.puzzle.CubePuzzle; +import org.worldcubeassociation.tnoodle.puzzle.ThreeByThreeCubePuzzle; +import org.worldcubeassociation.tnoodle.scrambles.InvalidScrambleException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class ScrambleProvider { + + public static List getScrambles(String fileName) throws IOException { + List scrambles = new ArrayList(); + + // Read scrambles + File file = new File(fileName); + Scanner input = new Scanner(file); + + try { + while (input.hasNextLine()) { + String scramble = input.nextLine().trim(); + if (scramble.length() > 0) { + scrambles.add(scramble); + } + } + } catch (Exception e) { + throw new IOException("There was an error reading the file."); + } finally { + input.close(); + } + + return scrambles; + } + + // This is the main test + public static List generateWcaScrambles(CubePuzzle cube, int N) { + List scrambles = new ArrayList(N); + + for (int i = 0; i < N; i++) { + // Give some status to the user + if (i % 1000 == 0) { + System.out.println("Generating scramble " + (i + 1) + "/" + N); + } + + String scramble = cube.generateScramble(); + scrambles.add(scramble); + } + + return scrambles; + } + + static CubePuzzle defaultCube = new ThreeByThreeCubePuzzle(); + + public static List generateWcaScrambles(int N) { + return generateWcaScrambles(defaultCube, N); + } + + public static List convertToCubeStates(List scrambles) throws InvalidScrambleException { + List cubeStates = new ArrayList(scrambles.size()); + CubePuzzle puzzle = new CubePuzzle(3); + + for (String scramble : scrambles) { + CubePuzzle.CubeState solved = puzzle.getSolvedState(); + CubePuzzle.CubeState cubeState = (CubePuzzle.CubeState) solved.applyAlgorithm(scramble); + + cubeStates.add(cubeState); + } + + return cubeStates; + } +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/Distribution.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/Distribution.java new file mode 100644 index 0000000..16ab944 --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/Distribution.java @@ -0,0 +1,127 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis.statistics; + +import java.util.Arrays; + +import static org.worldcubeassociation.tnoodle.scrambleanalysis.utils.MathUtils.nCp; + +public class Distribution { + + private static final int edges = 12; + private static final int corners = 8; + + /** + * This is the expected probability distribution for edge orientation + * considering random state. + * + * @return An array whose size is 7. On the index 0, the chance of 0 pairs + * oriented; on the index 1, the probability for 1 misoriented pair; on + * the index 2, the probability for 2 misoriented pairs; + */ + public static double[] expectedEdgesOrientationProbability() { + + long[] array = new long[7]; + + long total = 0L; + for (int i = 0; i < array.length; i++) { + long binom = nCp(12, 2 * i); + + array[i] = binom; + total += binom; + } + + double[] expected = new double[array.length]; + for (int i = 0; i < array.length; i++) { + expected[i] = 1.0 * array[i] / total; + } + return expected; + } + + /** + * @param N number of trials + * @return An array[12] in which all elements have the same value N/12. + */ + public static long[] expectedEdgesFinalPosition(long N) { + long[] array = new long[edges]; + Arrays.fill(array, N / edges); + + return array; + } + + /** + * This is the expected probability distribution for corner orientation + * considering random state. We assign 0 for oriented corner, 1 for corners + * twisted clockwise, 2 for counter clockwise. In a valid cube, the sum of the + * orientation is a multiple of 3. + * + * @return An array whose size is 6. On the index 0, the probability of sum 0 in + * corner orientation. On the index 1, the probability of sum 3 = 3 * 1. + * On the index 2, the probability of sum 6 = 3 * 2. + */ + public static double[] expectedCornersOrientationProbability() { + long partial; + long total = 0L; + + // Corners must sum 0, 3, 6, ..., 15 + long[] array = new long[6]; + + for (int i = 0; i < array.length; i++) { + partial = 0; + int sum = 3 * i; + for (int j = 0; j < corners; j++) { + for (int k = 0; k < corners; k++) { + // if j + k > 8, then the second nCp will always be 0. Adds nothing to the sum. + if (j + k <= 8 && j * 2 + k * 1 == sum) { + partial += nCp(8, j) * nCp(8 - j, k); + } + } + } + total += partial; + array[i] = partial; + } + + double[] expected = new double[array.length]; + for (int i = 0; i < array.length; i++) { + expected[i] = 1.0 * array[i] / total; + } + + return expected; + } + + /** + * @param N number of trials + * @return An array[8] in which all elements have the same value N/8. + */ + public static long[] expectedCornersFinalPosition(long N) { + long[] array = new long[corners]; + Arrays.fill(array, N / corners); + + return array; + } + + /** + * @return The minimum sample size for our tests. + */ + public static long minimumSampleSize() { + long min = 0; + + // Actually, this is fixed to 6144, but it's nice to have a way to know where + // does this comes from. + + // Minimum number required so we have at least 3 expected result for edges or corners. + // Some places say we must have at least 3 results. + double[] expectedEdges = expectedEdgesOrientationProbability(); + for (double item : expectedEdges) { + long number = Math.round(3.0 / item); + min = Math.max(number, min); + } + + // Minimum number required so we have at least 1 expected result for corners. + double[] expectedCorners = expectedCornersOrientationProbability(); + for (double item : expectedCorners) { + long number = Math.round(1.0 / item); + min = Math.max(number, min); + } + + return min; + } +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtils.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtils.java new file mode 100644 index 0000000..afb9bff --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtils.java @@ -0,0 +1,10 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis.utils; + +import org.apache.commons.math3.util.CombinatoricsUtils; + +public class MathUtils { + + public static long nCp(int n, int p) { + return CombinatoricsUtils.binomialCoefficient(n, p); + } +} diff --git a/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtils.java b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtils.java new file mode 100644 index 0000000..f6a4b8b --- /dev/null +++ b/scrambleanalysis/src/main/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtils.java @@ -0,0 +1,28 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis.utils; + +import java.util.Arrays; + +public class StringUtils { + /** + * Give two string, compares them ignoring order of chars. + * UFR == FRU == FRU + * + * @param st1 The first string + * @param st2 The second string + * @return The comparison result + */ + public static boolean stringCompareIgnoringOrder(String st1, String st2) { + if (st1.length() != st2.length()) { + return false; + } + + char[] chars1 = st1.toCharArray(); + char[] chars2 = st2.toCharArray(); + + Arrays.sort(chars1); + Arrays.sort(chars2); + + return Arrays.equals(chars1, chars2); + } + +} diff --git a/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelperTest.java b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelperTest.java new file mode 100644 index 0000000..bc79111 --- /dev/null +++ b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeHelperTest.java @@ -0,0 +1,130 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.logging.Logger; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.worldcubeassociation.tnoodle.puzzle.CubePuzzle; +import org.worldcubeassociation.tnoodle.puzzle.ThreeByThreeCubePuzzle; +import org.worldcubeassociation.tnoodle.scrambles.InvalidMoveException; +import org.worldcubeassociation.tnoodle.scrambles.InvalidScrambleException; + +public class CubeHelperTest { + + ThreeByThreeCubePuzzle cube = new ThreeByThreeCubePuzzle(); + Logger logger = Logger.getLogger(CubeHelperTest.class.getName()); + + @Test + public void orientationTest() throws InvalidScrambleException, RepresentationException { + int n = 1; + + // The number of misoriented edge must be even, corner orientation sum must be a + // multiple of 3. + for (int i = 0; i < n; i++) { + String scramble = cube.generateScramble(); + CubePuzzle.CubeState state = (CubePuzzle.CubeState) cube.getSolvedState().applyAlgorithm(scramble); + String representation = state.toFaceCube(); + + int misorientedEdges = CubeHelper.countMisorientedEdges(representation); + int cornerSum = CubeHelper.cornerOrientationSum(representation); + + logger.info("Scramble: " + scramble); + logger.info("Misoriented edges: " + misorientedEdges); + logger.info("Corner sum: " + cornerSum); + logger.info("Parity: " + CubeHelper.hasParity(representation)); + + assertEquals(misorientedEdges % 2, 0); + assertEquals(cornerSum % 3, 0); + } + } + + @Test + public void hasParityTest() throws InvalidScrambleException { + String scramble = "U"; + Assertions.assertTrue(CubeHelper.hasParity(getRepresentation(scramble))); + + scramble = "U'"; + Assertions.assertTrue(CubeHelper.hasParity(getRepresentation(scramble))); + + scramble = "U2"; + Assertions.assertFalse(CubeHelper.hasParity(getRepresentation(scramble))); + + String yPerm = "F R U' R' U' R U R' F' R U R' U' R' F R F'"; + Assertions.assertTrue(CubeHelper.hasParity(getRepresentation(yPerm))); + + String uPerm = "R2 U' R' U' R U R U R U' R"; + Assertions.assertFalse(CubeHelper.hasParity(getRepresentation(uPerm))); + } + + @Test + public void countMisorientedEdgesTest() throws InvalidScrambleException, RepresentationException { + String scramble1 = "F"; + String scramble2 = "F' B"; + String scramble3 = "F U F"; + + CubePuzzle.CubeState state1 = (CubePuzzle.CubeState) cube.getSolvedState().applyAlgorithm(scramble1); + String representation1 = state1.toFaceCube(); + int result1 = CubeHelper.countMisorientedEdges(representation1); + + CubePuzzle.CubeState state2 = (CubePuzzle.CubeState) cube.getSolvedState().applyAlgorithm(scramble2); + String representation2 = state2.toFaceCube(); + int result2 = CubeHelper.countMisorientedEdges(representation2); + + CubePuzzle.CubeState state3 = (CubePuzzle.CubeState) cube.getSolvedState().applyAlgorithm(scramble3); + String representation3 = state3.toFaceCube(); + int result3 = CubeHelper.countMisorientedEdges(representation3); + + assertEquals(result1, 4); + assertEquals(result2, 8); + assertEquals(result3, 2); + + Assertions.assertEquals(CubeHelper.countMisorientedEdges(representation1), CubeHelper.countMisorientedEdges(state1)); + Assertions.assertEquals(CubeHelper.countMisorientedEdges(representation2), CubeHelper.countMisorientedEdges(state2)); + Assertions.assertEquals(CubeHelper.countMisorientedEdges(representation3), CubeHelper.countMisorientedEdges(state3)); + } + + @Test + public void isOrientedEdgeTest() throws InvalidScrambleException, RepresentationException { + String scramble = "F B'"; + String representation = getRepresentation(scramble); + + Assertions.assertTrue(CubeHelper.isOrientedEdge(representation, 1)); + Assertions.assertTrue(CubeHelper.isOrientedEdge(representation, 2)); + Assertions.assertTrue(CubeHelper.isOrientedEdge(representation, 5)); + Assertions.assertTrue(CubeHelper.isOrientedEdge(representation, 6)); + + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 0)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 3)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 4)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 7)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 8)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 9)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 10)); + Assertions.assertFalse(CubeHelper.isOrientedEdge(representation, 11)); + } + + @Test + public void getFinalPositionTest() throws InvalidScrambleException, RepresentationException { + String scramble1 = "U2"; + String representation1 = getRepresentation(scramble1); + + String scramble2 = "R U R' U R U2 R'"; + String representation2 = getRepresentation(scramble2); + + Assertions.assertEquals(CubeHelper.getFinalPositionOfEdge(representation1, 0), 3); + Assertions.assertEquals(CubeHelper.getFinalPositionOfEdge(representation2, 0), 1); + + Assertions.assertEquals(CubeHelper.getFinalPositionOfCorner(representation1, 0), 3); + Assertions.assertEquals(CubeHelper.getFinalPositionOfCorner(representation2, 0), 3); + + Assertions.assertEquals(CubeHelper.getFinalPositionOfCorner(getRepresentation("R"), 1), 7); + Assertions.assertEquals(CubeHelper.getFinalPositionOfCorner(getRepresentation("R'"), 1), 3); + } + + private String getRepresentation(String scramble) throws InvalidScrambleException { + CubePuzzle.CubeState state = (CubePuzzle.CubeState) cube.getSolvedState().applyAlgorithm(scramble); + return state.toFaceCube(); + } +} diff --git a/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTestTest.java b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTestTest.java new file mode 100644 index 0000000..449481b --- /dev/null +++ b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/CubeTestTest.java @@ -0,0 +1,40 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import org.junit.jupiter.api.Test; +import org.worldcubeassociation.tnoodle.puzzle.CubePuzzle; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class CubeTestTest { + private final CubePuzzle randomMoveThreeByThree = new CubePuzzle(3); + + @Test + public void test() throws Exception { + // NOTE: There is a very, very slim chance that the random move scrambles + // may "accidentally" be as good as random state scrambles, making this test fail. + // When this happens, we should pause and ponder about the qualities of our + // random state solver, rather than simply ignoring a false-positive test. + int N = 20000; + + List scrambles = randomMovesScrambles(N); + List representations = ScrambleProvider.convertToCubeStates(scrambles); + + assertFalse(CubeTest.testScrambles(representations)); + } + + private List randomMovesScrambles(int N) { + List result = new ArrayList(); + for (int i = 0; i < N; i++) { + result.add(randomMovesScramble()); + } + return result; + } + + private String randomMovesScramble() { + return randomMoveThreeByThree.generateScramble(); + } +} diff --git a/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProviderTest.java b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProviderTest.java new file mode 100644 index 0000000..41d48be --- /dev/null +++ b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/ScrambleProviderTest.java @@ -0,0 +1,14 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.worldcubeassociation.tnoodle.puzzle.ThreeByThreeCubePuzzle; + +public class ScrambleProviderTest { + @Test + public void test() { + // Ew, not null tests. + assertNotNull(ScrambleProvider.generateWcaScrambles(2)); + } +} diff --git a/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/DistributionTest.java b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/DistributionTest.java new file mode 100644 index 0000000..6527667 --- /dev/null +++ b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/statistics/DistributionTest.java @@ -0,0 +1,13 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis.statistics; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class DistributionTest { + @Test + public void minimumSampleSizeTest() { + assertTrue(Distribution.minimumSampleSize() > 0); + assertEquals(Distribution.minimumSampleSize(), 6144); + } +} diff --git a/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtilsTest.java b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtilsTest.java new file mode 100644 index 0000000..d704e66 --- /dev/null +++ b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/MathUtilsTest.java @@ -0,0 +1,14 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class MathUtilsTest { + + @Test + public void test() { + assertEquals(MathUtils.nCp(8, 2), 28); + } + +} diff --git a/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtilsTest.java b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtilsTest.java new file mode 100644 index 0000000..f871a29 --- /dev/null +++ b/scrambleanalysis/src/test/java/org/worldcubeassociation/tnoodle/scrambleanalysis/utils/StringUtilsTest.java @@ -0,0 +1,20 @@ +package org.worldcubeassociation.tnoodle.scrambleanalysis.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class StringUtilsTest { + + @Test + public void test() { + assertTrue(StringUtils.stringCompareIgnoringOrder("FRU", "RUF")); + assertTrue(StringUtils.stringCompareIgnoringOrder("UBL", "LBU")); + assertTrue(StringUtils.stringCompareIgnoringOrder("ABC", "BAC")); + + assertFalse(StringUtils.stringCompareIgnoringOrder("FRU", "FRR")); + assertFalse(StringUtils.stringCompareIgnoringOrder("AA", "AAA")); + assertFalse(StringUtils.stringCompareIgnoringOrder("FRU", "fru")); + } + +} diff --git a/scrambles/src/main/java/org/worldcubeassociation/tnoodle/puzzle/CubePuzzle.java b/scrambles/src/main/java/org/worldcubeassociation/tnoodle/puzzle/CubePuzzle.java index bc393a3..e0fa20f 100644 --- a/scrambles/src/main/java/org/worldcubeassociation/tnoodle/puzzle/CubePuzzle.java +++ b/scrambles/src/main/java/org/worldcubeassociation/tnoodle/puzzle/CubePuzzle.java @@ -484,6 +484,15 @@ public TwoByTwoState toTwoByTwoState() { return state; } + /** + * Returns a string representing the colors of the cube. + * Looking at the face, each sticker is described in the reading order, + * that is, from left to right, from up to bottom. + * Faces order follow U, R, F, D, L, B. + * @return A string, not spaced, containing the color of each sticker. + * This color refers to the solved state. If white is the color of the U face when solved, + * then every white sticker is described as U. + */ public String toFaceCube() { assert size == 3; String state = ""; diff --git a/settings.gradle.kts b/settings.gradle.kts index a05de06..3b20607 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ rootProject.name = "tnoodle-lib" include("min2phase") include("scrambles") +include("scrambleanalysis") include("sq12phase") include("svglite") include("threephase")