Skip to main content

qualia_core_db/specialized_libs/chemistry_modeling/
properties.rs

1use super::*;
2
3/// Property predictor for molecular property prediction
4pub struct PropertyPredictor {
5    property_models: HashMap<String, PropertyModel>,
6    descriptor_calculator: DescriptorCalculator,
7    machine_learning_models: HashMap<String, MLModel>,
8}
9
10/// Property models
11#[derive(Debug, Clone)]
12pub struct PropertyModel {
13    pub model_id: String,
14    pub property_type: PropertyType,
15    pub model_type: PropertyModelType,
16    pub parameters: PropertyModelParameters,
17}
18
19/// Property types
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum PropertyType {
22    BoilingPoint,
23    MeltingPoint,
24    Density,
25    Viscosity,
26    SurfaceTension,
27    HeatCapacity,
28    ThermalConductivity,
29    ElectricalConductivity,
30    OpticalProperties,
31    MagneticProperties,
32}
33
34/// Property model types
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub enum PropertyModelType {
37    GroupContribution,
38    QSPR,
39    MachineLearning,
40    MolecularDynamics,
41    QuantumMechanical,
42}
43
44/// Property model parameters
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PropertyModelParameters {
47    pub coefficients: HashMap<String, f64>,
48    pub descriptors: Vec<String>,
49    pub reference_data: Vec<ReferenceData>,
50}
51
52/// Reference data
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ReferenceData {
55    pub molecule_id: String,
56    pub property_value: f64,
57    pub conditions: ReferenceConditions,
58}
59
60/// Reference conditions
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ReferenceConditions {
63    pub temperature: f64,
64    pub pressure: f64,
65    pub phase: PhaseType,
66}
67
68/// Descriptor calculator
69pub struct DescriptorCalculator {
70    molecular_descriptors: MolecularDescriptors,
71    quantum_descriptors: QuantumDescriptors,
72    topological_descriptors: TopologicalDescriptors,
73}
74
75/// Molecular descriptors
76#[derive(Debug, Clone)]
77pub struct MolecularDescriptors {
78    pub molecular_weight: f64,
79    pub formula: String,
80    pub atom_count: HashMap<String, usize>,
81    pub bond_count: HashMap<String, usize>,
82    pub ring_count: usize,
83}
84
85/// Quantum descriptors
86#[derive(Debug, Clone)]
87pub struct QuantumDescriptors {
88    pub homo_energy: f64,
89    pub lumo_energy: f64,
90    pub gap: f64,
91    pub dipole_moment: f64,
92    pub polarizability: f64,
93}
94
95/// Topological descriptors
96#[derive(Debug, Clone)]
97pub struct TopologicalDescriptors {
98    pub connectivity_index: f64,
99    pub shape_index: f64,
100    pub wiener_index: f64,
101    pub randic_index: f64,
102}
103
104/// Machine learning models
105#[derive(Debug, Clone)]
106pub struct MLModel {
107    pub model_id: String,
108    pub model_type: MLModelType,
109    pub model_parameters: MLModelParameters,
110    pub training_data: TrainingData,
111}
112
113/// ML model types
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub enum MLModelType {
116    LinearRegression,
117    RandomForest,
118    NeuralNetwork,
119    SupportVector,
120    GaussianProcess,
121}
122
123/// ML model parameters
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct MLModelParameters {
126    pub hyperparameters: HashMap<String, f64>,
127    pub feature_importance: HashMap<String, f64>,
128    pub model_performance: ModelPerformance,
129}
130
131/// Model performance
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ModelPerformance {
134    pub r_squared: f64,
135    pub rmse: f64,
136    pub mae: f64,
137    pub cross_validation_score: f64,
138}
139
140/// Training data
141#[derive(Debug, Clone)]
142pub struct TrainingData {
143    pub data_id: String,
144    pub features: Vec<Vec<f64>>,
145    pub targets: Vec<f64>,
146    pub data_size: usize,
147}
148
149impl PropertyPredictor {
150    pub fn new() -> Self {
151        Self {
152            property_models: HashMap::new(),
153            descriptor_calculator: DescriptorCalculator::new(),
154            machine_learning_models: HashMap::new(),
155        }
156    }
157
158    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
159        self.descriptor_calculator.initialize()?;
160        // Register basic QSPR / group-contribution models for common properties.
161        // Each model stores its coefficients in `parameters.coefficients`: the
162        // special key `"intercept"` is added directly, every other key names a
163        // molecular descriptor whose value is multiplied by its coefficient
164        // (group-contribution form: predicted = Σ c_i·d_i + intercept).
165        self.register_standard_qspr_models();
166        Ok(())
167    }
168
169    /// Register the built-in QSPR property models.
170    fn register_standard_qspr_models(&mut self) {
171        // Boiling point — simple Joback-style group contribution:
172        //   Tb = 198.2 + Σ(group contributions)
173        self.register_model(
174            "boiling_point",
175            PropertyModel {
176                model_id: "qspr_boiling_point".to_string(),
177                property_type: PropertyType::BoilingPoint,
178                model_type: PropertyModelType::GroupContribution,
179                parameters: PropertyModelParameters {
180                    coefficients: {
181                        let mut c = HashMap::new();
182                        c.insert("intercept".to_string(), 198.2);
183                        c.insert("C".to_string(), 23.97);
184                        c.insert("H".to_string(), 22.88);
185                        c.insert("O".to_string(), 10.0);
186                        c.insert("N".to_string(), 5.0);
187                        c.insert("ring".to_string(), -50.0);
188                        c
189                    },
190                    descriptors: vec![
191                        "C".to_string(),
192                        "H".to_string(),
193                        "O".to_string(),
194                        "N".to_string(),
195                        "ring".to_string(),
196                    ],
197                    reference_data: Vec::new(),
198                },
199            },
200        );
201
202        // Melting point — Joback-style group contribution:
203        //   Tm = 122.5 + Σ(group contributions)
204        self.register_model(
205            "melting_point",
206            PropertyModel {
207                model_id: "qspr_melting_point".to_string(),
208                property_type: PropertyType::MeltingPoint,
209                model_type: PropertyModelType::GroupContribution,
210                parameters: PropertyModelParameters {
211                    coefficients: {
212                        let mut c = HashMap::new();
213                        c.insert("intercept".to_string(), 122.5);
214                        c.insert("C".to_string(), -5.51);
215                        c.insert("H".to_string(), 8.45);
216                        c.insert("O".to_string(), 4.0);
217                        c.insert("N".to_string(), 2.5);
218                        c.insert("ring".to_string(), -20.0);
219                        c
220                    },
221                    descriptors: vec![
222                        "C".to_string(),
223                        "H".to_string(),
224                        "O".to_string(),
225                        "N".to_string(),
226                        "ring".to_string(),
227                    ],
228                    reference_data: Vec::new(),
229                },
230            },
231        );
232
233        // Solubility — general solubility equation approximation:
234        //   logS = -0.5·logP - 0.01·MW + 0.5
235        self.register_model(
236            "solubility",
237            PropertyModel {
238                model_id: "qspr_solubility".to_string(),
239                property_type: PropertyType::Density, // no dedicated Solubility variant; closest physical property
240                model_type: PropertyModelType::GroupContribution,
241                parameters: PropertyModelParameters {
242                    coefficients: {
243                        let mut c = HashMap::new();
244                        c.insert("intercept".to_string(), 0.5);
245                        c.insert("logP".to_string(), -0.5);
246                        c.insert("molecular_weight".to_string(), -0.01);
247                        c
248                    },
249                    descriptors: vec!["logP".to_string(), "molecular_weight".to_string()],
250                    reference_data: Vec::new(),
251                },
252            },
253        );
254
255        // Molecular weight — atom-count model:
256        //   MW = Σ(atom_count · atomic_weight)
257        self.register_model(
258            "molecular_weight",
259            PropertyModel {
260                model_id: "qspr_molecular_weight".to_string(),
261                property_type: PropertyType::Density, // no dedicated MolecularWeight variant
262                model_type: PropertyModelType::GroupContribution,
263                parameters: PropertyModelParameters {
264                    coefficients: {
265                        let mut c = HashMap::new();
266                        c.insert("intercept".to_string(), 0.0);
267                        c.insert("C".to_string(), 12.011);
268                        c.insert("H".to_string(), 1.008);
269                        c.insert("O".to_string(), 15.999);
270                        c.insert("N".to_string(), 14.007);
271                        c.insert("S".to_string(), 32.06);
272                        c.insert("Cl".to_string(), 35.45);
273                        c
274                    },
275                    descriptors: vec![
276                        "C".to_string(),
277                        "H".to_string(),
278                        "O".to_string(),
279                        "N".to_string(),
280                        "S".to_string(),
281                        "Cl".to_string(),
282                    ],
283                    reference_data: Vec::new(),
284                },
285            },
286        );
287    }
288
289    pub fn validate_molecule(&self, molecule: &Molecule) -> Result<(), ChemistryError> {
290        if molecule.atoms.is_empty() {
291            return Err(ChemistryError::ValidationError(
292                "Molecule must have at least one atom".to_string(),
293            ));
294        }
295        Ok(())
296    }
297
298    /// Predict a molecular property using the registered QSPR models.
299    ///
300    /// Applies the group-contribution formula
301    /// `predicted = Σ(coefficient_i · descriptor_i) + intercept`, where the
302    /// special coefficient key `"intercept"` is added directly and every other
303    /// key names a descriptor in `molecular_descriptors` (missing descriptors
304    /// contribute zero). Returns [`ChemistryError::NotImplemented`] when no
305    /// model is registered for `property_name`.
306    pub fn predict(
307        &self,
308        property_name: &str,
309        molecular_descriptors: &HashMap<String, f64>,
310    ) -> Result<f64, ChemistryError> {
311        let model = self.property_models.get(property_name).ok_or_else(|| {
312            ChemistryError::NotImplemented(format!(
313                "no QSPR model registered for property '{}'",
314                property_name
315            ))
316        })?;
317
318        let mut predicted = 0.0;
319        for (descriptor, coefficient) in &model.parameters.coefficients {
320            if descriptor == "intercept" {
321                predicted += coefficient;
322            } else {
323                let value = molecular_descriptors
324                    .get(descriptor)
325                    .copied()
326                    .unwrap_or(0.0);
327                predicted += coefficient * value;
328            }
329        }
330        Ok(predicted)
331    }
332
333    /// Predict properties for a concrete molecule. Computes molecular
334    /// descriptors (molecular weight, per-element atom counts, logP defaulting
335    /// to 0.0 when unknown) from the molecule and dispatches to
336    /// [`predict`](Self::predict) for each requested property type that has a
337    /// registered model. Returns `NotImplemented` if none of the requested
338    /// property types have a model.
339    pub fn predict_from_molecule(
340        &self,
341        molecule: &Molecule,
342        properties: &[PropertyType],
343    ) -> Result<PredictedProperties, ChemistryError> {
344        // Compute molecular descriptors from the molecule.
345        let mut descriptors: HashMap<String, f64> = HashMap::new();
346        let mut molecular_weight = 0.0;
347        let mut atom_counts: HashMap<String, f64> = HashMap::new();
348        for atom in &molecule.atoms {
349            molecular_weight += atom.mass;
350            *atom_counts.entry(atom.element.clone()).or_insert(0.0) += 1.0;
351        }
352        descriptors.insert("molecular_weight".to_string(), molecular_weight);
353        for (element, count) in &atom_counts {
354            descriptors.insert(element.clone(), *count);
355        }
356        // logP is not derivable from the atom list alone here; default to 0.0
357        // (unknown) so the solubility model degrades gracefully.
358        descriptors.insert("logP".to_string(), 0.0);
359
360        let mut result = PredictedProperties::new();
361        for property_type in properties {
362            let name = match property_type {
363                PropertyType::BoilingPoint => "boiling_point",
364                PropertyType::MeltingPoint => "melting_point",
365                // No registered QSPR model for the remaining property types.
366                _ => continue,
367            };
368            match self.predict(name, &descriptors) {
369                Ok(value) => {
370                    result.properties.insert(name.to_string(), value);
371                }
372                Err(ChemistryError::NotImplemented(_)) => continue,
373                Err(e) => return Err(e),
374            }
375        }
376
377        if result.properties.is_empty() {
378            return Err(ChemistryError::NotImplemented(
379                "no QSPR models available for the requested property types".to_string(),
380            ));
381        }
382        Ok(result)
383    }
384
385    /// Register a custom property model under `name`, replacing any existing
386    /// entry.
387    pub fn register_model(&mut self, name: &str, model: PropertyModel) {
388        self.property_models.insert(name.to_string(), model);
389    }
390
391    /// List the names of all registered property models.
392    pub fn list_properties(&self) -> Vec<String> {
393        self.property_models.keys().cloned().collect()
394    }
395
396    /// Register a machine-learning model under its `model_id`, replacing any
397    /// existing entry.
398    pub fn register_ml_model(&mut self, model: MLModel) {
399        self.machine_learning_models
400            .insert(model.model_id.clone(), model);
401    }
402
403    /// Look up a machine-learning model by id.
404    pub fn get_ml_model(&self, model_id: &str) -> Option<&MLModel> {
405        self.machine_learning_models.get(model_id)
406    }
407
408    /// Mutably borrow a machine-learning model by id.
409    pub fn get_ml_model_mut(&mut self, model_id: &str) -> Option<&mut MLModel> {
410        self.machine_learning_models.get_mut(model_id)
411    }
412
413    /// List the ids of all registered machine-learning models.
414    pub fn list_ml_models(&self) -> Vec<String> {
415        self.machine_learning_models.keys().cloned().collect()
416    }
417
418    /// Remove a machine-learning model by id.
419    pub fn remove_ml_model(&mut self, model_id: &str) -> Option<MLModel> {
420        self.machine_learning_models.remove(model_id)
421    }
422}
423
424impl PropertyModel {
425    pub fn new() -> Self {
426        Self {
427            model_id: "model_1".to_string(),
428            property_type: PropertyType::BoilingPoint,
429            model_type: PropertyModelType::GroupContribution,
430            parameters: PropertyModelParameters::new(),
431        }
432    }
433}
434
435impl PropertyModelParameters {
436    pub fn new() -> Self {
437        Self {
438            coefficients: HashMap::new(),
439            descriptors: vec!["molecular_weight".to_string()],
440            reference_data: vec![ReferenceData::new()],
441        }
442    }
443}
444
445impl ReferenceData {
446    pub fn new() -> Self {
447        Self {
448            molecule_id: "mol_1".to_string(),
449            property_value: 100.0,
450            conditions: ReferenceConditions::new(),
451        }
452    }
453}
454
455impl ReferenceConditions {
456    pub fn new() -> Self {
457        Self {
458            temperature: 298.15,
459            pressure: 1.0,
460            phase: PhaseType::Liquid,
461        }
462    }
463}
464
465impl DescriptorCalculator {
466    pub fn new() -> Self {
467        Self {
468            molecular_descriptors: MolecularDescriptors::new(),
469            quantum_descriptors: QuantumDescriptors::new(),
470            topological_descriptors: TopologicalDescriptors::new(),
471        }
472    }
473
474    /// Borrow the molecular descriptors.
475    pub fn molecular_descriptors(&self) -> &MolecularDescriptors {
476        &self.molecular_descriptors
477    }
478
479    /// Mutably borrow the molecular descriptors.
480    pub fn molecular_descriptors_mut(&mut self) -> &mut MolecularDescriptors {
481        &mut self.molecular_descriptors
482    }
483
484    /// Borrow the quantum descriptors.
485    pub fn quantum_descriptors(&self) -> &QuantumDescriptors {
486        &self.quantum_descriptors
487    }
488
489    /// Mutably borrow the quantum descriptors.
490    pub fn quantum_descriptors_mut(&mut self) -> &mut QuantumDescriptors {
491        &mut self.quantum_descriptors
492    }
493
494    /// Borrow the topological descriptors.
495    pub fn topological_descriptors(&self) -> &TopologicalDescriptors {
496        &self.topological_descriptors
497    }
498
499    /// Mutably borrow the topological descriptors.
500    pub fn topological_descriptors_mut(&mut self) -> &mut TopologicalDescriptors {
501        &mut self.topological_descriptors
502    }
503
504    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
505        Ok(())
506    }
507}
508
509impl MolecularDescriptors {
510    pub fn new() -> Self {
511        Self {
512            molecular_weight: 16.04,
513            formula: "CH4".to_string(),
514            atom_count: HashMap::new(),
515            bond_count: HashMap::new(),
516            ring_count: 0,
517        }
518    }
519}
520
521impl QuantumDescriptors {
522    pub fn new() -> Self {
523        Self {
524            homo_energy: -13.6,
525            lumo_energy: 0.0,
526            gap: 13.6,
527            dipole_moment: 0.0,
528            polarizability: 0.0,
529        }
530    }
531}
532
533impl TopologicalDescriptors {
534    pub fn new() -> Self {
535        Self {
536            connectivity_index: 1.0,
537            shape_index: 1.0,
538            wiener_index: 1.0,
539            randic_index: 1.0,
540        }
541    }
542}
543
544impl MLModel {
545    pub fn new() -> Self {
546        Self {
547            model_id: "ml_1".to_string(),
548            model_type: MLModelType::LinearRegression,
549            model_parameters: MLModelParameters::new(),
550            training_data: TrainingData::new(),
551        }
552    }
553}
554
555impl MLModelParameters {
556    pub fn new() -> Self {
557        Self {
558            hyperparameters: HashMap::new(),
559            feature_importance: HashMap::new(),
560            model_performance: ModelPerformance::new(),
561        }
562    }
563}
564
565impl ModelPerformance {
566    pub fn new() -> Self {
567        Self {
568            r_squared: 0.95,
569            rmse: 0.1,
570            mae: 0.08,
571            cross_validation_score: 0.0, // not measured (scaffold default; no validation performed)
572        }
573    }
574}
575
576impl TrainingData {
577    pub fn new() -> Self {
578        Self {
579            data_id: "data_1".to_string(),
580            features: vec![vec![1.0; 10]; 100],
581            targets: vec![100.0; 100],
582            data_size: 100,
583        }
584    }
585}