1use crate::config::QMProgram;
45use configparser::ini::Ini;
46use log::{debug, info, warn};
47use serde::{Deserialize, Serialize};
48use std::fs;
49use std::path::{Path, PathBuf};
50use thiserror::Error;
51
52#[derive(Error, Debug)]
54pub enum ConfigError {
55 #[error("IO error: {0}")]
57 Io(#[from] std::io::Error),
58 #[error("INI parsing error: {0}")]
60 IniParse(String),
61 #[error("Invalid configuration value: {0}")]
63 InvalidValue(String),
64 #[error("Missing required section: {0}")]
66 MissingSection(String),
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
71pub struct Settings {
72 pub extensions: ExtensionSettings,
74 pub general: GeneralSettings,
76 pub logging: LoggingSettings,
78 pub cleanup: CleanupSettings,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ExtensionSettings {
85 pub gaussian: String,
87 pub orca: String,
89 pub xtb: String,
91 pub bagel: String,
93 pub custom: String,
95}
96
97impl Default for ExtensionSettings {
98 fn default() -> Self {
99 Self {
100 gaussian: "log".to_string(),
101 orca: "out".to_string(),
102 xtb: "out".to_string(),
103 bagel: "json".to_string(),
104 custom: "log".to_string(),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct GeneralSettings {
112 pub max_memory: String,
114 pub default_nprocs: u32,
116 pub print_level: u32,
119}
120
121impl Default for GeneralSettings {
122 fn default() -> Self {
123 Self {
124 max_memory: "4GB".to_string(),
125 default_nprocs: 4,
126 print_level: 1, }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct LoggingSettings {
134 pub level: String,
136 pub file_logging: bool,
139}
140
141impl Default for LoggingSettings {
142 fn default() -> Self {
143 Self {
144 level: "info".to_string(),
145 file_logging: false,
146 }
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct CleanupSettings {
153 pub enabled: bool,
155 pub preserve_extensions: Vec<String>,
159 pub verbose: u32,
162 pub cleanup_frequency: u32,
166}
167
168impl Default for CleanupSettings {
169 fn default() -> Self {
170 Self {
171 enabled: true,
172 preserve_extensions: Vec::new(),
173 verbose: 1,
174 cleanup_frequency: 5,
175 }
176 }
177}
178
179pub struct SettingsManager {
181 settings: Settings,
182 config_source: String,
183}
184
185impl SettingsManager {
186 pub fn load() -> Result<Self, ConfigError> {
209 let (settings, source) = Self::load_from_files()?;
210 info!("Configuration loaded from: {}", source);
211 Ok(Self {
212 settings,
213 config_source: source,
214 })
215 }
216
217 pub fn config_source(&self) -> &str {
219 &self.config_source
220 }
221
222 pub fn settings(&self) -> &Settings {
228 &self.settings
229 }
230
231 pub fn get_output_extension(&self, program: QMProgram) -> &str {
252 match program {
253 QMProgram::Gaussian => &self.settings.extensions.gaussian,
254 QMProgram::Orca => &self.settings.extensions.orca,
255 QMProgram::Xtb => &self.settings.extensions.xtb,
256 QMProgram::Bagel => &self.settings.extensions.bagel,
257 QMProgram::Custom => &self.settings.extensions.custom,
258 }
259 }
260
261 pub fn general(&self) -> &GeneralSettings {
263 &self.settings.general
264 }
265
266 pub fn general_mut(&mut self) -> &mut GeneralSettings {
268 &mut self.settings.general
269 }
270
271 pub fn logging(&self) -> &LoggingSettings {
273 &self.settings.logging
274 }
275
276 pub fn extensions(&self) -> &ExtensionSettings {
278 &self.settings.extensions
279 }
280
281 pub fn cleanup(&self) -> &CleanupSettings {
283 &self.settings.cleanup
284 }
285
286 fn load_from_files() -> Result<(Settings, String), ConfigError> {
288 let mut settings = Settings::default();
289 let mut config_source = "built-in defaults (no configuration file found)".to_string();
290
291 if let Some(system_path) = Self::get_system_config_path() {
293 if system_path.exists() {
294 match Self::load_config(&system_path) {
295 Ok(system_config) => {
296 settings.merge(system_config);
297 config_source = format!("system config ({})", system_path.display());
298 debug!(
299 "Loaded system configuration from: {}",
300 system_path.display()
301 );
302 }
303 Err(e) => {
304 warn!(
305 "Failed to load system config from {}: {}",
306 system_path.display(),
307 e
308 );
309 }
310 }
311 }
312 }
313
314 if let Some(user_path) = Self::get_user_config_path() {
316 if user_path.exists() {
317 match Self::load_config(&user_path) {
318 Ok(user_config) => {
319 settings.merge(user_config);
320 config_source = format!("user config ({})", user_path.display());
321 debug!("Loaded user configuration from: {}", user_path.display());
322 }
323 Err(e) => {
324 warn!(
325 "Failed to load user config from {}: {}",
326 user_path.display(),
327 e
328 );
329 }
330 }
331 }
332 }
333
334 let local_path = PathBuf::from("omecp_config.cfg");
336 if local_path.exists() {
337 match Self::load_config(&local_path) {
338 Ok(local_config) => {
339 settings.merge(local_config);
340 config_source = format!("local config ({})", local_path.display());
341 debug!("Loaded local configuration from: {}", local_path.display());
342 }
343 Err(e) => {
344 warn!(
345 "Failed to load local config from {}: {}",
346 local_path.display(),
347 e
348 );
349 }
350 }
351 }
352
353 Ok((settings, config_source))
354 }
355
356 fn load_config(path: &Path) -> Result<Settings, ConfigError> {
358 let content = fs::read_to_string(path)?;
359 let mut ini = Ini::new();
360 ini.read(content)
361 .map_err(|e| ConfigError::IniParse(format!("Failed to parse INI: {}", e)))?;
362
363 let mut settings = Settings::default();
364
365 if let Some(extensions_map) = ini.get_map_ref().get("extensions") {
367 settings.extensions = Self::parse_extensions(extensions_map)?;
368 }
369
370 if let Some(general_map) = ini.get_map_ref().get("general") {
372 settings.general = Self::parse_general(general_map)?;
373 }
374
375 if let Some(logging_map) = ini.get_map_ref().get("logging") {
377 settings.logging = Self::parse_logging(logging_map)?;
378 }
379
380 if let Some(cleanup_map) = ini.get_map_ref().get("cleanup") {
382 settings.cleanup = Self::parse_cleanup(cleanup_map)?;
383 }
384
385 Ok(settings)
386 }
387
388 fn parse_extensions(
390 section: &std::collections::HashMap<String, Option<String>>,
391 ) -> Result<ExtensionSettings, ConfigError> {
392 let mut extensions = ExtensionSettings::default();
393
394 if let Some(Some(gaussian)) = section.get("gaussian") {
395 extensions.gaussian = gaussian.clone();
396 }
397 if let Some(Some(orca)) = section.get("orca") {
398 extensions.orca = orca.clone();
399 }
400 if let Some(Some(xtb)) = section.get("xtb") {
401 extensions.xtb = xtb.clone();
402 }
403 if let Some(Some(bagel)) = section.get("bagel") {
404 extensions.bagel = bagel.clone();
405 }
406 if let Some(Some(custom)) = section.get("custom") {
407 extensions.custom = custom.clone();
408 }
409
410 Ok(extensions)
411 }
412
413 fn parse_general(
415 section: &std::collections::HashMap<String, Option<String>>,
416 ) -> Result<GeneralSettings, ConfigError> {
417 let mut general = GeneralSettings::default();
418
419 if let Some(Some(max_memory)) = section.get("max_memory") {
420 general.max_memory = max_memory.clone();
421 }
422 if let Some(Some(default_nprocs)) = section.get("default_nprocs") {
423 general.default_nprocs = default_nprocs.parse().map_err(|_| {
424 ConfigError::InvalidValue(format!("Invalid default_nprocs: {}", default_nprocs))
425 })?;
426 }
427 if let Some(Some(print_level)) = section.get("print_level") {
428 general.print_level = print_level.parse().map_err(|_| {
429 ConfigError::InvalidValue(format!("Invalid print_level: {}", print_level))
430 })?;
431 }
432
433 Ok(general)
434 }
435
436 fn parse_logging(
438 section: &std::collections::HashMap<String, Option<String>>,
439 ) -> Result<LoggingSettings, ConfigError> {
440 let mut logging = LoggingSettings::default();
441
442 if let Some(Some(level)) = section.get("level") {
443 logging.level = level.clone();
444 }
445 if let Some(Some(file_logging)) = section.get("file_logging") {
446 logging.file_logging = file_logging.parse().map_err(|_| {
447 ConfigError::InvalidValue(format!("Invalid file_logging value: {}", file_logging))
448 })?;
449 }
450
451 Ok(logging)
452 }
453
454 fn parse_cleanup(
456 section: &std::collections::HashMap<String, Option<String>>,
457 ) -> Result<CleanupSettings, ConfigError> {
458 let mut cleanup = CleanupSettings::default();
459
460 if let Some(Some(enabled)) = section.get("enabled") {
461 cleanup.enabled = enabled.parse().map_err(|_| {
462 ConfigError::InvalidValue(format!("Invalid enabled value: {}", enabled))
463 })?;
464 }
465
466 if let Some(Some(preserve_extensions)) = section.get("preserve_extensions") {
467 cleanup.preserve_extensions = preserve_extensions
469 .split(',')
470 .map(|s| s.trim().to_string())
471 .filter(|s| !s.is_empty())
472 .collect();
473 }
474
475 if let Some(Some(verbose)) = section.get("verbose") {
476 cleanup.verbose = verbose.parse().map_err(|_| {
477 ConfigError::InvalidValue(format!("Invalid verbose value: {}", verbose))
478 })?;
479 }
480
481 if let Some(Some(cleanup_frequency)) = section.get("cleanup_frequency") {
482 cleanup.cleanup_frequency = cleanup_frequency.parse().map_err(|_| {
483 ConfigError::InvalidValue(format!(
484 "Invalid cleanup_frequency value: {}",
485 cleanup_frequency
486 ))
487 })?;
488 }
489
490 Ok(cleanup)
491 }
492
493 fn get_system_config_path() -> Option<PathBuf> {
495 #[cfg(unix)]
496 {
497 Some(PathBuf::from("/etc/omecp/omecp_config.cfg"))
498 }
499 #[cfg(windows)]
500 {
501 std::env::var("PROGRAMDATA")
503 .ok()
504 .map(|pd| PathBuf::from(pd).join("omecp").join("omecp_config.cfg"))
505 }
506 }
507
508 fn get_user_config_path() -> Option<PathBuf> {
510 #[cfg(unix)]
511 {
512 std::env::var("HOME").ok().map(|home| {
513 PathBuf::from(home)
514 .join(".config")
515 .join("omecp")
516 .join("omecp_config.cfg")
517 })
518 }
519 #[cfg(windows)]
520 {
521 std::env::var("APPDATA").ok().map(|appdata| {
522 PathBuf::from(appdata)
523 .join("omecp")
524 .join("omecp_config.cfg")
525 })
526 }
527 }
528}
529
530impl SettingsManager {
531 pub fn create_template(path: &Path) -> Result<(), ConfigError> {
556 let template_content = Self::generate_template_content();
557 fs::write(path, template_content)?;
558 info!("Created settings template at: {}", path.display());
559 Ok(())
560 }
561
562 fn generate_template_content() -> String {
564 format!(
565 r#"# OpenMECP Configuration File
566#
567# This file allows you to customize OpenMECP behavior without modifying source code.
568# Configuration files are loaded in hierarchical order with local settings taking precedence:
569#
570# 1. Current working directory (./omecp_config.cfg) - highest priority
571# 2. User config directory (~/.config/omecp/omecp_config.cfg on Unix, %APPDATA%/omecp/omecp_config.cfg on Windows)
572# 3. System config directory (/etc/omecp/omecp_config.cfg on Unix, %PROGRAMDATA%/omecp/omecp_config.cfg on Windows)
573# 4. Built-in defaults (fallback)
574#
575# Any missing sections or values will use the built-in defaults shown below.
576
577[extensions]
578# Output file extensions for different quantum chemistry programs
579# These extensions are used when reading calculation output files
580
581# Gaussian output files (default: log)
582gaussian = {}
583
584# ORCA output files (default: out)
585orca = {}
586
587# XTB output files (default: out)
588xtb = {}
589
590# BAGEL output files (default: json)
591bagel = {}
592
593# Custom QM program output files (default: log)
594custom = {}
595
596[general]
597# General program settings
598
599# Maximum memory usage (default: 4GB)
600# Examples: 1GB, 2GB, 4GB, 8GB, 16GB, 32GB
601max_memory = {}
602
603# Default number of processors (default: 4)
604# Should match your system's CPU core count for optimal performance
605default_nprocs = {}
606
607# Print level for file operations and verbose output (default: 0)
608# 0 = quiet (minimal output, recommended for clean logs)
609# 1 = normal (standard output with key information)
610# 2 = verbose (show all file operations and detailed progress)
611print_level = {}
612
613[logging]
614# Logging configuration
615
616# Log level: debug, info, warn, error (default: info)
617# - debug: Detailed debugging information
618# - info: General information about program execution
619# - warn: Warning messages about potential issues
620# - error: Only error messages
621level = {}
622
623# Enable file-based logging (default: false)
624# When enabled, creates omecp_debug_<input_basename>.log with detailed debug info
625# Log messages go to file only (not console) for cleaner output
626# Useful for debugging and keeping detailed records of calculations
627file_logging = {}
628
629[cleanup]
630# Automatic file cleanup configuration
631# The cleanup system automatically removes temporary files after QM calculations
632# while preserving important output files (.out, .gbw, .engrad, etc.)
633
634# Enable or disable automatic cleanup (default: true)
635# Set to false to disable all automatic cleanup
636enabled = {}
637
638# Additional file extensions to preserve (comma-separated, optional)
639# These are extensions beyond the program's default output extension
640# Example: preserve_extensions = xyz,backup,important
641# Leave empty to only preserve program default extensions
642preserve_extensions = {}
643
644# Verbose logging for cleanup operations (default: 1)
645# 0 = quiet (minimal output)
646# 1 = normal (show cleanup summary)
647# 2 = verbose (show each file that is cleaned or preserved)
648verbose = {}
649
650# Perform cleanup every N optimization steps (default: 5)
651# Set to 0 to disable periodic cleanup during optimization
652# Example: 5 means cleanup after steps 5, 10, 15, etc.
653# This helps prevent file accumulation during long MECP optimization runs
654cleanup_frequency = {}
655
656# Example custom configurations:
657#
658# For a system with non-standard ORCA setup:
659# [extensions]
660# orca = out
661#
662# For high-memory calculations:
663# [general]
664# max_memory = 32GB
665# default_nprocs = 16
666#
667# For debugging calculations with file logging:
668# [logging]
669# level = debug
670# file_logging = true
671#
672# For custom XTB output format:
673# [extensions]
674# xtb = xyz
675#
676# For BAGEL with different output format:
677# [extensions]
678# bagel = out
679#
680# To disable automatic cleanup:
681# [cleanup]
682# enabled = false
683#
684# To preserve additional file types:
685# [cleanup]
686# preserve_extensions = xyz,backup
687"#,
688 ExtensionSettings::default().gaussian,
690 ExtensionSettings::default().orca,
691 ExtensionSettings::default().xtb,
692 ExtensionSettings::default().bagel,
693 ExtensionSettings::default().custom,
694 GeneralSettings::default().max_memory,
696 GeneralSettings::default().default_nprocs,
697 GeneralSettings::default().print_level,
698 LoggingSettings::default().level,
700 LoggingSettings::default().file_logging,
701 CleanupSettings::default().enabled,
703 CleanupSettings::default().preserve_extensions.join(","),
705 CleanupSettings::default().verbose,
706 CleanupSettings::default().cleanup_frequency,
707 )
708 }
709}
710
711impl Settings {
712 fn merge(&mut self, other: Settings) {
714 if !other.extensions.gaussian.is_empty() {
716 self.extensions.gaussian = other.extensions.gaussian;
717 }
718 if !other.extensions.orca.is_empty() {
719 self.extensions.orca = other.extensions.orca;
720 }
721 if !other.extensions.xtb.is_empty() {
722 self.extensions.xtb = other.extensions.xtb;
723 }
724 if !other.extensions.bagel.is_empty() {
725 self.extensions.bagel = other.extensions.bagel;
726 }
727 if !other.extensions.custom.is_empty() {
728 self.extensions.custom = other.extensions.custom;
729 }
730
731 if !other.general.max_memory.is_empty() {
733 self.general.max_memory = other.general.max_memory;
734 }
735 if other.general.default_nprocs > 0 {
736 self.general.default_nprocs = other.general.default_nprocs;
737 }
738 if other.general.print_level > 0 {
739 self.general.print_level = other.general.print_level;
740 }
741
742 if !other.logging.level.is_empty() {
744 self.logging.level = other.logging.level;
745 }
746 self.logging.file_logging = other.logging.file_logging;
747
748 self.cleanup.enabled = other.cleanup.enabled;
750 if !other.cleanup.preserve_extensions.is_empty() {
751 self.cleanup.preserve_extensions = other.cleanup.preserve_extensions.clone();
752 }
753 if other.cleanup.verbose > 0 {
754 self.cleanup.verbose = other.cleanup.verbose;
755 }
756 }
757}