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