Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,25 @@ the CLI only loads inputs and presents them.
The builder follows the `WSLCommand` conventions from WSLPlugins-rs: mutable
setters, consuming `with_*` variants, getters, and `prepare()` / `execute()`.
`PreparedCalculation` retains the validated molecule and basis and can be
executed repeatedly through the shared `CalculationExecution` trait.
executed repeatedly through the shared `CalculationExecution` trait. It also
exposes a normalized `CalculationRequest` with requested coordinates, units,
molecular state, and effective scientific options, without frontend source
spans or geometry paths. In text mode, `run` prints the original TOML and XYZ
sources verbatim so they can be copied back into files, preserving comments,
paths, and coordinate precision. Relative geometry paths require the same file
layout when reused. It then renders the normalized request as canonical TOML
with scientific defaults and XYZ in the requested units, followed by the
resolved configuration and XYZ in Bohr. These canonical pairs can each be
copied into `calculation.toml` and `molecule.xyz` to recreate the corresponding
semantic input. Canonical XYZ retains enough coordinate digits for an exact
floating-point round trip. Canonical TOML uses typed CLI adapters and
`toml-spanner`, without cache or terminal options. The requested basis label is
distinct from the resolved basis name and AO contents; only the resolved
scientific state determines artifact compatibility. Source provenance is
optional and never needed to render either semantic view. TOML and XYZ are CLI
presentation adapters; future input frontends should adapt into the same
scientific configuration and preparation path rather than introduce their
syntax into `rustiq-core`.
`run_hf()` returns `HfOutcome::Converged(HfSolution<Converged>)` or
`HfOutcome::Unconverged(HfSolution<Unconverged>)`. Both retain the HF summary,
orbitals and integrals; only the converged type exposes `mp2()`. Cloning a
Expand Down
2 changes: 2 additions & 0 deletions crates/rustiq-core/src/calculation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub use solution::{CalculationExecutionError, Converged, HfOutcome, HfSolution,
mod builder;
mod execution;
mod prepared_calculation;
mod request;
pub use crate::basis::{Basis, BasisError};
pub use crate::eri::EriError;
use crate::hf::{scf::ScfSetupError, uhf::UhfSetupError};
Expand All @@ -104,6 +105,7 @@ pub use execution::{
HfCalculationResult, Mp2MemoryPlan,
};
pub use prepared_calculation::PreparedCalculation;
pub use request::CalculationRequest;

use crate::{
config::{
Expand Down
85 changes: 82 additions & 3 deletions crates/rustiq-core/src/calculation/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::time::Instant;

use super::{
CalculationError, CalculationEvent, CalculationExecution, CalculationExecutionError,
CalculationResult, PreparedCalculation,
CalculationRequest, CalculationResult, PreparedCalculation,
};

/// Configure a calculation from explicitly loaded inputs.
Expand Down Expand Up @@ -40,6 +40,7 @@ use super::{
pub struct CalculationBuilder<'a> {
geometry: &'a Geometry,
basis_file: &'a BasisFile,
basis_label: Option<String>,
molecule_config: MoleculeConfig,
hf: HfConfig,
mp2: Option<Mp2Config>,
Expand All @@ -51,6 +52,7 @@ impl<'a> CalculationBuilder<'a> {
Self {
geometry,
basis_file,
basis_label: None,
molecule_config: MoleculeConfig::default(),
hf: HfConfig::default(),
mp2: None,
Expand All @@ -64,6 +66,18 @@ impl<'a> CalculationBuilder<'a> {
pub fn get_basis_file(&self) -> &BasisFile {
self.basis_file
}

/// Set a portable requested basis label; the loaded basis remains authoritative.
pub fn basis_label(&mut self, label: impl Into<String>) -> &mut Self {
self.basis_label = Some(label.into());
self
}

#[must_use]
pub fn with_basis_label(mut self, label: impl Into<String>) -> Self {
self.basis_label(label);
self
}
pub fn get_molecule_config(&self) -> &MoleculeConfig {
&self.molecule_config
}
Expand Down Expand Up @@ -141,9 +155,15 @@ impl<'a> CalculationBuilder<'a> {
&self,
mut events: impl FnMut(CalculationEvent<'_>),
) -> Result<PreparedCalculation, CalculationError> {
let mut molecule = self.molecule_config.build(self.geometry.clone())?;
let request = self.normalized_request();
let mut requested_geometry = self.geometry.clone();
requested_geometry.comment.clear();
let mut molecule = self.molecule_config.build(requested_geometry.clone())?;
molecule.convert_to(Units::Bohr);
let hf = (self.hf.clone(), self.hf.resolve_method(&molecule)?);
let method = self.hf.resolve_method(&molecule)?;
let mut execution_hf = self.hf.clone();
resolve_random_seeds(&mut execution_hf);
let hf = (execution_hf, method);
events(CalculationEvent::BasisStarted);
let start = Instant::now();
let basis = Basis::try_load(self.basis_file, &molecule)?;
Expand All @@ -152,13 +172,72 @@ impl<'a> CalculationBuilder<'a> {
elapsed: start.elapsed(),
});
Ok(PreparedCalculation {
request,
molecule,
basis,
basis_name: self.basis_file.name().to_owned(),
hf,
mp2: self.mp2,
eri_cache: self.eri_cache.clone(),
})
}

fn normalized_request(&self) -> CalculationRequest {
let mut geometry = self.geometry.clone();
geometry.comment.clear();
CalculationRequest {
geometry,
molecule: MoleculeConfig {
units: self.molecule_config.units,
charge: self.molecule_config.charge.value.into(),
multiplicity: self.molecule_config.multiplicity.value.into(),
},
basis_name: self
.basis_label
.clone()
.unwrap_or_else(|| self.basis_file.name().to_owned()),
hf: normalized_hf_config(&self.hf),
mp2: self.mp2.map(|config| Mp2Config {
frozen_orbitals: config.frozen_orbitals.value.into(),
memory_limit: config.memory_limit.value.into(),
}),
}
}
}

fn resolve_random_seeds(config: &mut HfConfig) {
let guess = &mut config.guess.value;
match guess {
crate::config::DensityGuessConfig::Random { config } => {
resolve_seed(&mut config.random.seed);
}
crate::config::DensityGuessConfig::CoreHamiltonian { perturbation }
| crate::config::DensityGuessConfig::OneElectron { perturbation } => {
if let Some(perturbation) = perturbation {
resolve_seed(&mut perturbation.random.seed);
}
}
crate::config::DensityGuessConfig::Zero => {}
}
}

fn resolve_seed(seed: &mut Option<u64>) {
if seed.is_none() {
*seed = Some(rand::random());
}
}

pub(super) fn normalized_hf_config(config: &HfConfig) -> HfConfig {
HfConfig {
method: config.method.value.into(),
max_iterations: config.max_iterations,
convergence_threshold: config.convergence_threshold,
linear_dependency_threshold: config.linear_dependency_threshold.value.into(),
eri_schwarz_threshold: config.eri_schwarz_threshold,
guess: config.guess.value.into(),
diis: config.diis,
diis_size: config.diis_size,
}
}

impl CalculationExecution for CalculationBuilder<'_> {
Expand Down
36 changes: 36 additions & 0 deletions crates/rustiq-core/src/calculation/prepared_calculation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,62 @@ use crate::{
};
use std::cell::RefCell;

use super::CalculationRequest;

/// A validated molecule in Bohr and its basis, prepared together by the builder.
///
/// Each execution starts fresh HF state and uses the same immutable inputs.
/// This avoids self-referential SCF storage and allows reuse of the basis.
pub struct PreparedCalculation {
pub(super) request: CalculationRequest,
pub(super) molecule: Molecule,
pub(super) basis: Basis,
pub(super) basis_name: String,
pub(super) hf: (HfConfig, ResolvedHfMethod),
pub(super) mp2: Option<Mp2Config>,
pub(super) eri_cache: Option<EriCache>,
}

impl PreparedCalculation {
/// Returns the normalized inputs as requested before scientific resolution.
pub fn request(&self) -> &CalculationRequest {
&self.request
}

pub fn get_molecule(&self) -> &Molecule {
&self.molecule
}

pub fn get_basis(&self) -> &Basis {
&self.basis
}

pub fn hf_method(&self) -> ResolvedHfMethod {
self.hf.1
}

/// Human-readable label of the loaded basis; this is not its scientific identity.
/// The resolved basis contents exposed by `get_basis()` are authoritative. Replaying
/// canonical TOML that uses this label assumes a compatible basis store.
pub fn basis_name(&self) -> &str {
&self.basis_name
}

/// Resolved HF presentation options with an explicit method and no frontend source spans.
/// Random seeds resolved during preparation are retained here.
pub fn hf_config(&self) -> HfConfig {
let mut config = super::builder::normalized_hf_config(&self.hf.0);
config.method.value = match self.hf.1 {
ResolvedHfMethod::Rhf => crate::config::HfMethod::Rhf,
ResolvedHfMethod::Uhf => crate::config::HfMethod::Uhf,
};
config
}

/// MP2 options without frontend source spans; automatic memory resolves at execution.
pub fn mp2_config(&self) -> Option<&Mp2Config> {
self.request.mp2()
}
}

impl PreparedCalculation {
Expand Down
42 changes: 42 additions & 0 deletions crates/rustiq-core/src/calculation/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::{
config::{HfConfig, MoleculeConfig, Mp2Config},
molecules::geometry::Geometry,
};

/// Frontend-independent, normalized inputs used to prepare a calculation.
///
/// This view keeps requested coordinates and units alongside effective
/// scientific options. It deliberately contains no source text, source spans,
/// frontend syntax, or filesystem paths.
#[derive(Debug, Clone)]
pub struct CalculationRequest {
pub(crate) geometry: Geometry,
pub(crate) molecule: MoleculeConfig,
pub(crate) basis_name: String,
pub(crate) hf: HfConfig,
pub(crate) mp2: Option<Mp2Config>,
}

impl CalculationRequest {
pub fn geometry(&self) -> &Geometry {
&self.geometry
}

pub fn molecule(&self) -> &MoleculeConfig {
&self.molecule
}

/// Requested portable basis label, or the loaded basis name for direct API use.
/// This label is not the scientific identity of the resolved basis.
pub fn basis_name(&self) -> &str {
&self.basis_name
}

pub fn hf(&self) -> &HfConfig {
&self.hf
}

pub fn mp2(&self) -> Option<&Mp2Config> {
self.mp2.as_ref()
}
}
2 changes: 0 additions & 2 deletions crates/rustiq-core/src/molecules/molecule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,10 @@ impl Molecule {
&self.geometry
}

#[allow(dead_code)]
pub fn unit(&self) -> Units {
self.unit
}

#[allow(dead_code)]
pub fn charge(&self) -> i32 {
self.charge
}
Expand Down
Loading
Loading