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