public void Test_EncodedS2PointVectorTest_SnappedFractalLoops() { S2Testing.Random.Reset(S2Testing.Random.RandomSeed); #if DEBUG const int kMaxPoints = 3 << 10; #else const int kMaxPoints = 3 << 14; #endif for (int num_points = 3; num_points <= kMaxPoints; num_points *= 4) { int s2polygon_size = 0, lax_polygon_size = 0; for (int i = 0; i < 10; ++i) { S2Testing.Fractal fractal = new(); fractal.SetLevelForApproxMaxEdges(num_points); var frame = S2Testing.GetRandomFrame(); var loop = fractal.MakeLoop(frame, S2Testing.KmToAngle(10)); List <S2Point> points = new(); for (int j = 0; j < loop.NumVertices; ++j) { points.Add(new S2CellId(loop.Vertex(j)).ToPoint()); } S2Polygon s2polygon = new(new S2Loop(points)); Encoder encoder = new(); s2polygon.Encode(encoder); s2polygon_size += encoder.Length(); // S2LaxPolygonShape has 2 extra bytes of overhead to encode one loop. lax_polygon_size += TestEncodedS2PointVector(points.ToArray(), CodingHint.COMPACT, -1) + 2; } _logger.WriteLine($"n={num_points:d5} s2={s2polygon_size:d9} lax={lax_polygon_size:9}"); } }
private static void TestFractal(int min_level, int max_level, double dimension) { // This function constructs a fractal and then computes various metrics // (number of vertices, total length, minimum and maximum radius) and // verifies that they are within expected tolerances. Essentially this // directly verifies that the shape constructed *is* a fractal, i.e. the // total length of the curve increases exponentially with the level, while // the area bounded by the fractal is more or lessant. // The radius needs to be fairly small to avoid spherical distortions. const double nominal_radius = 0.001; // radians, or about 6km const double kDistortionError = 1e-5; S2Testing.Fractal fractal = new() { MinLevel = min_level, MaxLevel = max_level, FractalDimension = dimension }; var frame = S2Testing.GetRandomFrame(); var loop = fractal.MakeLoop(frame, S1Angle.FromRadians(nominal_radius)); Assert.True(loop.IsValid()); // If min_level and max_level are not equal, then the number of vertices and // the total length of the curve are subject to random variation. Here we // compute an approximation of the standard deviation relative to the mean, // noting that most of the variance is due to the random choices about // whether to stop subdividing at "min_level" or not. (The random choices // at higher levels contribute progressively less and less to the variance.) // The "relative_error" below corresponds to *one* standard deviation of // error; it can be increased to a higher multiple if necessary. // // Details: Let n=3*(4**min_level) and k=(max_level-min_level+1). Each of // the "n" edges at min_level stops subdividing at that level with // probability (1/k). This gives a binomial distribution with mean u=(n/k) // and standard deviation s=Math.Sqrt((n/k)(1-1/k)). The relative error (s/u) // can be simplified to Math.Sqrt((k-1)/n). var num_levels = max_level - min_level + 1; var min_vertices = NumVerticesAtLevel(min_level); var relative_error = Math.Sqrt((num_levels - 1.0) / min_vertices); // "expansion_factor" is the total fractal length at level "n+1" divided by // the total fractal length at level "n". var expansion_factor = Math.Pow(4, 1 - 1 / dimension); var expected_num_vertices = 0; var expected_length_sum = 0.0; // "triangle_perim" is the perimeter of the original equilateral triangle // before any subdivision occurs. var triangle_perim = 3 * Math.Sqrt(3) * Math.Tan(nominal_radius); var min_length_sum = triangle_perim * Math.Pow(expansion_factor, min_level); for (var level = min_level; level <= max_level; ++level) { expected_num_vertices += NumVerticesAtLevel(level); expected_length_sum += Math.Pow(expansion_factor, level); } expected_num_vertices /= num_levels; expected_length_sum *= triangle_perim / num_levels; Assert.True(loop.NumVertices >= min_vertices); Assert.True(loop.NumVertices <= NumVerticesAtLevel(max_level)); Assert2.Near(expected_num_vertices, loop.NumVertices, relative_error * (expected_num_vertices - min_vertices)); var center = frame.Col(2); var min_radius = S2.M_2_PI; double max_radius = 0; double length_sum = 0; for (int i = 0; i < loop.NumVertices; ++i) { // Measure the radius of the fractal in the tangent plane at "center". double r = Math.Tan(center.Angle(loop.Vertex(i))); min_radius = Math.Min(min_radius, r); max_radius = Math.Max(max_radius, r); length_sum += loop.Vertex(i).Angle(loop.Vertex(i + 1)); } // kVertexError is an approximate bound on the error when computing vertex // positions of the fractal (due to S2.FromFrame, trig calculations, etc). double kVertexError = 1e-14; // Although min_radius_factor() is only a lower bound in general, it happens // to be exact (to within numerical errors) unless the dimension is in the // range (1.0, 1.09). if (dimension == 1.0 || dimension >= 1.09) { // Expect the min radius to match very closely. Assert2.Near(min_radius, fractal.MinRadiusRactor() * nominal_radius, kVertexError); } else { // Expect the min radius to satisfy the lower bound. Assert.True(min_radius >= fractal.MinRadiusRactor() * nominal_radius - kVertexError); } // max_radius_factor() is exact (modulo errors) for all dimensions. Assert2.Near(max_radius, fractal.MaxRadiusFactor() * nominal_radius, kVertexError); Assert2.Near(expected_length_sum, length_sum, relative_error * (expected_length_sum - min_length_sum) + kDistortionError * length_sum); }