From f61e8d1f7ebdde72605a0e44e3a3e25ec1f02f8e Mon Sep 17 00:00:00 2001 From: godardma Date: Mon, 27 Apr 2026 17:29:41 +0200 Subject: [PATCH 1/9] [zonotope] added operator+ and renamed center c --- doc/manual/development/changelog.rst | 8 +++++ doc/manual/manual/geometry/zonotope.png | Bin 12274 -> 65968 bytes doc/manual/manual/geometry/zonotope.rst | 16 +++++----- .../domains/zonotope/codac2_py_Zonotope.cpp | 8 +++-- .../zonotope/codac2_Parallelepiped.cpp | 20 ++++++------ .../domains/zonotope/codac2_Parallelepiped.h | 8 ++--- src/core/domains/zonotope/codac2_Zonotope.cpp | 27 +++++++++++----- src/core/domains/zonotope/codac2_Zonotope.h | 21 ++++++++---- .../analytic/codac2_AnalyticFunction.h | 6 ++-- src/graphics/figures/codac2_Figure2D.cpp | 12 +++---- src/graphics/figures/codac2_Figure2D.h | 10 +++--- src/graphics/figures/codac2_Figure3D.cpp | 24 +++++++------- src/graphics/figures/codac2_Figure3D.h | 2 +- tests/CMakeLists.txt | 3 +- .../zonotope/codac2_tests_Parallelepiped.cpp | 2 +- .../zonotope/codac2_tests_Parallelepiped.py | 2 +- .../codac2_tests_Parallelepiped_eval.cpp | 10 +++--- .../codac2_tests_Parallelepiped_eval.py | 10 +++--- .../zonotope/codac2_tests_Zonotope.cpp | 28 ++++++++++++++++ .../domains/zonotope/codac2_tests_Zonotope.py | 30 ++++++++++++++++++ tests/core/peibos/codac2_tests_peibos.cpp | 12 +++---- tests/core/peibos/codac2_tests_peibos.py | 12 +++---- 22 files changed, 181 insertions(+), 90 deletions(-) create mode 100644 tests/core/domains/zonotope/codac2_tests_Zonotope.cpp create mode 100644 tests/core/domains/zonotope/codac2_tests_Zonotope.py diff --git a/doc/manual/development/changelog.rst b/doc/manual/development/changelog.rst index dc12a2076..1002ecf90 100644 --- a/doc/manual/development/changelog.rst +++ b/doc/manual/development/changelog.rst @@ -3,6 +3,14 @@ Changelog ========= +Upcoming version +**************** + +Pull Request #379 from godardma (27/04) +--------------------------------------- + +``Zonotope`` and ``Parallelepiped`` center is now ``c`` instead of ``z`` to avoid ambiguity. + Version 2.0.2 ************* diff --git a/doc/manual/manual/geometry/zonotope.png b/doc/manual/manual/geometry/zonotope.png index fb74b67182c2107185837d61145d7d2baa13049d..cc0584583f372a1e46d73b5f1d720087671cb7e9 100644 GIT binary patch literal 65968 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®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 literal 12274 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 L_v) +void generate_vertices(Index i, Index n, const Vector& c, const Matrix& A, std::vector& L_v) { if (i == n) { - L_v.push_back(z); + L_v.push_back(c); } else if (i Parallelepiped::vertices() const { std::vector L_v; - generate_vertices(0, z.size(),z,A,L_v); + generate_vertices(0, c.size(),c,A,L_v); return L_v; } @@ -46,9 +46,9 @@ BoolInterval Parallelepiped::contains(const Vector& v) const BoolInterval Parallelepiped::is_superset(const IntervalVector& x) const { assert_release(A.rows() == A.cols() && "Matrix A must be square to check containment."); - assert_release(x.size() == z.size() && "Point dimension must match parallelepiped dimension."); + assert_release(x.size() == c.size() && "Point dimension must match parallelepiped dimension."); - IntervalVector B = inverse_enclosure(A)*(x - z); + IntervalVector B = inverse_enclosure(A)*(x - c); IntervalVector IV = IntervalVector::constant(A.cols(),{-1,1}); if (!(B.intersects(IV))) diff --git a/src/core/domains/zonotope/codac2_Parallelepiped.h b/src/core/domains/zonotope/codac2_Parallelepiped.h index 19b6d9338..20e17bff1 100644 --- a/src/core/domains/zonotope/codac2_Parallelepiped.h +++ b/src/core/domains/zonotope/codac2_Parallelepiped.h @@ -21,9 +21,9 @@ namespace codac2 /** * \class Parallelepiped * - * \brief Class representing a parallelepiped \f$\mathbf{z} + \mathbf{A}\cdot[-1,1]^m\f$ + * \brief Class representing a parallelepiped \f$\mathbf{c} + \mathbf{A}\cdot[-1,1]^m\f$ * - * This class represents a parallelepiped in n-dimensional space, defined by a center point \f$\mathbf{z}\f$ and a shape matrix \f$\mathbf{A}\f$. + * This class represents a parallelepiped in n-dimensional space, defined by a center point \f$\mathbf{c}\f$ and a shape matrix \f$\mathbf{A}\f$. * * A parallelepiped is a special case of a zonotope where the shape matrix \f$\mathbf{A}\f$ has \f$m\f$ columns with \f$m \leqslant n\f$. */ @@ -34,10 +34,10 @@ namespace codac2 /** * \brief Constructs a n-parallelepiped object with a given center and shape matrix * - * \param z Center of the parallelepiped (n-dimensional vector) + * \param c Center of the parallelepiped (n-dimensional vector) * \param A Shape matrix of the parallelepiped (\f$n\times m\f$ matrix with \f$m \leqslant n\f$) */ - Parallelepiped(const Vector& z, const Matrix& A); + Parallelepiped(const Vector& c, const Matrix& A); /** * \brief Computes the vertices of the parallelepiped diff --git a/src/core/domains/zonotope/codac2_Zonotope.cpp b/src/core/domains/zonotope/codac2_Zonotope.cpp index 9091e7f4d..8eb9dabe8 100644 --- a/src/core/domains/zonotope/codac2_Zonotope.cpp +++ b/src/core/domains/zonotope/codac2_Zonotope.cpp @@ -11,31 +11,42 @@ using namespace codac2; -Zonotope::Zonotope(const Vector& z_, const Matrix& A_) - : z(z_), A(A_) +Zonotope::Zonotope(const Vector& c_, const Matrix& A_) + : c(c_), A(A_) { - assert_release(z.size() == A.rows()); + assert_release(c.size() == A.rows()); } IntervalVector Zonotope::box() const { - return z + (A.template cast())*IntervalVector::constant(A.cols(),{-1,1}); + return c + (A.template cast())*IntervalVector::constant(A.cols(),{-1,1}); } Zonotope Zonotope::proj(const std::vector& indices) const { assert_release(*std::min_element(indices.begin(), indices.end()) >= 0 && "indices out of range"); - assert_release(*std::max_element(indices.begin(), indices.end()) <= z.size() && "indices out of range"); + assert_release(*std::max_element(indices.begin(), indices.end()) <= c.size() && "indices out of range"); Matrix A_cropped (indices.size(), A.cols()); - Vector z_cropped (indices.size()); + Vector c_cropped (indices.size()); for (size_t i = 0; i < indices.size(); ++i) { A_cropped.row(i) = A.row(indices[i]); - z_cropped[i] = z[indices[i]]; + c_cropped[i] = c[indices[i]]; } - return Zonotope(z_cropped, A_cropped); + return Zonotope(c_cropped, A_cropped); } +Zonotope Zonotope::operator+(const Zonotope& zonotope) +{ + assert_release(c.size() == zonotope.c.size() && "Zonotopes must have the same dimension"); + + Vector c_sum = c + zonotope.c; + Matrix A_sum (A.rows(), A.cols() + zonotope.A.cols()); + + A_sum << A, zonotope.A; + + return Zonotope(c_sum, A_sum); +} diff --git a/src/core/domains/zonotope/codac2_Zonotope.h b/src/core/domains/zonotope/codac2_Zonotope.h index 863a2681b..ad1255b61 100644 --- a/src/core/domains/zonotope/codac2_Zonotope.h +++ b/src/core/domains/zonotope/codac2_Zonotope.h @@ -18,11 +18,11 @@ namespace codac2 { /** * \class Zonotope - * \brief Class representing a zonotope \f$\mathbf{z} + \mathbf{A}\cdot[-1,1]^m\f$ + * \brief Class representing a zonotope \f$\mathbf{c} + \mathbf{A}\cdot[-1,1]^m\f$ * - * This class represents a zonotope in n-dimensional space, defined by a center point \f$\mathbf{z}\f$ and a shape matrix \f$\mathbf{A}\f$. + * This class represents a zonotope in n-dimensional space, defined by a center point \f$\mathbf{c}\f$ and a shape matrix \f$\mathbf{A}\f$. * - * The vector \f$\mathbf{z}\f$ and each column of the matrix \f$\mathbf{A}\f$ must have the same dimension \f$n\f$, but the matrix \f$\mathbf{A}\f$ can have any number of columns \f$m\f$. + * The vector \f$\mathbf{c}\f$ and each column of the matrix \f$\mathbf{A}\f$ must have the same dimension \f$n\f$, but the matrix \f$\mathbf{A}\f$ can have any number of columns \f$m\f$. */ class Zonotope { @@ -31,10 +31,10 @@ namespace codac2 /** * \brief Constructs a n-zonotope object with a given center and shape matrix * - * \param z Center of the zonotope (n-dimensional vector) + * \param c Center of the zonotope (n-dimensional vector) * \param A Shape matrix of the zonotope (\f$n\times m\f$ matrix) */ - Zonotope(const Vector& z, const Matrix& A); + Zonotope(const Vector& c, const Matrix& A); /** * \brief Computes the axis-aligned bounding box of the zonotope @@ -52,10 +52,19 @@ namespace codac2 */ Zonotope proj(const std::vector& indices) const; + /** + * \brief Computes the Minkowski sum of this Zonotope with another Zonotope + * + * \param zonotope The other Zonotope to add to this one + * + * \return A new Zonotope object representing the Minkowski sum of the two Zonotopes + */ + Zonotope operator+(const Zonotope& zonotope); + /** * \brief Center of the zonotope */ - Vector z; + Vector c; /** * \brief Shape matrix of the zonotope diff --git a/src/core/functions/analytic/codac2_AnalyticFunction.h b/src/core/functions/analytic/codac2_AnalyticFunction.h index 337a64d03..0fe6fd645 100644 --- a/src/core/functions/analytic/codac2_AnalyticFunction.h +++ b/src/core/functions/analytic/codac2_AnalyticFunction.h @@ -220,17 +220,17 @@ namespace codac2 "Parallelepiped evaluation requires at least one input."); IntervalVector Y = this->eval(((typename Wrapper::Domain)(x)).mid()...); - Vector z = Y.mid(); + Vector c = Y.mid(); Matrix A = this->diff(((typename Wrapper::Domain)(x)).mid()...).mid(); // Maximum error computation - double rho = error_peibos(Y, z, this->diff(x...), A, cart_prod(x...)); + double rho = error_peibos(Y, c, this->diff(x...), A, cart_prod(x...)); // Inflation of the parallelepiped Matrix A_inf = inflate_flat_parallelepiped(A, (cart_prod(x...).template cast()).rad(), rho); - return Parallelepiped(z, A_inf); + return Parallelepiped(c, A_inf); } Index output_size() const diff --git a/src/graphics/figures/codac2_Figure2D.cpp b/src/graphics/figures/codac2_Figure2D.cpp index 53045ebc1..33cbb962b 100644 --- a/src/graphics/figures/codac2_Figure2D.cpp +++ b/src/graphics/figures/codac2_Figure2D.cpp @@ -255,7 +255,7 @@ void Figure2D::draw_zonotope(const Zonotope& z, const StyleProperties& style) } } vector vertices; - Vector point=z.z; + Vector point=z.c; // Start from v[1] maximum (and v[0] min for horizontal side) for (const auto& a : sides) { point+=a.second; @@ -277,17 +277,17 @@ void Figure2D::draw_zonotope(const Zonotope& z, const StyleProperties& style) void Figure2D::draw_parallelepiped(const Parallelepiped& p, const StyleProperties& style) { - assert_release(p.A.is_squared() && p.A.rows() == p.z.size()); - assert_release(p.z.size() == 2); + assert_release(p.A.is_squared() && p.A.rows() == p.c.size()); + assert_release(p.c.size() == 2); auto a1 = p.A.col(0), a2 = p.A.col(1); if (a1.isZero() || a2.isZero()) - draw_polyline({p.z-a1-a2,p.z+a1+a2}, style); + draw_polyline({p.c-a1-a2,p.c+a1+a2}, style); else draw_polygon({ - p.z+a1+a2, p.z-a1+a2, - p.z-a1-a2, p.z+a1-a2 + p.c+a1+a2, p.c-a1+a2, + p.c-a1-a2, p.c+a1-a2 }, style); } diff --git a/src/graphics/figures/codac2_Figure2D.h b/src/graphics/figures/codac2_Figure2D.h index 9a950ea18..3b920c734 100644 --- a/src/graphics/figures/codac2_Figure2D.h +++ b/src/graphics/figures/codac2_Figure2D.h @@ -310,7 +310,7 @@ namespace codac2 void draw_parallelepiped(const Parallelepiped& p, const StyleProperties& style = StyleProperties()); /** - * \brief Draws a zonotope z+sum_i [-1,1] A_i on the figure + * \brief Draws a zonotope c+sum_i [-1,1] A_i on the figure * * \param z Zonotope to draw (center and shape matrix) * \param style Style of the zonotope (edge color and fill color) @@ -856,15 +856,15 @@ namespace codac2 } /** - * \brief Draws a zonotope z+sum_i [-1,1] A_i on the figure + * \brief Draws a zonotope c+sum_i [-1,1] A_i on the figure * - * \param z Zonotope to draw (center and shape matrix) + * \param c Zonotope to draw (center and shape matrix) * \param style Style of the zonotope (edge color and fill color) */ - static void draw_zonotope(const Zonotope& z, const StyleProperties& style = StyleProperties()) + static void draw_zonotope(const Zonotope& c, const StyleProperties& style = StyleProperties()) { auto_init(); - selected_fig()->draw_zonotope(z,style); + selected_fig()->draw_zonotope(c,style); } /** diff --git a/src/graphics/figures/codac2_Figure3D.cpp b/src/graphics/figures/codac2_Figure3D.cpp index 86d7fcdc0..52eb04d54 100644 --- a/src/graphics/figures/codac2_Figure3D.cpp +++ b/src/graphics/figures/codac2_Figure3D.cpp @@ -105,19 +105,19 @@ void Figure3D::draw_parallelogram(const Vector &c, const Matrix &A, void Figure3D::draw_parallelepiped(const Parallelepiped& p, const StyleProperties& style) { - assert_release(p.z.size() == 3); + assert_release(p.c.size() == 3); assert_release(p.A.rows() == 3 && p.A.cols() == 3); this->set_style_internal(style); - size_t ip0 = this->move_write_v(p.z,p.A,Vector({-1,-1,-1})); - size_t ip1 = this->move_write_v(p.z,p.A,Vector({-1,-1,1})); - size_t ip2 = this->move_write_v(p.z,p.A,Vector({-1,1,-1})); - size_t ip3 = this->move_write_v(p.z,p.A,Vector({-1,1,1})); - size_t ip4 = this->move_write_v(p.z,p.A,Vector({1,-1,-1})); - size_t ip5 = this->move_write_v(p.z,p.A,Vector({1,-1,1})); - size_t ip6 = this->move_write_v(p.z,p.A,Vector({1,1,-1})); - size_t ip7 = this->move_write_v(p.z,p.A,Vector({1,1,1})); + size_t ip0 = this->move_write_v(p.c,p.A,Vector({-1,-1,-1})); + size_t ip1 = this->move_write_v(p.c,p.A,Vector({-1,-1,1})); + size_t ip2 = this->move_write_v(p.c,p.A,Vector({-1,1,-1})); + size_t ip3 = this->move_write_v(p.c,p.A,Vector({-1,1,1})); + size_t ip4 = this->move_write_v(p.c,p.A,Vector({1,-1,-1})); + size_t ip5 = this->move_write_v(p.c,p.A,Vector({1,-1,1})); + size_t ip6 = this->move_write_v(p.c,p.A,Vector({1,1,-1})); + size_t ip7 = this->move_write_v(p.c,p.A,Vector({1,1,1})); _file << "f " << ip0 << " " << ip1 << " " << ip3 << " " << ip2 << "\n"; _file << "f " << ip4 << " " << ip5 << " " << ip7 << " " << ip6 << "\n"; @@ -137,7 +137,7 @@ void Figure3D::draw_box(const IntervalVector& x, const StyleProperties& style) } void Figure3D::draw_zonotope(const Zonotope& z, const StyleProperties& style) { - assert_release(z.z.size() == 3); + assert_release(z.c.size() == 3); Matrix id = Matrix::Identity(3,3); this->set_style_internal(style); lock_style=true; @@ -186,8 +186,8 @@ void Figure3D::draw_zonotope(const Zonotope& z, const StyleProperties& style) { R1 -= Ak; } } - this->draw_parallelogram(z.z,id,R1+R2,Ai,Aj,style); - this->draw_parallelogram(z.z,id,-R1+R2,Ai,Aj,style); + this->draw_parallelogram(z.c,id,R1+R2,Ai,Aj,style); + this->draw_parallelogram(z.c,id,-R1+R2,Ai,Aj,style); } } lock_style=false; diff --git a/src/graphics/figures/codac2_Figure3D.h b/src/graphics/figures/codac2_Figure3D.h index 3887bdee1..15ea876c3 100644 --- a/src/graphics/figures/codac2_Figure3D.h +++ b/src/graphics/figures/codac2_Figure3D.h @@ -116,7 +116,7 @@ namespace codac2 void draw_parallelepiped(const Parallelepiped& p, const StyleProperties& style = { Color::dark_gray(0.5) }); /** - * \brief Draws a zonotope z+sum_i [-1,1] A_i on the figure + * \brief Draws a zonotope c+sum_i [-1,1] A_i on the figure * * \param z Zonotope to draw (center and shape matrix) * \param style Style of the zonotope (edge color) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c38934659..4ea53368d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,8 +54,9 @@ list(APPEND SRC_TESTS # listing files without extension core/domains/interval/codac2_tests_IntervalMatrix core/domains/interval/codac2_tests_IntervalVector ../doc/manual/manual/intervals/src - core/domains/zonotope/codac2_tests_Parallelepiped core/domains/zonotope/codac2_tests_Parallelepiped_eval + core/domains/zonotope/codac2_tests_Parallelepiped + core/domains/zonotope/codac2_tests_Zonotope core/domains/tube/codac2_tests_TDomain core/domains/tube/codac2_tests_Slice core/domains/tube/codac2_tests_Slice_polygon diff --git a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp index 2c5766d4b..2d203740b 100644 --- a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp +++ b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp @@ -26,7 +26,7 @@ TEST_CASE("Parallelepiped") CHECK((p.is_superset(IntervalVector({{0.,5.},{2.,7.},{4.,9.}})))==BoolInterval::UNKNOWN); Zonotope z = p.proj({2,1,0}); - CHECK(z.z == Vector({4,2,0})); + CHECK(z.c == Vector({4,2,0})); CHECK(z.A == Matrix({{0.,1.,1.},{0.,1.,0.},{0.5,0.,0.}})); CHECK(z.box() == IntervalVector({{2.,6.},{1.,3.},{-0.5,0.5}})); } diff --git a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py index 75b4cd987..c0eaa0a17 100644 --- a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py +++ b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py @@ -27,7 +27,7 @@ def test_parallelepiped(self): self.assertTrue((p.is_superset(IntervalVector([[0.,5.],[2.,7.],[4.,9.]])))==BoolInterval.UNKNOWN) z = p.proj([2,1,0]) - self.assertTrue(z.z == Vector([4,2,0])) + self.assertTrue(z.c == Vector([4,2,0])) self.assertTrue(z.A == Matrix([[0,1,1],[0,1,0],[0.5,0,0]])) self.assertTrue(z.box() == IntervalVector([[2,6],[1,3],[-0.5,0.5]])) diff --git a/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.cpp b/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.cpp index 0ad038b38..9dd1705b2 100644 --- a/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.cpp +++ b/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.cpp @@ -28,17 +28,17 @@ TEST_CASE("Parallelepiped_eval") auto p1a = f1.parallelepiped_eval(Interval(-0.1,0.1)); auto p1b = f1.parallelepiped_eval(1.0); - CHECK(Approx(p1a.z,1e-6) == Vector({0,0})); + CHECK(Approx(p1a.c,1e-6) == Vector({0,0})); CHECK(Approx(p1a.A,1e-6) == Matrix({{0.12,0},{0,0.02}})); - CHECK(Approx(p1b.z,1e-6) == Vector({1,1})); + CHECK(Approx(p1b.c,1e-6) == Vector({1,1})); CHECK(Approx(p1b.A,1e-6) == Matrix({{0,0},{0,0}})); auto pa = f2.parallelepiped_eval(Interval(-0.1,0.1), Interval(-0.1,0.1)); auto pb = f2.parallelepiped_eval(1.0, Interval(-1,1)); - CHECK(Approx(pa.z,1e-6) == Vector({0,0,0})); + CHECK(Approx(pa.c,1e-6) == Vector({0,0,0})); CHECK(Approx(pa.A,1e-6) == Matrix({{0.14,0,0},{0,0.14,0},{0,0,0.04}})); - CHECK(Approx(pb.z,1e-6) == Vector({1,0,1})); + CHECK(Approx(pb.c,1e-6) == Vector({1,0,1})); CHECK(Approx(pb.A,1e-5) == Matrix({{0.894428,0,1.78886},{0,3,0},{1.78886,0,-0.894427}})); @@ -55,7 +55,7 @@ TEST_CASE("Parallelepiped_eval") auto p2 = f2.parallelepiped_eval(X0,Y0); auto p3 = f3.parallelepiped_eval(IntervalVector({X0,Y0})); - CHECK(Approx(p2.z,1e-6) == p3.z); + CHECK(Approx(p2.c,1e-6) == p3.c); CHECK(Approx(p2.A,1e-6) == p3.A); y0+=dx; } diff --git a/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.py b/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.py index c7dcd18ed..5cac5ffe7 100644 --- a/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.py +++ b/tests/core/domains/zonotope/codac2_tests_Parallelepiped_eval.py @@ -27,17 +27,17 @@ def test_parallelepiped_eval(self): p1a = f1.parallelepiped_eval(Interval(-0.1,0.1)) p1b = f1.parallelepiped_eval(1.0) - self.assertTrue(Approx(p1a.z,1e-6)==Vector([0.0,0.0])) + self.assertTrue(Approx(p1a.c,1e-6)==Vector([0.0,0.0])) self.assertTrue(Approx(p1a.A,1e-6)==Matrix([[0.12,0.0],[0.0,0.02]])) - self.assertTrue(Approx(p1b.z,1e-6)==Vector([1.0,1.0])) + self.assertTrue(Approx(p1b.c,1e-6)==Vector([1.0,1.0])) self.assertTrue(Approx(p1b.A,1e-6)==Matrix([[0.0,0.0],[0.0,0.0]])) pa = f2.parallelepiped_eval(Interval(-0.1,0.1), Interval(-0.1,0.1)) pb = f2.parallelepiped_eval(1.0,Interval(-1,1)) - self.assertTrue(Approx(pa.z,1e-6)==Vector([0,0,0])) + self.assertTrue(Approx(pa.c,1e-6)==Vector([0,0,0])) self.assertTrue(Approx(pa.A,1e-6)==Matrix([[0.14,0,0],[0,0.14,0],[0,0,0.04]])) - self.assertTrue(Approx(pb.z,1e-6)==Vector([1,0,1])) + self.assertTrue(Approx(pb.c,1e-6)==Vector([1,0,1])) self.assertTrue(Approx(pb.A,1e-5)==Matrix([[0.894428,0,1.78886],[0,3,0],[1.78886,0,-0.894427]])) @@ -52,7 +52,7 @@ def test_parallelepiped_eval(self): p2 = f2.parallelepiped_eval(X0,Y0) p3 = f3.parallelepiped_eval(IntervalVector([X0,Y0])) - self.assertTrue(Approx(p2.z,1e-6)==p3.z) + self.assertTrue(Approx(p2.c,1e-6)==p3.c) self.assertTrue(Approx(p2.A,1e-6)==p3.A) y0 += dx x0 += dx diff --git a/tests/core/domains/zonotope/codac2_tests_Zonotope.cpp b/tests/core/domains/zonotope/codac2_tests_Zonotope.cpp new file mode 100644 index 000000000..60da5d7ea --- /dev/null +++ b/tests/core/domains/zonotope/codac2_tests_Zonotope.cpp @@ -0,0 +1,28 @@ +/** + * Codac tests + * ---------------------------------------------------------------------------- + * \date 2025 + * \author Maël Godard + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#include +#include + +using namespace std; +using namespace codac2; + +TEST_CASE("Zonotope") +{ + Zonotope Z1 (Vector({2,1}), Matrix({{0.2 ,0.08}, {0.04,0.18}})); + Zonotope Z2 (Vector({2,0.5}), Matrix({{-0.2}, {0.1}})); + auto Zs = Z1+Z2; + + Vector c ({4,1.5}); + Matrix A ({{0.2, 0.08, -0.2}, + {0.04, 0.18, 0.1}}); + CHECK(Zs.c == c); + CHECK(Zs.A == A); +} + diff --git a/tests/core/domains/zonotope/codac2_tests_Zonotope.py b/tests/core/domains/zonotope/codac2_tests_Zonotope.py new file mode 100644 index 000000000..46ddd969a --- /dev/null +++ b/tests/core/domains/zonotope/codac2_tests_Zonotope.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# Codac tests +# ---------------------------------------------------------------------------- +# \date 2025 +# \author Maël Godard +# \copyright Copyright 2024 Codac Team +# \license GNU Lesser General Public License (LGPL) + +import unittest +from codac import * +import sys +import math + +class TestZonotope(unittest.TestCase): + + def test_zonotope(self): + + Z1 = Zonotope(Vector([2,1]), Matrix([[0.2 ,0.08], [0.04,0.18]])) + Z2 = Zonotope(Vector([2,0.5]), Matrix([[-0.2], [0.1]])) + Zs = Z1+Z2 + + c = Vector([4,1.5]) + A = Matrix([[0.2 ,0.08, -0.2], + [0.04, 0.18, 0.1]]) + self.assertTrue(Zs.c == c) + self.assertTrue(Zs.A == A) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/core/peibos/codac2_tests_peibos.cpp b/tests/core/peibos/codac2_tests_peibos.cpp index bda717048..755b741e7 100644 --- a/tests/core/peibos/codac2_tests_peibos.cpp +++ b/tests/core/peibos/codac2_tests_peibos.cpp @@ -69,12 +69,12 @@ TEST_CASE("Peibos") CHECK(v_par_3d.size() == 6); - CHECK(Approx(v_par_3d[0].z,1e-6) == Vector({1.,0.,0.})); - CHECK(Approx(v_par_3d[1].z,1e-6) == Vector({0.,1.,0.})); - CHECK(Approx(v_par_3d[2].z,1e-6) == Vector({-1.,0.,0.})); - CHECK(Approx(v_par_3d[3].z,1e-6) == Vector({0.,-1.,0.})); - CHECK(Approx(v_par_3d[4].z,1e-6) == Vector({0.,0.,-1.})); - CHECK(Approx(v_par_3d[5].z,1e-6) == Vector({0.,0.,1.})); + CHECK(Approx(v_par_3d[0].c,1e-6) == Vector({1.,0.,0.})); + CHECK(Approx(v_par_3d[1].c,1e-6) == Vector({0.,1.,0.})); + CHECK(Approx(v_par_3d[2].c,1e-6) == Vector({-1.,0.,0.})); + CHECK(Approx(v_par_3d[3].c,1e-6) == Vector({0.,-1.,0.})); + CHECK(Approx(v_par_3d[4].c,1e-6) == Vector({0.,0.,-1.})); + CHECK(Approx(v_par_3d[5].c,1e-6) == Vector({0.,0.,1.})); double a = 4.35066; diff --git a/tests/core/peibos/codac2_tests_peibos.py b/tests/core/peibos/codac2_tests_peibos.py index 66042d8e0..f4f12191e 100644 --- a/tests/core/peibos/codac2_tests_peibos.py +++ b/tests/core/peibos/codac2_tests_peibos.py @@ -63,12 +63,12 @@ def test_peibos(self): self.assertTrue(len(v_par_3d) == 6) - self.assertTrue(Approx(v_par_3d[0].z,1e-6) == Vector([1.,0.,0.])) - self.assertTrue(Approx(v_par_3d[1].z,1e-6) == Vector([0.,1.,0.])) - self.assertTrue(Approx(v_par_3d[2].z,1e-6) == Vector([-1,0.,0.])) - self.assertTrue(Approx(v_par_3d[3].z,1e-6) == Vector([0.,-1.,0.])) - self.assertTrue(Approx(v_par_3d[4].z,1e-6) == Vector([0.,0.,-1.])) - self.assertTrue(Approx(v_par_3d[5].z,1e-6) == Vector([0.,0.,1.])) + self.assertTrue(Approx(v_par_3d[0].c,1e-6) == Vector([1.,0.,0.])) + self.assertTrue(Approx(v_par_3d[1].c,1e-6) == Vector([0.,1.,0.])) + self.assertTrue(Approx(v_par_3d[2].c,1e-6) == Vector([-1,0.,0.])) + self.assertTrue(Approx(v_par_3d[3].c,1e-6) == Vector([0.,-1.,0.])) + self.assertTrue(Approx(v_par_3d[4].c,1e-6) == Vector([0.,0.,-1.])) + self.assertTrue(Approx(v_par_3d[5].c,1e-6) == Vector([0.,0.,1.])) a = 4.35066 From f815210ad58823d5e8474af095d1fe1dc01513bc Mon Sep 17 00:00:00 2001 From: godardma Date: Thu, 30 Apr 2026 22:46:33 +0200 Subject: [PATCH 2/9] [graphics] possibility to choose origin for axes --- python/src/graphics/figures/codac2_py_Figure3D.cpp | 4 ++-- src/graphics/figures/codac2_Figure3D.cpp | 5 +++-- src/graphics/figures/codac2_Figure3D.h | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/python/src/graphics/figures/codac2_py_Figure3D.cpp b/python/src/graphics/figures/codac2_py_Figure3D.cpp index 24dbf165f..afbc719d9 100644 --- a/python/src/graphics/figures/codac2_py_Figure3D.cpp +++ b/python/src/graphics/figures/codac2_py_Figure3D.cpp @@ -35,8 +35,8 @@ void export_Figure3D(py::module& m) CONST_STRING_REF_FIGURE3D_NAME_CONST) .def("draw_axes", &Figure3D::draw_axes, - VOID_FIGURE3D_DRAW_AXES_DOUBLE, - "size"_a=1.0) + VOID_FIGURE3D_DRAW_AXES_DOUBLE_CONST_VECTOR_REF, + "size"_a=1.0, "origin"_a=Vector::Zero(3)) // Geometric shapes .def("draw_triangle", (void(Figure3D::*)(const Vector &c, const Matrix &A, const Vector &p1, const Vector &p2, const Vector &p3, const StyleProperties &s))&Figure3D::draw_triangle, diff --git a/src/graphics/figures/codac2_Figure3D.cpp b/src/graphics/figures/codac2_Figure3D.cpp index 52eb04d54..ca0aee3ef 100644 --- a/src/graphics/figures/codac2_Figure3D.cpp +++ b/src/graphics/figures/codac2_Figure3D.cpp @@ -214,10 +214,11 @@ void Figure3D::draw_arrow(const Vector& c, const Matrix &A, } -void Figure3D::draw_axes(double size) +void Figure3D::draw_axes(double size, const Vector& origin) { + assert_release(origin.size()==3); const std::string name="axes"; - Vector z = Vector::zero(3); + Vector z = origin; // X axis Matrix AX = Matrix::Identity(3,3); draw_arrow(z,size*AX,StyleProperties(Color::red(),name)); diff --git a/src/graphics/figures/codac2_Figure3D.h b/src/graphics/figures/codac2_Figure3D.h index 15ea876c3..442293832 100644 --- a/src/graphics/figures/codac2_Figure3D.h +++ b/src/graphics/figures/codac2_Figure3D.h @@ -146,8 +146,9 @@ namespace codac2 * \brief Draws the (x,y,z) axes on the figure in red, green and blue * * \param size Size of the axes + * \param origin Origin of the axes */ - void draw_axes(double size = 1.0); + void draw_axes(double size = 1.0, const Vector& origin = Vector::Zero(3)); /** * \brief Draws a parametric surface From 8a68ae878ce27a8b7759fdb7292d8c1cca6687ed Mon Sep 17 00:00:00 2001 From: godardma Date: Mon, 4 May 2026 17:24:09 +0200 Subject: [PATCH 3/9] [graphics] patch for python bindings --- python/src/graphics/figures/codac2_py_Figure3D.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/graphics/figures/codac2_py_Figure3D.cpp b/python/src/graphics/figures/codac2_py_Figure3D.cpp index afbc719d9..58068aba5 100644 --- a/python/src/graphics/figures/codac2_py_Figure3D.cpp +++ b/python/src/graphics/figures/codac2_py_Figure3D.cpp @@ -36,7 +36,7 @@ void export_Figure3D(py::module& m) .def("draw_axes", &Figure3D::draw_axes, VOID_FIGURE3D_DRAW_AXES_DOUBLE_CONST_VECTOR_REF, - "size"_a=1.0, "origin"_a=Vector::Zero(3)) + "size"_a=1.0, "origin"_a=py::none()) // Geometric shapes .def("draw_triangle", (void(Figure3D::*)(const Vector &c, const Matrix &A, const Vector &p1, const Vector &p2, const Vector &p3, const StyleProperties &s))&Figure3D::draw_triangle, From 0031132f799cf053cdae44b5f0219f265bf08a91 Mon Sep 17 00:00:00 2001 From: godardma Date: Wed, 6 May 2026 13:39:06 +0200 Subject: [PATCH 4/9] [graphics] patch for python bindings --- python/src/graphics/figures/codac2_py_Figure3D.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/src/graphics/figures/codac2_py_Figure3D.cpp b/python/src/graphics/figures/codac2_py_Figure3D.cpp index 58068aba5..620e9ecdc 100644 --- a/python/src/graphics/figures/codac2_py_Figure3D.cpp +++ b/python/src/graphics/figures/codac2_py_Figure3D.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "codac2_py_Figure3D_docs.h" // Generated file from Doxygen XML (doxygen2docstring.py): #include "codac2_py_matlab.h" @@ -36,7 +37,7 @@ void export_Figure3D(py::module& m) .def("draw_axes", &Figure3D::draw_axes, VOID_FIGURE3D_DRAW_AXES_DOUBLE_CONST_VECTOR_REF, - "size"_a=1.0, "origin"_a=py::none()) + "size"_a=1.0, "origin"_a=Vector::zero(3)) // Geometric shapes .def("draw_triangle", (void(Figure3D::*)(const Vector &c, const Matrix &A, const Vector &p1, const Vector &p2, const Vector &p3, const StyleProperties &s))&Figure3D::draw_triangle, From d221c852487bacf6c528336f99b42aaab55cec2f Mon Sep 17 00:00:00 2001 From: godardma Date: Wed, 6 May 2026 14:09:43 +0200 Subject: [PATCH 5/9] [graphics] completed 3D graphic example --- examples/06_graphics_3D/main.cpp | 4 +++- examples/06_graphics_3D/main.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/06_graphics_3D/main.cpp b/examples/06_graphics_3D/main.cpp index 3736d7950..a77f85495 100644 --- a/examples/06_graphics_3D/main.cpp +++ b/examples/06_graphics_3D/main.cpp @@ -38,7 +38,9 @@ int main() Figure3D fig_examples("3D examples"); - fig_examples.draw_axes(1.0); + fig_examples.draw_axes(); + fig_examples.draw_axes(0.5); + fig_examples.draw_axes(2.0,{0.5,0.5,0.5}); fig_examples.draw_triangle({1,0,0},{0,1,0},{0,0,1},{ Color::dark_green(0.5), "triangle1" }); fig_examples.draw_triangle({2,0,0},{{-1,0,0},{0,1,1},{0,0,-1}}, {1,0,0},{0,1,0},{0,0,1},Color::purple(0.5)); diff --git a/examples/06_graphics_3D/main.py b/examples/06_graphics_3D/main.py index af87fcd5d..4ebf27354 100644 --- a/examples/06_graphics_3D/main.py +++ b/examples/06_graphics_3D/main.py @@ -22,7 +22,9 @@ fig_sep.draw_paving(p_sep) fig_examples = Figure3D("3D examples") -fig_examples.draw_axes(1.0) +fig_examples.draw_axes() +fig_examples.draw_axes(0.5) +fig_examples.draw_axes(2.0,[0.5,0.5,0.5]) fig_examples.draw_triangle([1,0,0],[0,1,0],[0,0,1],StyleProperties(Color.dark_green(0.5),"triangle")) fig_examples.draw_triangle([2,0,0], Matrix([[2,0,0],[-1,0,0],[0,1,1]]), From 1860083a7f895e04fb37d83eea1ae67585aaf747 Mon Sep 17 00:00:00 2001 From: godardma Date: Wed, 6 May 2026 16:37:58 +0200 Subject: [PATCH 6/9] [ctc] Adding CtcParallelepiped with tests and doc --- doc/manual/index.rst | 1 + .../geometric/ctcparallelepiped.rst | 37 +++++++++++++++ .../manual/contractors/geometric/index.rst | 1 + .../manual/contractors/geometric/src.cpp | 13 ++++++ .../manual/contractors/geometric/src.py | 10 +++++ doc/manual/manual/contractors/index.rst | 1 + python/src/core/CMakeLists.txt | 1 + python/src/core/codac2_py_core.cpp | 2 + .../codac2_py_CtcParallelepiped.cpp | 36 +++++++++++++++ src/core/CMakeLists.txt | 2 + .../contractors/codac2_CtcParallelepiped.cpp | 27 +++++++++++ .../contractors/codac2_CtcParallelepiped.h | 45 +++++++++++++++++++ tests/CMakeLists.txt | 1 + .../codac2_tests_CtcParallelepiped.cpp | 44 ++++++++++++++++++ .../codac2_tests_CtcParallelepiped.py | 43 ++++++++++++++++++ 15 files changed, 264 insertions(+) create mode 100644 doc/manual/manual/contractors/geometric/ctcparallelepiped.rst create mode 100644 python/src/core/contractors/codac2_py_CtcParallelepiped.cpp create mode 100644 src/core/contractors/codac2_CtcParallelepiped.cpp create mode 100644 src/core/contractors/codac2_CtcParallelepiped.h create mode 100644 tests/core/contractors/codac2_tests_CtcParallelepiped.cpp create mode 100644 tests/core/contractors/codac2_tests_CtcParallelepiped.py diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 8c38197fd..2eeaa077c 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -246,6 +246,7 @@ User manual * :ref:`sec-ctc-dynamic-ctclohner` * Geometric contractors * :ref:`sec-ctc-geom-ctcdist` + * :ref:`sec-ctc-geom-ctcparallelepiped` * :ref:`sec-ctc-geom-ctcpolar` * :ref:`sec-ctc-geom-ctcvisible` * CtcSegment diff --git a/doc/manual/manual/contractors/geometric/ctcparallelepiped.rst b/doc/manual/manual/contractors/geometric/ctcparallelepiped.rst new file mode 100644 index 000000000..45f32549f --- /dev/null +++ b/doc/manual/manual/contractors/geometric/ctcparallelepiped.rst @@ -0,0 +1,37 @@ +.. _sec-ctc-geom-ctcparallelepiped: + +The CtcParallelpiped contractor +=============================== + + Main author: `Maël Godard `_ + +.. doxygenclass:: codac2::CtcParallelepiped + :project: codac + +Methods +------- + +.. doxygenfunction:: codac2::CtcParallelepiped::contract(IntervalVector&) const + :project: codac + +.. tabs:: + + .. group-tab:: Python + + .. literalinclude:: src.py + :language: py + :start-after: [ctcparallelepiped-1-beg] + :end-before: [ctcparallelepiped-1-end] + :dedent: 4 + + .. group-tab:: C++ + + .. literalinclude:: src.cpp + :language: c++ + :start-after: [ctcparallelepiped-1-beg] + :end-before: [ctcparallelepiped-1-end] + :dedent: 4 + +.. admonition:: Technical documentation + + See the `C++ API documentation of this class <../../api/html/classcodac2_1_1_ctc_parallelepiped.html>`_. \ No newline at end of file diff --git a/doc/manual/manual/contractors/geometric/index.rst b/doc/manual/manual/contractors/geometric/index.rst index 938c40df6..98434784e 100644 --- a/doc/manual/manual/contractors/geometric/index.rst +++ b/doc/manual/manual/contractors/geometric/index.rst @@ -4,6 +4,7 @@ Geometric contractors .. toctree:: ctcdist.rst + ctcparallelepiped.rst ctcpolar.rst ctcvisible.rst CtcSegment diff --git a/doc/manual/manual/contractors/geometric/src.cpp b/doc/manual/manual/contractors/geometric/src.cpp index 1eb97ca13..bef9115c5 100644 --- a/doc/manual/manual/contractors/geometric/src.cpp +++ b/doc/manual/manual/contractors/geometric/src.cpp @@ -69,6 +69,19 @@ TEST_CASE("CtcDist - manual") } } +TEST_CASE("CtcParallelepiped - manual") +{ + { + // [ctcparallelepiped-1-beg] + IntervalVector x ({{0,5}, {0,5}}); + Parallelepiped p (Vector({1.5,2.8}), Matrix({{0.5,0.4},{0,0.2}})); + CtcParallelepiped c(p); + c.contract(x); + // x = [ [0.599999, 2.40001] ; [2.59999, 3] ] + // [ctcparallelepiped-1-end] + } +} + TEST_CASE("CtcPolar - manual") { { diff --git a/doc/manual/manual/contractors/geometric/src.py b/doc/manual/manual/contractors/geometric/src.py index f65d6e925..325fac7a9 100644 --- a/doc/manual/manual/contractors/geometric/src.py +++ b/doc/manual/manual/contractors/geometric/src.py @@ -65,6 +65,16 @@ def tests_CtcDist_manual(test): #DefaultFigure.draw_circle(b1, y1.lb()); DefaultFigure.draw_circle(b1, y1.ub()) #DefaultFigure.draw_circle(b2, y2.lb()); DefaultFigure.draw_circle(b2, y2.ub()) + def tests_CtcParallelepiped_manual(test): + + # [ctcparallelepiped-1-beg] + x = IntervalVector([[0,5], [0,5]]) + p = Parallelepiped(Vector([1.5,2.8]), Matrix([[0.5,0.4],[0,0.2]])) + c = CtcParallelepiped(p) + x = c.contract(x) + # x = [ [0.599999, 2.40001] ; [2.59999, 3] ] + # [ctcparallelepiped-1-end] + def tests_CtcPolar_manual(test): # [ctcpolar-1-beg] diff --git a/doc/manual/manual/contractors/index.rst b/doc/manual/manual/contractors/index.rst index 0267838da..389b698da 100644 --- a/doc/manual/manual/contractors/index.rst +++ b/doc/manual/manual/contractors/index.rst @@ -8,6 +8,7 @@ Contractors, separators CtcInverse CtcLohner CtcDist + CtcParallelepiped CtcPolar CtcVisible diff --git a/python/src/core/CMakeLists.txt b/python/src/core/CMakeLists.txt index d569f74bd..f3c3965d0 100644 --- a/python/src/core/CMakeLists.txt +++ b/python/src/core/CMakeLists.txt @@ -29,6 +29,7 @@ contractors/codac2_py_CtcLazy.cpp contractors/codac2_py_CtcLohner.cpp contractors/codac2_py_CtcNot.cpp + contractors/codac2_py_CtcParallelepiped.cpp contractors/codac2_py_CtcPointCloud.cpp contractors/codac2_py_CtcPolar.cpp contractors/codac2_py_CtcPolygon.cpp diff --git a/python/src/core/codac2_py_core.cpp b/python/src/core/codac2_py_core.cpp index 370630623..0d7ca7173 100644 --- a/python/src/core/codac2_py_core.cpp +++ b/python/src/core/codac2_py_core.cpp @@ -49,6 +49,7 @@ void export_CtcInter(py::module& m, py::class_,pyCtcInte void export_CtcLazy(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcLohner(py::module& m); void export_CtcNot(py::module& m, py::class_,pyCtcIntervalVector>& ctc); +void export_CtcParallelepiped(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcPointCloud(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcPolar(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcPolygon(py::module& m, py::class_,pyCtcIntervalVector>& ctc); @@ -210,6 +211,7 @@ PYBIND11_MODULE(_core, m) export_CtcLazy(m, py_ctc_iv); export_CtcLohner(m); export_CtcNot(m, py_ctc_iv); + export_CtcParallelepiped(m, py_ctc_iv); export_CtcPointCloud(m, py_ctc_iv); export_CtcPolar(m, py_ctc_iv); export_CtcPolygon(m, py_ctc_iv); diff --git a/python/src/core/contractors/codac2_py_CtcParallelepiped.cpp b/python/src/core/contractors/codac2_py_CtcParallelepiped.cpp new file mode 100644 index 000000000..6905f3d97 --- /dev/null +++ b/python/src/core/contractors/codac2_py_CtcParallelepiped.cpp @@ -0,0 +1,36 @@ +/** + * Codac binding (core) + * ---------------------------------------------------------------------------- + * \date 2026 + * \author Maël Godard + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#include +#include +#include +#include +#include +#include "codac2_py_Ctc.h" +#include "codac2_py_CtcParallelepiped_docs.h" // Generated file from Doxygen XML (doxygen2docstring.py): + +using namespace std; +using namespace codac2; +namespace py = pybind11; +using namespace pybind11::literals; + +void export_CtcParallelepiped(py::module& m, py::class_,pyCtcIntervalVector>& pyctc) +{ + py::class_ exported(m, "CtcParallelepiped", pyctc, CTCPARALLELEPIPED_MAIN); + exported + + .def(py::init(), + CTCPARALLELEPIPED_CTCPARALLELEPIPED_CONST_PARALLELEPIPED_REF, + "p"_a) + + ; + + CONTRACT_METHODS(exported, CtcParallelepiped, + VOID_CTCPARALLELEPIPED_CONTRACT_INTERVALVECTOR_REF_CONST) +} \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d06db4fd8..330760139 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -40,6 +40,8 @@ ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcLohner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcLohner.h ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcNot.h + ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcParallelepiped.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcParallelepiped.h ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcUnion.h ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcVisible.cpp ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcVisible.h diff --git a/src/core/contractors/codac2_CtcParallelepiped.cpp b/src/core/contractors/codac2_CtcParallelepiped.cpp new file mode 100644 index 000000000..fcf9fa263 --- /dev/null +++ b/src/core/contractors/codac2_CtcParallelepiped.cpp @@ -0,0 +1,27 @@ +/** + * CtcParallelepiped.cpp + * ---------------------------------------------------------------------------- + * \date 2026 + * \author Maël Godard + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#include "codac2_CtcParallelepiped.h" +#include "codac2_inversion.h" + +// TO DELETE + +#include + +using namespace std; + +namespace codac2 +{ + void CtcParallelepiped::contract(IntervalVector& x) const + { + assert_release(x.size()==_p.c.size()); + IntervalVector x_p = inverse_enclosure(_p.A)*(x - _p.c); // projection in the parallelepiped's coordinate system + x &= _p.c + _p.A*(x_p & IntervalVector::constant(x.size(),{-1,1})); + } +} \ No newline at end of file diff --git a/src/core/contractors/codac2_CtcParallelepiped.h b/src/core/contractors/codac2_CtcParallelepiped.h new file mode 100644 index 000000000..661aeff26 --- /dev/null +++ b/src/core/contractors/codac2_CtcParallelepiped.h @@ -0,0 +1,45 @@ +/** + * \file codac2_CtcParallelepiped.h + * ---------------------------------------------------------------------------- + * \date 2026 + * \author Simon Rohou, Benoit Desrochers + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#pragma once + +#include "codac2_Parallelepiped.h" +#include "codac2_Ctc.h" + +namespace codac2 +{ + /** + * \class CtcParallelepiped + * \brief Implements the contractor associated with a parallelepiped constraint. + * + * This contractor requires a parallelepiped with a square shape matrix \f$A\f$. + */ + class CtcParallelepiped : public Ctc + { + public: + + /** + * \brief Creates the contractor associated with a parallelepiped constraint. + */ + CtcParallelepiped(const Parallelepiped& p) + : Ctc(p.c.size()), _p(p) + { + assert_release(p.A.rows()==p.A.cols() && "Parallelepiped's matrix A must be square"); + } + + /** + * \brief Applies \f$\mathcal{C}_{\textrm{parallelepiped}}\big([\mathbf{x}]\big)\f$. + */ + void contract(IntervalVector& x) const; + + protected: + + const Parallelepiped _p; + }; +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4ea53368d..0d8702b26 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,7 @@ list(APPEND SRC_TESTS # listing files without extension core/contractors/codac2_tests_CtcInverseNotIn core/contractors/codac2_tests_CtcLazy core/contractors/codac2_tests_CtcLohner + core/contractors/codac2_tests_CtcParallelepiped core/contractors/codac2_tests_CtcPolygon core/contractors/codac2_tests_CtcSegment core/contractors/codac2_tests_CtcVisible diff --git a/tests/core/contractors/codac2_tests_CtcParallelepiped.cpp b/tests/core/contractors/codac2_tests_CtcParallelepiped.cpp new file mode 100644 index 000000000..2e0b8add0 --- /dev/null +++ b/tests/core/contractors/codac2_tests_CtcParallelepiped.cpp @@ -0,0 +1,44 @@ +/** + * Codac tests + * ---------------------------------------------------------------------------- + * \date 2026 + * \author Maël Godard + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#include +#include +#include + +using namespace std; +using namespace codac2; + + +TEST_CASE("CtcParallelepiped") +{ + CHECK(true); + Parallelepiped p1 ({1.5,2.8},Matrix({{0.5,0.4},{0,0.2}})); + CtcParallelepiped ctc_par1 (p1); + + IntervalVector x0 ({{0,5},{0,5}}); + IntervalVector x1 ({{0,0.5},{0,0.5}}); + + ctc_par1.contract(x0); + ctc_par1.contract(x1); + + CHECK(Approx(x0) == IntervalVector({{0.6,2.4},{2.6,3}})); + CHECK(x1.is_empty()); + + Parallelepiped p2 ({-5,-4,3,8},Matrix({{0.5,0.4,0.1,0},{0,0.2,0.3,0},{0,0,0.1,0.4},{0,0,0,0.2}})); + CtcParallelepiped ctc_par2 (p2); + + IntervalVector x2 ({{-10,10},{-10,10},{-10,10},{-10,10}}); + IntervalVector x3 ({{-0.5,0.5},{-0.5,0.5},{-0.5,0.5},{-0.5,0.5}}); + + ctc_par2.contract(x2); + ctc_par2.contract(x3); + + CHECK(Approx(x2) == IntervalVector({{-6,-4},{-4.5,-3.5},{2.5,3.5},{7.8,8.2}})); + CHECK(x3.is_empty()); +} \ No newline at end of file diff --git a/tests/core/contractors/codac2_tests_CtcParallelepiped.py b/tests/core/contractors/codac2_tests_CtcParallelepiped.py new file mode 100644 index 000000000..81a785c99 --- /dev/null +++ b/tests/core/contractors/codac2_tests_CtcParallelepiped.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +# Codac tests +# ---------------------------------------------------------------------------- +# \date 2026 +# \author Maël Godard +# \copyright Copyright 2024 Codac Team +# \license GNU Lesser General Public License (LGPL) + +import unittest +from codac import * + +class TestCtcParallelepiped(unittest.TestCase): + + def test_CtcParallelepiped(self): + + p1 = Parallelepiped([1.5,2.8],Matrix([[0.5,0.4],[0,0.2]])) + ctc_par1 = CtcParallelepiped(p1) + + x0 = IntervalVector([[0,5],[0,5]]) + x1 = IntervalVector([[0,0.5],[0,0.5]]) + + ctc_par1.contract(x0) + ctc_par1.contract(x1) + + self.assertTrue(Approx(x0) == IntervalVector([[0.6,2.4],[2.6,3]])) + self.assertTrue(x1.is_empty()) + + p2 = Parallelepiped([-5,-4,3,8],Matrix([[0.5,0.4,0.1,0],[0,0.2,0.3,0],[0,0,0.1,0.4],[0,0,0,0.2]])) + ctc_par2 = CtcParallelepiped(p2) + + x2 = IntervalVector([[-10,10],[-10,10],[-10,10],[-10,10]]) + x3 = IntervalVector([[-0.5,0.5],[-0.5,0.5],[-0.5,0.5],[-0.5,0.5]]) + + ctc_par2.contract(x2) + ctc_par2.contract(x3) + + self.assertTrue(Approx(x2) == IntervalVector([[-6,-4],[-4.5,-3.5],[2.5,3.5],[7.8,8.2]])) + self.assertTrue(x3.is_empty()) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 64a4d668dc76ad4772511d94b0b384597a39b10c Mon Sep 17 00:00:00 2001 From: godardma Date: Thu, 7 May 2026 02:44:53 +0200 Subject: [PATCH 7/9] [doc] correcting source file --- doc/manual/manual/contractors/geometric/src.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/manual/manual/contractors/geometric/src.cpp b/doc/manual/manual/contractors/geometric/src.cpp index bef9115c5..c1d949fd8 100644 --- a/doc/manual/manual/contractors/geometric/src.cpp +++ b/doc/manual/manual/contractors/geometric/src.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From 8c7b17e6d3867832327c9deb55b3fa149a6187ed Mon Sep 17 00:00:00 2001 From: godardma Date: Mon, 11 May 2026 17:52:30 +0200 Subject: [PATCH 8/9] [ctc] CtcParallepiped is now CtcWrapper, adding empty management of Zonotopes --- doc/manual/index.rst | 7 +- .../manual/contractors/geometric/index.rst | 1 - .../manual/contractors/geometric/src.cpp | 13 -- .../manual/contractors/geometric/src.py | 10 -- doc/manual/manual/contractors/index.rst | 4 +- .../ctcwrapper.rst} | 17 ++- doc/manual/manual/contractors/shape/index.rst | 9 ++ doc/manual/manual/contractors/shape/src.cpp | 32 +++++ doc/manual/manual/contractors/shape/src.py | 30 +++++ python/src/core/CMakeLists.txt | 1 - python/src/core/codac2_py_core.cpp | 4 +- .../codac2_py_CtcParallelepiped.cpp | 36 ----- .../core/contractors/codac2_py_CtcWrapper.cpp | 12 ++ .../zonotope/codac2_py_Parallelepiped.cpp | 14 ++ .../domains/zonotope/codac2_py_Zonotope.cpp | 18 +++ src/core/CMakeLists.txt | 2 - .../contractors/codac2_CtcParallelepiped.cpp | 27 ---- .../contractors/codac2_CtcParallelepiped.h | 45 ------- src/core/contractors/codac2_CtcWrapper.h | 2 +- src/core/domains/paving/codac2_Paving.cpp | 5 + src/core/domains/paving/codac2_Paving.h | 2 + .../zonotope/codac2_Parallelepiped.cpp | 125 ++++++++++++------ .../domains/zonotope/codac2_Parallelepiped.h | 26 ++++ src/core/domains/zonotope/codac2_Zonotope.cpp | 88 ++++++++---- src/core/domains/zonotope/codac2_Zonotope.h | 44 +++++- src/graphics/figures/codac2_Figure2D.cpp | 84 ++++++------ tests/CMakeLists.txt | 1 - .../codac2_tests_CtcParallelepiped.cpp | 44 ------ .../codac2_tests_CtcParallelepiped.py | 43 ------ .../zonotope/codac2_tests_Parallelepiped.cpp | 35 +++++ .../zonotope/codac2_tests_Parallelepiped.py | 35 +++++ 31 files changed, 471 insertions(+), 345 deletions(-) rename doc/manual/manual/contractors/{geometric/ctcparallelepiped.rst => shape/ctcwrapper.rst} (59%) create mode 100644 doc/manual/manual/contractors/shape/index.rst create mode 100644 doc/manual/manual/contractors/shape/src.cpp create mode 100644 doc/manual/manual/contractors/shape/src.py delete mode 100644 python/src/core/contractors/codac2_py_CtcParallelepiped.cpp delete mode 100644 src/core/contractors/codac2_CtcParallelepiped.cpp delete mode 100644 src/core/contractors/codac2_CtcParallelepiped.h delete mode 100644 tests/core/contractors/codac2_tests_CtcParallelepiped.cpp delete mode 100644 tests/core/contractors/codac2_tests_CtcParallelepiped.py diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 2eeaa077c..f217d3970 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -246,7 +246,6 @@ User manual * :ref:`sec-ctc-dynamic-ctclohner` * Geometric contractors * :ref:`sec-ctc-geom-ctcdist` - * :ref:`sec-ctc-geom-ctcparallelepiped` * :ref:`sec-ctc-geom-ctcpolar` * :ref:`sec-ctc-geom-ctcvisible` * CtcSegment @@ -256,7 +255,7 @@ User manual * CtcCross / CtcNoCross * Shape contractors * CtcCtcBoundary - * CtcWrapper + * :ref:`sec-ctc-shape-ctcwrapper` * CtcImage * CtcDiscreteSet * Temporal contractors @@ -318,8 +317,8 @@ User manual * :ref:`sec-zonotope` * Polyhedron -* :ref:`sec-actions` - * :ref:`sec-actions-octasym` +* :ref:`sec-tools` + * :ref:`sec-tools-octasym` * :ref:`sec-ellipsoids` * :ref:`sec-ellipsoids-intro` diff --git a/doc/manual/manual/contractors/geometric/index.rst b/doc/manual/manual/contractors/geometric/index.rst index 98434784e..938c40df6 100644 --- a/doc/manual/manual/contractors/geometric/index.rst +++ b/doc/manual/manual/contractors/geometric/index.rst @@ -4,7 +4,6 @@ Geometric contractors .. toctree:: ctcdist.rst - ctcparallelepiped.rst ctcpolar.rst ctcvisible.rst CtcSegment diff --git a/doc/manual/manual/contractors/geometric/src.cpp b/doc/manual/manual/contractors/geometric/src.cpp index c1d949fd8..ee93eb7b0 100644 --- a/doc/manual/manual/contractors/geometric/src.cpp +++ b/doc/manual/manual/contractors/geometric/src.cpp @@ -70,19 +70,6 @@ TEST_CASE("CtcDist - manual") } } -TEST_CASE("CtcParallelepiped - manual") -{ - { - // [ctcparallelepiped-1-beg] - IntervalVector x ({{0,5}, {0,5}}); - Parallelepiped p (Vector({1.5,2.8}), Matrix({{0.5,0.4},{0,0.2}})); - CtcParallelepiped c(p); - c.contract(x); - // x = [ [0.599999, 2.40001] ; [2.59999, 3] ] - // [ctcparallelepiped-1-end] - } -} - TEST_CASE("CtcPolar - manual") { { diff --git a/doc/manual/manual/contractors/geometric/src.py b/doc/manual/manual/contractors/geometric/src.py index 325fac7a9..f65d6e925 100644 --- a/doc/manual/manual/contractors/geometric/src.py +++ b/doc/manual/manual/contractors/geometric/src.py @@ -65,16 +65,6 @@ def tests_CtcDist_manual(test): #DefaultFigure.draw_circle(b1, y1.lb()); DefaultFigure.draw_circle(b1, y1.ub()) #DefaultFigure.draw_circle(b2, y2.lb()); DefaultFigure.draw_circle(b2, y2.ub()) - def tests_CtcParallelepiped_manual(test): - - # [ctcparallelepiped-1-beg] - x = IntervalVector([[0,5], [0,5]]) - p = Parallelepiped(Vector([1.5,2.8]), Matrix([[0.5,0.4],[0,0.2]])) - c = CtcParallelepiped(p) - x = c.contract(x) - # x = [ [0.599999, 2.40001] ; [2.59999, 3] ] - # [ctcparallelepiped-1-end] - def tests_CtcPolar_manual(test): # [ctcpolar-1-beg] diff --git a/doc/manual/manual/contractors/index.rst b/doc/manual/manual/contractors/index.rst index 389b698da..9b2537821 100644 --- a/doc/manual/manual/contractors/index.rst +++ b/doc/manual/manual/contractors/index.rst @@ -8,9 +8,9 @@ Contractors, separators CtcInverse CtcLohner CtcDist - CtcParallelepiped CtcPolar CtcVisible + CtcWrapper .. What are contractors? .. The Ctc class @@ -110,7 +110,7 @@ Overview of contractors and separators * - ``CtcCtcBoundary`` - ``SepCtcBoundary`` - * - ``CtcWrapper`` + * - :ref:`CtcWrapper ` - ``SepWrapper`` * - ``CtcImage`` diff --git a/doc/manual/manual/contractors/geometric/ctcparallelepiped.rst b/doc/manual/manual/contractors/shape/ctcwrapper.rst similarity index 59% rename from doc/manual/manual/contractors/geometric/ctcparallelepiped.rst rename to doc/manual/manual/contractors/shape/ctcwrapper.rst index 45f32549f..17c7a625a 100644 --- a/doc/manual/manual/contractors/geometric/ctcparallelepiped.rst +++ b/doc/manual/manual/contractors/shape/ctcwrapper.rst @@ -1,18 +1,21 @@ -.. _sec-ctc-geom-ctcparallelepiped: +.. _sec-ctc-shape-ctcwrapper: -The CtcParallelpiped contractor +CtcWrapper =============================== Main author: `Maël Godard `_ -.. doxygenclass:: codac2::CtcParallelepiped - :project: codac +The CtcWrapper is a contractor to contract a box with respect to a set represented by a Codac object. +Currently supported objects are: + +- :ref:`IntervalVector ` +- :ref:`Parallelepiped ` +- PavingOut Methods ------- -.. doxygenfunction:: codac2::CtcParallelepiped::contract(IntervalVector&) const - :project: codac +Below is an example of the use of the CtcWrapper with a Parallelepiped. .. tabs:: @@ -34,4 +37,4 @@ Methods .. admonition:: Technical documentation - See the `C++ API documentation of this class <../../api/html/classcodac2_1_1_ctc_parallelepiped.html>`_. \ No newline at end of file + See the `C++ API documentation of this class <../../api/html/classcodac2_1_1_ctc_wrapper.html>`_. \ No newline at end of file diff --git a/doc/manual/manual/contractors/shape/index.rst b/doc/manual/manual/contractors/shape/index.rst new file mode 100644 index 000000000..793066cca --- /dev/null +++ b/doc/manual/manual/contractors/shape/index.rst @@ -0,0 +1,9 @@ +Geometric contractors +===================== + +.. toctree:: + + CtcCtcBoundary + ctcwrapper.rst + CtcImage + CtcDiscreteSet \ No newline at end of file diff --git a/doc/manual/manual/contractors/shape/src.cpp b/doc/manual/manual/contractors/shape/src.cpp new file mode 100644 index 000000000..ab9c1eee3 --- /dev/null +++ b/doc/manual/manual/contractors/shape/src.cpp @@ -0,0 +1,32 @@ +/** + * Codac tests + * ---------------------------------------------------------------------------- + * \date 2026 + * \author Maël Godard + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#include +#include +#include + +#include + +using namespace std; +using namespace codac2; + +TEST_CASE("CtcWrapper - Parallelepiped - manual") +{ + { + // [ctcparallelepiped-1-beg] + IntervalVector x ({{0,5}, {0,5}}); + Parallelepiped p (Vector({1.5,2.8}), Matrix({{0.5,0.4},{0,0.2}})); + CtcWrapper c(p); + c.contract(x); + // x = [ [0.599999, 2.40001] ; [2.59999, 3] ] + // [ctcparallelepiped-1-end] + + CHECK(Approx(x) == IntervalVector({{0.6, 2.4}, {2.6, 3}})); + } +} diff --git a/doc/manual/manual/contractors/shape/src.py b/doc/manual/manual/contractors/shape/src.py new file mode 100644 index 000000000..08344e907 --- /dev/null +++ b/doc/manual/manual/contractors/shape/src.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# Codac tests +# ---------------------------------------------------------------------------- +# \date 2026 +# \author Maël Godard +# \copyright Copyright 2024 Codac Team +# \license GNU Lesser General Public License (LGPL) + +import sys, os +import unittest +import math +from codac import * + +class TestCtcShapeManual(unittest.TestCase): + + def tests_CtcWrapper_Parallelepiped_manual(test): + + # [ctcparallelepiped-1-beg] + x = IntervalVector([[0,5], [0,5]]) + p = Parallelepiped(Vector([1.5,2.8]), Matrix([[0.5,0.4],[0,0.2]])) + c = CtcWrapper_Parallelepiped(p) + x = c.contract(x) + # x = [ [0.599999, 2.40001] ; [2.59999, 3] ] + # [ctcparallelepiped-1-end] + + test.assertTrue(Approx(x) == IntervalVector([[0.6, 2.4], [2.6, 3]])) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/python/src/core/CMakeLists.txt b/python/src/core/CMakeLists.txt index f3c3965d0..d569f74bd 100644 --- a/python/src/core/CMakeLists.txt +++ b/python/src/core/CMakeLists.txt @@ -29,7 +29,6 @@ contractors/codac2_py_CtcLazy.cpp contractors/codac2_py_CtcLohner.cpp contractors/codac2_py_CtcNot.cpp - contractors/codac2_py_CtcParallelepiped.cpp contractors/codac2_py_CtcPointCloud.cpp contractors/codac2_py_CtcPolar.cpp contractors/codac2_py_CtcPolygon.cpp diff --git a/python/src/core/codac2_py_core.cpp b/python/src/core/codac2_py_core.cpp index 0d7ca7173..daa890f19 100644 --- a/python/src/core/codac2_py_core.cpp +++ b/python/src/core/codac2_py_core.cpp @@ -49,7 +49,6 @@ void export_CtcInter(py::module& m, py::class_,pyCtcInte void export_CtcLazy(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcLohner(py::module& m); void export_CtcNot(py::module& m, py::class_,pyCtcIntervalVector>& ctc); -void export_CtcParallelepiped(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcPointCloud(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcPolar(py::module& m, py::class_,pyCtcIntervalVector>& ctc); void export_CtcPolygon(py::module& m, py::class_,pyCtcIntervalVector>& ctc); @@ -211,7 +210,6 @@ PYBIND11_MODULE(_core, m) export_CtcLazy(m, py_ctc_iv); export_CtcLohner(m); export_CtcNot(m, py_ctc_iv); - export_CtcParallelepiped(m, py_ctc_iv); export_CtcPointCloud(m, py_ctc_iv); export_CtcPolar(m, py_ctc_iv); export_CtcPolygon(m, py_ctc_iv); @@ -370,4 +368,4 @@ PYBIND11_MODULE(_core, m) // Extension > sympy auto ms = m.def_submodule("_sympy"); // to keep a dedicated namespace export_sympy(ms); -} \ No newline at end of file +} diff --git a/python/src/core/contractors/codac2_py_CtcParallelepiped.cpp b/python/src/core/contractors/codac2_py_CtcParallelepiped.cpp deleted file mode 100644 index 6905f3d97..000000000 --- a/python/src/core/contractors/codac2_py_CtcParallelepiped.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Codac binding (core) - * ---------------------------------------------------------------------------- - * \date 2026 - * \author Maël Godard - * \copyright Copyright 2024 Codac Team - * \license GNU Lesser General Public License (LGPL) - */ - -#include -#include -#include -#include -#include -#include "codac2_py_Ctc.h" -#include "codac2_py_CtcParallelepiped_docs.h" // Generated file from Doxygen XML (doxygen2docstring.py): - -using namespace std; -using namespace codac2; -namespace py = pybind11; -using namespace pybind11::literals; - -void export_CtcParallelepiped(py::module& m, py::class_,pyCtcIntervalVector>& pyctc) -{ - py::class_ exported(m, "CtcParallelepiped", pyctc, CTCPARALLELEPIPED_MAIN); - exported - - .def(py::init(), - CTCPARALLELEPIPED_CTCPARALLELEPIPED_CONST_PARALLELEPIPED_REF, - "p"_a) - - ; - - CONTRACT_METHODS(exported, CtcParallelepiped, - VOID_CTCPARALLELEPIPED_CONTRACT_INTERVALVECTOR_REF_CONST) -} \ No newline at end of file diff --git a/python/src/core/contractors/codac2_py_CtcWrapper.cpp b/python/src/core/contractors/codac2_py_CtcWrapper.cpp index d07306bad..4d3b6155e 100644 --- a/python/src/core/contractors/codac2_py_CtcWrapper.cpp +++ b/python/src/core/contractors/codac2_py_CtcWrapper.cpp @@ -34,6 +34,18 @@ void export_CtcWrapper(py::module& m, py::class_,pyCtcIn CONTRACT_METHODS(exported_ctcwrapper_intervalvector, CtcWrapper, VOID_CTCWRAPPER_YX_CONTRACT_X_REF_CONST) + py::class_> exported_ctcwrapper_parallelepiped(m, "CtcWrapper_Parallelepiped", pyctc, CTCWRAPPER_MAIN); + exported_ctcwrapper_parallelepiped + + .def(py::init(), + CTCWRAPPER_YX_CTCWRAPPER_CONST_Y_REF, + "y"_a) + + ; + + CONTRACT_METHODS(exported_ctcwrapper_parallelepiped, CtcWrapper, + VOID_CTCWRAPPER_YX_CONTRACT_X_REF_CONST) + py::class_> exported_ctcwrapper_pavingout(m, "CtcWrapper_PavingOut", pyctc, CTCWRAPPER_MAIN); exported_ctcwrapper_pavingout diff --git a/python/src/core/domains/zonotope/codac2_py_Parallelepiped.cpp b/python/src/core/domains/zonotope/codac2_py_Parallelepiped.cpp index 0cfd384bd..d3181936b 100644 --- a/python/src/core/domains/zonotope/codac2_py_Parallelepiped.cpp +++ b/python/src/core/domains/zonotope/codac2_py_Parallelepiped.cpp @@ -31,6 +31,10 @@ void export_Parallelepiped(py::module& m) PARALLELEPIPED_PARALLELEPIPED_CONST_VECTOR_REF_CONST_MATRIX_REF, "z"_a, "A"_a) + .def_static("empty", &Parallelepiped::empty, + STATIC_PARALLELEPIPED_PARALLELEPIPED_EMPTY_INDEX, + "n"_a) + .def("vertices", &Parallelepiped::vertices, VECTOR_VECTOR_PARALLELEPIPED_VERTICES_CONST) @@ -42,5 +46,15 @@ void export_Parallelepiped(py::module& m) BOOLINTERVAL_PARALLELEPIPED_IS_SUPERSET_CONST_INTERVALVECTOR_REF_CONST, "x"_a) + .def("__and__", &Parallelepiped::operator&, + PARALLELEPIPED_PARALLELEPIPED_OPERATORINTER_CONST_INTERVALVECTOR_REF_CONST, + "x"_a) + + .def("__rand__", [](const Parallelepiped& p, const IntervalVector& x) + { + return x & p; + }, + INTERVALVECTOR_OPERATORINTER_CONST_INTERVALVECTOR_REF_CONST_PARALLELEPIPED_REF, + "x"_a) ; } diff --git a/python/src/core/domains/zonotope/codac2_py_Zonotope.cpp b/python/src/core/domains/zonotope/codac2_py_Zonotope.cpp index ab4e578f1..89f25b699 100644 --- a/python/src/core/domains/zonotope/codac2_py_Zonotope.cpp +++ b/python/src/core/domains/zonotope/codac2_py_Zonotope.cpp @@ -29,6 +29,16 @@ void export_Zonotope(py::module& m) ZONOTOPE_ZONOTOPE_CONST_VECTOR_REF_CONST_MATRIX_REF, "z"_a, "A"_a) + .def_static("empty", &Zonotope::empty, + STATIC_ZONOTOPE_ZONOTOPE_EMPTY_INDEX, + "n"_a) + + .def("is_empty", &Zonotope::is_empty, + BOOL_ZONOTOPE_IS_EMPTY_CONST) + + .def("set_empty", &Zonotope::set_empty, + VOID_ZONOTOPE_SET_EMPTY) + .def("box", &Zonotope::box, INTERVALVECTOR_ZONOTOPE_BOX_CONST) @@ -43,6 +53,14 @@ void export_Zonotope(py::module& m) ZONOTOPE_ZONOTOPE_OPERATORPLUS_CONST_ZONOTOPE_REF, py::is_operator()) + .def("__repr__", [](const Zonotope& z) { + std::ostringstream stream; + stream << z; + return string(stream.str()); + }, + OSTREAM_REF_OPERATOROUT_OSTREAM_REF_CONST_ZONOTOPE_REF) + + .def_readwrite("c", &Zonotope::c, VECTOR_ZONOTOPE_C) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 330760139..d06db4fd8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -40,8 +40,6 @@ ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcLohner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcLohner.h ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcNot.h - ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcParallelepiped.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcParallelepiped.h ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcUnion.h ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcVisible.cpp ${CMAKE_CURRENT_SOURCE_DIR}/contractors/codac2_CtcVisible.h diff --git a/src/core/contractors/codac2_CtcParallelepiped.cpp b/src/core/contractors/codac2_CtcParallelepiped.cpp deleted file mode 100644 index fcf9fa263..000000000 --- a/src/core/contractors/codac2_CtcParallelepiped.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * CtcParallelepiped.cpp - * ---------------------------------------------------------------------------- - * \date 2026 - * \author Maël Godard - * \copyright Copyright 2024 Codac Team - * \license GNU Lesser General Public License (LGPL) - */ - -#include "codac2_CtcParallelepiped.h" -#include "codac2_inversion.h" - -// TO DELETE - -#include - -using namespace std; - -namespace codac2 -{ - void CtcParallelepiped::contract(IntervalVector& x) const - { - assert_release(x.size()==_p.c.size()); - IntervalVector x_p = inverse_enclosure(_p.A)*(x - _p.c); // projection in the parallelepiped's coordinate system - x &= _p.c + _p.A*(x_p & IntervalVector::constant(x.size(),{-1,1})); - } -} \ No newline at end of file diff --git a/src/core/contractors/codac2_CtcParallelepiped.h b/src/core/contractors/codac2_CtcParallelepiped.h deleted file mode 100644 index 661aeff26..000000000 --- a/src/core/contractors/codac2_CtcParallelepiped.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * \file codac2_CtcParallelepiped.h - * ---------------------------------------------------------------------------- - * \date 2026 - * \author Simon Rohou, Benoit Desrochers - * \copyright Copyright 2024 Codac Team - * \license GNU Lesser General Public License (LGPL) - */ - -#pragma once - -#include "codac2_Parallelepiped.h" -#include "codac2_Ctc.h" - -namespace codac2 -{ - /** - * \class CtcParallelepiped - * \brief Implements the contractor associated with a parallelepiped constraint. - * - * This contractor requires a parallelepiped with a square shape matrix \f$A\f$. - */ - class CtcParallelepiped : public Ctc - { - public: - - /** - * \brief Creates the contractor associated with a parallelepiped constraint. - */ - CtcParallelepiped(const Parallelepiped& p) - : Ctc(p.c.size()), _p(p) - { - assert_release(p.A.rows()==p.A.cols() && "Parallelepiped's matrix A must be square"); - } - - /** - * \brief Applies \f$\mathcal{C}_{\textrm{parallelepiped}}\big([\mathbf{x}]\big)\f$. - */ - void contract(IntervalVector& x) const; - - protected: - - const Parallelepiped _p; - }; -} \ No newline at end of file diff --git a/src/core/contractors/codac2_CtcWrapper.h b/src/core/contractors/codac2_CtcWrapper.h index 83b7745fd..278e6ef93 100644 --- a/src/core/contractors/codac2_CtcWrapper.h +++ b/src/core/contractors/codac2_CtcWrapper.h @@ -29,7 +29,7 @@ namespace codac2 void contract(X& x) const { assert_release(x.size() == this->size()); - x = _y & x; + x = x & _y; } protected: diff --git a/src/core/domains/paving/codac2_Paving.cpp b/src/core/domains/paving/codac2_Paving.cpp index f7eb538d8..c14c2142f 100644 --- a/src/core/domains/paving/codac2_Paving.cpp +++ b/src/core/domains/paving/codac2_Paving.cpp @@ -74,6 +74,11 @@ namespace codac2 return list(); }; + IntervalVector operator&(const IntervalVector& x, const PavingOut& p) + { + return p & x; + }; + // PavingInOut class diff --git a/src/core/domains/paving/codac2_Paving.h b/src/core/domains/paving/codac2_Paving.h index 3bd70a2a7..25f5c8f39 100644 --- a/src/core/domains/paving/codac2_Paving.h +++ b/src/core/domains/paving/codac2_Paving.h @@ -173,6 +173,8 @@ namespace codac2 static const NodeValue_ outer, outer_complem; }; + IntervalVector operator&(const IntervalVector& x, const PavingOut& p); + class PavingInOut; using PavingInOut_Node = PavingNode; diff --git a/src/core/domains/zonotope/codac2_Parallelepiped.cpp b/src/core/domains/zonotope/codac2_Parallelepiped.cpp index 280ff0896..79594db6f 100644 --- a/src/core/domains/zonotope/codac2_Parallelepiped.cpp +++ b/src/core/domains/zonotope/codac2_Parallelepiped.cpp @@ -10,55 +10,106 @@ #include "codac2_Parallelepiped.h" #include "codac2_inversion.h" -using namespace codac2; +using namespace std; -Parallelepiped::Parallelepiped(const Vector& c_, const Matrix& A_) - : Zonotope(c_, A_) +namespace codac2 { - assert_release(A.cols() <= c.size() && "too many vectors, you are describing a zonotope"); -} + Parallelepiped::Parallelepiped(const Vector& c_, const Matrix& A_) + : Zonotope(c_, A_) + { + assert_release(A.cols() <= c.size() && "too many vectors, you are describing a zonotope"); + } -void generate_vertices(Index i, Index n, const Vector& c, const Matrix& A, std::vector& L_v) -{ - if (i == n) + Parallelepiped Parallelepiped::empty(Index n) { - L_v.push_back(c); + Parallelepiped p(Vector::Constant(n, std::numeric_limits::quiet_NaN()), Matrix::Identity(n,n)); + p.empty_flag = true; + return p; } - else if (i& L_v) { - generate_vertices(i+1, n, c + A.col(i), A, L_v); - generate_vertices(i+1, n, c - A.col(i), A, L_v); + if (i == n) + { + L_v.push_back(c); + } + else if (i Parallelepiped::vertices() const -{ - std::vector L_v; - generate_vertices(0, c.size(),c,A,L_v); - return L_v; -} + std::vector Parallelepiped::vertices() const + { + std::vector L_v; + generate_vertices(0, size(),c,A,L_v); + return L_v; + } -BoolInterval Parallelepiped::contains(const Vector& v) const -{ - return is_superset(v.template cast()); -} + BoolInterval Parallelepiped::contains(const Vector& v) const + { + return is_superset(v.template cast()); + } -BoolInterval Parallelepiped::is_superset(const IntervalVector& x) const -{ - assert_release(A.rows() == A.cols() && "Matrix A must be square to check containment."); - assert_release(x.size() == c.size() && "Point dimension must match parallelepiped dimension."); + BoolInterval Parallelepiped::is_superset(const IntervalVector& x) const + { + assert_release(A.rows() == A.cols() && "Matrix A must be square to check containment."); + assert_release(x.size() == size() && "Point dimension must match parallelepiped dimension."); - IntervalVector B = inverse_enclosure(A)*(x - c); - IntervalVector IV = IntervalVector::constant(A.cols(),{-1,1}); + IntervalVector B = inverse_enclosure(A)*(x - c); + IntervalVector IV = IntervalVector::constant(A.cols(),{-1,1}); - if (!(B.intersects(IV))) - return BoolInterval::FALSE; + if (!(B.intersects(IV))) + return BoolInterval::FALSE; - else - { - if (B.is_subset(IV)) - return BoolInterval::TRUE; else - return BoolInterval::UNKNOWN; + { + if (B.is_subset(IV)) + return BoolInterval::TRUE; + else + return BoolInterval::UNKNOWN; + } } -} \ No newline at end of file + + Parallelepiped Parallelepiped::operator&(const IntervalVector& x) const + { + assert_release(A.rows() == A.cols() && "Matrix A must be square to compute intersection."); + assert_release(x.size() == size() && "Box dimension must match parallelepiped dimension."); + + if (is_empty() || x.is_empty()) + return Parallelepiped::empty(size()); + + IntervalVector x_p = inverse_enclosure(A)*(x-c); + x_p &= IntervalVector::constant(c.size(),Interval(-1,1)); + + if (x_p.is_empty()) + return Parallelepiped::empty(size()); + + Vector c_i = c + A*x_p.mid(); + + Matrix A_i (A); + for (int i = 0; i < c.size(); ++i) + A_i.col(i) *= x_p[i].rad(); + + return Parallelepiped(c_i,A_i); + } + + IntervalVector operator&(const IntervalVector& x, const Parallelepiped& p) + { + assert_release(p.A.rows() == p.A.cols() && "Matrix A must be square to compute intersection."); + assert_release(x.size() == p.size() && "Box dimension must match parallelepiped dimension."); + + if (p.is_empty() || x.is_empty()) + return IntervalVector::empty(x.size()); + + IntervalVector x_p = inverse_enclosure(p.A)*(x-p.c); + x_p &= IntervalVector::constant(p.c.size(),Interval(-1,1)); + + if (x_p.is_empty()) + return IntervalVector::empty(x.size()); + + IntervalVector x_i = p.c + p.A*x_p; + return x_i & x; + } +} diff --git a/src/core/domains/zonotope/codac2_Parallelepiped.h b/src/core/domains/zonotope/codac2_Parallelepiped.h index 20e17bff1..9dfbfd81a 100644 --- a/src/core/domains/zonotope/codac2_Parallelepiped.h +++ b/src/core/domains/zonotope/codac2_Parallelepiped.h @@ -38,6 +38,14 @@ namespace codac2 * \param A Shape matrix of the parallelepiped (\f$n\times m\f$ matrix with \f$m \leqslant n\f$) */ Parallelepiped(const Vector& c, const Matrix& A); + + /** + * \brief Constructs an empty n-parallelepiped + * + * \param n Dimension of the parallelepiped + * \return A new Parallelepiped object representing an empty parallelepiped in n-dimensional space + */ + static Parallelepiped empty(Index n); /** * \brief Computes the vertices of the parallelepiped @@ -64,5 +72,23 @@ namespace codac2 */ BoolInterval is_superset(const IntervalVector& x) const; + /** + * \brief Computes the intersection of the parallelepiped with a given box. The matrix A has to be square and invertible. + * + * \param x The box to intersect with + * + * \return A new Parallelepiped representing the intersection of the original parallelepiped with the box. + */ + Parallelepiped operator&(const IntervalVector& x) const; }; + + /** + * \brief Computes the intersection of a box with a parallelepiped. The matrix A has to be square and invertible. + * + * \param x The box to intersect with + * \param p The parallelepiped to intersect with + * + * \return A new IntervalVector representing the intersection of the box with the parallelepiped. + */ + IntervalVector operator&(const IntervalVector& x, const Parallelepiped& p); } diff --git a/src/core/domains/zonotope/codac2_Zonotope.cpp b/src/core/domains/zonotope/codac2_Zonotope.cpp index 8eb9dabe8..0e1844d43 100644 --- a/src/core/domains/zonotope/codac2_Zonotope.cpp +++ b/src/core/domains/zonotope/codac2_Zonotope.cpp @@ -9,44 +9,76 @@ #include "codac2_Zonotope.h" -using namespace codac2; - -Zonotope::Zonotope(const Vector& c_, const Matrix& A_) - : c(c_), A(A_) +namespace codac2 { - assert_release(c.size() == A.rows()); -} + Zonotope::Zonotope(const Vector& c_, const Matrix& A_) + : c(c_), A(A_) + { + assert_release(c.size() == A.rows()); + } -IntervalVector Zonotope::box() const -{ - return c + (A.template cast())*IntervalVector::constant(A.cols(),{-1,1}); -} + Zonotope Zonotope::empty(Index n) + { + Zonotope z(Vector::Constant(n, std::numeric_limits::quiet_NaN()), Matrix::Identity(n,n)); + z.empty_flag = true; + return z; + } -Zonotope Zonotope::proj(const std::vector& indices) const -{ - assert_release(*std::min_element(indices.begin(), indices.end()) >= 0 && "indices out of range"); - assert_release(*std::max_element(indices.begin(), indices.end()) <= c.size() && "indices out of range"); + bool Zonotope::is_empty() const + { + return empty_flag; + } + + void Zonotope::set_empty() + { + empty_flag = true; + c = Vector::Constant(c.size(), std::numeric_limits::quiet_NaN()); + } - Matrix A_cropped (indices.size(), A.cols()); - Vector c_cropped (indices.size()); + IntervalVector Zonotope::box() const + { + return c + (A.template cast())*IntervalVector::constant(A.cols(),{-1,1}); + } - for (size_t i = 0; i < indices.size(); ++i) + Zonotope Zonotope::proj(const std::vector& indices) const { - A_cropped.row(i) = A.row(indices[i]); - c_cropped[i] = c[indices[i]]; + assert_release(*std::min_element(indices.begin(), indices.end()) >= 0 && "indices out of range"); + assert_release(*std::max_element(indices.begin(), indices.end()) <= c.size() && "indices out of range"); + + Matrix A_cropped (indices.size(), A.cols()); + Vector c_cropped (indices.size()); + + for (size_t i = 0; i < indices.size(); ++i) + { + A_cropped.row(i) = A.row(indices[i]); + c_cropped[i] = c[indices[i]]; + } + + return Zonotope(c_cropped, A_cropped); } - return Zonotope(c_cropped, A_cropped); -} + Zonotope Zonotope::operator+(const Zonotope& zonotope) + { + assert_release(c.size() == zonotope.c.size() && "Zonotopes must have the same dimension"); -Zonotope Zonotope::operator+(const Zonotope& zonotope) -{ - assert_release(c.size() == zonotope.c.size() && "Zonotopes must have the same dimension"); + Vector c_sum = c + zonotope.c; + Matrix A_sum (A.rows(), A.cols() + zonotope.A.cols()); + + A_sum << A, zonotope.A; - Vector c_sum = c + zonotope.c; - Matrix A_sum (A.rows(), A.cols() + zonotope.A.cols()); + return Zonotope(c_sum, A_sum); + } - A_sum << A, zonotope.A; + std::ostream& operator<<(std::ostream& str, const Zonotope& z) + { + str << "{ "; + + if (z.is_empty()) + str << "empty " << (z.size()) << "-d zonotope"; + else + str << "Zonotope center: \n" << z.c.transpose() << "\nZonotope shape matrix: \n" << z.A; - return Zonotope(c_sum, A_sum); + str << " }"; + return str; + } } diff --git a/src/core/domains/zonotope/codac2_Zonotope.h b/src/core/domains/zonotope/codac2_Zonotope.h index ad1255b61..bd9e6e043 100644 --- a/src/core/domains/zonotope/codac2_Zonotope.h +++ b/src/core/domains/zonotope/codac2_Zonotope.h @@ -36,6 +36,35 @@ namespace codac2 */ Zonotope(const Vector& c, const Matrix& A); + /** + * \brief Constructs an empty n-zonotope + * + * \param n Dimension of the zonotope + * + * \return A new Zonotope object representing an empty zonotope in n-dimensional space + */ + static Zonotope empty(Index n); + + /** + * \brief Checks if the zonotope is empty + * + * \return True if the zonotope is empty, false otherwise + */ + bool is_empty() const; + + /** + * \brief Sets the zonotope to be empty + */ + void set_empty(); + + /** + * \brief Outputs the size of the zonotope + * + * \return The size of the zonotope, defined as the size of its center vector + */ + + Index size() const { return c.size(); } + /** * \brief Computes the axis-aligned bounding box of the zonotope * @@ -70,6 +99,19 @@ namespace codac2 * \brief Shape matrix of the zonotope */ Matrix A; - + + /** + * \brief Flag indicating whether the zonotope is empty + */ + bool empty_flag = false; }; + + /** + * \brief Stream output operator for ``Zonotope``. + * + * \param str Output stream. + * \param z The zonotope to print. + * \return The output stream with the zonotope information. + */ + std::ostream& operator<<(std::ostream& str, const Zonotope& z); } diff --git a/src/graphics/figures/codac2_Figure2D.cpp b/src/graphics/figures/codac2_Figure2D.cpp index 33cbb962b..15c9503c8 100644 --- a/src/graphics/figures/codac2_Figure2D.cpp +++ b/src/graphics/figures/codac2_Figure2D.cpp @@ -240,38 +240,41 @@ void Figure2D::draw_polygon(const Polygon& x, const StyleProperties& style) void Figure2D::draw_zonotope(const Zonotope& z, const StyleProperties& style) { - map sides; - for (int i=0; i < z.A.cols(); i++) { - auto u = z.A.col(i); - assert_release(u.size()==2); - if (u==Vector::zero(2)) continue; - double theta = std::atan2(u[1],u[0]); - Vector v(u); - if (theta<=0.0) { theta=theta+PI; v=-v; } + if (!z.is_empty()) + { + map sides; + for (int i=0; i < z.A.cols(); i++) { + auto u = z.A.col(i); + assert_release(u.size()==2); + if (u==Vector::zero(2)) continue; + double theta = std::atan2(u[1],u[0]); + Vector v(u); + if (theta<=0.0) { theta=theta+PI; v=-v; } // Theta in ]0,PI] , v[1]>=0 and if v[1]=0, v[0]<0 - auto try_insert=sides.insert({theta,v}); - if (try_insert.second==false) { - (try_insert.first)->second += v; - } - } - vector vertices; - Vector point=z.c; - // Start from v[1] maximum (and v[0] min for horizontal side) - for (const auto& a : sides) { - point+=a.second; - } - // Turn anticlockwise : first half - for (const auto& a : sides) { - vertices.push_back(point); - point-=2*a.second; - } - // Turn anticlockwise : second half - for (const auto& a : sides) { - vertices.push_back(point); - point+=2*a.second; - } - for(const auto& output_fig : _output_figures) + auto try_insert=sides.insert({theta,v}); + if (try_insert.second==false) { + (try_insert.first)->second += v; + } + } + vector vertices; + Vector point=z.c; + // Start from v[1] maximum (and v[0] min for horizontal side) + for (const auto& a : sides) { + point+=a.second; + } + // Turn anticlockwise : first half + for (const auto& a : sides) { + vertices.push_back(point); + point-=2*a.second; + } + // Turn anticlockwise : second half + for (const auto& a : sides) { + vertices.push_back(point); + point+=2*a.second; + } + for(const auto& output_fig : _output_figures) output_fig->draw_polygon(vertices,style); + } } @@ -280,15 +283,18 @@ void Figure2D::draw_parallelepiped(const Parallelepiped& p, const StylePropertie assert_release(p.A.is_squared() && p.A.rows() == p.c.size()); assert_release(p.c.size() == 2); - auto a1 = p.A.col(0), a2 = p.A.col(1); - - if (a1.isZero() || a2.isZero()) - draw_polyline({p.c-a1-a2,p.c+a1+a2}, style); - else - draw_polygon({ - p.c+a1+a2, p.c-a1+a2, - p.c-a1-a2, p.c+a1-a2 - }, style); + if (!p.is_empty()) + { + auto a1 = p.A.col(0), a2 = p.A.col(1); + + if (a1.isZero() || a2.isZero()) + draw_polyline({p.c-a1-a2,p.c+a1+a2}, style); + else + draw_polygon({ + p.c+a1+a2, p.c-a1+a2, + p.c-a1-a2, p.c+a1-a2 + }, style); + } } void Figure2D::draw_pie(const Vector& c, const Interval& r, const Interval& theta, const StyleProperties& style) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0d8702b26..4ea53368d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,7 +39,6 @@ list(APPEND SRC_TESTS # listing files without extension core/contractors/codac2_tests_CtcInverseNotIn core/contractors/codac2_tests_CtcLazy core/contractors/codac2_tests_CtcLohner - core/contractors/codac2_tests_CtcParallelepiped core/contractors/codac2_tests_CtcPolygon core/contractors/codac2_tests_CtcSegment core/contractors/codac2_tests_CtcVisible diff --git a/tests/core/contractors/codac2_tests_CtcParallelepiped.cpp b/tests/core/contractors/codac2_tests_CtcParallelepiped.cpp deleted file mode 100644 index 2e0b8add0..000000000 --- a/tests/core/contractors/codac2_tests_CtcParallelepiped.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Codac tests - * ---------------------------------------------------------------------------- - * \date 2026 - * \author Maël Godard - * \copyright Copyright 2024 Codac Team - * \license GNU Lesser General Public License (LGPL) - */ - -#include -#include -#include - -using namespace std; -using namespace codac2; - - -TEST_CASE("CtcParallelepiped") -{ - CHECK(true); - Parallelepiped p1 ({1.5,2.8},Matrix({{0.5,0.4},{0,0.2}})); - CtcParallelepiped ctc_par1 (p1); - - IntervalVector x0 ({{0,5},{0,5}}); - IntervalVector x1 ({{0,0.5},{0,0.5}}); - - ctc_par1.contract(x0); - ctc_par1.contract(x1); - - CHECK(Approx(x0) == IntervalVector({{0.6,2.4},{2.6,3}})); - CHECK(x1.is_empty()); - - Parallelepiped p2 ({-5,-4,3,8},Matrix({{0.5,0.4,0.1,0},{0,0.2,0.3,0},{0,0,0.1,0.4},{0,0,0,0.2}})); - CtcParallelepiped ctc_par2 (p2); - - IntervalVector x2 ({{-10,10},{-10,10},{-10,10},{-10,10}}); - IntervalVector x3 ({{-0.5,0.5},{-0.5,0.5},{-0.5,0.5},{-0.5,0.5}}); - - ctc_par2.contract(x2); - ctc_par2.contract(x3); - - CHECK(Approx(x2) == IntervalVector({{-6,-4},{-4.5,-3.5},{2.5,3.5},{7.8,8.2}})); - CHECK(x3.is_empty()); -} \ No newline at end of file diff --git a/tests/core/contractors/codac2_tests_CtcParallelepiped.py b/tests/core/contractors/codac2_tests_CtcParallelepiped.py deleted file mode 100644 index 81a785c99..000000000 --- a/tests/core/contractors/codac2_tests_CtcParallelepiped.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python - -# Codac tests -# ---------------------------------------------------------------------------- -# \date 2026 -# \author Maël Godard -# \copyright Copyright 2024 Codac Team -# \license GNU Lesser General Public License (LGPL) - -import unittest -from codac import * - -class TestCtcParallelepiped(unittest.TestCase): - - def test_CtcParallelepiped(self): - - p1 = Parallelepiped([1.5,2.8],Matrix([[0.5,0.4],[0,0.2]])) - ctc_par1 = CtcParallelepiped(p1) - - x0 = IntervalVector([[0,5],[0,5]]) - x1 = IntervalVector([[0,0.5],[0,0.5]]) - - ctc_par1.contract(x0) - ctc_par1.contract(x1) - - self.assertTrue(Approx(x0) == IntervalVector([[0.6,2.4],[2.6,3]])) - self.assertTrue(x1.is_empty()) - - p2 = Parallelepiped([-5,-4,3,8],Matrix([[0.5,0.4,0.1,0],[0,0.2,0.3,0],[0,0,0.1,0.4],[0,0,0,0.2]])) - ctc_par2 = CtcParallelepiped(p2) - - x2 = IntervalVector([[-10,10],[-10,10],[-10,10],[-10,10]]) - x3 = IntervalVector([[-0.5,0.5],[-0.5,0.5],[-0.5,0.5],[-0.5,0.5]]) - - ctc_par2.contract(x2) - ctc_par2.contract(x3) - - self.assertTrue(Approx(x2) == IntervalVector([[-6,-4],[-4.5,-3.5],[2.5,3.5],[7.8,8.2]])) - self.assertTrue(x3.is_empty()) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp index 2d203740b..68f9b5950 100644 --- a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp +++ b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.cpp @@ -8,6 +8,7 @@ */ #include +#include #include using namespace std; @@ -29,5 +30,39 @@ TEST_CASE("Parallelepiped") CHECK(z.c == Vector({4,2,0})); CHECK(z.A == Matrix({{0.,1.,1.},{0.,1.,0.},{0.5,0.,0.}})); CHECK(z.box() == IntervalVector({{2.,6.},{1.,3.},{-0.5,0.5}})); + + Parallelepiped p1 ({1.5,2.8},Matrix({{0.5,0.4},{0,0.2}})); + + IntervalVector x0 ({{0,5},{0,5}}); + IntervalVector x1 ({{0,0.5},{0,0.5}}); + IntervalVector x2 ({{1.7,5},{0,3}}); + + CHECK(Approx(x0 & p1) == IntervalVector({{0.6,2.4},{2.6,3}})); + CHECK(Approx((p1 & x0).c) == p1.c); + CHECK(Approx((p1 & x0).A) == p1.A); + + CHECK((x1 & p1).is_empty()); + CHECK((p1 & x1).is_empty()); + + CHECK(Approx(x2 & p1) == IntervalVector({{1.7,2.4},{2.6,3}})); + CHECK(Approx((p1 & x2).c) == Vector({1.65, 2.8})); + CHECK(Approx((p1 & x2).A) == Matrix({{0.35,0.4},{0,0.2}})); + + Parallelepiped p2 ({-5,-4,3,8},Matrix({{0.5,0.4,0.1,0},{0,0.2,0.3,0},{0,0,0.1,0.4},{0,0,0,0.2}})); + Parallelepiped p_empty = Parallelepiped::empty(4); + + IntervalVector x3 ({{-10,10},{-10,10},{-10,10},{-10,10}}); + IntervalVector x4 ({{-0.5,0.5},{-0.5,0.5},{-0.5,0.5},{-0.5,0.5}}); + IntervalVector x_empty = IntervalVector::empty(4); + + CHECK(Approx(x3 & p2) == IntervalVector({{-6,-4},{-4.5,-3.5},{2.5,3.5},{7.8,8.2}})); + CHECK(Approx((p2 & x3).c) == Vector({-5,-4,3,8})); + CHECK(Approx((p2 & x3).A) == Matrix({{0.5,0.4,0.1,0},{0,0.2,0.3,0},{0,0,0.1,0.4},{0,0,0,0.2}})); + + CHECK((x4 & p2).is_empty()); + CHECK((p2 & x4).is_empty()); + + CHECK((x_empty & p2).is_empty()); + CHECK((p_empty & x3).is_empty()); } diff --git a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py index c0eaa0a17..bc201cc0d 100644 --- a/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py +++ b/tests/core/domains/zonotope/codac2_tests_Parallelepiped.py @@ -30,6 +30,41 @@ def test_parallelepiped(self): self.assertTrue(z.c == Vector([4,2,0])) self.assertTrue(z.A == Matrix([[0,1,1],[0,1,0],[0.5,0,0]])) self.assertTrue(z.box() == IntervalVector([[2,6],[1,3],[-0.5,0.5]])) + + + p1 = Parallelepiped([1.5,2.8],Matrix([[0.5,0.4],[0,0.2]])) + + x0 = IntervalVector([[0,5],[0,5]]) + x1 = IntervalVector([[0,0.5],[0,0.5]]) + x2 = IntervalVector([[1.7,5],[0,3]]) + + self.assertTrue(Approx(x0 & p1) == IntervalVector([[0.6,2.4],[2.6,3]])) + self.assertTrue(Approx((p1 & x0).c) == p1.c) + self.assertTrue(Approx((p1 & x0).A) == p1.A) + + self.assertTrue((x1 & p1).is_empty()) + self.assertTrue((p1 & x1).is_empty()) + + self.assertTrue(Approx(x2 & p1) == IntervalVector([[1.7,2.4],[2.6,3]])) + self.assertTrue(Approx((p1 & x2).c) == Vector([1.65, 2.8])) + self.assertTrue(Approx((p1 & x2).A) == Matrix([[0.35,0.4],[0,0.2]])) + + p2 = Parallelepiped([-5,-4,3,8],Matrix([[0.5,0.4,0.1,0],[0,0.2,0.3,0],[0,0,0.1,0.4],[0,0,0,0.2]])) + p_empty = Parallelepiped.empty(4) + + x3 = IntervalVector([[-10,10],[-10,10],[-10,10],[-10,10]]) + x4 = IntervalVector([[-0.5,0.5],[-0.5,0.5],[-0.5,0.5],[-0.5,0.5]]) + x_empty = IntervalVector.empty(4) + + self.assertTrue(Approx(x3 & p2) == IntervalVector([[-6,-4],[-4.5,-3.5],[2.5,3.5],[7.8,8.2]])) + self.assertTrue(Approx((p2 & x3).c) == Vector([-5,-4,3,8])) + self.assertTrue(Approx((p2 & x3).A) == Matrix([[0.5,0.4,0.1,0],[0,0.2,0.3,0],[0,0,0.1,0.4],[0,0,0,0.2]])) + + self.assertTrue((x4 & p2).is_empty()) + self.assertTrue((p2 & x4).is_empty()) + + self.assertTrue((x_empty & p2).is_empty()) + self.assertTrue((p_empty & x3).is_empty()) if __name__ == '__main__': unittest.main() \ No newline at end of file From 8abf2919d28e7efd4dbf6eb42ea683010d6004d5 Mon Sep 17 00:00:00 2001 From: godardma Date: Mon, 11 May 2026 18:09:55 +0200 Subject: [PATCH 9/9] [doc] correcting source file --- doc/manual/manual/contractors/geometric/src.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/manual/manual/contractors/geometric/src.cpp b/doc/manual/manual/contractors/geometric/src.cpp index ee93eb7b0..1eb97ca13 100644 --- a/doc/manual/manual/contractors/geometric/src.cpp +++ b/doc/manual/manual/contractors/geometric/src.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include