1use crate::error::Error;
2use crate::num::{IntoRational, Rational};
3
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Sense {
9 Maximize,
10 Minimize,
11}
12
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Relation {
17 #[cfg_attr(feature = "serde", serde(rename = "<="))]
18 Le,
19 #[cfg_attr(feature = "serde", serde(rename = ">="))]
20 Ge,
21 #[cfg_attr(feature = "serde", serde(rename = "="))]
22 Eq,
23}
24
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum Sign {
36 #[default]
38 NonNegative,
39 Free,
41 NonPositive,
43}
44
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[derive(Clone, Debug, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct Constraint {
50 pub coeffs: Vec<Rational>,
51 pub relation: Relation,
52 pub rhs: Rational,
53}
54
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[derive(Clone, Debug, PartialEq, Eq)]
84#[non_exhaustive]
85pub struct Problem {
86 pub sense: Sense,
87 pub objective: Vec<Rational>,
88 pub constraints: Vec<Constraint>,
89 #[cfg_attr(
91 feature = "serde",
92 serde(default, skip_serializing_if = "Vec::is_empty")
93 )]
94 pub var_names: Vec<String>,
95 #[cfg_attr(
97 feature = "serde",
98 serde(default, skip_serializing_if = "Vec::is_empty")
99 )]
100 pub signs: Vec<Sign>,
101}
102
103pub(crate) struct Expansion {
107 pub problem: Problem,
108 pub parts: Vec<Vec<(usize, bool)>>,
109}
110
111impl Expansion {
112 pub fn combine(&self, columns: &[Rational]) -> Vec<Rational> {
114 self.parts
115 .iter()
116 .map(|cols| {
117 cols.iter()
118 .map(|&(col, negated)| {
119 if negated {
120 -&columns[col]
121 } else {
122 columns[col].clone()
123 }
124 })
125 .sum()
126 })
127 .collect()
128 }
129}
130
131impl Problem {
132 pub fn maximize<T: IntoRational>(objective: impl IntoIterator<Item = T>) -> Self {
133 Self::new(Sense::Maximize, objective)
134 }
135
136 pub fn minimize<T: IntoRational>(objective: impl IntoIterator<Item = T>) -> Self {
137 Self::new(Sense::Minimize, objective)
138 }
139
140 fn new<T: IntoRational>(sense: Sense, objective: impl IntoIterator<Item = T>) -> Self {
141 let objective: Vec<Rational> = objective
142 .into_iter()
143 .map(IntoRational::into_rational)
144 .collect();
145 Problem {
146 sense,
147 objective,
148 constraints: Vec::new(),
149 var_names: Vec::new(),
150 signs: Vec::new(),
151 }
152 }
153
154 pub fn free(self, j: usize) -> Self {
156 self.with_sign(j, Sign::Free)
157 }
158
159 pub fn non_positive(self, j: usize) -> Self {
161 self.with_sign(j, Sign::NonPositive)
162 }
163
164 pub fn with_sign(mut self, j: usize, sign: Sign) -> Self {
167 let len = self.signs.len().max(self.num_vars()).max(j + 1);
168 self.signs.resize(len, Sign::NonNegative);
169 self.signs[j] = sign;
170 self
171 }
172
173 pub fn sign(&self, j: usize) -> Sign {
175 self.signs.get(j).copied().unwrap_or_default()
176 }
177
178 pub(crate) fn expand(&self) -> Expansion {
180 let n = self.num_vars();
181 let mut parts = Vec::with_capacity(n);
182 let mut names = Vec::with_capacity(n);
183 let mut columns: Vec<(usize, bool)> = Vec::with_capacity(n);
185 for j in 0..n {
186 let name = self.var_name(j);
187 let mut add = |suffix: &str, negated: bool, parts: &mut Vec<(usize, bool)>| {
188 parts.push((columns.len(), negated));
189 columns.push((j, negated));
190 names.push(format!("{name}{suffix}"));
191 };
192 let mut own = Vec::new();
193 match self.sign(j) {
194 Sign::NonNegative => add("", false, &mut own),
195 Sign::Free => {
196 add("+", false, &mut own);
197 add("-", true, &mut own);
198 }
199 Sign::NonPositive => add("-", true, &mut own),
200 }
201 parts.push(own);
202 }
203 let expand_row = |row: &[Rational]| -> Vec<Rational> {
204 columns
205 .iter()
206 .map(|&(j, negated)| if negated { -&row[j] } else { row[j].clone() })
207 .collect()
208 };
209 let problem = Problem {
210 sense: self.sense,
211 objective: expand_row(&self.objective),
212 constraints: self
213 .constraints
214 .iter()
215 .map(|c| Constraint {
216 coeffs: expand_row(&c.coeffs),
217 relation: c.relation,
218 rhs: c.rhs.clone(),
219 })
220 .collect(),
221 var_names: names,
222 signs: Vec::new(),
223 };
224 Expansion { problem, parts }
225 }
226
227 pub fn subject_to<T: IntoRational>(
229 mut self,
230 coeffs: impl IntoIterator<Item = T>,
231 relation: Relation,
232 rhs: impl IntoRational,
233 ) -> Self {
234 self.constraints.push(Constraint {
235 coeffs: coeffs
236 .into_iter()
237 .map(IntoRational::into_rational)
238 .collect(),
239 relation,
240 rhs: rhs.into_rational(),
241 });
242 self
243 }
244
245 pub fn with_names<S: Into<String>>(mut self, names: impl IntoIterator<Item = S>) -> Self {
250 self.var_names = names.into_iter().map(Into::into).collect();
251 self
252 }
253
254 pub fn num_vars(&self) -> usize {
255 self.objective.len()
256 }
257
258 pub fn var_name(&self, j: usize) -> String {
260 self.var_names
261 .get(j)
262 .cloned()
263 .unwrap_or_else(|| format!("x{}", j + 1))
264 }
265
266 pub(crate) fn validate(&self) -> Result<(), Error> {
267 let n = self.num_vars();
268 if n == 0 {
269 return Err(Error::InvalidProblem(
270 "the objective has no variables".into(),
271 ));
272 }
273 if !self.var_names.is_empty() && self.var_names.len() != n {
274 return Err(Error::InvalidProblem(format!(
275 "{} variable names for {n} variables",
276 self.var_names.len()
277 )));
278 }
279 if !self.signs.is_empty() && self.signs.len() != n {
280 return Err(Error::InvalidProblem(format!(
281 "{} sign restrictions for {n} variables",
282 self.signs.len()
283 )));
284 }
285 for (j, name) in self.var_names.iter().enumerate() {
286 if name.ends_with(['+', '-']) {
287 return Err(Error::InvalidProblem(format!(
288 "variable name {name:?} may not end in '+' or '-' (reserved for the columns of free and non-positive variables)"
289 )));
290 }
291 if name.is_empty() {
292 return Err(Error::InvalidProblem(format!(
293 "variable {} has an empty name",
294 j + 1
295 )));
296 }
297 if self.var_names[..j].contains(name) {
298 return Err(Error::InvalidProblem(format!(
299 "duplicate variable name {name:?}"
300 )));
301 }
302 let mut chars = name.chars();
304 let reserved = matches!(chars.next(), Some('s' | 'a'))
305 && chars.clone().next().is_some()
306 && chars.all(|c| c.is_ascii_digit());
307 if reserved {
308 return Err(Error::InvalidProblem(format!(
309 "variable name {name:?} is reserved for slack and artificial columns"
310 )));
311 }
312 }
313 for (i, c) in self.constraints.iter().enumerate() {
314 if c.coeffs.len() != n {
315 return Err(Error::InvalidProblem(format!(
316 "constraint {} has {} coefficients, expected {n}",
317 i + 1,
318 c.coeffs.len()
319 )));
320 }
321 }
322 Ok(())
323 }
324}