From a9e2db8c1ca702b65eaf840d512f4b6b5f6c969e Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Tue, 10 Dec 2024 20:37:02 +0900 Subject: [PATCH 01/18] instance_to_{pubo,qubo} in ommx._ommx_rust --- python/ommx/ommx/_ommx_rust.pyi | 2 ++ python/ommx/src/instance.rs | 28 ++++++++++++++++++++++++++++ python/ommx/src/lib.rs | 6 ++++++ rust/ommx/src/convert/sorted_ids.rs | 20 ++++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 python/ommx/src/instance.rs diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index 59215c4b..1f3b3d5e 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -148,6 +148,8 @@ def evaluate_instance(function: bytes, state: bytes) -> tuple[bytes, set[int]]: def evaluate_linear(function: bytes, state: bytes) -> tuple[float, set[int]]: ... def evaluate_polynomial(function: bytes, state: bytes) -> tuple[float, set[int]]: ... def evaluate_quadratic(function: bytes, state: bytes) -> tuple[float, set[int]]: ... +def instance_to_pubo(instance_bytes: bytes) -> dict: ... +def instance_to_qubo(instance_bytes: bytes) -> tuple[dict, float]: ... def load_mps_bytes(path: str) -> bytes: ... def miplib2017_instance_annotations() -> dict[str, dict[str, str]]: ... def partial_evaluate_constraint(obj: bytes, state: bytes) -> tuple[bytes, set[int]]: ... diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs new file mode 100644 index 00000000..19635a66 --- /dev/null +++ b/python/ommx/src/instance.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use ommx::Message; +use pyo3::{ + prelude::*, + types::{PyBytes, PyDict}, +}; + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyfunction)] +#[pyfunction] +pub fn instance_to_pubo<'py>( + py: Python<'py>, + instance_bytes: Bound<'_, PyBytes>, +) -> Result> { + let instance = ommx::v1::Instance::decode(instance_bytes.as_bytes())?; + let pubo = instance.to_pubo()?; + Ok(serde_pyobject::to_pyobject(py, &pubo)?.extract()?) +} + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyfunction)] +#[pyfunction] +pub fn instance_to_qubo<'py>( + py: Python<'py>, + instance_bytes: Bound<'_, PyBytes>, +) -> Result<(Bound<'py, PyDict>, f64)> { + let instance = ommx::v1::Instance::decode(instance_bytes.as_bytes())?; + let (qubo, constant) = instance.to_qubo()?; + Ok((serde_pyobject::to_pyobject(py, &qubo)?.extract()?, constant)) +} diff --git a/python/ommx/src/lib.rs b/python/ommx/src/lib.rs index 78e65059..eeff42f1 100644 --- a/python/ommx/src/lib.rs +++ b/python/ommx/src/lib.rs @@ -3,6 +3,7 @@ mod builder; mod dataset; mod descriptor; mod evaluate; +mod instance; mod message; mod mps; @@ -11,6 +12,7 @@ pub use builder::*; pub use dataset::*; pub use descriptor::*; pub use evaluate::*; +pub use instance::*; pub use message::*; pub use mps::*; @@ -48,6 +50,10 @@ fn _ommx_rust(_py: Python, m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(partial_evaluate_instance, m)?)?; m.add_function(wrap_pyfunction!(used_decision_variable_ids, m)?)?; + // Instance + m.add_function(wrap_pyfunction!(instance_to_pubo, m)?)?; + m.add_function(wrap_pyfunction!(instance_to_qubo, m)?)?; + // MPS m.add_function(wrap_pyfunction!(load_mps_bytes, m)?)?; m.add_function(wrap_pyfunction!(write_mps_file, m)?)?; diff --git a/rust/ommx/src/convert/sorted_ids.rs b/rust/ommx/src/convert/sorted_ids.rs index c057ea51..82bedce9 100644 --- a/rust/ommx/src/convert/sorted_ids.rs +++ b/rust/ommx/src/convert/sorted_ids.rs @@ -1,5 +1,6 @@ use anyhow::bail; use proptest::prelude::*; +use serde::{ser::*, Serialize}; use std::{collections::BTreeSet, ops::*}; /// A sorted list of decision variable and parameter IDs @@ -136,6 +137,16 @@ impl Deref for BinaryIds { } } +impl Serialize for BinaryIds { + fn serialize(&self, serializer: S) -> Result { + let mut tup = serializer.serialize_tuple(self.0.len())?; + for id in &self.0 { + tup.serialize_element(id)?; + } + tup.end() + } +} + /// ID pair for QUBO problems #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] pub struct BinaryIdPair(pub u64, pub u64); @@ -166,3 +177,12 @@ impl TryFrom for BinaryIdPair { Self::try_from(ids.0.into_iter().collect::>()) } } + +impl Serialize for BinaryIdPair { + fn serialize(&self, serializer: S) -> Result { + let mut tup = serializer.serialize_tuple(2)?; + tup.serialize_element(&self.0)?; + tup.serialize_element(&self.1)?; + tup.end() + } +} From 1cbed37fa1ca58c29d1639e056ba5f2a9cfe21cb Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Tue, 10 Dec 2024 20:49:59 +0900 Subject: [PATCH 02/18] Instance and ParametricInstance in ommx._ommx_rust --- python/ommx/ommx/_ommx_rust.pyi | 10 +++++++++ python/ommx/src/instance.rs | 36 +++++++++++++++++++++++++++++++++ python/ommx/src/lib.rs | 2 ++ 3 files changed, 48 insertions(+) diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index 1f3b3d5e..ca4dcb1f 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -98,6 +98,11 @@ class Function: def mul_quadratic(self, quadratic: Quadratic) -> Function: ... def mul_polynomial(self, polynomial: Polynomial) -> Function: ... +class Instance: + @staticmethod + def from_bytes(bytes: bytes) -> Instance: ... + def to_bytes(self) -> bytes: ... + class Linear: @staticmethod def single_term(id: int, coefficient: float) -> Linear: ... @@ -112,6 +117,11 @@ class Linear: def add_scalar(self, scalar: float) -> Linear: ... def mul_scalar(self, scalar: float) -> Linear: ... +class ParametricInstance: + @staticmethod + def from_bytes(bytes: bytes) -> ParametricInstance: ... + def to_bytes(self) -> bytes: ... + class Polynomial: @staticmethod def decode(bytes: bytes) -> Polynomial: ... diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index 19635a66..108fc686 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -26,3 +26,39 @@ pub fn instance_to_qubo<'py>( let (qubo, constant) = instance.to_qubo()?; Ok((serde_pyobject::to_pyobject(py, &qubo)?.extract()?, constant)) } + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyclass)] +#[pyclass] +pub struct Instance(ommx::v1::Instance); + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pymethods)] +#[pymethods] +impl Instance { + #[staticmethod] + pub fn from_bytes(bytes: &Bound) -> Result { + let inner = ommx::v1::Instance::decode(bytes.as_bytes())?; + Ok(Self(inner)) + } + + pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) + } +} + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyclass)] +#[pyclass] +pub struct ParametricInstance(ommx::v1::ParametricInstance); + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pymethods)] +#[pymethods] +impl ParametricInstance { + #[staticmethod] + pub fn from_bytes(bytes: &Bound) -> Result { + let inner = ommx::v1::ParametricInstance::decode(bytes.as_bytes())?; + Ok(Self(inner)) + } + + pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) + } +} diff --git a/python/ommx/src/lib.rs b/python/ommx/src/lib.rs index eeff42f1..9f8fe5e2 100644 --- a/python/ommx/src/lib.rs +++ b/python/ommx/src/lib.rs @@ -34,6 +34,8 @@ fn _ommx_rust(_py: Python, m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; // Evaluate m.add_function(wrap_pyfunction!(evaluate_function, m)?)?; From 6a29f0377ad9d0b5424840c5b77b7f0ed9f17b8a Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Tue, 10 Dec 2024 21:00:23 +0900 Subject: [PATCH 03/18] _ommx_rust.Instance.to_{pubo,qubo} --- python/ommx/ommx/_ommx_rust.pyi | 4 ++-- python/ommx/src/instance.rs | 32 ++++++++++---------------------- python/ommx/src/lib.rs | 4 ---- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index ca4dcb1f..b83e04d1 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -102,6 +102,8 @@ class Instance: @staticmethod def from_bytes(bytes: bytes) -> Instance: ... def to_bytes(self) -> bytes: ... + def to_pubo(self) -> dict: ... + def to_qubo(self) -> tuple[dict, float]: ... class Linear: @staticmethod @@ -158,8 +160,6 @@ def evaluate_instance(function: bytes, state: bytes) -> tuple[bytes, set[int]]: def evaluate_linear(function: bytes, state: bytes) -> tuple[float, set[int]]: ... def evaluate_polynomial(function: bytes, state: bytes) -> tuple[float, set[int]]: ... def evaluate_quadratic(function: bytes, state: bytes) -> tuple[float, set[int]]: ... -def instance_to_pubo(instance_bytes: bytes) -> dict: ... -def instance_to_qubo(instance_bytes: bytes) -> tuple[dict, float]: ... def load_mps_bytes(path: str) -> bytes: ... def miplib2017_instance_annotations() -> dict[str, dict[str, str]]: ... def partial_evaluate_constraint(obj: bytes, state: bytes) -> tuple[bytes, set[int]]: ... diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index 108fc686..d2dcb593 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -5,28 +5,6 @@ use pyo3::{ types::{PyBytes, PyDict}, }; -#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyfunction)] -#[pyfunction] -pub fn instance_to_pubo<'py>( - py: Python<'py>, - instance_bytes: Bound<'_, PyBytes>, -) -> Result> { - let instance = ommx::v1::Instance::decode(instance_bytes.as_bytes())?; - let pubo = instance.to_pubo()?; - Ok(serde_pyobject::to_pyobject(py, &pubo)?.extract()?) -} - -#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyfunction)] -#[pyfunction] -pub fn instance_to_qubo<'py>( - py: Python<'py>, - instance_bytes: Bound<'_, PyBytes>, -) -> Result<(Bound<'py, PyDict>, f64)> { - let instance = ommx::v1::Instance::decode(instance_bytes.as_bytes())?; - let (qubo, constant) = instance.to_qubo()?; - Ok((serde_pyobject::to_pyobject(py, &qubo)?.extract()?, constant)) -} - #[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyclass)] #[pyclass] pub struct Instance(ommx::v1::Instance); @@ -43,6 +21,16 @@ impl Instance { pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) } + + pub fn to_pubo<'py>(&self, py: Python<'py>) -> Result> { + let pubo = self.0.to_pubo()?; + Ok(serde_pyobject::to_pyobject(py, &pubo)?.extract()?) + } + + pub fn to_qubo<'py>(&self, py: Python<'py>) -> Result<(Bound<'py, PyDict>, f64)> { + let (qubo, constant) = self.0.to_qubo()?; + Ok((serde_pyobject::to_pyobject(py, &qubo)?.extract()?, constant)) + } } #[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyclass)] diff --git a/python/ommx/src/lib.rs b/python/ommx/src/lib.rs index 9f8fe5e2..c62cae14 100644 --- a/python/ommx/src/lib.rs +++ b/python/ommx/src/lib.rs @@ -52,10 +52,6 @@ fn _ommx_rust(_py: Python, m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(partial_evaluate_instance, m)?)?; m.add_function(wrap_pyfunction!(used_decision_variable_ids, m)?)?; - // Instance - m.add_function(wrap_pyfunction!(instance_to_pubo, m)?)?; - m.add_function(wrap_pyfunction!(instance_to_qubo, m)?)?; - // MPS m.add_function(wrap_pyfunction!(load_mps_bytes, m)?)?; m.add_function(wrap_pyfunction!(write_mps_file, m)?)?; From 137c0bbf0e301a074a725f8a2483f9243c18563b Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Tue, 10 Dec 2024 21:12:53 +0900 Subject: [PATCH 04/18] Instance.penalty_method, ParametricInstance.with_parameters --- python/ommx/ommx/_ommx_rust.pyi | 7 +++++++ python/ommx/src/instance.rs | 27 +++++++++++++++++++++++++++ python/ommx/src/lib.rs | 1 + 3 files changed, 35 insertions(+) diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index b83e04d1..8eee20fb 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -104,6 +104,7 @@ class Instance: def to_bytes(self) -> bytes: ... def to_pubo(self) -> dict: ... def to_qubo(self) -> tuple[dict, float]: ... + def penalty_method(self) -> ParametricInstance: ... class Linear: @staticmethod @@ -119,10 +120,16 @@ class Linear: def add_scalar(self, scalar: float) -> Linear: ... def mul_scalar(self, scalar: float) -> Linear: ... +class Parameters: + @staticmethod + def from_bytes(bytes: bytes) -> Parameters: ... + def to_bytes(self) -> bytes: ... + class ParametricInstance: @staticmethod def from_bytes(bytes: bytes) -> ParametricInstance: ... def to_bytes(self) -> bytes: ... + def with_parameters(self, parameters: Parameters) -> Instance: ... class Polynomial: @staticmethod diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index d2dcb593..61db832b 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -31,6 +31,10 @@ impl Instance { let (qubo, constant) = self.0.to_qubo()?; Ok((serde_pyobject::to_pyobject(py, &qubo)?.extract()?, constant)) } + + pub fn penalty_method(&self) -> ParametricInstance { + ParametricInstance(self.0.clone().penalty_method()) + } } #[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyclass)] @@ -49,4 +53,27 @@ impl ParametricInstance { pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) } + + pub fn with_parameters<'py>(&self, parameters: &Parameters) -> Result { + let instance = self.0.clone().with_parameters(parameters.0.clone())?; + Ok(Instance(instance)) + } +} + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyclass)] +#[pyclass] +pub struct Parameters(ommx::v1::Parameters); + +#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pymethods)] +#[pymethods] +impl Parameters { + #[staticmethod] + pub fn from_bytes(bytes: &Bound) -> Result { + let inner = ommx::v1::Parameters::decode(bytes.as_bytes())?; + Ok(Self(inner)) + } + + pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) + } } diff --git a/python/ommx/src/lib.rs b/python/ommx/src/lib.rs index c62cae14..cc0b3d3a 100644 --- a/python/ommx/src/lib.rs +++ b/python/ommx/src/lib.rs @@ -36,6 +36,7 @@ fn _ommx_rust(_py: Python, m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // Evaluate m.add_function(wrap_pyfunction!(evaluate_function, m)?)?; From 569e15c8fbc4fb5cff14a71c1f119d64be1c088e Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Tue, 10 Dec 2024 21:16:59 +0900 Subject: [PATCH 05/18] Remove redundant lifetime parameter --- python/ommx/src/instance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index 61db832b..3d6c6ace 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -54,7 +54,7 @@ impl ParametricInstance { Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) } - pub fn with_parameters<'py>(&self, parameters: &Parameters) -> Result { + pub fn with_parameters(&self, parameters: &Parameters) -> Result { let instance = self.0.clone().with_parameters(parameters.0.clone())?; Ok(Instance(instance)) } From 17cfc0edd0f836b8643868295dec56beed144de2 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 16:25:34 +0900 Subject: [PATCH 06/18] Rename `to_qubo` to `as_qubo_format` --- python/ommx/ommx/_ommx_rust.pyi | 4 ++-- python/ommx/ommx/v1/__init__.py | 29 +++++++++++++++++++++++++++++ python/ommx/src/instance.rs | 8 ++++---- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index 8eee20fb..e9c25f00 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -102,8 +102,8 @@ class Instance: @staticmethod def from_bytes(bytes: bytes) -> Instance: ... def to_bytes(self) -> bytes: ... - def to_pubo(self) -> dict: ... - def to_qubo(self) -> tuple[dict, float]: ... + def as_pubo_format(self) -> dict: ... + def as_qubo_format(self) -> tuple[dict, float]: ... def penalty_method(self) -> ParametricInstance: ... class Linear: diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index fbcb9dcd..835342ed 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -232,6 +232,35 @@ def partial_evaluate(self, state: State | Mapping[int, float]) -> Instance: ) return Instance.from_bytes(out) + def to_qubo(self) -> tuple[dict[tuple[int, int], float], float]: + """ + Easy-to-use method to convert non-QUBO instance into QUBO and return as a QUBO format. + """ + raise NotImplementedError + + def to_pubo(self) -> dict[tuple[int, ...], float]: + raise NotImplementedError + + def as_qubo_format(self) -> tuple[dict[tuple[int, int], float], float]: + """ + Convert unconstrained quadratic instance to PyQUBO-style format. + + This method is designed for better composability rather than easy-to-use. + This does not execute any conversion of the instance, only translates the data format. + """ + instance = _ommx_rust.Instance.from_bytes(self.to_bytes()) + return instance.as_qubo_format() + + def as_pubo_format(self) -> dict[tuple[int, ...], float]: + """ + Convert unconstrained polynomial instance to simple PUBO format. + + This method is designed for better composability rather than easy-to-use. + This does not execute any conversion of the instance, only translates the data format. + """ + instance = _ommx_rust.Instance.from_bytes(self.to_bytes()) + return instance.as_pubo_format() + @dataclass class Solution: diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index 3d6c6ace..2f05b5e6 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -22,13 +22,13 @@ impl Instance { Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) } - pub fn to_pubo<'py>(&self, py: Python<'py>) -> Result> { - let pubo = self.0.to_pubo()?; + pub fn as_pubo_format<'py>(&self, py: Python<'py>) -> Result> { + let pubo = self.0.as_pubo_format()?; Ok(serde_pyobject::to_pyobject(py, &pubo)?.extract()?) } - pub fn to_qubo<'py>(&self, py: Python<'py>) -> Result<(Bound<'py, PyDict>, f64)> { - let (qubo, constant) = self.0.to_qubo()?; + pub fn as_qubo_format<'py>(&self, py: Python<'py>) -> Result<(Bound<'py, PyDict>, f64)> { + let (qubo, constant) = self.0.as_qubo_format()?; Ok((serde_pyobject::to_pyobject(py, &qubo)?.extract()?, constant)) } From 535853865db7a12d2f8085e2bf8e0020d0a4043b Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 17:18:59 +0900 Subject: [PATCH 07/18] Add validate --- python/ommx/src/instance.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index 2f05b5e6..06a06674 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -15,6 +15,7 @@ impl Instance { #[staticmethod] pub fn from_bytes(bytes: &Bound) -> Result { let inner = ommx::v1::Instance::decode(bytes.as_bytes())?; + inner.validate()?; Ok(Self(inner)) } @@ -22,6 +23,10 @@ impl Instance { Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) } + pub fn validate(&self) -> Result<()> { + self.0.validate() + } + pub fn as_pubo_format<'py>(&self, py: Python<'py>) -> Result> { let pubo = self.0.as_pubo_format()?; Ok(serde_pyobject::to_pyobject(py, &pubo)?.extract()?) From a8cdc45a73f0028d298558bc10d710ee14e28afa Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 17:45:40 +0900 Subject: [PATCH 08/18] Wrapper for ParametricInstance --- python/ommx/ommx/v1/__init__.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index 835342ed..e310a47b 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -6,17 +6,22 @@ from pandas import DataFrame, concat, MultiIndex from .solution_pb2 import State, Optimality, Relaxation, Solution as _Solution -from .instance_pb2 import Instance as _Instance +from .instance_pb2 import Instance as _Instance, Parameters from .function_pb2 import Function as _Function from .quadratic_pb2 import Quadratic as _Quadratic from .polynomial_pb2 import Polynomial as _Polynomial, Monomial as _Monomial from .linear_pb2 import Linear as _Linear from .constraint_pb2 import Equality, Constraint as _Constraint from .decision_variables_pb2 import DecisionVariable as _DecisionVariable, Bound +from .parametric_instance_pb2 import ( + ParametricInstance as _ParametricInstance, + Parameter as _Parameter, +) from .. import _ommx_rust -__all__ = ["Bound"] +# Exposes as it is for basic classes which does not require any additional logic +__all__ = ["Bound", "State", "Optimality", "Relaxation", "Parameters"] @dataclass @@ -262,6 +267,30 @@ def as_pubo_format(self) -> dict[tuple[int, ...], float]: return instance.as_pubo_format() +@dataclass +class ParametricInstance: + """ + Idiomatic wrapper of ``ommx.v1.ParametricInstance`` protobuf message. + """ + + raw: _ParametricInstance + + @staticmethod + def from_bytes(data: bytes) -> ParametricInstance: + raw = _ParametricInstance() + raw.ParseFromString(data) + return ParametricInstance(raw) + + def to_bytes(self) -> bytes: + return self.raw.SerializeToString() + + def with_parameters(self, parameters: Parameters) -> Instance: + pi = _ommx_rust.ParametricInstance.from_bytes(self.to_bytes()) + ps = _ommx_rust.Parameters.from_bytes(parameters.SerializeToString()) + instance = pi.with_parameters(ps) + return Instance.from_bytes(instance.to_bytes()) + + @dataclass class Solution: """ From 4a3ecae8b623d6b5c8fc13cf226cb78c9dd168fd Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 17:56:52 +0900 Subject: [PATCH 09/18] Regenerate stub --- python/ommx/ommx/_ommx_rust.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index e9c25f00..8ecb12fa 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -102,6 +102,7 @@ class Instance: @staticmethod def from_bytes(bytes: bytes) -> Instance: ... def to_bytes(self) -> bytes: ... + def validate(self) -> None: ... def as_pubo_format(self) -> dict: ... def as_qubo_format(self) -> tuple[dict, float]: ... def penalty_method(self) -> ParametricInstance: ... From 722397b6bf4acfcb29648828108a76161233aa27 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 20:32:56 +0900 Subject: [PATCH 10/18] ParametricInstance::validate --- rust/ommx/src/convert/instance.rs | 7 ++- rust/ommx/src/convert/parametric_instance.rs | 53 +++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/rust/ommx/src/convert/instance.rs b/rust/ommx/src/convert/instance.rs index 89c04ca7..3ea5a14a 100644 --- a/rust/ommx/src/convert/instance.rs +++ b/rust/ommx/src/convert/instance.rs @@ -68,7 +68,12 @@ impl Instance { /// Validate that all decision variable IDs used in the instance are defined. pub fn validate_decision_variable_ids(&self) -> Result<()> { let used_ids = self.used_decision_variable_ids()?; - let defined_ids = self.defined_ids(); + let mut defined_ids = BTreeSet::new(); + for dv in &self.decision_variables { + if !defined_ids.insert(dv.id) { + bail!("Duplicated definition of decision variable ID: {}", dv.id); + } + } if !used_ids.is_subset(&defined_ids) { let undefined_ids = used_ids.difference(&defined_ids).collect::>(); bail!("Undefined decision variable IDs: {:?}", undefined_ids); diff --git a/rust/ommx/src/convert/parametric_instance.rs b/rust/ommx/src/convert/parametric_instance.rs index 9b60703b..1e28017e 100644 --- a/rust/ommx/src/convert/parametric_instance.rs +++ b/rust/ommx/src/convert/parametric_instance.rs @@ -117,6 +117,49 @@ impl ParametricInstance { pub fn defined_parameter_ids(&self) -> BTreeSet { self.parameters.iter().map(|p| p.id).collect() } + + pub fn validate(&self) -> Result<()> { + self.validate_ids()?; + self.validate_constraint_ids()?; + Ok(()) + } + + pub fn validate_ids(&self) -> Result<()> { + let mut ids = BTreeSet::new(); + for dv in &self.decision_variables { + if !ids.insert(dv.id) { + bail!("Duplicate decision variable ID: {}", dv.id); + } + } + for p in &self.parameters { + if !ids.insert(p.id) { + bail!("Duplicate parameter ID: {}", p.id); + } + } + let used_ids = self.used_ids()?; + if !used_ids.is_subset(&ids) { + let sub = used_ids.difference(&ids).collect::>(); + bail!("Undefined ID is used: {:?}", sub); + } + Ok(()) + } + + pub fn validate_constraint_ids(&self) -> Result<()> { + let mut ids = BTreeSet::new(); + for c in &self.constraints { + if !ids.insert(c.id) { + bail!("Duplicate constraint ID: {}", c.id); + } + } + for c in &self.removed_constraints { + if let Some(c) = c.constraint.as_ref() { + if !ids.insert(c.id) { + bail!("Duplicate removed constraint ID: {}", c.id); + } + } + } + Ok(()) + } } impl Arbitrary for ParametricInstance { @@ -220,14 +263,8 @@ mod tests { } #[test] - fn test_ids(pi in ParametricInstance::arbitrary()) { - let dv_ids = pi.defined_decision_variable_ids(); - let p_ids = pi.defined_parameter_ids(); - prop_assert!(dv_ids.is_disjoint(&p_ids)); - - let all_ids: BTreeSet = dv_ids.union(&p_ids).cloned().collect(); - let used_ids = pi.used_ids().unwrap(); - prop_assert!(used_ids.is_subset(&all_ids)); + fn validate(pi in ParametricInstance::arbitrary()) { + pi.validate().unwrap(); } } } From de79246e204f55193eeed62d9b40c72bb23f9717 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 20:34:55 +0900 Subject: [PATCH 11/18] Validate while ParametricInstance.from_bytes --- python/ommx/src/instance.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/ommx/src/instance.rs b/python/ommx/src/instance.rs index 06a06674..0e310518 100644 --- a/python/ommx/src/instance.rs +++ b/python/ommx/src/instance.rs @@ -52,6 +52,7 @@ impl ParametricInstance { #[staticmethod] pub fn from_bytes(bytes: &Bound) -> Result { let inner = ommx::v1::ParametricInstance::decode(bytes.as_bytes())?; + inner.validate()?; Ok(Self(inner)) } @@ -59,6 +60,10 @@ impl ParametricInstance { Ok(PyBytes::new_bound(py, &self.0.encode_to_vec())) } + pub fn validate(&self) -> Result<()> { + self.0.validate() + } + pub fn with_parameters(&self, parameters: &Parameters) -> Result { let instance = self.0.clone().with_parameters(parameters.0.clone())?; Ok(Instance(instance)) From 8724f683b290ca55e9af8d38f2867ffe561ea915 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 20:41:46 +0900 Subject: [PATCH 12/18] Instance.get_{decision_variables,decision_variable,constraints,constraint} methods --- python/ommx/ommx/v1/__init__.py | 38 ++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index e310a47b..1216578c 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -15,7 +15,6 @@ from .decision_variables_pb2 import DecisionVariable as _DecisionVariable, Bound from .parametric_instance_pb2 import ( ParametricInstance as _ParametricInstance, - Parameter as _Parameter, ) from .. import _ommx_rust @@ -189,6 +188,21 @@ def decision_variables(self) -> DataFrame: df.columns = MultiIndex.from_product([df.columns, [""]]) return concat([df, parameters], axis=1).set_index("id") + def get_decision_variables(self) -> list[DecisionVariable]: + """ + Get decision variables as a list of :class:`DecisionVariable` instances. + """ + return [DecisionVariable(raw) for raw in self.raw.decision_variables] + + def get_decision_variable(self, variable_id: int) -> DecisionVariable: + """ + Get a decision variable by ID. + """ + for v in self.raw.decision_variables: + if v.id == variable_id: + return DecisionVariable(v) + raise ValueError(f"Decision variable ID {variable_id} is not found") + @property def objective(self) -> Function: return Function(self.raw.objective) @@ -217,6 +231,21 @@ def constraints(self) -> DataFrame: df.columns = MultiIndex.from_product([df.columns, [""]]) return concat([df, parameters], axis=1).set_index("id") + def get_constraints(self) -> list[Constraint]: + """ + Get constraints as a list of :class:`Constraint` instances. + """ + return [Constraint.from_raw(raw) for raw in self.raw.constraints] + + def get_constraint(self, constraint_id: int) -> Constraint: + """ + Get a constraint by ID. + """ + for c in self.raw.constraints: + if c.id == constraint_id: + return Constraint.from_raw(c) + raise ValueError(f"Constraint ID {constraint_id} is not found") + @property def sense(self) -> _Instance.Sense.ValueType: return self.raw.sense @@ -1539,6 +1568,13 @@ def __init__( parameters=parameters, ) + @staticmethod + def from_raw(raw: _Constraint) -> Constraint: + new = Constraint(function=0, equality=Equality.EQUALITY_UNSPECIFIED) + new.raw = raw + Constraint._counter = max(Constraint._counter, raw.id + 1) + return new + @staticmethod def from_bytes(data: bytes) -> Constraint: raw = _Constraint() From d8f51b868c7ed038b814f986c6cca7b77620a691 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 20:49:53 +0900 Subject: [PATCH 13/18] Regenerate stub --- python/ommx/ommx/_ommx_rust.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ommx/ommx/_ommx_rust.pyi b/python/ommx/ommx/_ommx_rust.pyi index 8ecb12fa..25126b78 100644 --- a/python/ommx/ommx/_ommx_rust.pyi +++ b/python/ommx/ommx/_ommx_rust.pyi @@ -130,6 +130,7 @@ class ParametricInstance: @staticmethod def from_bytes(bytes: bytes) -> ParametricInstance: ... def to_bytes(self) -> bytes: ... + def validate(self) -> None: ... def with_parameters(self, parameters: Parameters) -> Instance: ... class Polynomial: From 4afd2b56c91ec4adf0c3a9537b8edfc4c530a94f Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 20:56:09 +0900 Subject: [PATCH 14/18] Instance.penalty_method --- python/ommx/ommx/v1/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index 1216578c..a23704ae 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -266,15 +266,6 @@ def partial_evaluate(self, state: State | Mapping[int, float]) -> Instance: ) return Instance.from_bytes(out) - def to_qubo(self) -> tuple[dict[tuple[int, int], float], float]: - """ - Easy-to-use method to convert non-QUBO instance into QUBO and return as a QUBO format. - """ - raise NotImplementedError - - def to_pubo(self) -> dict[tuple[int, ...], float]: - raise NotImplementedError - def as_qubo_format(self) -> tuple[dict[tuple[int, int], float], float]: """ Convert unconstrained quadratic instance to PyQUBO-style format. @@ -295,6 +286,13 @@ def as_pubo_format(self) -> dict[tuple[int, ...], float]: instance = _ommx_rust.Instance.from_bytes(self.to_bytes()) return instance.as_pubo_format() + def penalty_method(self) -> ParametricInstance: + """ + Convert the instance to a parametric instance for penalty method. + """ + instance = _ommx_rust.Instance.from_bytes(self.to_bytes()) + return ParametricInstance.from_bytes(instance.penalty_method().to_bytes()) + @dataclass class ParametricInstance: From 1ed5bd7ae7ff7867772a7767c03fbefc1b3342e7 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 21:04:10 +0900 Subject: [PATCH 15/18] APIs for ParametricInstance --- python/ommx/ommx/v1/__init__.py | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index a23704ae..98ba279a 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -15,6 +15,7 @@ from .decision_variables_pb2 import DecisionVariable as _DecisionVariable, Bound from .parametric_instance_pb2 import ( ParametricInstance as _ParametricInstance, + Parameter as _Parameter, ) from .. import _ommx_rust @@ -311,13 +312,99 @@ def from_bytes(data: bytes) -> ParametricInstance: def to_bytes(self) -> bytes: return self.raw.SerializeToString() + def get_decision_variables(self) -> list[DecisionVariable]: + """ + Get decision variables as a list of :class:`DecisionVariable` instances. + """ + return [DecisionVariable(raw) for raw in self.raw.decision_variables] + + def get_decision_variable(self, variable_id: int) -> DecisionVariable: + """ + Get a decision variable by ID. + """ + for v in self.raw.decision_variables: + if v.id == variable_id: + return DecisionVariable(v) + raise ValueError(f"Decision variable ID {variable_id} is not found") + + def get_constraints(self) -> list[Constraint]: + """ + Get constraints as a list of :class:`Constraint + """ + return [Constraint.from_raw(raw) for raw in self.raw.constraints] + + def get_constraint(self, constraint_id: int) -> Constraint: + """ + Get a constraint by ID. + """ + for c in self.raw.constraints: + if c.id == constraint_id: + return Constraint.from_raw(c) + raise ValueError(f"Constraint ID {constraint_id} is not found") + + def get_parameters(self) -> list[Parameter]: + """ + Get parameters as a list of :class:`Parameter`. + """ + return [Parameter(raw) for raw in self.raw.parameters] + + def get_parameter(self, parameter_id: int) -> Parameter: + """ + Get a parameter by ID. + """ + for p in self.raw.parameters: + if p.id == parameter_id: + return Parameter(p) + raise ValueError(f"Parameter ID {parameter_id} is not found") + def with_parameters(self, parameters: Parameters) -> Instance: + """ + Substitute parameters to yield an instance. + """ pi = _ommx_rust.ParametricInstance.from_bytes(self.to_bytes()) ps = _ommx_rust.Parameters.from_bytes(parameters.SerializeToString()) instance = pi.with_parameters(ps) return Instance.from_bytes(instance.to_bytes()) +@dataclass +class Parameter: + """ + Idiomatic wrapper of ``ommx.v1.Parameter`` protobuf message. + """ + + raw: _Parameter + + @staticmethod + def from_bytes(data: bytes) -> Parameter: + raw = _Parameter() + raw.ParseFromString(data) + return Parameter(raw) + + def to_bytes(self) -> bytes: + return self.raw.SerializeToString() + + @property + def id(self) -> int: + return self.raw.id + + @property + def name(self) -> str: + return self.raw.name + + @property + def subscripts(self) -> list[int]: + return list(self.raw.subscripts) + + @property + def description(self) -> str: + return self.raw.description + + @property + def parameters(self) -> dict[str, str]: + return dict(self.raw.parameters) + + @dataclass class Solution: """ From 8e93643ce31e3d80a7b3d763d58d73751bcd8cf7 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 11 Dec 2024 21:29:35 +0900 Subject: [PATCH 16/18] Fill __all__ for sphinx --- python/ommx/ommx/v1/__init__.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index 98ba279a..841cc76f 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -21,7 +21,27 @@ from .. import _ommx_rust # Exposes as it is for basic classes which does not require any additional logic -__all__ = ["Bound", "State", "Optimality", "Relaxation", "Parameters"] +__all__ = [ + # Imported + "Bound", + "State", + "Optimality", + "Relaxation", + "Parameters", + # Composed classes + "ParametricInstance", + "Instance", + "Solution", + "Constraint", + # Function + "DecisionVariable", + "Parameter", + "Linear", + "Quadratic", + "Polynomial", + "Equality", + "Function", +] @dataclass From 0c4f148f3dcd596ddcc739c5d602bc292c98d8d0 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Thu, 12 Dec 2024 14:45:13 +0900 Subject: [PATCH 17/18] Support Mapping[int, float] in with_parameters --- python/ommx/ommx/v1/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index d856cf3b..c1d28ab1 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -374,10 +374,12 @@ def get_parameter(self, parameter_id: int) -> Parameter: return Parameter(p) raise ValueError(f"Parameter ID {parameter_id} is not found") - def with_parameters(self, parameters: Parameters) -> Instance: + def with_parameters(self, parameters: Parameters | Mapping[int, float]) -> Instance: """ Substitute parameters to yield an instance. """ + if not isinstance(parameters, Parameters): + parameters = Parameters(entries=parameters) pi = _ommx_rust.ParametricInstance.from_bytes(self.to_bytes()) ps = _ommx_rust.Parameters.from_bytes(parameters.SerializeToString()) instance = pi.with_parameters(ps) From ba8237914862a0a186accfb4cd5372d16dc1a01f Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Thu, 12 Dec 2024 15:00:39 +0900 Subject: [PATCH 18/18] Split __add__ and other operator overloads of DecisionVariable into a separate ABC class --- python/ommx/ommx/v1/__init__.py | 168 ++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 72 deletions(-) diff --git a/python/ommx/ommx/v1/__init__.py b/python/ommx/ommx/v1/__init__.py index c1d28ab1..ce14d3b0 100644 --- a/python/ommx/ommx/v1/__init__.py +++ b/python/ommx/ommx/v1/__init__.py @@ -4,6 +4,7 @@ from datetime import datetime from dataclasses import dataclass, field from pandas import DataFrame, concat, MultiIndex +from abc import ABC, abstractmethod from .solution_pb2 import State, Optimality, Relaxation, Solution as _Solution from .instance_pb2 import Instance as _Instance, Parameters @@ -386,8 +387,71 @@ def with_parameters(self, parameters: Parameters | Mapping[int, float]) -> Insta return Instance.from_bytes(instance.to_bytes()) +class VariableBase(ABC): + @property + @abstractmethod + def id(self) -> int: ... + + def __add__(self, other: int | float | VariableBase) -> Linear: + if isinstance(other, float) or isinstance(other, int): + return Linear(terms={self.id: 1}, constant=other) + if isinstance(other, VariableBase): + if self.id == other.id: + return Linear(terms={self.id: 2}) + else: + return Linear(terms={self.id: 1, other.id: 1}) + return NotImplemented + + def __sub__(self, other) -> Linear: + return self + (-other) + + def __neg__(self) -> Linear: + return Linear(terms={self.id: -1}) + + def __radd__(self, other) -> Linear: + return self + other + + def __rsub__(self, other) -> Linear: + return -self + other + + @overload + def __mul__(self, other: int | float) -> Linear: ... + + @overload + def __mul__(self, other: VariableBase) -> Quadratic: ... + + def __mul__(self, other: int | float | VariableBase) -> Linear | Quadratic: + if isinstance(other, float) or isinstance(other, int): + return Linear(terms={self.id: other}) + if isinstance(other, VariableBase): + return Quadratic(columns=[self.id], rows=[other.id], values=[1.0]) + return NotImplemented + + def __rmul__(self, other): + return self * other + + def __le__(self, other) -> Constraint: + return Constraint( + function=self - other, equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO + ) + + def __ge__(self, other) -> Constraint: + return Constraint( + function=other - self, equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO + ) + + def __req__(self, other) -> Constraint: + return self == other + + def __rle__(self, other) -> Constraint: + return self.__ge__(other) + + def __rge__(self, other) -> Constraint: + return self.__le__(other) + + @dataclass -class Parameter: +class Parameter(VariableBase): """ Idiomatic wrapper of ``ommx.v1.Parameter`` protobuf message. """ @@ -423,6 +487,18 @@ def description(self) -> str: def parameters(self) -> dict[str, str]: return dict(self.raw.parameters) + def equals_to(self, other: Parameter) -> bool: + """ + Alternative to ``==`` operator to compare two decision variables. + """ + return self.raw == other.raw + + # The special function __eq__ cannot be inherited from VariableBase + def __eq__(self, other) -> Constraint: # type: ignore[reportIncompatibleMethodOverride] + return Constraint( + function=self - other, equality=Equality.EQUALITY_EQUAL_TO_ZERO + ) + @dataclass class Solution: @@ -579,7 +655,24 @@ def _equality(equality: Equality.ValueType) -> str: @dataclass -class DecisionVariable: +class DecisionVariable(VariableBase): + """ + Idiomatic wrapper of ``ommx.v1.DecisionVariable`` protobuf message. + + Note that this object overloads `==` for creating a constraint, not for equality comparison for better integration to mathematical programming. + + >>> x = DecisionVariable.integer(1) + >>> x == 1 + Constraint(...) + + To compare two objects, use :py:meth:`equals_to` method. + + >>> y = DecisionVariable.integer(2) + >>> x.equals_to(y) + False + + """ + raw: _DecisionVariable Kind = _DecisionVariable.Kind.ValueType @@ -770,81 +863,12 @@ def equals_to(self, other: DecisionVariable) -> bool: """ return self.raw == other.raw - def __add__(self, other: int | float | DecisionVariable) -> Linear: - if isinstance(other, float) or isinstance(other, int): - return Linear(terms={self.raw.id: 1}, constant=other) - if isinstance(other, DecisionVariable): - if self.raw.id == other.raw.id: - return Linear(terms={self.raw.id: 2}) - else: - return Linear(terms={self.raw.id: 1, other.raw.id: 1}) - return NotImplemented - - def __sub__(self, other) -> Linear: - return self + (-other) - - def __neg__(self) -> Linear: - return Linear(terms={self.raw.id: -1}) - - def __radd__(self, other) -> Linear: - return self + other - - def __rsub__(self, other) -> Linear: - return -self + other - - @overload - def __mul__(self, other: int | float) -> Linear: ... - - @overload - def __mul__(self, other: DecisionVariable) -> Quadratic: ... - - def __mul__(self, other: int | float | DecisionVariable) -> Linear | Quadratic: - if isinstance(other, float) or isinstance(other, int): - return Linear(terms={self.raw.id: other}) - if isinstance(other, DecisionVariable): - return Quadratic(columns=[self.raw.id], rows=[other.raw.id], values=[1.0]) - return NotImplemented - - def __rmul__(self, other): - return self * other - + # The special function __eq__ cannot be inherited from VariableBase def __eq__(self, other) -> Constraint: # type: ignore[reportIncompatibleMethodOverride] - """ - Create a constraint that this decision variable is equal to another decision variable or a constant. - - To compare two objects, use :py:meth:`equals_to` method. - - Examples - ======== - - >>> x = DecisionVariable.integer(1) - >>> x == 1 - Constraint(...) - - """ return Constraint( function=self - other, equality=Equality.EQUALITY_EQUAL_TO_ZERO ) - def __le__(self, other) -> Constraint: - return Constraint( - function=self - other, equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO - ) - - def __ge__(self, other) -> Constraint: - return Constraint( - function=other - self, equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO - ) - - def __req__(self, other) -> Constraint: - return self == other - - def __rle__(self, other) -> Constraint: - return self.__ge__(other) - - def __rge__(self, other) -> Constraint: - return self.__le__(other) - @dataclass class Linear: