From adbbf5e32537f588d2354ec8ee78e9400ae5b9dd Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 17:36:19 +0000 Subject: [PATCH 01/13] EpigraphSquaredNorm: first untested implementation --- Cargo.toml | 1 + src/constraints/epigraph_squared_norm.rs | 56 ++++++++++++++++++++++++ src/constraints/mod.rs | 2 + src/constraints/tests.rs | 8 ++++ 4 files changed, 67 insertions(+) create mode 100644 src/constraints/epigraph_squared_norm.rs diff --git a/Cargo.toml b/Cargo.toml index e130711f..4fcbbf8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ rpmalloc = { version = "0.2.0", features = [ [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = { version = "0.5.0", optional = true } +roots = "0.0.8" # -------------------------------------------------------------------------- # F.E.A.T.U.R.E.S. diff --git a/src/constraints/epigraph_squared_norm.rs b/src/constraints/epigraph_squared_norm.rs new file mode 100644 index 00000000..fbb09626 --- /dev/null +++ b/src/constraints/epigraph_squared_norm.rs @@ -0,0 +1,56 @@ +use crate::matrix_operations; + +use super::Constraint; + +#[derive(Copy, Clone)] +/// A +pub struct EpigraphSquaredNorm {} + +impl EpigraphSquaredNorm { + /// A + pub fn new() -> Self { + EpigraphSquaredNorm {} + } +} + +impl Constraint for EpigraphSquaredNorm { + fn project(&self, x: &mut [f64]) { + let nx = x.len() - 1; + let z: &[f64] = &x[..nx]; + let t: f64 = x[nx]; + let norm_z_sq = matrix_operations::norm2_squared(&z); + if norm_z_sq <= t { + return; + } + let theta = 1. - 2. * t; + let a0 = 1.; + let a1 = theta; + let a2 = 0.25 * theta * theta; + let a3 = -0.25 * norm_z_sq; + + let cubic_poly_roots = roots::find_roots_cubic(a3, a2, a1, a0); + let mut right_root = f64::NAN; + let mut scaling = f64::NAN; + + // Find right root + cubic_poly_roots.as_ref().iter().for_each(|ri| { + if *ri > 0. { + let denom = 1. + 2. * (*ri - t); + if ((norm_z_sq / (denom * denom)) - *ri) < 1e-6f64 { + right_root = *ri; + scaling = denom; + } + return; + } + }); + + // Project + x.iter_mut().for_each(|xi| { + *xi /= scaling; + }); + } + + fn is_convex(&self) -> bool { + true + } +} diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs index ea8895da..66ac7b33 100644 --- a/src/constraints/mod.rs +++ b/src/constraints/mod.rs @@ -12,6 +12,7 @@ mod ball1; mod ball2; mod ballinf; mod cartesian_product; +mod epigraph_squared_norm; mod finite; mod halfspace; mod hyperplane; @@ -26,6 +27,7 @@ pub use ball1::Ball1; pub use ball2::Ball2; pub use ballinf::BallInf; pub use cartesian_product::CartesianProduct; +pub use epigraph_squared_norm::EpigraphSquaredNorm; pub use finite::FiniteSet; pub use halfspace::Halfspace; pub use hyperplane::Hyperplane; diff --git a/src/constraints/tests.rs b/src/constraints/tests.rs index a09b3c16..5e9216d8 100644 --- a/src/constraints/tests.rs +++ b/src/constraints/tests.rs @@ -872,3 +872,11 @@ fn t_sphere2_center_projection_of_center() { fn t_ball1_alpha_negative() { let _ = Ball1::new(None, -1.); } + +#[test] +fn t_cubic_roots() { + let epi = EpigraphSquaredNorm::new(); + let mut x = [1., 2., 3., 4.]; + epi.project(&mut x); + println!("x = {:?}", x); +} From 3ec808b9258c94299080faf3b66d6a9e32c7a22b Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 17:54:23 +0000 Subject: [PATCH 02/13] EpigraphSquaredNorm: tested --- src/constraints/epigraph_squared_norm.rs | 22 +++++++++++++--------- src/constraints/tests.rs | 12 +++++++++--- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/constraints/epigraph_squared_norm.rs b/src/constraints/epigraph_squared_norm.rs index fbb09626..a60ac473 100644 --- a/src/constraints/epigraph_squared_norm.rs +++ b/src/constraints/epigraph_squared_norm.rs @@ -22,11 +22,12 @@ impl Constraint for EpigraphSquaredNorm { if norm_z_sq <= t { return; } + let theta = 1. - 2. * t; - let a0 = 1.; - let a1 = theta; - let a2 = 0.25 * theta * theta; - let a3 = -0.25 * norm_z_sq; + let a3 = 4.; + let a2 = 4. * theta; + let a1 = theta * theta; + let a0 = -norm_z_sq; let cubic_poly_roots = roots::find_roots_cubic(a3, a2, a1, a0); let mut right_root = f64::NAN; @@ -36,7 +37,7 @@ impl Constraint for EpigraphSquaredNorm { cubic_poly_roots.as_ref().iter().for_each(|ri| { if *ri > 0. { let denom = 1. + 2. * (*ri - t); - if ((norm_z_sq / (denom * denom)) - *ri) < 1e-6f64 { + if ((norm_z_sq / (denom * denom)) - *ri).abs() < 1e-6 { right_root = *ri; scaling = denom; } @@ -44,10 +45,13 @@ impl Constraint for EpigraphSquaredNorm { } }); - // Project - x.iter_mut().for_each(|xi| { - *xi /= scaling; - }); + // TODO: refinement of root + + // Projection + for i in 0..nx { + x[i] /= scaling; + } + x[nx] = right_root; } fn is_convex(&self) -> bool { diff --git a/src/constraints/tests.rs b/src/constraints/tests.rs index 5e9216d8..77956722 100644 --- a/src/constraints/tests.rs +++ b/src/constraints/tests.rs @@ -1,3 +1,5 @@ +use crate::matrix_operations; + use super::*; use rand; @@ -876,7 +878,11 @@ fn t_ball1_alpha_negative() { #[test] fn t_cubic_roots() { let epi = EpigraphSquaredNorm::new(); - let mut x = [1., 2., 3., 4.]; - epi.project(&mut x); - println!("x = {:?}", x); + for i in 0..100 { + let t = 0.01 * i as f64; + let mut x = [1., 2., 3., t]; + epi.project(&mut x); + let err = (matrix_operations::norm2_squared(&x[..3]) - x[3]).abs(); + assert!(err < 1e-10, "wrong projection on epigraph of squared norm"); + } } From 2bb00cc81581211957858fd409b25f07fd08727c Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 18:32:47 +0000 Subject: [PATCH 03/13] EpigraphSquaredNorm: refinement of root --- src/constraints/epigraph_squared_norm.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/constraints/epigraph_squared_norm.rs b/src/constraints/epigraph_squared_norm.rs index a60ac473..bac317e4 100644 --- a/src/constraints/epigraph_squared_norm.rs +++ b/src/constraints/epigraph_squared_norm.rs @@ -45,7 +45,22 @@ impl Constraint for EpigraphSquaredNorm { } }); - // TODO: refinement of root + // Refinement of root with Newton-Raphson + let mut refinement_error = 1.; + let newton_max_iters: usize = 5; + let newton_eps = 1e-14; + let mut zsol = right_root; + let mut iter = 0; + while refinement_error > newton_eps && iter < newton_max_iters { + let zsol_sq = zsol * zsol; + let zsol_cb = zsol_sq * zsol; + let p_z = a3 * zsol_cb + a2 * zsol_sq + a1 * zsol + a0; + let dp_z = 3. * a3 * zsol_sq + 2. * a2 * zsol + a1; + zsol = zsol - p_z / dp_z; + refinement_error = p_z.abs(); + iter += 1; + } + right_root = zsol; // Projection for i in 0..nx { From 78802a9c79990b16efcd2ae5cee509eee8fa6b43 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 18:57:09 +0000 Subject: [PATCH 04/13] EpigraphSquaredNorm: more tests --- appveyor.yml | 1 + src/constraints/tests.rs | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 614b3b7e..066a1956 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -68,4 +68,5 @@ build: false #directly or perform other testing commands. Rust will automatically be placed in the PATH # environment variable. test_script: + - cargo build - cargo test --verbose %cargoflags% diff --git a/src/constraints/tests.rs b/src/constraints/tests.rs index 77956722..d0cf1d0b 100644 --- a/src/constraints/tests.rs +++ b/src/constraints/tests.rs @@ -876,7 +876,22 @@ fn t_ball1_alpha_negative() { } #[test] -fn t_cubic_roots() { +fn t_epigraph_squared_norm_inside() { + let epi = EpigraphSquaredNorm::new(); + let mut x = [1., 2., 10.]; + let x_correct = x.clone(); + epi.project(&mut x); + unit_test_utils::assert_nearly_equal_array( + &x_correct, + &x, + 1e-12, + 1e-14, + "wrong projection on epigraph of squared norm", + ); +} + +#[test] +fn t_epigraph_squared_norm() { let epi = EpigraphSquaredNorm::new(); for i in 0..100 { let t = 0.01 * i as f64; @@ -886,3 +901,23 @@ fn t_cubic_roots() { assert!(err < 1e-10, "wrong projection on epigraph of squared norm"); } } + +#[test] +fn t_epigraph_squared_norm_correctness() { + let epi = EpigraphSquaredNorm::new(); + let mut x = [1., 2., 3., 4.]; + let x_correct = [ + 0.560142228903570, + 1.120284457807140, + 1.680426686710711, + 4.392630432414829, + ]; + epi.project(&mut x); + unit_test_utils::assert_nearly_equal_array( + &x_correct, + &x, + 1e-12, + 1e-14, + "wrong projection on epigraph of squared norm", + ); +} From 89e72f63bebd0e495163bbf08996bee6450007ad Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 19:11:49 +0000 Subject: [PATCH 05/13] fighting with appveyor --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 066a1956..20bd0b3d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -68,5 +68,6 @@ build: false #directly or perform other testing commands. Rust will automatically be placed in the PATH # environment variable. test_script: + - cargo add roots - cargo build - cargo test --verbose %cargoflags% From 036784e009105a8b7202d92e4b7afbcef4e68f03 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 22:32:00 +0000 Subject: [PATCH 06/13] [ci-skip] add documentation --- Cargo.toml | 2 ++ src/constraints/epigraph_squared_norm.rs | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4fcbbf8f..be7a64b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,8 @@ rpmalloc = { version = "0.2.0", features = [ [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = { version = "0.5.0", optional = true } +# computation of roots of cubic equation needed for the projection on the +# epigraph of the squared Eucliean norm roots = "0.0.8" # -------------------------------------------------------------------------- diff --git a/src/constraints/epigraph_squared_norm.rs b/src/constraints/epigraph_squared_norm.rs index bac317e4..3ab2bc7c 100644 --- a/src/constraints/epigraph_squared_norm.rs +++ b/src/constraints/epigraph_squared_norm.rs @@ -3,19 +3,31 @@ use crate::matrix_operations; use super::Constraint; #[derive(Copy, Clone)] -/// A +/// The epigraph of the squared Eucliden norm is a set of the form +/// $X = \\{x = (z, t) \in \mathbb{R}^{n}\times \mathbb{R} {}:{} \\|z\\|^2 \leq t \\}.$ pub struct EpigraphSquaredNorm {} impl EpigraphSquaredNorm { - /// A + /// Create a new instance of the epigraph of the squared norm. + /// + /// Note that you do not need to specify the dimension. pub fn new() -> Self { EpigraphSquaredNorm {} } } impl Constraint for EpigraphSquaredNorm { + ///Project on the epigraph of the squared Euclidean norm. + /// + /// The projection is computed as detailed + /// [here](https://mathematix.wordpress.com/2017/05/02/projection-on-the-epigraph-of-the-squared-euclidean-norm/). + /// + /// ## Arguments + /// - `x`: The given vector $x$ is updated with the projection on the set + /// fn project(&self, x: &mut [f64]) { let nx = x.len() - 1; + assert!(nx > 0, "x must have a length of at least 2"); let z: &[f64] = &x[..nx]; let t: f64 = x[nx]; let norm_z_sq = matrix_operations::norm2_squared(&z); @@ -69,6 +81,7 @@ impl Constraint for EpigraphSquaredNorm { x[nx] = right_root; } + /// This is a convex set, so this function returns `True` fn is_convex(&self) -> bool { true } From e2a6b7b4c34cb9ef1ba94a077227b2fc93c1c071 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 22:34:02 +0000 Subject: [PATCH 07/13] [ci-skip] update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b22078..64ac85cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,11 @@ Note: This is the main Changelog file for the Rust solver. The Changelog file fo -## Unreleased +## [v0.9.0] - Unreleased + +### Added + +- Rust implementation of epigraph of squared Euclidean norm (constraint) ### Fixed @@ -275,6 +279,7 @@ This is a breaking API change. --------------------- --> +[v0.9.0]: https://github.com/alphaville/optimization-engine/compare/v0.8.1...v0.9.0 [v0.8.1]: https://github.com/alphaville/optimization-engine/compare/v0.8.0...v0.8.1 [v0.8.0]: https://github.com/alphaville/optimization-engine/compare/v0.7.7...v0.8.0 [v0.7.7]: https://github.com/alphaville/optimization-engine/compare/v0.7.6...v0.7.7 From b8eaeea72c923c5f557c63717bf57ca897926093 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Mon, 30 Oct 2023 22:35:14 +0000 Subject: [PATCH 08/13] [ci skip] update Cargo --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index be7a64b8..963f797b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ homepage = "https://alphaville.github.io/optimization-engine/" repository = "https://github.com/alphaville/optimization-engine" # Version of this crate (SemVer) -version = "0.8.1" +version = "0.9.0" edition = "2018" From 29c125721e54be08a3b837f3f09891df22ca3a0a Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Tue, 31 Oct 2023 01:20:03 +0000 Subject: [PATCH 09/13] [ci skip] epigraph sq norm: API docs --- src/constraints/epigraph_squared_norm.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/constraints/epigraph_squared_norm.rs b/src/constraints/epigraph_squared_norm.rs index 3ab2bc7c..e8d128d5 100644 --- a/src/constraints/epigraph_squared_norm.rs +++ b/src/constraints/epigraph_squared_norm.rs @@ -25,6 +25,15 @@ impl Constraint for EpigraphSquaredNorm { /// ## Arguments /// - `x`: The given vector $x$ is updated with the projection on the set /// + /// ## Example + /// + /// ```rust + /// use optimization_engine::constraints::*; + /// + /// let epi = EpigraphSquaredNorm::new(); + /// let mut x = [1., 2., 3., 4.]; + /// epi.project(&mut x); + /// ``` fn project(&self, x: &mut [f64]) { let nx = x.len() - 1; assert!(nx > 0, "x must have a length of at least 2"); From 1d19ab304f7180d9d7ceed851b8d47dc4b73fde7 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Wed, 20 Mar 2024 13:49:04 +0000 Subject: [PATCH 10/13] merge master; fix clippy issues; fix typo in Cargo --- Cargo.toml | 2 +- src/constraints/epigraph_squared_norm.rs | 11 +++++------ src/constraints/tests.rs | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4ca6231..c0e43aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ jemallocator = { version = "0.5.0", optional = true } # computation of roots of cubic equation needed for the projection on the -# epigraph of the squared Eucliean norm +# epigraph of the squared Euclidean norm roots = "0.0.8" # Least squares solver diff --git a/src/constraints/epigraph_squared_norm.rs b/src/constraints/epigraph_squared_norm.rs index e8d128d5..560b34bd 100644 --- a/src/constraints/epigraph_squared_norm.rs +++ b/src/constraints/epigraph_squared_norm.rs @@ -2,7 +2,7 @@ use crate::matrix_operations; use super::Constraint; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Default)] /// The epigraph of the squared Eucliden norm is a set of the form /// $X = \\{x = (z, t) \in \mathbb{R}^{n}\times \mathbb{R} {}:{} \\|z\\|^2 \leq t \\}.$ pub struct EpigraphSquaredNorm {} @@ -39,7 +39,7 @@ impl Constraint for EpigraphSquaredNorm { assert!(nx > 0, "x must have a length of at least 2"); let z: &[f64] = &x[..nx]; let t: f64 = x[nx]; - let norm_z_sq = matrix_operations::norm2_squared(&z); + let norm_z_sq = matrix_operations::norm2_squared(z); if norm_z_sq <= t { return; } @@ -62,7 +62,6 @@ impl Constraint for EpigraphSquaredNorm { right_root = *ri; scaling = denom; } - return; } }); @@ -77,15 +76,15 @@ impl Constraint for EpigraphSquaredNorm { let zsol_cb = zsol_sq * zsol; let p_z = a3 * zsol_cb + a2 * zsol_sq + a1 * zsol + a0; let dp_z = 3. * a3 * zsol_sq + 2. * a2 * zsol + a1; - zsol = zsol - p_z / dp_z; + zsol -= p_z / dp_z; refinement_error = p_z.abs(); iter += 1; } right_root = zsol; // Projection - for i in 0..nx { - x[i] /= scaling; + for xi in x.iter_mut().take(nx) { + *xi /= scaling; } x[nx] = right_root; } diff --git a/src/constraints/tests.rs b/src/constraints/tests.rs index 1533ec55..26343688 100644 --- a/src/constraints/tests.rs +++ b/src/constraints/tests.rs @@ -1,7 +1,6 @@ use crate::matrix_operations; use super::*; -use modcholesky::ModCholeskySE99; use rand; #[test] @@ -922,6 +921,8 @@ fn t_epigraph_squared_norm_correctness() { "wrong projection on epigraph of squared norm", ); } + +#[test] fn t_affine_space() { let a = vec![ 0.5, 0.1, 0.2, -0.3, -0.6, 0.3, 0., 0.5, 1.0, 0.1, -1.0, -0.4, From 5c98f5ff9b92e8842340d451fadb5c47b6fa0b50 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Wed, 20 Mar 2024 14:09:48 +0000 Subject: [PATCH 11/13] update dependencies in Cargo.toml --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c0e43aa9..ae50f398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ rustdoc-args = ["--html-in-header", "katex-header.html"] # D.E.P.E.N.D.E.N.C.I.E.S # -------------------------------------------------------------------------- [dependencies] -num = "0.4.0" +num = "0.4" # Our own stuff - L-BFGS: limited-memory BFGS directions lbfgs = "0.2" @@ -86,10 +86,10 @@ lbfgs = "0.2" instant = { version = "0.1" } # Wasm-bindgen is only activated if OpEn is compiled with `--features wasm` -wasm-bindgen = { version = "0.2.74", optional = true } +wasm-bindgen = { version = "0.2", optional = true } # sc-allocator provides an implementation of a bump allocator -rpmalloc = { version = "0.2.0", features = [ +rpmalloc = { version = "0.2", features = [ "guards", "statistics", ], optional = true } @@ -97,7 +97,7 @@ rpmalloc = { version = "0.2.0", features = [ # jemallocator is an optional feature; it will only be loaded if the feature # `jem` is used (i.e., if we compile with `cargo build --features jem`) [target.'cfg(not(target_env = "msvc"))'.dependencies] -jemallocator = { version = "0.5.0", optional = true } +jemallocator = { version = "0.5", optional = true } # computation of roots of cubic equation needed for the projection on the @@ -106,7 +106,7 @@ roots = "0.0.8" # Least squares solver ndarray = { version = "0.15", features = ["approx"] } -modcholesky = "0.1.3" +modcholesky = "0.1" # -------------------------------------------------------------------------- From 55188a8327601b16d101a4458e12e686fddf36d8 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Wed, 20 Mar 2024 14:18:45 +0000 Subject: [PATCH 12/13] [ci skip] update webside docs for EpigraphSquaredNorm --- docs/python-interface.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/python-interface.md b/docs/python-interface.md index a332ea29..77983d8a 100644 --- a/docs/python-interface.md +++ b/docs/python-interface.md @@ -87,6 +87,7 @@ following types of constraints: | `NoConstraints` | No constraints - the whole $\mathbb{R}^{n}$| | `Rectangle` | Rectangle, $$R = \\{u \in \mathbb{R}^{n_u} {}:{} f_{\min} \leq u \leq f_{\max}\\},$$ for example, `Rectangle(fmin, fmax)` | | `SecondOrderCone` | Second-order aka "ice cream" aka "Lorenz" cone | +| `EpigraphSquaredNorm`| The epigraph of the squared Eucliden norm is a set of the form $X = \\{(z, t) \in \mathbb{R}^{n+1}: \Vert z \Vert \leq t\\}$. | | `CartesianProduct` | Cartesian product of any of the above. See more information below. | From ad45f27bbd1379a9f26add188a39c38230108042 Mon Sep 17 00:00:00 2001 From: Pantelis Sopasakis Date: Wed, 20 Mar 2024 14:20:25 +0000 Subject: [PATCH 13/13] update main changelog (v0.9.0) --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8fa985d..076bda35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Note: This is the main Changelog file for the Rust solver. The Changelog file for the Python interface (`opengen`) can be found in [/open-codegen/CHANGELOG.md](open-codegen/CHANGELOG.md) + + + + -## [v0.9.0] - Unreleased +## [v0.9.0] - 2024-03-20 ### Added