1use nalgebra::{DMatrix, DVector};
4use runmat_builtins::{
5 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
6 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
7 CellArray, StructValue, Tensor, Value,
8};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::common::spec::{
12 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13 ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::math::optim::common::{lookup_option, option_f64, option_usize};
16use crate::builtins::math::optim::type_resolvers::{linear_programming_type, optim_options_type};
17use crate::{build_runtime_error, BuiltinResult, RuntimeError};
18
19const CONEPROG: &str = "coneprog";
20const ALGORITHM: &str = "interior-point conic barrier";
21const DEFAULT_TOL: f64 = 1.0e-7;
22const DEFAULT_MAX_ITER: usize = 200;
23const STRICT_MARGIN: f64 = 1.0e-7;
24
25const OUTPUT_X: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
26 name: "x",
27 ty: BuiltinParamType::NumericArray,
28 arity: BuiltinParamArity::Required,
29 default: None,
30 description: "Optimal decision vector.",
31}];
32
33const OUTPUT_X_FVAL: [BuiltinParamDescriptor; 2] = [
34 BuiltinParamDescriptor {
35 name: "x",
36 ty: BuiltinParamType::NumericArray,
37 arity: BuiltinParamArity::Required,
38 default: None,
39 description: "Optimal decision vector.",
40 },
41 BuiltinParamDescriptor {
42 name: "fval",
43 ty: BuiltinParamType::NumericScalar,
44 arity: BuiltinParamArity::Required,
45 default: None,
46 description: "Objective value f'*x at the solution.",
47 },
48];
49
50const OUTPUT_X_FVAL_EXITFLAG: [BuiltinParamDescriptor; 3] = [
51 BuiltinParamDescriptor {
52 name: "x",
53 ty: BuiltinParamType::NumericArray,
54 arity: BuiltinParamArity::Required,
55 default: None,
56 description: "Optimal decision vector.",
57 },
58 BuiltinParamDescriptor {
59 name: "fval",
60 ty: BuiltinParamType::NumericScalar,
61 arity: BuiltinParamArity::Required,
62 default: None,
63 description: "Objective value f'*x at the solution.",
64 },
65 BuiltinParamDescriptor {
66 name: "exitflag",
67 ty: BuiltinParamType::NumericScalar,
68 arity: BuiltinParamArity::Required,
69 default: None,
70 description: "Solver status code.",
71 },
72];
73
74const OUTPUT_ALL: [BuiltinParamDescriptor; 5] = [
75 BuiltinParamDescriptor {
76 name: "x",
77 ty: BuiltinParamType::NumericArray,
78 arity: BuiltinParamArity::Required,
79 default: None,
80 description: "Optimal decision vector.",
81 },
82 BuiltinParamDescriptor {
83 name: "fval",
84 ty: BuiltinParamType::NumericScalar,
85 arity: BuiltinParamArity::Required,
86 default: None,
87 description: "Objective value f'*x at the solution.",
88 },
89 BuiltinParamDescriptor {
90 name: "exitflag",
91 ty: BuiltinParamType::NumericScalar,
92 arity: BuiltinParamArity::Required,
93 default: None,
94 description: "Solver status code.",
95 },
96 BuiltinParamDescriptor {
97 name: "output",
98 ty: BuiltinParamType::Any,
99 arity: BuiltinParamArity::Required,
100 default: None,
101 description: "Diagnostic metadata struct.",
102 },
103 BuiltinParamDescriptor {
104 name: "lambda",
105 ty: BuiltinParamType::Any,
106 arity: BuiltinParamArity::Required,
107 default: None,
108 description: "Lagrange multiplier compatibility struct.",
109 },
110];
111
112const INPUTS_CORE: [BuiltinParamDescriptor; 2] = [
113 BuiltinParamDescriptor {
114 name: "f",
115 ty: BuiltinParamType::NumericArray,
116 arity: BuiltinParamArity::Required,
117 default: None,
118 description: "Linear objective vector.",
119 },
120 BuiltinParamDescriptor {
121 name: "socConstraints",
122 ty: BuiltinParamType::Any,
123 arity: BuiltinParamArity::Required,
124 default: None,
125 description: "Second-order cone constraints from secondordercone.",
126 },
127];
128
129const INPUTS_LINEAR: [BuiltinParamDescriptor; 4] = [
130 BuiltinParamDescriptor {
131 name: "f",
132 ty: BuiltinParamType::NumericArray,
133 arity: BuiltinParamArity::Required,
134 default: None,
135 description: "Linear objective vector.",
136 },
137 BuiltinParamDescriptor {
138 name: "socConstraints",
139 ty: BuiltinParamType::Any,
140 arity: BuiltinParamArity::Required,
141 default: None,
142 description: "Second-order cone constraints from secondordercone.",
143 },
144 BuiltinParamDescriptor {
145 name: "A",
146 ty: BuiltinParamType::NumericArray,
147 arity: BuiltinParamArity::Optional,
148 default: Some("[]"),
149 description: "Inequality constraint matrix.",
150 },
151 BuiltinParamDescriptor {
152 name: "b",
153 ty: BuiltinParamType::NumericArray,
154 arity: BuiltinParamArity::Optional,
155 default: Some("[]"),
156 description: "Inequality constraint right-hand side.",
157 },
158];
159
160const INPUTS_FULL: [BuiltinParamDescriptor; 9] = [
161 BuiltinParamDescriptor {
162 name: "f",
163 ty: BuiltinParamType::NumericArray,
164 arity: BuiltinParamArity::Required,
165 default: None,
166 description: "Linear objective vector.",
167 },
168 BuiltinParamDescriptor {
169 name: "socConstraints",
170 ty: BuiltinParamType::Any,
171 arity: BuiltinParamArity::Required,
172 default: None,
173 description: "Second-order cone constraints from secondordercone.",
174 },
175 BuiltinParamDescriptor {
176 name: "A",
177 ty: BuiltinParamType::NumericArray,
178 arity: BuiltinParamArity::Optional,
179 default: Some("[]"),
180 description: "Inequality constraint matrix.",
181 },
182 BuiltinParamDescriptor {
183 name: "b",
184 ty: BuiltinParamType::NumericArray,
185 arity: BuiltinParamArity::Optional,
186 default: Some("[]"),
187 description: "Inequality constraint right-hand side.",
188 },
189 BuiltinParamDescriptor {
190 name: "Aeq",
191 ty: BuiltinParamType::NumericArray,
192 arity: BuiltinParamArity::Optional,
193 default: Some("[]"),
194 description: "Equality constraint matrix.",
195 },
196 BuiltinParamDescriptor {
197 name: "beq",
198 ty: BuiltinParamType::NumericArray,
199 arity: BuiltinParamArity::Optional,
200 default: Some("[]"),
201 description: "Equality constraint right-hand side.",
202 },
203 BuiltinParamDescriptor {
204 name: "lb",
205 ty: BuiltinParamType::NumericArray,
206 arity: BuiltinParamArity::Optional,
207 default: Some("[]"),
208 description: "Lower bounds.",
209 },
210 BuiltinParamDescriptor {
211 name: "ub",
212 ty: BuiltinParamType::NumericArray,
213 arity: BuiltinParamArity::Optional,
214 default: Some("[]"),
215 description: "Upper bounds.",
216 },
217 BuiltinParamDescriptor {
218 name: "options",
219 ty: BuiltinParamType::Any,
220 arity: BuiltinParamArity::Optional,
221 default: Some("optimoptions('coneprog')"),
222 description: "Options struct from optimoptions or optimset.",
223 },
224];
225
226const INPUTS_PROBLEM: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
227 name: "problem",
228 ty: BuiltinParamType::Any,
229 arity: BuiltinParamArity::Required,
230 default: None,
231 description: "Problem struct with f, socConstraints, linear constraints, bounds, and options.",
232}];
233
234const CONEPROG_SIGNATURES: [BuiltinSignatureDescriptor; 16] = [
235 BuiltinSignatureDescriptor {
236 label: "x = coneprog(f, socConstraints)",
237 inputs: &INPUTS_CORE,
238 outputs: &OUTPUT_X,
239 },
240 BuiltinSignatureDescriptor {
241 label: "x = coneprog(f, socConstraints, A, b)",
242 inputs: &INPUTS_LINEAR,
243 outputs: &OUTPUT_X,
244 },
245 BuiltinSignatureDescriptor {
246 label: "x = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
247 inputs: &INPUTS_FULL,
248 outputs: &OUTPUT_X,
249 },
250 BuiltinSignatureDescriptor {
251 label: "x = coneprog(problem)",
252 inputs: &INPUTS_PROBLEM,
253 outputs: &OUTPUT_X,
254 },
255 BuiltinSignatureDescriptor {
256 label: "[x, fval] = coneprog(f, socConstraints)",
257 inputs: &INPUTS_CORE,
258 outputs: &OUTPUT_X_FVAL,
259 },
260 BuiltinSignatureDescriptor {
261 label: "[x, fval] = coneprog(f, socConstraints, A, b)",
262 inputs: &INPUTS_LINEAR,
263 outputs: &OUTPUT_X_FVAL,
264 },
265 BuiltinSignatureDescriptor {
266 label: "[x, fval] = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
267 inputs: &INPUTS_FULL,
268 outputs: &OUTPUT_X_FVAL,
269 },
270 BuiltinSignatureDescriptor {
271 label: "[x, fval] = coneprog(problem)",
272 inputs: &INPUTS_PROBLEM,
273 outputs: &OUTPUT_X_FVAL,
274 },
275 BuiltinSignatureDescriptor {
276 label: "[x, fval, exitflag] = coneprog(f, socConstraints)",
277 inputs: &INPUTS_CORE,
278 outputs: &OUTPUT_X_FVAL_EXITFLAG,
279 },
280 BuiltinSignatureDescriptor {
281 label: "[x, fval, exitflag] = coneprog(f, socConstraints, A, b)",
282 inputs: &INPUTS_LINEAR,
283 outputs: &OUTPUT_X_FVAL_EXITFLAG,
284 },
285 BuiltinSignatureDescriptor {
286 label: "[x, fval, exitflag] = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
287 inputs: &INPUTS_FULL,
288 outputs: &OUTPUT_X_FVAL_EXITFLAG,
289 },
290 BuiltinSignatureDescriptor {
291 label: "[x, fval, exitflag] = coneprog(problem)",
292 inputs: &INPUTS_PROBLEM,
293 outputs: &OUTPUT_X_FVAL_EXITFLAG,
294 },
295 BuiltinSignatureDescriptor {
296 label: "[x, fval, exitflag, output, lambda] = coneprog(f, socConstraints)",
297 inputs: &INPUTS_CORE,
298 outputs: &OUTPUT_ALL,
299 },
300 BuiltinSignatureDescriptor {
301 label: "[x, fval, exitflag, output, lambda] = coneprog(f, socConstraints, A, b)",
302 inputs: &INPUTS_LINEAR,
303 outputs: &OUTPUT_ALL,
304 },
305 BuiltinSignatureDescriptor {
306 label: "[x, fval, exitflag, output, lambda] = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
307 inputs: &INPUTS_FULL,
308 outputs: &OUTPUT_ALL,
309 },
310 BuiltinSignatureDescriptor {
311 label: "[x, fval, exitflag, output, lambda] = coneprog(problem)",
312 inputs: &INPUTS_PROBLEM,
313 outputs: &OUTPUT_ALL,
314 },
315];
316
317const SOC_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
318 name: "socConstraint",
319 ty: BuiltinParamType::Any,
320 arity: BuiltinParamArity::Required,
321 default: None,
322 description: "Second-order cone constraint descriptor.",
323}];
324
325const SOC_INPUTS: [BuiltinParamDescriptor; 4] = [
326 BuiltinParamDescriptor {
327 name: "A",
328 ty: BuiltinParamType::NumericArray,
329 arity: BuiltinParamArity::Required,
330 default: None,
331 description: "Cone matrix in norm(A*x + b) <= d'*x + gamma.",
332 },
333 BuiltinParamDescriptor {
334 name: "b",
335 ty: BuiltinParamType::NumericArray,
336 arity: BuiltinParamArity::Required,
337 default: None,
338 description: "Cone offset vector.",
339 },
340 BuiltinParamDescriptor {
341 name: "d",
342 ty: BuiltinParamType::NumericArray,
343 arity: BuiltinParamArity::Required,
344 default: None,
345 description: "Linear cone right-hand-side vector.",
346 },
347 BuiltinParamDescriptor {
348 name: "gamma",
349 ty: BuiltinParamType::NumericScalar,
350 arity: BuiltinParamArity::Required,
351 default: None,
352 description: "Scalar cone right-hand-side offset.",
353 },
354];
355
356const SOC_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
357 label: "socConstraint = secondordercone(A, b, d, gamma)",
358 inputs: &SOC_INPUTS,
359 outputs: &SOC_OUTPUT,
360}];
361
362const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
363 code: "RM.CONEPROG.INVALID_ARGUMENT",
364 identifier: Some("RunMat:coneprog:InvalidArgument"),
365 when: "The argument count or optional argument grammar is invalid.",
366 message: "coneprog: invalid argument",
367};
368
369const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
370 code: "RM.CONEPROG.INVALID_INPUT",
371 identifier: Some("RunMat:coneprog:InvalidInput"),
372 when: "Objective, cone, constraint, option, or bound dimensions/types are invalid.",
373 message: "coneprog: invalid input",
374};
375
376const CONEPROG_ERRORS: [BuiltinErrorDescriptor; 2] = [ERROR_INVALID_ARGUMENT, ERROR_INVALID_INPUT];
377
378pub const CONEPROG_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
379 signatures: &CONEPROG_SIGNATURES,
380 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
381 completion_policy: BuiltinCompletionPolicy::Public,
382 errors: &CONEPROG_ERRORS,
383};
384
385pub const SECONDORDERCONE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
386 signatures: &SOC_SIGNATURES,
387 output_mode: BuiltinOutputMode::Fixed,
388 completion_policy: BuiltinCompletionPolicy::Public,
389 errors: &CONEPROG_ERRORS,
390};
391
392#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::coneprog")]
393pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
394 name: "coneprog",
395 op_kind: GpuOpKind::Custom("conic-programming"),
396 supported_precisions: &[],
397 broadcast: BroadcastSemantics::None,
398 provider_hooks: &[],
399 constant_strategy: ConstantStrategy::InlineLiteral,
400 residency: ResidencyPolicy::GatherImmediately,
401 nan_mode: ReductionNaN::Include,
402 two_pass_threshold: None,
403 workgroup_size: None,
404 accepts_nan_mode: false,
405 notes:
406 "Host conic interior-point solver. GPU-resident numeric inputs are gathered before solving.",
407};
408
409#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::coneprog")]
410pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
411 name: "coneprog",
412 shape: ShapeRequirements::Any,
413 constant_strategy: ConstantStrategy::InlineLiteral,
414 elementwise: None,
415 reduction: None,
416 emits_nan: false,
417 notes: "Conic programming is a solver boundary and terminates fusion planning.",
418};
419
420#[runtime_builtin(
421 name = "secondordercone",
422 category = "math/optim",
423 summary = "Create a second-order cone constraint for coneprog.",
424 keywords = "secondordercone,coneprog,second order cone,socp,optimization",
425 accel = "cpu",
426 type_resolver(optim_options_type),
427 descriptor(crate::builtins::math::optim::coneprog::SECONDORDERCONE_DESCRIPTOR),
428 builtin_path = "crate::builtins::math::optim::coneprog"
429)]
430async fn secondordercone_builtin(
431 a: Value,
432 b: Value,
433 d: Value,
434 gamma: Value,
435) -> BuiltinResult<Value> {
436 let a = numeric_matrix("A", a).await?.ok_or_else(|| {
437 coneprog_error(&ERROR_INVALID_INPUT, "secondordercone: A must be nonempty")
438 })?;
439 let b = numeric_vector("b", b, FiniteMode::Finite).await?;
440 let d = numeric_vector("d", d, FiniteMode::Finite).await?;
441 let gamma = numeric_scalar("gamma", gamma, FiniteMode::Finite).await?;
442 validate_soc_dimensions(&a, &b, &d, gamma)?;
443
444 let mut st = StructValue::new();
445 st.insert("A", matrix_value(&a)?);
446 st.insert("b", vector_value_with_len(b));
447 st.insert("d", vector_value_with_len(d));
448 st.insert("gamma", Value::Num(gamma));
449 st.insert("Type", Value::from("secondordercone"));
450 Ok(Value::Struct(st))
451}
452
453#[runtime_builtin(
454 name = "coneprog",
455 category = "math/optim",
456 summary = "Solve a linear conic programming problem with second-order cone constraints.",
457 keywords = "coneprog,conic programming,second order cone,socp,optimization,bounds",
458 accel = "sink",
459 type_resolver(linear_programming_type),
460 descriptor(crate::builtins::math::optim::coneprog::CONEPROG_DESCRIPTOR),
461 builtin_path = "crate::builtins::math::optim::coneprog"
462)]
463async fn coneprog_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
464 let problem = parse_problem(args).await?;
465 Ok(finalize(solve_coneprog(&problem)))
466}
467
468#[derive(Clone, Copy)]
469enum FiniteMode {
470 Finite,
471 Bounds,
472}
473
474#[derive(Clone)]
475struct MatrixInput {
476 rows: usize,
477 cols: usize,
478 data: Vec<f64>,
479}
480
481impl MatrixInput {
482 fn row(&self, row: usize) -> Vec<f64> {
483 (0..self.cols)
484 .map(|col| self.data[row + col * self.rows])
485 .collect()
486 }
487
488 fn mul_vec(&self, x: &[f64]) -> Vec<f64> {
489 (0..self.rows).map(|row| dot(&self.row(row), x)).collect()
490 }
491}
492
493#[derive(Clone)]
494struct SocConstraint {
495 a: MatrixInput,
496 b: Vec<f64>,
497 d: Vec<f64>,
498 gamma: f64,
499}
500
501#[derive(Clone)]
502struct ConeProgram {
503 f: Vec<f64>,
504 soc: Vec<SocConstraint>,
505 a_ineq: Vec<Vec<f64>>,
506 b_ineq: Vec<f64>,
507 a_eq: Vec<Vec<f64>>,
508 b_eq: Vec<f64>,
509 lb: Vec<f64>,
510 ub: Vec<f64>,
511 options: SolverOptions,
512}
513
514#[derive(Clone, Copy)]
515struct SolverOptions {
516 tol: f64,
517 max_iter: usize,
518}
519
520#[derive(Clone)]
521struct ConeOutcome {
522 x: Option<Vec<f64>>,
523 fval: Option<f64>,
524 exitflag: i32,
525 iterations: usize,
526 constrviolation: f64,
527 message: String,
528 lambda: LambdaEstimate,
529}
530
531#[derive(Clone)]
532struct LambdaEstimate {
533 ineqlin: Vec<f64>,
534 eqlin: Vec<f64>,
535 lower: Vec<f64>,
536 upper: Vec<f64>,
537 soc: Vec<f64>,
538}
539
540impl ConeOutcome {
541 fn infeasible(problem: &ConeProgram, message: &str) -> Self {
542 Self {
543 x: None,
544 fval: None,
545 exitflag: -2,
546 iterations: 0,
547 constrviolation: 0.0,
548 message: message.to_string(),
549 lambda: LambdaEstimate::zeros(problem),
550 }
551 }
552
553 fn unbounded(problem: &ConeProgram) -> Self {
554 Self {
555 x: None,
556 fval: None,
557 exitflag: -3,
558 iterations: 0,
559 constrviolation: 0.0,
560 message: "Problem is unbounded.".to_string(),
561 lambda: LambdaEstimate::zeros(problem),
562 }
563 }
564}
565
566impl LambdaEstimate {
567 fn zeros(problem: &ConeProgram) -> Self {
568 Self {
569 ineqlin: vec![0.0; problem.a_ineq.len()],
570 eqlin: vec![0.0; problem.a_eq.len()],
571 lower: vec![0.0; problem.f.len()],
572 upper: vec![0.0; problem.f.len()],
573 soc: vec![0.0; problem.soc.len()],
574 }
575 }
576}
577
578async fn parse_problem(args: Vec<Value>) -> BuiltinResult<ConeProgram> {
579 if args.len() == 1 {
580 let value = gather(args.into_iter().next().unwrap()).await?;
581 let Value::Struct(problem) = value else {
582 return Err(coneprog_error(
583 &ERROR_INVALID_ARGUMENT,
584 "coneprog: single-argument form expects a problem struct",
585 ));
586 };
587 return parse_problem_struct(problem).await;
588 }
589 if args.len() < 2 {
590 return Err(coneprog_error(
591 &ERROR_INVALID_ARGUMENT,
592 "coneprog: expected f and socConstraints",
593 ));
594 }
595 let mut args = args.into_iter();
596 let f_value = args.next().unwrap();
597 let soc_value = args.next().unwrap();
598 let rest = args.collect::<Vec<_>>();
599
600 let f = numeric_vector("f", f_value, FiniteMode::Finite).await?;
601 if f.is_empty() {
602 return Err(coneprog_error(
603 &ERROR_INVALID_INPUT,
604 "coneprog: f must be a nonempty numeric vector",
605 ));
606 }
607 let n = f.len();
608 let soc = parse_soc_constraints(soc_value, n).await?;
609 let (rest, options) = split_options(rest).await?;
610 let (a_ineq, b_ineq, a_eq, b_eq, lb, ub) = parse_positional_constraints(rest, n).await?;
611 Ok(ConeProgram {
612 f,
613 soc,
614 a_ineq,
615 b_ineq,
616 a_eq,
617 b_eq,
618 lb,
619 ub,
620 options,
621 })
622}
623
624async fn parse_problem_struct(problem: StructValue) -> BuiltinResult<ConeProgram> {
625 let f = required_field(&problem, "f")?;
626 let f = numeric_vector("f", f, FiniteMode::Finite).await?;
627 if f.is_empty() {
628 return Err(coneprog_error(
629 &ERROR_INVALID_INPUT,
630 "coneprog: f must be a nonempty numeric vector",
631 ));
632 }
633 let n = f.len();
634 let soc_value = optional_field(&problem, "socConstraints")
635 .or_else(|| optional_field(&problem, "socConstraints".to_ascii_lowercase().as_str()))
636 .unwrap_or_else(empty_value);
637 let soc = parse_soc_constraints(soc_value, n).await?;
638 let a = optional_field(&problem, "Aineq")
639 .or_else(|| optional_field(&problem, "A"))
640 .unwrap_or_else(empty_value);
641 let b = optional_field(&problem, "bineq")
642 .or_else(|| optional_field(&problem, "b"))
643 .unwrap_or_else(empty_value);
644 let (a_ineq, b_ineq) = parse_constraint_pair("A", a, "b", b, n).await?;
645 let aeq = optional_field(&problem, "Aeq").unwrap_or_else(empty_value);
646 let beq = optional_field(&problem, "beq").unwrap_or_else(empty_value);
647 let (a_eq, b_eq) = parse_constraint_pair("Aeq", aeq, "beq", beq, n).await?;
648 let lb = optional_field(&problem, "lb").unwrap_or_else(empty_value);
649 let ub = optional_field(&problem, "ub").unwrap_or_else(empty_value);
650 let (lb, ub) = parse_bounds(Some(&lb), Some(&ub), n).await?;
651 let options = match optional_field(&problem, "options") {
652 Some(Value::Struct(st)) => parse_options(Some(&st))?,
653 Some(other) => {
654 return Err(coneprog_error(
655 &ERROR_INVALID_INPUT,
656 format!("coneprog: options must be a struct, got {other:?}"),
657 ))
658 }
659 None => SolverOptions::default(),
660 };
661 Ok(ConeProgram {
662 f,
663 soc,
664 a_ineq,
665 b_ineq,
666 a_eq,
667 b_eq,
668 lb,
669 ub,
670 options,
671 })
672}
673
674async fn split_options(rest: Vec<Value>) -> BuiltinResult<(Vec<Value>, SolverOptions)> {
675 let Some(last) = rest.last() else {
676 return Ok((rest, SolverOptions::default()));
677 };
678 let gathered = gather(last.clone()).await?;
679 if let Value::Struct(st) = gathered {
680 if is_options_struct(&st) {
681 let mut without = rest;
682 without.pop();
683 return Ok((without, parse_options(Some(&st))?));
684 }
685 }
686 Ok((rest, SolverOptions::default()))
687}
688
689async fn parse_positional_constraints(
690 rest: Vec<Value>,
691 n: usize,
692) -> BuiltinResult<(
693 Vec<Vec<f64>>,
694 Vec<f64>,
695 Vec<Vec<f64>>,
696 Vec<f64>,
697 Vec<f64>,
698 Vec<f64>,
699)> {
700 if !matches!(rest.len(), 0 | 2 | 4 | 6) {
701 return Err(coneprog_error(
702 &ERROR_INVALID_ARGUMENT,
703 "coneprog: optional linear constraints must be A,b,Aeq,beq,lb,ub",
704 ));
705 }
706 let a = rest.first().cloned().unwrap_or_else(empty_value);
707 let b = rest.get(1).cloned().unwrap_or_else(empty_value);
708 let (a_ineq, b_ineq) = parse_constraint_pair("A", a, "b", b, n).await?;
709 let aeq = rest.get(2).cloned().unwrap_or_else(empty_value);
710 let beq = rest.get(3).cloned().unwrap_or_else(empty_value);
711 let (a_eq, b_eq) = parse_constraint_pair("Aeq", aeq, "beq", beq, n).await?;
712 let (lb, ub) = parse_bounds(rest.get(4), rest.get(5), n).await?;
713 Ok((a_ineq, b_ineq, a_eq, b_eq, lb, ub))
714}
715
716fn is_options_struct(st: &StructValue) -> bool {
717 [
718 "Solver",
719 "TolX",
720 "TolFun",
721 "MaxIter",
722 "MaxFunEvals",
723 "Display",
724 "Algorithm",
725 ]
726 .iter()
727 .any(|name| lookup_option(st, name).is_some())
728}
729
730impl Default for SolverOptions {
731 fn default() -> Self {
732 Self {
733 tol: DEFAULT_TOL,
734 max_iter: DEFAULT_MAX_ITER,
735 }
736 }
737}
738
739fn parse_options(options: Option<&StructValue>) -> BuiltinResult<SolverOptions> {
740 let tol = option_f64(CONEPROG, options, "TolFun", DEFAULT_TOL)?.min(option_f64(
741 CONEPROG,
742 options,
743 "TolX",
744 DEFAULT_TOL,
745 )?);
746 let max_iter = option_usize(CONEPROG, options, "MaxIter", DEFAULT_MAX_ITER)?.max(1);
747 Ok(SolverOptions {
748 tol: tol.max(1.0e-12),
749 max_iter,
750 })
751}
752
753async fn parse_soc_constraints(value: Value, n: usize) -> BuiltinResult<Vec<SocConstraint>> {
754 let value = gather(value).await?;
755 if is_empty_value(&value) {
756 return Ok(Vec::new());
757 }
758 match value {
759 Value::Struct(st) => parse_single_soc(st, n).await.map(|constraint| vec![constraint]),
760 Value::Cell(cell) => {
761 let mut out = Vec::with_capacity(cell.data.len());
762 for value in cell.data {
763 let value = gather(value).await?;
764 let Value::Struct(st) = value else {
765 return Err(coneprog_error(
766 &ERROR_INVALID_INPUT,
767 "coneprog: socConstraints cell entries must be secondordercone structs",
768 ));
769 };
770 out.push(parse_single_soc(st, n).await?);
771 }
772 Ok(out)
773 }
774 Value::OutputList(values) => {
775 let mut out = Vec::with_capacity(values.len());
776 for value in values {
777 let value = gather(value).await?;
778 let Value::Struct(st) = value else {
779 return Err(coneprog_error(
780 &ERROR_INVALID_INPUT,
781 "coneprog: socConstraints entries must be secondordercone structs",
782 ));
783 };
784 out.push(parse_single_soc(st, n).await?);
785 }
786 Ok(out)
787 }
788 other => Err(coneprog_error(
789 &ERROR_INVALID_INPUT,
790 format!("coneprog: socConstraints must be a secondordercone struct or cell array, got {other:?}"),
791 )),
792 }
793}
794
795async fn parse_single_soc(st: StructValue, n: usize) -> BuiltinResult<SocConstraint> {
796 let a = numeric_matrix("socConstraints.A", required_field(&st, "A")?)
797 .await?
798 .ok_or_else(|| coneprog_error(&ERROR_INVALID_INPUT, "coneprog: cone A must be nonempty"))?;
799 let b = numeric_vector(
800 "socConstraints.b",
801 required_field(&st, "b")?,
802 FiniteMode::Finite,
803 )
804 .await?;
805 let d = numeric_vector(
806 "socConstraints.d",
807 required_field(&st, "d")?,
808 FiniteMode::Finite,
809 )
810 .await?;
811 let gamma = numeric_scalar(
812 "socConstraints.gamma",
813 required_field(&st, "gamma")?,
814 FiniteMode::Finite,
815 )
816 .await?;
817 validate_soc_dimensions(&a, &b, &d, gamma)?;
818 if a.cols != n || d.len() != n {
819 return Err(coneprog_error(
820 &ERROR_INVALID_INPUT,
821 "coneprog: cone A columns and d length must match f",
822 ));
823 }
824 Ok(SocConstraint { a, b, d, gamma })
825}
826
827fn validate_soc_dimensions(a: &MatrixInput, b: &[f64], d: &[f64], gamma: f64) -> BuiltinResult<()> {
828 if b.len() != a.rows {
829 return Err(coneprog_error(
830 &ERROR_INVALID_INPUT,
831 "secondordercone: b length must match rows of A",
832 ));
833 }
834 if d.len() != a.cols {
835 return Err(coneprog_error(
836 &ERROR_INVALID_INPUT,
837 "secondordercone: d length must match columns of A",
838 ));
839 }
840 if !gamma.is_finite() {
841 return Err(coneprog_error(
842 &ERROR_INVALID_INPUT,
843 "secondordercone: gamma must be finite",
844 ));
845 }
846 Ok(())
847}
848
849async fn gather(value: Value) -> BuiltinResult<Value> {
850 crate::dispatcher::gather_if_needed_async(&value)
851 .await
852 .map_err(|err| coneprog_error(&ERROR_INVALID_INPUT, err.message()))
853}
854
855fn required_field(st: &StructValue, name: &str) -> BuiltinResult<Value> {
856 optional_field(st, name).ok_or_else(|| {
857 coneprog_error(
858 &ERROR_INVALID_INPUT,
859 format!("coneprog: problem struct is missing {name}"),
860 )
861 })
862}
863
864fn optional_field(st: &StructValue, name: &str) -> Option<Value> {
865 st.fields
866 .iter()
867 .find(|(key, _)| key.eq_ignore_ascii_case(name))
868 .map(|(_, value)| value.clone())
869}
870
871fn is_empty_value(value: &Value) -> bool {
872 matches!(value, Value::Tensor(t) if t.data.is_empty())
873}
874
875fn empty_value() -> Value {
876 Value::Tensor(Tensor::zeros(vec![0, 0]))
877}
878
879async fn numeric_scalar(label: &str, value: Value, finite_mode: FiniteMode) -> BuiltinResult<f64> {
880 let values = numeric_vector(label, value, finite_mode).await?;
881 if values.len() == 1 {
882 Ok(values[0])
883 } else {
884 Err(coneprog_error(
885 &ERROR_INVALID_INPUT,
886 format!("coneprog: {label} must be a scalar"),
887 ))
888 }
889}
890
891async fn numeric_vector(
892 label: &str,
893 value: Value,
894 finite_mode: FiniteMode,
895) -> BuiltinResult<Vec<f64>> {
896 let value = gather(value).await?;
897 if is_empty_value(&value) {
898 return Ok(Vec::new());
899 }
900 let data = match value {
901 Value::Num(n) => vec![n],
902 Value::Int(i) => vec![i.to_f64()],
903 Value::Bool(flag) => vec![if flag { 1.0 } else { 0.0 }],
904 Value::Tensor(tensor) => {
905 if tensor.shape.len() > 2 || (tensor.rows() != 1 && tensor.cols() != 1) {
906 return Err(coneprog_error(
907 &ERROR_INVALID_INPUT,
908 format!("coneprog: {label} must be a vector"),
909 ));
910 }
911 tensor.data
912 }
913 other => {
914 return Err(coneprog_error(
915 &ERROR_INVALID_INPUT,
916 format!("coneprog: {label} must be a real numeric vector, got {other:?}"),
917 ))
918 }
919 };
920 validate_numbers(label, &data, finite_mode)?;
921 Ok(data)
922}
923
924async fn numeric_matrix(label: &str, value: Value) -> BuiltinResult<Option<MatrixInput>> {
925 let value = gather(value).await?;
926 if is_empty_value(&value) {
927 return Ok(None);
928 }
929 match value {
930 Value::Num(n) => {
931 validate_numbers(label, &[n], FiniteMode::Finite)?;
932 Ok(Some(MatrixInput {
933 rows: 1,
934 cols: 1,
935 data: vec![n],
936 }))
937 }
938 Value::Int(i) => {
939 let n = i.to_f64();
940 validate_numbers(label, &[n], FiniteMode::Finite)?;
941 Ok(Some(MatrixInput {
942 rows: 1,
943 cols: 1,
944 data: vec![n],
945 }))
946 }
947 Value::Tensor(tensor) => {
948 if tensor.shape.len() > 2 {
949 return Err(coneprog_error(
950 &ERROR_INVALID_INPUT,
951 format!("coneprog: {label} must be a numeric matrix"),
952 ));
953 }
954 validate_numbers(label, &tensor.data, FiniteMode::Finite)?;
955 Ok(Some(MatrixInput {
956 rows: tensor.rows(),
957 cols: tensor.cols(),
958 data: tensor.data,
959 }))
960 }
961 other => Err(coneprog_error(
962 &ERROR_INVALID_INPUT,
963 format!("coneprog: {label} must be a real numeric matrix, got {other:?}"),
964 )),
965 }
966}
967
968fn validate_numbers(label: &str, data: &[f64], finite_mode: FiniteMode) -> BuiltinResult<()> {
969 for value in data {
970 match finite_mode {
971 FiniteMode::Finite if !value.is_finite() => {
972 return Err(coneprog_error(
973 &ERROR_INVALID_INPUT,
974 format!("coneprog: {label} values must be finite"),
975 ))
976 }
977 FiniteMode::Bounds if value.is_nan() => {
978 return Err(coneprog_error(
979 &ERROR_INVALID_INPUT,
980 format!("coneprog: {label} bounds cannot be NaN"),
981 ))
982 }
983 _ => {}
984 }
985 }
986 Ok(())
987}
988
989async fn parse_constraint_pair(
990 matrix_label: &str,
991 matrix: Value,
992 rhs_label: &str,
993 rhs: Value,
994 n: usize,
995) -> BuiltinResult<(Vec<Vec<f64>>, Vec<f64>)> {
996 let matrix = numeric_matrix(matrix_label, matrix).await?;
997 let rhs = numeric_vector(rhs_label, rhs, FiniteMode::Finite).await?;
998 match (matrix, rhs.is_empty()) {
999 (None, true) => Ok((Vec::new(), Vec::new())),
1000 (None, false) => Err(coneprog_error(
1001 &ERROR_INVALID_INPUT,
1002 format!("coneprog: {matrix_label} cannot be empty when {rhs_label} is nonempty"),
1003 )),
1004 (Some(matrix), _) => {
1005 if matrix.cols != n {
1006 return Err(coneprog_error(
1007 &ERROR_INVALID_INPUT,
1008 format!("coneprog: {matrix_label} must have one column per element of f"),
1009 ));
1010 }
1011 if rhs.len() != matrix.rows {
1012 return Err(coneprog_error(
1013 &ERROR_INVALID_INPUT,
1014 format!("coneprog: {rhs_label} length must match rows of {matrix_label}"),
1015 ));
1016 }
1017 let rows = (0..matrix.rows).map(|row| matrix.row(row)).collect();
1018 Ok((rows, rhs))
1019 }
1020 }
1021}
1022
1023async fn parse_bounds(
1024 lb: Option<&Value>,
1025 ub: Option<&Value>,
1026 n: usize,
1027) -> BuiltinResult<(Vec<f64>, Vec<f64>)> {
1028 let lb = match lb {
1029 None => vec![f64::NEG_INFINITY; n],
1030 Some(value) if is_empty_value(value) => vec![f64::NEG_INFINITY; n],
1031 Some(value) => {
1032 let values = numeric_vector("lb", value.clone(), FiniteMode::Bounds).await?;
1033 normalize_bound("lb", values, n)?
1034 }
1035 };
1036 let ub = match ub {
1037 None => vec![f64::INFINITY; n],
1038 Some(value) if is_empty_value(value) => vec![f64::INFINITY; n],
1039 Some(value) => {
1040 let values = numeric_vector("ub", value.clone(), FiniteMode::Bounds).await?;
1041 normalize_bound("ub", values, n)?
1042 }
1043 };
1044 Ok((lb, ub))
1045}
1046
1047fn normalize_bound(label: &str, values: Vec<f64>, n: usize) -> BuiltinResult<Vec<f64>> {
1048 if values.len() == n {
1049 Ok(values)
1050 } else {
1051 Err(coneprog_error(
1052 &ERROR_INVALID_INPUT,
1053 format!("coneprog: {label} length must match f"),
1054 ))
1055 }
1056}
1057
1058fn solve_coneprog(problem: &ConeProgram) -> ConeOutcome {
1059 if let Some(reason) = impossible_bounds(problem) {
1060 return ConeOutcome::infeasible(problem, reason);
1061 }
1062 if problem.soc.is_empty()
1063 && problem.a_ineq.is_empty()
1064 && problem.a_eq.is_empty()
1065 && problem.lb.iter().all(|v| !v.is_finite())
1066 && problem.ub.iter().all(|v| !v.is_finite())
1067 && norm(&problem.f) > problem.options.tol
1068 {
1069 return ConeOutcome::unbounded(problem);
1070 }
1071
1072 let mut linear_rows = problem.a_ineq.clone();
1073 let mut linear_rhs = problem.b_ineq.clone();
1074 append_bound_rows(problem, &mut linear_rows, &mut linear_rhs);
1075
1076 let Some(mut x) = initial_strict_point(problem, &linear_rows, &linear_rhs) else {
1077 return ConeOutcome::infeasible(problem, "No feasible point found.");
1078 };
1079 if constraint_violation(problem, &x) > 1.0e-5 {
1080 return ConeOutcome::infeasible(problem, "No feasible point found.");
1081 }
1082 if !is_strictly_feasible(problem, &linear_rows, &linear_rhs, &x) {
1083 relax_into_interior(problem, &linear_rows, &linear_rhs, &mut x);
1084 }
1085 if !is_strictly_feasible(problem, &linear_rows, &linear_rhs, &x)
1086 && norm(&problem.f) <= problem.options.tol
1087 {
1088 let fval = dot(&problem.f, &x);
1089 return optimal_outcome(problem, x, fval, 0);
1090 }
1091 if !is_strictly_feasible(problem, &linear_rows, &linear_rhs, &x) {
1092 return ConeOutcome::infeasible(problem, "No strict feasible point found.");
1093 }
1094
1095 let mut iterations = 0usize;
1096 let outer = 9usize;
1097 let inner = (problem.options.max_iter / outer.max(1)).max(8);
1098 let mut t = 1.0;
1099 for _ in 0..outer {
1100 for _ in 0..inner {
1101 iterations += 1;
1102 let grad = barrier_gradient(problem, &linear_rows, &linear_rhs, &x, t);
1103 let projected = project_gradient(&problem.a_eq, &grad);
1104 let grad_norm = norm(&projected);
1105 if grad_norm <= problem.options.tol {
1106 break;
1107 }
1108 let current = barrier_value(problem, &linear_rows, &linear_rhs, &x, t);
1109 let mut step = 1.0 / (1.0 + grad_norm);
1110 let mut accepted = false;
1111 for _ in 0..40 {
1112 let mut candidate = x
1113 .iter()
1114 .zip(&projected)
1115 .map(|(xi, gi)| xi - step * gi)
1116 .collect::<Vec<_>>();
1117 project_to_equalities(&problem.a_eq, &problem.b_eq, &mut candidate);
1118 if is_strictly_feasible(problem, &linear_rows, &linear_rhs, &candidate)
1119 && barrier_value(problem, &linear_rows, &linear_rhs, &candidate, t)
1120 <= current - 1.0e-4 * step * grad_norm * grad_norm
1121 {
1122 x = candidate;
1123 accepted = true;
1124 break;
1125 }
1126 step *= 0.5;
1127 }
1128 if !accepted {
1129 break;
1130 }
1131 }
1132 t *= 10.0;
1133 }
1134
1135 let violation = constraint_violation(problem, &x);
1136 if violation > 1.0e-4 {
1137 return ConeOutcome::infeasible(problem, "No feasible point found.");
1138 }
1139 let fval = dot(&problem.f, &x);
1140 optimal_outcome(problem, x, fval, iterations)
1141}
1142
1143fn impossible_bounds(problem: &ConeProgram) -> Option<&'static str> {
1144 for i in 0..problem.f.len() {
1145 if problem.lb[i] > problem.ub[i] + problem.options.tol {
1146 return Some("No feasible point found: lower bound exceeds upper bound.");
1147 }
1148 }
1149 None
1150}
1151
1152fn append_bound_rows(problem: &ConeProgram, rows: &mut Vec<Vec<f64>>, rhs: &mut Vec<f64>) {
1153 for i in 0..problem.f.len() {
1154 if problem.lb[i].is_finite() {
1155 let mut row = vec![0.0; problem.f.len()];
1156 row[i] = -1.0;
1157 rows.push(row);
1158 rhs.push(-problem.lb[i]);
1159 }
1160 if problem.ub[i].is_finite() {
1161 let mut row = vec![0.0; problem.f.len()];
1162 row[i] = 1.0;
1163 rows.push(row);
1164 rhs.push(problem.ub[i]);
1165 }
1166 }
1167}
1168
1169fn initial_strict_point(
1170 problem: &ConeProgram,
1171 linear_rows: &[Vec<f64>],
1172 linear_rhs: &[f64],
1173) -> Option<Vec<f64>> {
1174 let n = problem.f.len();
1175 let mut x = if problem.a_eq.is_empty() {
1176 vec![0.0; n]
1177 } else {
1178 pseudo_solve(&problem.a_eq, &problem.b_eq, n)?
1179 };
1180 for i in 0..n {
1181 x[i] = match (problem.lb[i].is_finite(), problem.ub[i].is_finite()) {
1182 (true, true) => 0.5 * (problem.lb[i] + problem.ub[i]),
1183 (true, false) if x[i] <= problem.lb[i] => problem.lb[i] + 1.0,
1184 (false, true) if x[i] >= problem.ub[i] => problem.ub[i] - 1.0,
1185 _ => x[i],
1186 };
1187 }
1188 project_to_equalities(&problem.a_eq, &problem.b_eq, &mut x);
1189 relax_into_interior(problem, linear_rows, linear_rhs, &mut x);
1190 Some(x)
1191}
1192
1193fn relax_into_interior(
1194 problem: &ConeProgram,
1195 linear_rows: &[Vec<f64>],
1196 linear_rhs: &[f64],
1197 x: &mut [f64],
1198) {
1199 for _ in 0..800 {
1200 let mut changed = false;
1201 for (row, rhs) in linear_rows.iter().zip(linear_rhs) {
1202 let slack = rhs - dot(row, x);
1203 if slack <= STRICT_MARGIN {
1204 let denom = dot(row, row).max(STRICT_MARGIN);
1205 let correction = (STRICT_MARGIN - slack) / denom;
1206 for (xi, ai) in x.iter_mut().zip(row) {
1207 *xi -= correction * ai;
1208 }
1209 changed = true;
1210 }
1211 }
1212 for constraint in &problem.soc {
1213 let z = cone_z(constraint, x);
1214 let z_norm = norm(&z);
1215 let q = dot(&constraint.d, x) + constraint.gamma;
1216 let violation = z_norm - q + STRICT_MARGIN;
1217 if violation > 0.0 {
1218 let grad = cone_violation_gradient(constraint, &z, z_norm);
1219 let denom = dot(&grad, &grad).max(STRICT_MARGIN);
1220 for (xi, gi) in x.iter_mut().zip(&grad) {
1221 *xi -= violation * gi / denom;
1222 }
1223 changed = true;
1224 }
1225 }
1226 project_to_equalities(&problem.a_eq, &problem.b_eq, x);
1227 if !changed {
1228 break;
1229 }
1230 }
1231}
1232
1233fn barrier_value(
1234 problem: &ConeProgram,
1235 linear_rows: &[Vec<f64>],
1236 linear_rhs: &[f64],
1237 x: &[f64],
1238 t: f64,
1239) -> f64 {
1240 let mut value = t * dot(&problem.f, x);
1241 for (row, rhs) in linear_rows.iter().zip(linear_rhs) {
1242 let slack = rhs - dot(row, x);
1243 if slack <= 0.0 {
1244 return f64::INFINITY;
1245 }
1246 value -= slack.ln();
1247 }
1248 for constraint in &problem.soc {
1249 let z = cone_z(constraint, x);
1250 let q = dot(&constraint.d, x) + constraint.gamma;
1251 let margin = q * q - dot(&z, &z);
1252 if q <= 0.0 || margin <= 0.0 {
1253 return f64::INFINITY;
1254 }
1255 value -= margin.ln();
1256 }
1257 value
1258}
1259
1260fn barrier_gradient(
1261 problem: &ConeProgram,
1262 linear_rows: &[Vec<f64>],
1263 linear_rhs: &[f64],
1264 x: &[f64],
1265 t: f64,
1266) -> Vec<f64> {
1267 let mut grad = problem.f.iter().map(|value| t * value).collect::<Vec<_>>();
1268 for (row, rhs) in linear_rows.iter().zip(linear_rhs) {
1269 let slack = (rhs - dot(row, x)).max(STRICT_MARGIN);
1270 for (gi, ai) in grad.iter_mut().zip(row) {
1271 *gi += ai / slack;
1272 }
1273 }
1274 for constraint in &problem.soc {
1275 let z = cone_z(constraint, x);
1276 let q = dot(&constraint.d, x) + constraint.gamma;
1277 let margin = (q * q - dot(&z, &z)).max(STRICT_MARGIN);
1278 let mut margin_grad = constraint
1279 .d
1280 .iter()
1281 .map(|di| 2.0 * q * di)
1282 .collect::<Vec<_>>();
1283 for col in 0..constraint.a.cols {
1284 let a_col_z = (0..constraint.a.rows)
1285 .map(|row| constraint.a.data[row + col * constraint.a.rows] * z[row])
1286 .sum::<f64>();
1287 margin_grad[col] -= 2.0 * a_col_z;
1288 }
1289 for (gi, mi) in grad.iter_mut().zip(margin_grad) {
1290 *gi -= mi / margin;
1291 }
1292 }
1293 grad
1294}
1295
1296fn project_gradient(a_eq: &[Vec<f64>], grad: &[f64]) -> Vec<f64> {
1297 if a_eq.is_empty() {
1298 return grad.to_vec();
1299 }
1300 let mut projected = grad.to_vec();
1301 let rhs = a_eq.iter().map(|row| dot(row, grad)).collect::<Vec<_>>();
1302 if let Some(rowspace) = minimum_norm_equality_correction(a_eq, &rhs, grad.len()) {
1303 for (pi, ri) in projected.iter_mut().zip(rowspace) {
1304 *pi -= ri;
1305 }
1306 }
1307 projected
1308}
1309
1310fn project_to_equalities(a_eq: &[Vec<f64>], b_eq: &[f64], x: &mut [f64]) {
1311 if a_eq.is_empty() {
1312 return;
1313 }
1314 let residual = a_eq
1315 .iter()
1316 .zip(b_eq)
1317 .map(|(row, rhs)| rhs - dot(row, x))
1318 .collect::<Vec<_>>();
1319 if let Some(correction) = minimum_norm_equality_correction(a_eq, &residual, x.len()) {
1320 for (xi, ci) in x.iter_mut().zip(correction) {
1321 *xi += ci;
1322 }
1323 }
1324}
1325
1326fn minimum_norm_equality_correction(rows: &[Vec<f64>], rhs: &[f64], n: usize) -> Option<Vec<f64>> {
1327 if rows.is_empty() {
1328 return Some(vec![0.0; n]);
1329 }
1330 let a = dmatrix_from_rows(rows, n);
1331 let rhs = DVector::from_column_slice(rhs);
1332 let gram = &a * a.transpose();
1333 let svd = gram.svd(true, true);
1334 let y = svd.solve(&rhs, DEFAULT_TOL).ok()?;
1335 let correction = a.transpose() * y;
1336 Some(correction.iter().copied().collect())
1337}
1338
1339fn pseudo_solve(rows: &[Vec<f64>], rhs: &[f64], n: usize) -> Option<Vec<f64>> {
1340 if rows.is_empty() {
1341 return Some(vec![0.0; n]);
1342 }
1343 let matrix = dmatrix_from_rows(rows, n);
1344 let rhs_vec = DVector::from_column_slice(rhs);
1345 let svd = matrix.svd(true, true);
1346 let x = svd.solve(&rhs_vec, DEFAULT_TOL).ok()?;
1347 let out = x.iter().copied().collect::<Vec<_>>();
1348 let residual = rows
1349 .iter()
1350 .zip(rhs)
1351 .map(|(row, target)| (dot(row, &out) - target).abs())
1352 .fold(0.0, f64::max);
1353 (residual <= 1.0e-7).then_some(out)
1354}
1355
1356fn dmatrix_from_rows(rows: &[Vec<f64>], n: usize) -> DMatrix<f64> {
1357 let data = rows
1358 .iter()
1359 .flat_map(|row| row.iter().copied())
1360 .collect::<Vec<_>>();
1361 DMatrix::from_row_slice(rows.len(), n, &data)
1362}
1363
1364fn is_strictly_feasible(
1365 problem: &ConeProgram,
1366 linear_rows: &[Vec<f64>],
1367 linear_rhs: &[f64],
1368 x: &[f64],
1369) -> bool {
1370 linear_rows
1371 .iter()
1372 .zip(linear_rhs)
1373 .all(|(row, rhs)| rhs - dot(row, x) > STRICT_MARGIN)
1374 && problem.soc.iter().all(|constraint| {
1375 let z = cone_z(constraint, x);
1376 let q = dot(&constraint.d, x) + constraint.gamma;
1377 q > 0.0 && q - norm(&z) > STRICT_MARGIN
1378 })
1379 && equality_violation(problem, x) <= 1.0e-6
1380}
1381
1382fn constraint_violation(problem: &ConeProgram, x: &[f64]) -> f64 {
1383 let mut violation = equality_violation(problem, x);
1384 for (row, rhs) in problem.a_ineq.iter().zip(&problem.b_ineq) {
1385 violation = violation.max((dot(row, x) - rhs).max(0.0));
1386 }
1387 for i in 0..x.len() {
1388 violation = violation.max((problem.lb[i] - x[i]).max(0.0));
1389 violation = violation.max((x[i] - problem.ub[i]).max(0.0));
1390 }
1391 for constraint in &problem.soc {
1392 let z = cone_z(constraint, x);
1393 let q = dot(&constraint.d, x) + constraint.gamma;
1394 violation = violation.max((norm(&z) - q).max(0.0));
1395 }
1396 violation
1397}
1398
1399fn equality_violation(problem: &ConeProgram, x: &[f64]) -> f64 {
1400 problem
1401 .a_eq
1402 .iter()
1403 .zip(&problem.b_eq)
1404 .map(|(row, rhs)| (dot(row, x) - rhs).abs())
1405 .fold(0.0, f64::max)
1406}
1407
1408fn cone_z(constraint: &SocConstraint, x: &[f64]) -> Vec<f64> {
1409 let mut z = constraint.a.mul_vec(x);
1410 for (zi, bi) in z.iter_mut().zip(&constraint.b) {
1411 *zi += bi;
1412 }
1413 z
1414}
1415
1416fn cone_violation_gradient(constraint: &SocConstraint, z: &[f64], z_norm: f64) -> Vec<f64> {
1417 let mut grad = constraint.d.iter().map(|di| -*di).collect::<Vec<_>>();
1418 if z_norm > STRICT_MARGIN {
1419 for col in 0..constraint.a.cols {
1420 let a_col_z = (0..constraint.a.rows)
1421 .map(|row| constraint.a.data[row + col * constraint.a.rows] * z[row])
1422 .sum::<f64>();
1423 grad[col] += a_col_z / z_norm;
1424 }
1425 }
1426 grad
1427}
1428
1429fn optimal_outcome(
1430 problem: &ConeProgram,
1431 x: Vec<f64>,
1432 fval: f64,
1433 iterations: usize,
1434) -> ConeOutcome {
1435 let lambda = estimate_lambda(problem, &x);
1436 ConeOutcome {
1437 x: Some(x.clone()),
1438 fval: Some(fval),
1439 exitflag: 1,
1440 iterations,
1441 constrviolation: constraint_violation(problem, &x),
1442 message: "Optimal solution found.".to_string(),
1443 lambda,
1444 }
1445}
1446
1447#[derive(Clone, Copy)]
1448enum ActiveConstraint {
1449 Ineq(usize),
1450 Eq(usize),
1451 Lower(usize),
1452 Upper(usize),
1453 Soc(usize),
1454}
1455
1456fn estimate_lambda(problem: &ConeProgram, x: &[f64]) -> LambdaEstimate {
1457 let mut lambda = LambdaEstimate::zeros(problem);
1458 let mut columns = Vec::new();
1459 let mut kinds = Vec::new();
1460 let active_tol = problem.options.tol.max(1.0e-6);
1461
1462 for (idx, (row, rhs)) in problem.a_ineq.iter().zip(&problem.b_ineq).enumerate() {
1463 if rhs - dot(row, x) <= active_tol {
1464 columns.push(DVector::from_column_slice(row));
1465 kinds.push(ActiveConstraint::Ineq(idx));
1466 }
1467 }
1468 for (idx, row) in problem.a_eq.iter().enumerate() {
1469 columns.push(DVector::from_column_slice(row));
1470 kinds.push(ActiveConstraint::Eq(idx));
1471 }
1472 for idx in 0..problem.f.len() {
1473 if problem.lb[idx].is_finite() && x[idx] - problem.lb[idx] <= active_tol {
1474 let mut column = vec![0.0; problem.f.len()];
1475 column[idx] = -1.0;
1476 columns.push(DVector::from_column_slice(&column));
1477 kinds.push(ActiveConstraint::Lower(idx));
1478 }
1479 if problem.ub[idx].is_finite() && problem.ub[idx] - x[idx] <= active_tol {
1480 let mut column = vec![0.0; problem.f.len()];
1481 column[idx] = 1.0;
1482 columns.push(DVector::from_column_slice(&column));
1483 kinds.push(ActiveConstraint::Upper(idx));
1484 }
1485 }
1486 for (idx, constraint) in problem.soc.iter().enumerate() {
1487 let z = cone_z(constraint, x);
1488 let z_norm = norm(&z);
1489 let q = dot(&constraint.d, x) + constraint.gamma;
1490 if q - z_norm <= active_tol {
1491 let column = cone_violation_gradient(constraint, &z, z_norm);
1492 if norm(&column) > DEFAULT_TOL {
1493 columns.push(DVector::from_column_slice(&column));
1494 kinds.push(ActiveConstraint::Soc(idx));
1495 }
1496 }
1497 }
1498
1499 if columns.is_empty() {
1500 return lambda;
1501 }
1502
1503 let matrix = DMatrix::from_columns(&columns);
1504 let rhs = DVector::from_iterator(problem.f.len(), problem.f.iter().map(|value| -*value));
1505 let Some(solution) = matrix.svd(true, true).solve(&rhs, DEFAULT_TOL).ok() else {
1506 return lambda;
1507 };
1508 for (value, kind) in solution.iter().zip(kinds) {
1509 match kind {
1510 ActiveConstraint::Ineq(idx) => lambda.ineqlin[idx] = (*value).max(0.0),
1511 ActiveConstraint::Eq(idx) => lambda.eqlin[idx] = *value,
1512 ActiveConstraint::Lower(idx) => lambda.lower[idx] = (*value).max(0.0),
1513 ActiveConstraint::Upper(idx) => lambda.upper[idx] = (*value).max(0.0),
1514 ActiveConstraint::Soc(idx) => lambda.soc[idx] = (*value).max(0.0),
1515 }
1516 }
1517 lambda
1518}
1519
1520fn finalize(outcome: ConeOutcome) -> Value {
1521 let x = outcome
1522 .x
1523 .clone()
1524 .map(vector_value_with_len)
1525 .unwrap_or_else(empty_value);
1526 let fval = outcome.fval.map(Value::Num).unwrap_or_else(empty_value);
1527 let exitflag = Value::Num(outcome.exitflag as f64);
1528 let output = Value::Struct(build_output_struct(&outcome));
1529 let lambda = Value::Struct(build_lambda_struct(&outcome));
1530
1531 match crate::output_count::current_output_count() {
1532 None => x,
1533 Some(0) => Value::OutputList(Vec::new()),
1534 Some(1) => crate::output_count::output_list_with_padding(1, vec![x]),
1535 Some(2) => crate::output_count::output_list_with_padding(2, vec![x, fval]),
1536 Some(3) => crate::output_count::output_list_with_padding(3, vec![x, fval, exitflag]),
1537 Some(4) => {
1538 crate::output_count::output_list_with_padding(4, vec![x, fval, exitflag, output])
1539 }
1540 Some(n) => crate::output_count::output_list_with_padding(
1541 n,
1542 vec![x, fval, exitflag, output, lambda],
1543 ),
1544 }
1545}
1546
1547fn build_output_struct(outcome: &ConeOutcome) -> StructValue {
1548 let mut fields = StructValue::new();
1549 fields.insert("iterations", Value::Num(outcome.iterations as f64));
1550 fields.insert("algorithm", Value::from(ALGORITHM));
1551 fields.insert("constrviolation", Value::Num(outcome.constrviolation));
1552 fields.insert("message", Value::from(outcome.message.clone()));
1553 fields
1554}
1555
1556fn build_lambda_struct(outcome: &ConeOutcome) -> StructValue {
1557 let mut fields = StructValue::new();
1558 fields.insert(
1559 "ineqlin",
1560 vector_value_with_len(outcome.lambda.ineqlin.clone()),
1561 );
1562 fields.insert("eqlin", vector_value_with_len(outcome.lambda.eqlin.clone()));
1563 fields.insert("lower", vector_value_with_len(outcome.lambda.lower.clone()));
1564 fields.insert("upper", vector_value_with_len(outcome.lambda.upper.clone()));
1565 let soc = outcome
1566 .lambda
1567 .soc
1568 .iter()
1569 .map(|value| Value::Num(*value))
1570 .collect::<Vec<_>>();
1571 fields.insert(
1572 "soc",
1573 Value::Cell(
1574 CellArray::new(soc, outcome.lambda.soc.len().max(1), 1).unwrap_or_else(|_| {
1575 CellArray::new(Vec::new(), 0, 0).expect("empty lambda soc cell")
1576 }),
1577 ),
1578 );
1579 fields
1580}
1581
1582fn vector_value_with_len(values: Vec<f64>) -> Value {
1583 let n = values.len();
1584 Tensor::new(values, vec![n, 1])
1585 .map(Value::Tensor)
1586 .unwrap_or_else(|_| empty_value())
1587}
1588
1589fn matrix_value(matrix: &MatrixInput) -> BuiltinResult<Value> {
1590 Tensor::new(matrix.data.clone(), vec![matrix.rows, matrix.cols])
1591 .map(Value::Tensor)
1592 .map_err(|err| coneprog_error(&ERROR_INVALID_INPUT, err))
1593}
1594
1595fn dot(a: &[f64], b: &[f64]) -> f64 {
1596 a.iter().zip(b).map(|(x, y)| x * y).sum()
1597}
1598
1599fn norm(values: &[f64]) -> f64 {
1600 dot(values, values).sqrt()
1601}
1602
1603fn coneprog_error(error: &'static BuiltinErrorDescriptor, detail: impl AsRef<str>) -> RuntimeError {
1604 let detail = detail.as_ref();
1605 let message = if detail.starts_with("coneprog:") || detail.starts_with("secondordercone:") {
1606 detail.to_string()
1607 } else {
1608 format!("{}: {detail}", error.message)
1609 };
1610 let mut builder = build_runtime_error(message).with_builtin(CONEPROG);
1611 if let Some(identifier) = error.identifier {
1612 builder = builder.with_identifier(identifier);
1613 }
1614 builder.build()
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620 use futures::executor::block_on;
1621
1622 fn tensor(data: Vec<f64>, rows: usize, cols: usize) -> Value {
1623 Value::Tensor(Tensor::new(data, vec![rows, cols]).unwrap())
1624 }
1625
1626 fn empty() -> Value {
1627 Value::Tensor(Tensor::zeros(vec![0, 0]))
1628 }
1629
1630 fn run(args: Vec<Value>, outputs: usize) -> Vec<Value> {
1631 let _guard = crate::output_count::push_output_count(Some(outputs));
1632 let value = block_on(coneprog_builtin(args)).expect("coneprog");
1633 match value {
1634 Value::OutputList(values) => values,
1635 other => vec![other],
1636 }
1637 }
1638
1639 fn soc_unit_disk() -> Value {
1640 block_on(secondordercone_builtin(
1641 tensor(vec![1.0, 0.0, 0.0, 1.0], 2, 2),
1642 tensor(vec![0.0, 0.0], 2, 1),
1643 tensor(vec![0.0, 0.0], 2, 1),
1644 Value::Num(1.0),
1645 ))
1646 .unwrap()
1647 }
1648
1649 #[test]
1650 fn secondordercone_constructs_descriptor() {
1651 let Value::Struct(st) = soc_unit_disk() else {
1652 panic!("expected struct");
1653 };
1654 assert!(st.fields.contains_key("A"));
1655 assert!(st.fields.contains_key("b"));
1656 assert!(st.fields.contains_key("d"));
1657 assert_eq!(st.fields.get("Type"), Some(&Value::from("secondordercone")));
1658 }
1659
1660 #[test]
1661 fn coneprog_solves_soc_with_linear_bound() {
1662 let outputs = run(
1663 vec![
1664 tensor(vec![-1.0, 0.0], 2, 1),
1665 soc_unit_disk(),
1666 tensor(vec![1.0, 0.0], 1, 2),
1667 Value::Num(0.75),
1668 empty(),
1669 empty(),
1670 empty(),
1671 empty(),
1672 ],
1673 5,
1674 );
1675 let Value::Tensor(x) = &outputs[0] else {
1676 panic!("expected x");
1677 };
1678 assert!((x.data[0] - 0.75).abs() < 1.0e-3, "{x:?}");
1679 assert!(x.data[1].abs() < 1.0e-3, "{x:?}");
1680 assert!(matches!(&outputs[1], Value::Num(fval) if (*fval + 0.75).abs() < 1.0e-3));
1681 assert!(matches!(&outputs[2], Value::Num(flag) if *flag == 1.0));
1682 assert!(matches!(&outputs[3], Value::Struct(st) if st.fields.contains_key("algorithm")));
1683 let Value::Struct(lambda) = &outputs[4] else {
1684 panic!("expected lambda struct");
1685 };
1686 let Value::Tensor(ineqlin) = lambda.fields.get("ineqlin").unwrap() else {
1687 panic!("expected ineqlin multipliers");
1688 };
1689 assert!((ineqlin.data[0] - 1.0).abs() < 1.0e-2, "{ineqlin:?}");
1690 assert!(lambda.fields.contains_key("soc"));
1691 }
1692
1693 #[test]
1694 fn coneprog_accepts_problem_struct_and_options() {
1695 let mut options = StructValue::new();
1696 options.insert("Solver", Value::from("coneprog"));
1697 options.insert("TolFun", Value::Num(1.0e-8));
1698 options.insert("MaxIter", Value::Num(120.0));
1699
1700 let mut problem = StructValue::new();
1701 problem.insert("f", tensor(vec![0.0, 1.0], 2, 1));
1702 problem.insert("socConstraints", soc_unit_disk());
1703 problem.insert("Aineq", tensor(vec![0.0, -1.0], 1, 2));
1704 problem.insert("bineq", Value::Num(0.25));
1705 problem.insert("options", Value::Struct(options));
1706
1707 let outputs = run(vec![Value::Struct(problem)], 3);
1708 let Value::Tensor(x) = &outputs[0] else {
1709 panic!("expected x");
1710 };
1711 assert!((x.data[1] + 0.25).abs() < 1.0e-3, "{x:?}");
1712 assert!(matches!(&outputs[2], Value::Num(flag) if *flag == 1.0));
1713 }
1714
1715 #[test]
1716 fn coneprog_reports_infeasible_bounds() {
1717 let outputs = run(
1718 vec![
1719 tensor(vec![1.0, 0.0], 2, 1),
1720 empty(),
1721 empty(),
1722 empty(),
1723 empty(),
1724 empty(),
1725 tensor(vec![2.0, 0.0], 2, 1),
1726 tensor(vec![1.0, 1.0], 2, 1),
1727 ],
1728 4,
1729 );
1730 assert!(matches!(&outputs[0], Value::Tensor(t) if t.data.is_empty()));
1731 assert!(matches!(&outputs[2], Value::Num(flag) if *flag == -2.0));
1732 }
1733
1734 #[test]
1735 fn coneprog_validates_cone_dimensions() {
1736 let err = block_on(secondordercone_builtin(
1737 tensor(vec![1.0, 0.0], 1, 2),
1738 tensor(vec![0.0, 0.0], 2, 1),
1739 tensor(vec![0.0, 0.0], 2, 1),
1740 Value::Num(1.0),
1741 ))
1742 .unwrap_err();
1743 assert_eq!(err.identifier(), Some("RunMat:coneprog:InvalidInput"));
1744 }
1745}