Commit 8496e143 authored by JasonPries's avatar JasonPries
Browse files

Add tolerance testing for Sketch::solve() in tests

parent 75ee5b12
......@@ -66,7 +66,7 @@ size_t Distance<LineSegment>::set_equation_index(size_t i) {
EquationIndex = i;
return 2;
};
#include <iostream>
template<>
void Distance<LineSegment>::update(Eigen::MatrixXd &J, Eigen::VectorXd &r) const {
/*
......@@ -111,7 +111,9 @@ void Distance<LineSegment>::update(Eigen::MatrixXd &J, Eigen::VectorXd &r) const
d01 = d0 + d1;
cross = (v0mx * v1my - v1mx * v0my) / d01;
d01 *= SIGN(cross);
if (cross < 0) {
d01 *= -1.0;
}
r(EquationIndex) = abs(cross) - Dim / 2.0;
......@@ -135,7 +137,9 @@ void Distance<LineSegment>::update(Eigen::MatrixXd &J, Eigen::VectorXd &r) const
d01 = d0 + d1;
cross = (v0mx * v1my - v1mx * v0my) / d01;
d01 *= SIGN(cross);
if (cross < 0) {
d01 *= -1.0;
}
r(EquationIndex) += abs(cross) - Dim / 2.0;
......@@ -157,6 +161,7 @@ void Distance<LineSegment>::update(Eigen::MatrixXd &J, Eigen::VectorXd &r) const
scale = std::fmax(d0, d1);
if (abs(cross) < abs(dot)) {
//std::cerr << "CROSS" << std::endl;
// Use cross product equation
r(EquationIndex + 1) = scale * cross;
......@@ -180,10 +185,16 @@ void Distance<LineSegment>::update(Eigen::MatrixXd &J, Eigen::VectorXd &r) const
J(EquationIndex + 1, Element1->end()->y_index()) += f;
} else {
// Use dot product equation
//std::cerr << "DOT" << std::endl;
r(EquationIndex + 1) = scale * (abs(dot) - 1.0);
d0 /= (scale * SIGN(dot));
d1 /= (scale * SIGN(dot));
if (dot >= 0) {
d0 /= (scale);
d1 /= (scale);
} else {
d0 /= (-scale);
d1 /= (-scale);
}
f = (v1x - dot * v0x) / d0;
J(EquationIndex + 1, Element0->start()->x_index()) -= f;
......
......@@ -17,22 +17,20 @@
double Sketch::solve() {
// TODO: Time to finish the bells and whistles here
// TODO: Use sparse matrix representation for Jacobian, (this is lower priority since typical uses will not produce very large matrices)
// TODO: Add residual instrumentation to tests
// TODO: Try extracting sub matrix and solving (Certain constraints are known to eliminate variables (Radius, Fixation), if the matrix can be permuted so that a submatrix on the block diagonal is lower-triangular, then a subsystem of equations can be solved independently of the remaining equations. Moreover, if the jacobian can be permuted into a block lower triangular form, subsets of equations can be solved blockwise)
// TODO: Extract newton_update over Verticies, Curves, and Constraints into a private method
// TODO: Extract armijo_update into a private method
// TODO: Strictly decreasing armijo_update from armijo_int = 0; (Typical expected case)
if (NumVariables < NumEquations) {
std::cerr << "Variables = " << NumVariables << ", Equations = " << NumEquations << ". Number of variables is less than the number of equations. The sketch may be over-constrained." << std::endl;
}
Eigen::VectorXd delta = Eigen::VectorXd::Zero(NumVariables);
Eigen::VectorXd x = Eigen::VectorXd::Zero(NumVariables);
Eigen::VectorXd r = Eigen::VectorXd::Zero(NumEquations);
Eigen::VectorXd dr = Eigen::VectorXd::Zero(NumEquations);
Eigen::MatrixXd J = Eigen::MatrixXd::Zero(NumEquations, NumVariables);
//Eigen::MatrixXd JJT = Eigen::MatrixXd::Zero(NumEquations, NumEquations);
//Eigen::FullPivHouseholderQR<Eigen::MatrixXd> QR;
//Eigen::FullPivLU<Eigen::MatrixXd> LU;
// Calculate scale parameter for tolerances
// TODO: Sketch::scale();
double scale = 0.0;
if (Curves.size() > 0) {
for (size_t i = 0;i!= Curves.size();++i){
......@@ -57,6 +55,7 @@ double Sketch::solve() {
}
// Initial residual calculation
// TODO: Sketch::update_equation(J,r)
for (size_t i = 0; i != Verticies.size(); ++i) {
Verticies[i]->update(J, r);
}
......@@ -70,104 +69,98 @@ double Sketch::solve() {
}
// Setup iteration
double residual_norm{r.norm() / scale};
double residual_tol = std::sqrt(NumEquations + 1.0) * DBL_EPSILON;
int armijo_int{0};
long max_rank(std::min(NumEquations, NumVariables));
for (size_t j = 0; residual_norm > residual_tol && j != (8 * sizeof(double)); ++j) {
// TODO: Try extracting sub matrix and solving (Certain constraints are known to eliminate variables (Radius, Fixation), if the matrix can be permuted so that a submatrix on the block diagonal is lower-triangular, then a subsystem of equations can be solved independently of the remaining equations. Moreover, if the jacobian can be permuted into a block lower triangular form, subsets of equations can be solved blockwise)
// TODO: Extract newton_update over Verticies, Curves, and Constraints into a private method
// TODO: Extract armijo_update into a private method
// TODO: Factor out rank deficiency perturbation to be independent of QR/LU/LDL^T choice
double res_norm{r.norm() / scale};
double res_tol{std::sqrt(NumEquations + 1.0) * DBL_EPSILON};
size_t double_bits = (CHAR_BIT * sizeof(double));
long prev_rank{std::min(NumEquations,NumVariables)};
for (size_t j = 0; res_norm > res_tol && j != double_bits; ++j) {
Eigen::FullPivHouseholderQR<Eigen::MatrixXd> QR = J.fullPivHouseholderQr();
delta = QR.solve(r) * std::pow(2.0, (double) armijo_int);
Eigen::VectorXd delta = QR.solve(r);
long iter_rank = QR.rank();
if (iter_rank < max_rank) {
max_rank = iter_rank;
if (iter_rank < prev_rank) {
std::cerr << "Rank = " << iter_rank << ", Equations = " << NumEquations << ", Variables = " << NumVariables << ", Iteration = " << j
<< ". The jacobian rank is less than the number of equations. Some sketch elements may be over-constrained." << std::endl;
// Randomly perturb the delta vector to prevent stagnation at inflection point
std::random_device rdev;
std::mt19937 rgen(rdev());
auto rdist = std::uniform_real_distribution<>(-FLT_EPSILON * scale, FLT_EPSILON * scale);
auto rdist = std::uniform_real_distribution<>(-DBL_EPSILON * scale, DBL_EPSILON * scale);
for (size_t i = 0; i != Variables.size(); ++i) {
delta(Variables[i]->get_index()) += rdist(rgen);
}
}
prev_rank = iter_rank;
// Update Jacobian, residual, and Armijo step-size
double iter_norm{DBL_MAX};
int armijo_dir{0};
while (iter_norm > residual_norm && residual_norm > std::sqrt(NumEquations + 1) * DBL_EPSILON) {
for (size_t i = 0; i < Variables.size(); ++i) {
Variables[i]->update(delta);
}
// Update variables
// TODO: Sketch::update_variables(delta);
for (size_t i = 0; i < Variables.size(); ++i) {
Variables[i]->update(delta);
}
J.setZero();
// TODO: Sketch::update_equation
J.setZero();
for (size_t i = 0; i != Verticies.size(); ++i) {
Verticies[i]->update(J, r);
}
for (size_t i = 0; i != Verticies.size(); ++i) {
Verticies[i]->update(J, r);
}
for (size_t i = 0; i != Curves.size(); ++i) {
Curves[i]->update(J, r);
}
for (size_t i = 0; i != Curves.size(); ++i) {
Curves[i]->update(J, r);
}
for (size_t i = 0; i != Constraints.size(); ++i) {
Constraints[i]->update(J, r);
}
for (size_t i = 0; i != Constraints.size(); ++i) {
Constraints[i]->update(J, r);
}
iter_norm = r.norm() / scale;
// Update Jacobian, residual, and take Armijo step
// TODO: Sketch::armijo(res_norm,res_tol);
double iter_norm{r.norm() / scale};
if (iter_norm > res_norm) {
delta *= -0.5;
size_t armijo_iter{0};
while (iter_norm > res_norm && iter_norm > res_tol && res_norm > res_tol && armijo_iter != double_bits) {
// TODO: Sketch::update_variables(delta);
for (size_t i = 0; i < Variables.size(); ++i) {
Variables[i]->update(delta);
}
// TODO: Sketch::update_equation
J.setZero();
if (armijo_dir == 0) {
if (iter_norm < residual_norm) {
armijo_dir = +1;
} else {
delta = -delta;
armijo_dir = -1;
for (size_t i = 0; i != Verticies.size(); ++i) {
Verticies[i]->update(J, r);
}
}
if (armijo_dir == 1) {
if (iter_norm < residual_norm) {
if (armijo_int == 0) {
residual_norm = iter_norm;
break;
} else {
delta /= 2.0;
armijo_int += 1;
}
} else {
delta *= -2.0;
armijo_dir = -1;
for (size_t i = 0; i != Curves.size(); ++i) {
Curves[i]->update(J, r);
}
}
if (armijo_dir == -1) {
if (iter_norm > residual_norm) {
if (armijo_int == -(8 * sizeof(double))) {
std::cerr << "Residual norm was not reduced for Armijo step-sizes greater than 2^-(8*sizeof(double))" << std::endl;
residual_norm = iter_norm;
break;
} else {
delta /= 2.0;
armijo_int -= 1;
for (size_t i = 0; i != Constraints.size(); ++i) {
Constraints[i]->update(J, r);
}
iter_norm = r.norm() / scale;
if (iter_norm > res_norm) {
delta *= 0.5;
armijo_iter += 1;
if (armijo_iter == double_bits) {
std::cerr << "Residual norm was not reduced for Armijo step-sizes greater than 2^-(CHAR_BIT * sizeof(double))" << std::endl;
}
} else {
residual_norm = iter_norm;
break;
}
}
}
res_norm = iter_norm;
}
return residual_norm;
return res_norm;
}
bool Sketch::build() {
bool Sketch::build() { // TOOD: Instrumentation in tests
Constellation c = Constellation(this);
Boundary = c.boundary();
......
......@@ -7,7 +7,8 @@ TEST(Constraint, Vertex) {
auto f = s.new_element<Fixation>(v);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(M_E, v->x(), TOL);
EXPECT_NEAR(M_PI, v->y(), TOL);
......@@ -24,7 +25,8 @@ TEST(Constraint, Fixation_Length) {
auto fix = s.new_element<Fixation>(v0);
auto len = s.new_element<Length>(line, M_LN2);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Fixation_Length");
......@@ -42,7 +44,8 @@ TEST(Constraint, Horizontal) {
auto line = s.new_element<LineSegment>(v0, v1);
auto horz = s.new_element<Horizontal>(line);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Horizontal");
......@@ -59,7 +62,8 @@ TEST(Constraint, Vertical) {
auto vert = s.new_element<Vertical>(line);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Vertical");
......@@ -76,7 +80,8 @@ TEST(Constraint, Length) {
auto length = s.new_element<Length>(line, 1.0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Length");
......@@ -93,7 +98,8 @@ TEST(Constraint, CircularArc) {
auto c = s.new_element<CircularArc>(v0, v1, vc, 2.1);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__CircularArc");
......@@ -112,7 +118,8 @@ TEST(Constraint, Radius) {
auto r = s.new_element<Radius>(c, 3.14);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Radius");
......@@ -163,7 +170,8 @@ TEST(Constraint, Tangency_CircularArc_LineSegment) {
auto tan45 = s.new_element<Tangency>(c, l45);
auto vert45 = s.new_element<Vertical>(l45v);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Tangency_CircularArc_LineSegment_0");
......@@ -203,7 +211,8 @@ TEST(Constraint, Tangency_CircularArc_LineSegment) {
auto t = s.new_element<Tangency>(c, l);
auto r = s.new_element<Radius>(c, 1.0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Tangency_CircularArc_LineSegment_1");
......@@ -235,6 +244,7 @@ TEST(Constraint, Angle) {
ang->dim(a);
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, std::string("Constraint__Angle_LineSegment_LineSegment_")+std::to_string(i));
......@@ -277,7 +287,8 @@ TEST(Constraint, Angle_Coincident) {
ang->dim(a);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, std::string("Constraint__Angle_Coincident_")+std::to_string(i));
......@@ -300,9 +311,10 @@ TEST(Constraint, Distance_Vertex_Vertex) {
auto d = s.new_element<Distance<Vertex>>(v0, v1, M_LN2);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
//s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_Vertex_Vertex");
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_Vertex_Vertex");
EXPECT_NEAR(M_LN2, hypot(v0->x() - v1->x(), v0->y() - v1->y()), TOL * M_LN2);
}
......@@ -320,7 +332,8 @@ TEST(Constraint, Distance_LineSegment) {
auto l1 = s.new_element<LineSegment>(v10, v11);
auto d = s.new_element<Distance<LineSegment>>(l0, l1, M_PI);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_LineSegment");
......@@ -363,7 +376,8 @@ TEST(Constraint, Distance_LineSegment) {
auto d01 = s.new_element<Distance<LineSegment>>(l0, l1, 1.0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_LineSegment_initially_perpendicular");
......@@ -417,9 +431,11 @@ TEST(Constraint, Distance_LineSegment) {
auto l0 = s.new_element<LineSegment>(v00, v01);
auto l1 = s.new_element<LineSegment>(v10, v11);
auto d = s.new_element<Distance<LineSegment>>(l0, l1, 1.0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_LineSegment_midpoint_intersection");
......@@ -456,11 +472,13 @@ TEST(Constraint, Distance_CircularArc) {
auto c0 = s.new_element<CircularArc>(v00, v01, vc0, 0.2);
auto c1 = s.new_element<CircularArc>(v10, v11, vc1, 0.2);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
auto d = s.new_element<Distance<CircularArc>>(c0, c1, 1.0 / 3.0);
s.solve();
res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_CircularArc_exterior");
......@@ -481,11 +499,13 @@ TEST(Constraint, Distance_CircularArc) {
auto c0 = s.new_element<CircularArc>(v00, v01, vc0, 1.0);
auto c1 = s.new_element<CircularArc>(v10, v11, vc1, 0.2);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
auto d = s.new_element<Distance<CircularArc>>(c0, c1, 1.0 / 3.0);
s.solve();
res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_CircularArc_interior_0");
......@@ -506,11 +526,13 @@ TEST(Constraint, Distance_CircularArc) {
auto c0 = s.new_element<CircularArc>(v00, v01, vc0, 1.0);
auto c1 = s.new_element<CircularArc>(v10, v11, vc1, 0.2);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
auto d = s.new_element<Distance<CircularArc>>(c1, c0, 1.0 / 3.0);
s.solve();
res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Distance_CircularArc_interior_1");
......@@ -531,7 +553,8 @@ TEST(Constraint, Coincident_CircularArc) {
auto co = s.new_element<Coincident<CircularArc>>(vp, ca);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Coincident_CircularArc");
......@@ -553,7 +576,8 @@ TEST(Constraint, Coincident_LineSegment) {
auto c = s.new_element<Coincident<LineSegment>>(vp, l);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Coincident_LineSegment");
......@@ -582,7 +606,8 @@ TEST(Constraint, Coincident_LineSegment) {
auto c = s.new_element<Coincident<LineSegment>>(vp, l);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Constraint__Coincident_LineSegment_deg180.csv");
......@@ -628,11 +653,12 @@ TEST(Constraint, Symmetry) {
auto l0 = s.new_element<LineSegment>(v0, v1);
s.new_element<Fixation>(v0);
s.new_element<Horizontal>(l0);
s.new_element<Length>(l0, 1.0);
auto f0 = s.new_element<Fixation>(v0);
auto h0 =s.new_element<Horizontal>(l0);
auto len0 = s.new_element<Length>(l0, 1.0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(0.0, v0->x(), TOL);
EXPECT_NEAR(0.0, v0->y(), TOL);
......@@ -640,8 +666,10 @@ TEST(Constraint, Symmetry) {
EXPECT_NEAR(1.0, l0->length(), TOL);
EXPECT_NEAR(1.0, v1->x(), TOL);
s.new_element<Symmetry>(v2, v3, l0);
s.solve();
auto sym0 = s.new_element<Symmetry>(v2, v3, l0);
res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(v2->x(), v3->x(), TOL);
EXPECT_NEAR(v2->y(), -v3->y(), TOL);
......@@ -657,11 +685,12 @@ TEST(Constraint, Symmetry) {
auto l0 = s.new_element<LineSegment>(v0, v1);
s.new_element<Fixation>(v0);
s.new_element<Vertical>(l0);
s.new_element<Length>(l0, 1.0);
auto f0 = s.new_element<Fixation>(v0);
auto vert0 = s.new_element<Vertical>(l0);
auto len0 = s.new_element<Length>(l0, 1.0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(0.0, v0->x(), TOL);
EXPECT_NEAR(0.0, v0->y(), TOL);
......@@ -669,8 +698,9 @@ TEST(Constraint, Symmetry) {
EXPECT_NEAR(1.0, l0->length(), TOL);
EXPECT_NEAR(1.0, v1->y(), TOL);
s.new_element<Symmetry>(v2, v3, l0);
s.solve();
auto sym0 = s.new_element<Symmetry>(v2, v3, l0);
res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(v2->x(), -v3->x(), TOL);
EXPECT_NEAR(v2->y(), v3->y(), TOL);
......@@ -685,10 +715,12 @@ TEST(Constraint, Symmetry) {
auto v3 = s.new_element<Vertex>(0.8, 0.2);
auto l0 = s.new_element<LineSegment>(v0, v1);
s.new_element<Fixation>(v0);
s.new_element<Fixation>(v1);
s.solve();
auto f0 = s.new_element<Fixation>(v0);
auto f1 = s.new_element<Fixation>(v1);
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(0.0, v0->x(), TOL);
EXPECT_NEAR(0.0, v0->y(), TOL);
......@@ -696,8 +728,10 @@ TEST(Constraint, Symmetry) {
EXPECT_NEAR(1.0, v1->y(), TOL);
EXPECT_NEAR(M_SQRT2, l0->length(), TOL);
s.new_element<Symmetry>(v2, v3, l0);
s.solve();
auto sym0 = s.new_element<Symmetry>(v2, v3, l0);
res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
EXPECT_NEAR(v3->y(), v2->x(), TOL);
EXPECT_NEAR(v3->x(), v2->y(), TOL);
......@@ -718,7 +752,8 @@ TEST(Constraint, Rotation) {
auto r = s.new_element<Rotation>(v1, v2, v0, a);
a *= M_PI / 180.0;
s.solve();
auto res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
double dx1 = v1->x() - v0->x();
double dy1 = v1->y() - v0->y();
......
......@@ -26,6 +26,8 @@ TEST(Curve, supremum) {
EXPECT_GT(sc0, sl0);
s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.build();
}
\ No newline at end of file
......@@ -78,31 +78,30 @@ TEST(MirrorCopy, nonoverlapping) {
s.save_as<SaveMethod::Rasterize>(SAVE_DIR, "Pattern__Mirror_nonoverlapping_square");
double residual = s.solve();
double res_norm = s.solve();
EXPECT_LE(res_norm, FLT_EPSILON);
s.build();
// Run Tests
{
test_sketch_size(s, 10, 9, 6, 2);
test_mirror_verticies(s, {0, 1, 2, 3}, l4);
test_mirror_curves(s, {0, 1, 2, 3}, l4);
}
test_sketch_size(s, 10, 9, 6, 2);
test_mirror_verticies(s, {0, 1, 2, 3}, l4);
test_mirror_curves(s, {0, 1, 2, 3}, l4);
// Change elements
{
s.new_element<Length>(l0, 0.5);
residual = s.solve();
s.build();
s.new_element<Length>(l0, 0.5);