1use crate::exception::{ExceptionKind, SolverException};
14use crate::reg_options::{DefaultValue, OptionType, RegisteredOptions};
15use crate::throw;
16use crate::types::{Index, Number};
17use std::collections::BTreeMap;
18use std::io::Read;
19use std::rc::Rc;
20
21#[derive(Debug, Clone)]
22struct OptionValue {
23 value: String,
24 counter: std::cell::Cell<Index>,
25 allow_clobber: bool,
26 dont_print: bool,
27}
28
29impl OptionValue {
30 fn new(value: String, allow_clobber: bool, dont_print: bool) -> Self {
31 Self {
32 value,
33 counter: std::cell::Cell::new(0),
34 allow_clobber,
35 dont_print,
36 }
37 }
38 fn get_value(&self) -> &str {
39 self.counter.set(self.counter.get() + 1);
40 &self.value
41 }
42}
43
44#[derive(Debug, Default, Clone)]
46pub struct OptionsList {
47 options: BTreeMap<String, OptionValue>,
48 reg_options: Option<Rc<RegisteredOptions>>,
49}
50
51impl OptionsList {
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn with_registered(reg: Rc<RegisteredOptions>) -> Self {
57 Self {
58 options: BTreeMap::new(),
59 reg_options: Some(reg),
60 }
61 }
62
63 pub fn set_registered_options(&mut self, reg: Rc<RegisteredOptions>) {
64 self.reg_options = Some(reg);
65 }
66
67 pub fn registered_options(&self) -> Option<Rc<RegisteredOptions>> {
68 self.reg_options.clone()
69 }
70
71 pub fn clear(&mut self) {
72 self.options.clear();
73 }
74
75 pub fn names(&self) -> impl Iterator<Item = &str> {
84 self.options.keys().map(String::as_str)
85 }
86
87 fn key(name: &str) -> String {
88 name.to_ascii_lowercase()
89 }
90
91 fn find_tag(&self, tag: &str, prefix: &str) -> Option<&OptionValue> {
94 if !prefix.is_empty() {
95 let key = Self::key(&format!("{prefix}{tag}"));
96 if let Some(v) = self.options.get(&key) {
97 return Some(v);
98 }
99 }
100 self.options.get(&Self::key(tag))
101 }
102
103 fn will_allow_clobber(&self, tag: &str) -> bool {
104 match self.options.get(&Self::key(tag)) {
105 Some(v) => v.allow_clobber,
106 None => true,
107 }
108 }
109
110 pub fn set_string_value(
112 &mut self,
113 tag: &str,
114 value: &str,
115 allow_clobber: bool,
116 dont_print: bool,
117 ) -> Result<bool, SolverException> {
118 if let Some(reg) = &self.reg_options {
119 let opt = reg.get_option(tag).ok_or_else(|| {
120 SolverException::new(
121 ExceptionKind::OPTION_INVALID,
122 format!("Unknown option \"{tag}\"."),
123 file!(),
124 line!() as Index,
125 )
126 })?;
127 if opt.option_type != OptionType::OT_String {
128 throw!(
129 ExceptionKind::OPTION_INVALID,
130 format!("Option \"{tag}\" is not a string option.")
131 );
132 }
133 if !opt.is_valid_string(value) {
134 throw!(
135 ExceptionKind::OPTION_INVALID,
136 format!("Invalid value \"{value}\" for string option \"{tag}\".")
137 );
138 }
139 }
140 if !self.will_allow_clobber(tag) {
141 return Ok(false);
142 }
143 let stored = value.to_ascii_lowercase();
144 self.options.insert(
145 Self::key(tag),
146 OptionValue::new(stored, allow_clobber, dont_print),
147 );
148 Ok(true)
149 }
150
151 pub fn set_numeric_value(
153 &mut self,
154 tag: &str,
155 value: Number,
156 allow_clobber: bool,
157 dont_print: bool,
158 ) -> Result<bool, SolverException> {
159 if let Some(reg) = &self.reg_options {
160 let opt = reg.get_option(tag).ok_or_else(|| {
161 SolverException::new(
162 ExceptionKind::OPTION_INVALID,
163 format!("Unknown option \"{tag}\"."),
164 file!(),
165 line!() as Index,
166 )
167 })?;
168 if opt.option_type != OptionType::OT_Number {
169 throw!(
170 ExceptionKind::OPTION_INVALID,
171 format!("Option \"{tag}\" is not a numeric option.")
172 );
173 }
174 if !opt.is_valid_number(value) {
175 throw!(
176 ExceptionKind::OPTION_INVALID,
177 format!("Numeric value {value} for option \"{tag}\" out of range.")
178 );
179 }
180 }
181 if !self.will_allow_clobber(tag) {
182 return Ok(false);
183 }
184 let s = format!("{value:.18e}");
186 self.options.insert(
187 Self::key(tag),
188 OptionValue::new(s, allow_clobber, dont_print),
189 );
190 Ok(true)
191 }
192
193 pub fn set_integer_value(
195 &mut self,
196 tag: &str,
197 value: Index,
198 allow_clobber: bool,
199 dont_print: bool,
200 ) -> Result<bool, SolverException> {
201 if let Some(reg) = &self.reg_options {
202 let opt = reg.get_option(tag).ok_or_else(|| {
203 SolverException::new(
204 ExceptionKind::OPTION_INVALID,
205 format!("Unknown option \"{tag}\"."),
206 file!(),
207 line!() as Index,
208 )
209 })?;
210 if opt.option_type != OptionType::OT_Integer {
211 throw!(
212 ExceptionKind::OPTION_INVALID,
213 format!("Option \"{tag}\" is not an integer option.")
214 );
215 }
216 if !opt.is_valid_integer(value) {
217 throw!(
218 ExceptionKind::OPTION_INVALID,
219 format!("Integer value {value} for option \"{tag}\" out of range.")
220 );
221 }
222 }
223 if !self.will_allow_clobber(tag) {
224 return Ok(false);
225 }
226 self.options.insert(
227 Self::key(tag),
228 OptionValue::new(value.to_string(), allow_clobber, dont_print),
229 );
230 Ok(true)
231 }
232
233 pub fn set_bool_value(
235 &mut self,
236 tag: &str,
237 value: bool,
238 allow_clobber: bool,
239 dont_print: bool,
240 ) -> Result<bool, SolverException> {
241 self.set_string_value(
242 tag,
243 if value { "yes" } else { "no" },
244 allow_clobber,
245 dont_print,
246 )
247 }
248
249 pub fn unset_value(&mut self, tag: &str) -> bool {
251 let key = Self::key(tag);
252 if let Some(v) = self.options.get(&key) {
253 if !v.allow_clobber {
254 return false;
255 }
256 self.options.remove(&key);
257 true
258 } else {
259 false
260 }
261 }
262
263 pub fn get_string_value(
266 &self,
267 tag: &str,
268 prefix: &str,
269 ) -> Result<(String, bool), SolverException> {
270 if let Some(v) = self.find_tag(tag, prefix) {
271 return Ok((v.get_value().to_string(), true));
272 }
273 if let Some(reg) = &self.reg_options {
274 if let Some(opt) = reg.get_option(tag) {
275 if let DefaultValue::String(d) = &opt.default {
276 return Ok((d.clone(), false));
277 }
278 throw!(
279 ExceptionKind::OPTION_INVALID,
280 format!("Option \"{tag}\" is not a string option.")
281 );
282 }
283 }
284 Ok((String::new(), false))
285 }
286
287 pub fn get_numeric_value(
289 &self,
290 tag: &str,
291 prefix: &str,
292 ) -> Result<(Number, bool), SolverException> {
293 if let Some(v) = self.find_tag(tag, prefix) {
294 let s = v.get_value().to_string();
295 let parsed = parse_ipopt_number(&s).ok_or_else(|| {
296 SolverException::new(
297 ExceptionKind::OPTION_INVALID,
298 format!("Option \"{tag}\": cannot parse value \"{s}\" as Number."),
299 file!(),
300 line!() as Index,
301 )
302 })?;
303 return Ok((parsed, true));
304 }
305 if let Some(reg) = &self.reg_options {
306 if let Some(opt) = reg.get_option(tag) {
307 if let DefaultValue::Number(d) = &opt.default {
308 return Ok((*d, false));
309 }
310 throw!(
311 ExceptionKind::OPTION_INVALID,
312 format!("Option \"{tag}\" is not a numeric option.")
313 );
314 }
315 }
316 Ok((0.0, false))
317 }
318
319 pub fn get_integer_value(
321 &self,
322 tag: &str,
323 prefix: &str,
324 ) -> Result<(Index, bool), SolverException> {
325 if let Some(v) = self.find_tag(tag, prefix) {
326 let s = v.get_value().to_string();
327 let parsed: Index = s.trim().parse().map_err(|_| {
328 SolverException::new(
329 ExceptionKind::OPTION_INVALID,
330 format!("Option \"{tag}\": cannot parse value \"{s}\" as Integer."),
331 file!(),
332 line!() as Index,
333 )
334 })?;
335 return Ok((parsed, true));
336 }
337 if let Some(reg) = &self.reg_options {
338 if let Some(opt) = reg.get_option(tag) {
339 if let DefaultValue::Integer(d) = &opt.default {
340 return Ok((*d, false));
341 }
342 throw!(
343 ExceptionKind::OPTION_INVALID,
344 format!("Option \"{tag}\" is not an integer option.")
345 );
346 }
347 }
348 Ok((0, false))
349 }
350
351 pub fn get_bool_value(&self, tag: &str, prefix: &str) -> Result<(bool, bool), SolverException> {
353 let (s, found) = self.get_string_value(tag, prefix)?;
354 let v = match s.to_ascii_lowercase().as_str() {
355 "yes" => true,
356 "no" => false,
357 other => throw!(
358 ExceptionKind::OPTION_INVALID,
359 format!("Option \"{tag}\" has non-boolean value \"{other}\".")
360 ),
361 };
362 Ok((v, found))
363 }
364
365 pub fn get_enum_value(
368 &self,
369 tag: &str,
370 prefix: &str,
371 ) -> Result<(Index, bool), SolverException> {
372 let (s, found) = self.get_string_value(tag, prefix)?;
373 let reg = self.reg_options.as_ref().ok_or_else(|| {
374 SolverException::new(
375 ExceptionKind::OPTION_INVALID,
376 "GetEnumValue requires a RegisteredOptions registry.".to_string(),
377 file!(),
378 line!() as Index,
379 )
380 })?;
381 let opt = reg.get_option(tag).ok_or_else(|| {
382 SolverException::new(
383 ExceptionKind::OPTION_INVALID,
384 format!("Unknown option \"{tag}\"."),
385 file!(),
386 line!() as Index,
387 )
388 })?;
389 let idx = opt.map_string_to_enum(&s).ok_or_else(|| {
390 SolverException::new(
391 ExceptionKind::ERROR_CONVERTING_STRING_TO_ENUM,
392 format!("Cannot map \"{s}\" to enum for option \"{tag}\"."),
393 file!(),
394 line!() as Index,
395 )
396 })?;
397 Ok((idx, found))
398 }
399
400 pub fn read_from_stream<R: Read>(
404 &mut self,
405 mut r: R,
406 allow_clobber: bool,
407 ) -> Result<(), SolverException> {
408 let mut s = String::new();
409 r.read_to_string(&mut s).map_err(|e| {
410 SolverException::new(
411 ExceptionKind::OPTION_INVALID,
412 format!("I/O error reading options: {e}"),
413 file!(),
414 line!() as Index,
415 )
416 })?;
417 self.read_from_str(&s, allow_clobber)
418 }
419
420 pub fn read_from_str(&mut self, s: &str, allow_clobber: bool) -> Result<(), SolverException> {
421 let mut tokens = Tokenizer::new(s);
422 loop {
423 let Some(tag) = tokens.next_token()? else {
424 return Ok(());
425 };
426 let Some(value) = tokens.next_token()? else {
427 throw!(
428 ExceptionKind::OPTION_INVALID,
429 format!("Error reading value for tag {tag} from option file.")
430 );
431 };
432 self.set_from_text(&tag, &value, allow_clobber)?;
433 }
434 }
435
436 fn set_from_text(
437 &mut self,
438 tag: &str,
439 value: &str,
440 allow_clobber: bool,
441 ) -> Result<(), SolverException> {
442 if let Some(reg) = self.reg_options.clone() {
443 let opt = reg.get_option(tag).ok_or_else(|| SolverException::new(
444 ExceptionKind::OPTION_INVALID,
445 format!("Read Option: \"{tag}\". It is not a valid option. Check the list of available options."),
446 file!(), line!() as Index,
447 ))?;
448 match opt.option_type {
449 OptionType::OT_String => {
450 let ok = self.set_string_value(tag, value, allow_clobber, false)?;
451 if !ok {
452 throw!(
453 ExceptionKind::OPTION_INVALID,
454 "Error setting string value read from option file.".to_string()
455 );
456 }
457 }
458 OptionType::OT_Number => {
459 let v = parse_ipopt_number(value).ok_or_else(|| SolverException::new(
460 ExceptionKind::OPTION_INVALID,
461 format!("Option \"{tag}\": Double value expected, but non-numeric option value \"{value}\" found.\n"),
462 file!(), line!() as Index,
463 ))?;
464 let ok = self.set_numeric_value(tag, v, allow_clobber, false)?;
465 if !ok {
466 throw!(
467 ExceptionKind::OPTION_INVALID,
468 "Error setting numeric value read from file.".to_string()
469 );
470 }
471 }
472 OptionType::OT_Integer => {
473 let v: Index = value.parse().map_err(|_| SolverException::new(
474 ExceptionKind::OPTION_INVALID,
475 format!("Option \"{tag}\": Integer value expected, but non-integer option value \"{value}\" found.\n"),
476 file!(), line!() as Index,
477 ))?;
478 let ok = self.set_integer_value(tag, v, allow_clobber, false)?;
479 if !ok {
480 throw!(
481 ExceptionKind::OPTION_INVALID,
482 "Error setting integer value read from option file.".to_string()
483 );
484 }
485 }
486 OptionType::OT_Unknown => {
487 throw!(
488 ExceptionKind::OPTION_INVALID,
489 format!("Option \"{tag}\" has unknown type.")
490 );
491 }
492 }
493 } else {
494 self.set_string_value(tag, value, allow_clobber, false)?;
495 }
496 Ok(())
497 }
498
499 pub fn print_list(&self) -> String {
501 let mut out = String::new();
502 out.push_str(" Name Value # times used\n");
503 for (k, v) in &self.options {
504 out.push_str(&format!(
505 "{:>40} = {:<30} # {}\n",
506 k,
507 v.value,
508 v.counter.get()
509 ));
510 }
511 out
512 }
513
514 pub fn print_user_options(&self) -> String {
516 let mut out = String::new();
517 for (k, v) in &self.options {
518 if v.dont_print {
519 continue;
520 }
521 let used = if v.counter.get() > 0 {
522 "used"
523 } else {
524 "notused"
525 };
526 out.push_str(&format!("{} {} ({})\n", k, v.value, used));
527 }
528 out
529 }
530}
531
532fn parse_ipopt_number(s: &str) -> Option<Number> {
535 let mut buf = String::with_capacity(s.len());
536 for c in s.chars() {
537 if c == 'd' || c == 'D' {
538 buf.push('e');
539 } else {
540 buf.push(c);
541 }
542 }
543 buf.trim().parse().ok()
544}
545
546struct Tokenizer<'a> {
550 chars: std::str::Chars<'a>,
551 peeked: Option<char>,
552}
553
554impl<'a> Tokenizer<'a> {
555 fn new(s: &'a str) -> Self {
556 Self {
557 chars: s.chars(),
558 peeked: None,
559 }
560 }
561
562 fn next_char(&mut self) -> Option<char> {
563 self.peeked.take().or_else(|| self.chars.next())
564 }
565
566 fn next_token(&mut self) -> Result<Option<String>, SolverException> {
567 let mut c = match self.next_char() {
568 Some(c) => c,
569 None => return Ok(None),
570 };
571 loop {
572 if c.is_whitespace() { } else if c == '#' {
574 loop {
576 match self.next_char() {
577 Some('\n') | None => break,
578 _ => {}
579 }
580 }
581 } else {
582 break;
583 }
584 c = match self.next_char() {
585 Some(c) => c,
586 None => return Ok(None),
587 };
588 }
589 let inside_quotes = c == '"';
590 let mut tok = String::new();
591 if inside_quotes {
592 c = match self.next_char() {
593 Some(c) => c,
594 None => throw!(
595 ExceptionKind::OPTION_INVALID,
596 "Unterminated quoted string in option file.".to_string()
597 ),
598 };
599 }
600 loop {
601 if !inside_quotes && c.is_whitespace() {
602 return Ok(Some(tok));
603 }
604 if inside_quotes && c == '"' {
605 return Ok(Some(tok));
606 }
607 tok.push(c);
608 c = match self.next_char() {
609 Some(c) => c,
610 None => {
611 if inside_quotes {
612 throw!(
613 ExceptionKind::OPTION_INVALID,
614 "Unterminated quoted string in option file.".to_string()
615 );
616 }
617 return Ok(Some(tok));
618 }
619 };
620 }
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 fn registry_with_basic() -> Rc<RegisteredOptions> {
629 let r = RegisteredOptions::new();
630 r.set_registering_category("Test");
631 r.add_lower_bounded_number_option("tol", "Convergence tolerance", 0.0, true, 1e-8, "")
632 .unwrap();
633 r.add_string_option(
634 "linear_solver",
635 "Linear solver",
636 "mumps",
637 &[("mumps", ""), ("feral", "")],
638 "",
639 )
640 .unwrap();
641 r.add_lower_bounded_integer_option("max_iter", "Maximum iterations", 0, 3000, "")
642 .unwrap();
643 r.add_bool_option("print_user_options", "", false, "")
644 .unwrap();
645 r
646 }
647
648 #[test]
649 fn prefix_lookup_overrides() {
650 let reg = registry_with_basic();
651 let mut o = OptionsList::with_registered(reg);
652 o.set_numeric_value("tol", 1e-6, true, false).unwrap();
653 o.set_numeric_value("resto.tol", 1e-3, true, false).unwrap();
654 let (v_main, _) = o.get_numeric_value("tol", "").unwrap();
655 let (v_resto, _) = o.get_numeric_value("tol", "resto.").unwrap();
656 let (v_other, _) = o.get_numeric_value("tol", "noprefix.").unwrap();
657 assert!((v_main - 1e-6).abs() < 1e-20);
658 assert!((v_resto - 1e-3).abs() < 1e-20);
659 assert!((v_other - 1e-6).abs() < 1e-20);
660 }
661
662 #[test]
663 fn defaults_returned_when_unset() {
664 let reg = registry_with_basic();
665 let o = OptionsList::with_registered(reg);
666 let (v, found) = o.get_numeric_value("tol", "").unwrap();
667 assert!((v - 1e-8).abs() < 1e-20);
668 assert!(!found);
669 }
670
671 #[test]
672 fn read_options_file_text() {
673 let reg = registry_with_basic();
674 let mut o = OptionsList::with_registered(reg);
675 let opt_file = "
676# A comment line
677tol 1.0e-7
678max_iter 500
679linear_solver mumps
680print_user_options yes
681";
682 o.read_from_str(opt_file, false).unwrap();
683 assert_eq!(o.get_numeric_value("tol", "").unwrap().0, 1e-7);
684 assert_eq!(o.get_integer_value("max_iter", "").unwrap().0, 500);
685 assert_eq!(o.get_string_value("linear_solver", "").unwrap().0, "mumps");
686 assert!(o.get_bool_value("print_user_options", "").unwrap().0);
687 }
688
689 #[test]
690 fn fortran_d_exponent_accepted() {
691 let reg = registry_with_basic();
692 let mut o = OptionsList::with_registered(reg);
693 o.read_from_str("tol 1.0d-9\n", false).unwrap();
694 assert!((o.get_numeric_value("tol", "").unwrap().0 - 1e-9).abs() < 1e-30);
695 }
696
697 #[test]
698 fn unknown_option_in_file_is_error() {
699 let reg = registry_with_basic();
700 let mut o = OptionsList::with_registered(reg);
701 let err = o.read_from_str("nonsense_option 1.0\n", false).unwrap_err();
702 assert_eq!(err.kind, ExceptionKind::OPTION_INVALID);
703 }
704
705 #[test]
706 fn invalid_string_value_rejected() {
707 let reg = registry_with_basic();
708 let mut o = OptionsList::with_registered(reg);
709 let err = o
710 .set_string_value("linear_solver", "ma27", true, false)
711 .unwrap_err();
712 assert_eq!(err.kind, ExceptionKind::OPTION_INVALID);
713 }
714
715 #[test]
716 fn out_of_range_number_rejected() {
717 let reg = registry_with_basic();
718 let mut o = OptionsList::with_registered(reg);
719 let err = o.set_numeric_value("tol", 0.0, true, false).unwrap_err();
720 assert_eq!(err.kind, ExceptionKind::OPTION_INVALID);
721 }
722
723 #[test]
724 fn enum_value_index() {
725 let reg = registry_with_basic();
726 let mut o = OptionsList::with_registered(reg);
727 o.set_string_value("linear_solver", "feral", true, false)
728 .unwrap();
729 assert_eq!(o.get_enum_value("linear_solver", "").unwrap().0, 1);
730 }
731
732 #[test]
733 fn get_value_increments_use_counter() {
734 let reg = registry_with_basic();
735 let mut o = OptionsList::with_registered(reg);
736 o.set_numeric_value("tol", 1e-6, true, false).unwrap();
737 let _ = o.get_numeric_value("tol", "").unwrap();
738 let _ = o.get_numeric_value("tol", "").unwrap();
739 let listing = o.print_list();
740 assert!(listing.contains("# 2"));
741 }
742}