Repository: batistagroup/calcflow
Pull request #21: examples of use in readme, copy method on inputs, allow changing charge for second mom job
Author: login: anmorgunov
Labels: name: documentation
description: Improvements or additions to documentation
name: new-feature
description: a new feature for an outside user
Created: 2025-05-22T01:38:13Z | Merged: 2025-05-22T01:51:18Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to specify a separate charge for the second job in MOM (Maximum Overlap Method) calculations.

- **Improvements**
  - Renamed the "solute_orbitals" parameter to "initial_orbitals" for greater clarity throughout the interface and documentation.
  - Enhanced the README with a comprehensive user guide, detailed examples, and clearer explanations of key features and usage scenarios.

- **Bug Fixes**
  - Improved error messages for unsupported MOM occupation determination scenarios.

- **Tests**
  - Updated tests to reflect the renaming to "initial_orbitals" and added tests for the new copy functionality and deep copying behavior.

- **Documentation**
  - Expanded and restructured the README for better usability, including new sections on features, quick start, advanced usage, and contributing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/README.md b/README.md
index 38449c8..89b8168 100644
--- a/README.md
+++ b/README.md
@@ -8,14 +8,246 @@
 [![codecov](https://codecov.io/gh/batistagroup/calcflow/graph/badge.svg?token=bO5X75J8li)](https://codecov.io/gh/batistagroup/calcflow)
 ![codequal](https://github.com/batistagroup/calcflow/actions/workflows/quality.yml/badge.svg)
 
-A Python package to simplify preparing and parsing quantum chemistry calculations, with no external dependencies (is true now, will be true forever). Provides a clean, Pythonic API for interacting with QChem, ORCA, and potentially other quantum chemistry software.
+**CalcFlow: Quantum Chemistry Calculation I/O. Done Right.**
 
-## Features
+CalcFlow provides a robust, Pythonic interface for preparing inputs and parsing outputs for quantum chemistry software like Q-Chem and ORCA. It has **zero external dependencies** and is built for clarity and reliability. Get your calculations set up and results processed without the usual boilerplate.
 
-- **No Dependencies**: Built entirely on Python standard library
-- **Immutable Objects**: Dataclass-based with a fluent API for transparent, thread-safe state changes
-- **Strongly Typed**: Comprehensive type hints and validation for safer code
-- **Comprehensive Validation**: Prevent errors before they happen with rigorous validation of inputs
-- **Program-Agnostic API**: Abstract away program-specific syntax with a consistent interface
-- **Extensible Design**: Abstract base classes make it easy to add support for additional programs
+WARNING: Package is in pre-release alpha stage. May introduce backwards-incompatible changes. Contributions & suggestions & advice are welcome.
+
+## Key Features
+
+what you care about:
+
+- **Zero Dependencies**: is true now, will be true forever. Integrate into any project without worrying about whether it matches your numpy or rdkit version.
+- **Intuitive Pythonic Interface**: specify calculation parameters how you think about them and calcflow will handle the translation into what QChem or ORCA expects.
+- **Proactive Input Validation**: Minimizes runtime errors by rigorously validating all calculation parameters against both general and program-specific constraints.
+- **Comprehensive Parsing**: stop wasting time on extracting data you need from a txt out file and start doing meaningful work (analysis).
+
+what's nice under the hood:
+
+- **Immutable & Fluent API**: ensures data integrity and predictable state transitions via `frozen` dataclasses and an expressive, chainable API.
+- **Fully Type Annotated**: code is more robust and easier to understand.
+- **Extensible Core Architecture**: designed with clear abstract base classes, simplifying the integration of support for additional quantum chemistry programs.
+- **Assured Reliability via Comprehensive Testing**: so you don't have to worry if the parser is working correctly.
+
+## Quick Start: Q-Chem TDDFT Example
+
+Set up a Q-Chem tddft calculation for a water molecule with SMD solvation:
+
+```python
+from calcflow.geometry.static import Geometry
+from calcflow.inputs.qchem import QchemInput
+
+# 1. Define Molecular Geometry
+atoms = [
+    ("O", (0.000000, 0.000000, 0.117300)),
+    ("H", (0.000000, 0.757200, -0.469200)),
+    ("H", (0.000000, -0.757200, -0.469200)),
+]
+water_molecule = Geometry(atoms=atoms, comment="Water molecule for Q-Chem SP")
+# or load from xyz file
+water_molecule = Geometry.from_xyz_file(data_path / "1h2o.xyz")
+
+# 2. Configure Q-Chem Calculation Input
+base_job = QchemInput(
+    charge=0, spin_multiplicity=1, task="energy", 
+    level_of_theory="wB97X-D3", basis_set="def2-tzvp", n_cores=16,
+    run_tddft=True, tddft_nroots=10, tddft_singlets=True, tddft_triplets=False
+)
+
+# Uses a fluent API for modifications.
+qchem_job = base_job.set_solvation(model="smd", solvent="water") 
+
+# 3. Export the Input File Content
+with open("h2o_tddft.in", "w") as f:
+    f.write(qchem_job.export_input_file(water_molecule))
+```
+
+Want to create another input file for a tddft with triplets or with state analysis? ezy.
+
+```python
+job2 = qchem_job.set_tddft(nroots=10, singlets=True, triplets=True, state_analysis=True)
+with open("h2o_tddft.in", "w") as f:
+    f.write(job2.export_input_file(water_molecule))
+```
+
+what about getting an O K-Edge XAS spectrum with an element-specific basis?
+
+```python
+job3 = (
+    qchem_job.set_tddft(nroots=10, singlets=True, triplets=False, state_analysis=True)
+    .set_basis({"H": "pc-2", "O": "pcX-2"})
+    .set_reduced_excitation_space(initial_orbitals=[1])
+)
+with open("h2o_xas.in", "w") as f:
+    f.write(job3.export_input_file(water_molecule))
+```
+
+By the way, this pythonic (arguably intuitive) method
+
+```py
+.set_reduced_excitation_space(initial_orbitals=[1])
+```
+
+translates into, take a guess
+
+```
+$rem
+    TRNSS = TRUE
+    TRTYPE = 3
+    N_SOL = 1
+$end
+
+$solute
+    1
+$end
+```
+
+that was obvious, wasn't it. Honestly, a major reason why this package exists. Btw, if you want to get a XAS spectrum from S1 state, you can do that too:
+
+```python
+job4 = (
+    qchem_job.set_tddft(nroots=10, singlets=True, triplets=False, state_analysis=True)
+    .set_basis({"H": "pc-2", "O": "pcX-2"})
+    .set_reduced_excitation_space(initial_orbitals=[1])
+    .set_unrestricted()
+    .enable_mom()
+    .set_mom_transition("HOMO->LUMO")
+)
+with open("h2o_s1_xas.in", "w") as f:
+    f.write(job4.export_input_file(water_molecule))
+```
+
+this will create a 2-job input file, initial SCF calculation followed by MOM with HOMO electron moved to LUMO and TDDFT from orbital 1 on top of that.
+
+fun fact: because water_molecule is a Geometry instance, it has `.total_nuclear_charge` property, which is used to calculate indexes of HOMO and LUMO orbitals. You can specify same transition numerically `5->6` or even specify occupation numbers manually:
+
+```py
+.set_mom_occupation(alpha_occ="1 2 3 4 6", beta_occ="1 2 3 4 5")
+# or
+.set_mom_occupation(alpha_occ="1:4 6", beta_occ="1:4 5")
+```
+
+## Parsing Q-Chem Output Files
+
+Once your Q-Chem calculation is complete, use `CalcFlow` parsers. Example for the last calculation
+
+```python
+from calcflow.parsers.qchem import parse_qchem_mom_output
+
+# Replace with your actual file path
+out_path = "h2o_qchem_sp.out" 
+mom_pc2 = parse_qchem_mom_output((clc_folder / "mom-smd-xas.out").read_text())
+```
+
+And just like that you have access to all relevant results.
+
+```py
+> mom_pc2.job2
+CalculationData(method='src1-r1', basis='gen', status='NORMAL')
+> print(mom_pc2.job2.scf)
+ScfResults(status='Converged', energy=-76.53682225, n_iterations=9)
+> print(mom_pc2.job2.tddft)
+TddftResults(tda_states=10 states, tddft_states=None, excited_state_analyses=10 analyses, transition_dm_analyses=10 analyses, nto_analyses=10 analyses)
+```
+
+Say you want to get excitation energies and oscillator strenghts? Be my guest:
+
+```py
+eVs = [state.excitation_energy_ev for state in mom_pc2.job2.tddft.tda_states]
+intens = [state.oscillator_strength for state in mom_pc2.job2.tddft.tda_states]
+```
+
+Mulliken populations for 3rd state?
+
+```py
+> mom_pc2.job2.tddft.transition_dm_analyses[2].mulliken
+TransitionDMMulliken(
+    populations=[
+        TransitionDMAtomPopulation(atom_index=0, symbol='H', transition_charge_e=-0.000558, hole_charge_rks=None, electron_charge_rks=None, delta_charge_rks=None, hole_charge_alpha_uks=7.2e-05, hole_charge_beta_uks=7.2e-05, electron_charge_alpha_uks=-0.241416, electron_charge_beta_uks=-0.241421), 
+        TransitionDMAtomPopulation(atom_index=1, symbol='O', transition_charge_e=0.001113, hole_charge_rks=None, electron_charge_rks=None, delta_charge_rks=None, hole_charge_alpha_uks=0.499858, hole_charge_beta_uks=0.499855, electron_charge_alpha_uks=-0.021788, electron_charge_beta_uks=-0.021788), 
+        TransitionDMAtomPopulation(atom_index=2, symbol='H', transition_charge_e=-0.000555, hole_charge_rks=None, electron_charge_rks=None, delta_charge_rks=None, hole_charge_alpha_uks=7.2e-05, hole_charge_beta_uks=7.2e-05, electron_charge_alpha_uks=-0.236798, electron_charge_beta_uks=-0.23679)
+        ], 
+    sum_abs_trans_charges_qta=0.002226, sum_sq_trans_charges_qt2=2e-06)
+```
+
+See [scripts/create-parse-qchem.py](scripts/create-parse-qchem.py) for more examples or to play with outputs used for tests (stored in [data/calculations/examples/qchem/](data/calculations/examples/qchem/))
+
+### Which versions of QChem do you support?
+
+It's tested on 6.2 and 5.4. Good news is that I'm intending to make it easy to adjust for version-specific printing. For example,
+
+```txt
+QChem 5.4. prints final SCF energy as
+SCF   energy in the final basis set =      -75.3184602363
+while 6.2. prints it as
+SCF   energy =   -75.32080770
+
+p.s. nevermind the difference in energy, 5.4. mistakenly prints SCF energy same as Total energy, which includes solvation terms
+```
+
+which is why the [calcflow/parsers/qchem/blocks/scf.py](src/calcflow/parsers/qchem/blocks/scf.py) block defines different patterns for different versions:
+
+```py
+PatternDefinition(field_name="scf_energy", required=True, description="Final SCF energy value",
+versioned_patterns=[
+        (re.compile(r"^\s*SCF\s+energy\s*=\s*(-?\d+\.\d+)"), "6.2", lambda m: float(m.group(1))),
+        (re.compile(r"^\s*SCF\s+energy in the final basis set\s*=\s*(-?\d+\.\d+)"), "5.4", lambda m: float(m.group(1))),
+    ],
+),
+```
+
+## Slurm submission scripts
+
+As a bonus, there's also a pythonic way of preparing slurm submission files. Most likely you have system-specific variations in which modules should be loaded or which env variables should be defined, so you could inherit from a general SlurmArgs (in [calcflow/inputs/slurm.py](src/calcflow/inputs/slurm.py)):
+
+```py
+from dataclasses import dataclass
+
+from calcflow.inputs.slurm import SlurmArgs
+
+
+@dataclass(frozen=True)
+class NerscSlurmArgs(SlurmArgs):
+    def get_modules(self) -> str:
+        if self.software == "qchem":
+            return """
+module load qchem
+            """
+        elif self.software == "orca":
+            return """
+module load openmpi
+            """
+        return ""
+
+    def get_temp_variables(self) -> str:
+        if self.software == "qchem":
+            return f"""
+export QCSCRATCH=$PSCRATCH
+export QCLOCALSCR=$QCSCRATCH
+export OMP_NUM_THREADS={self.n_cores}
+export QC_THREADS={self.n_cores}
+        """
+        return ""
+```
+
+and then in a very similar manner you can create custom configs:
+
+```py
+nersc_args = NerscSlurmArgs(
+    exec_fname="mom-sp", time="01:00:00", n_cores=16,
+    constraint="cpu", account="mxxxx", queue="regular")
+with (clc_folder / "submit.sh").open("w") as f:
+    f.write(base_args.set_software("qchem").create_submit_script(f"{name}-{state}-{mom_type}"))
+```
+
+where `.set_software` dictates which modules/temp variables will be printed, and arg to `create_submit_script` is the job name.
+
+## Contributing
+
+Direct, effective contributions are welcome. Fork, modify, test, and pull request. Adhere to existing quality standards.
+
+## License
+
+MIT. Pure and simple.
 
diff --git a/scripts/create-parse-qchem.py b/scripts/create-parse-qchem.py
index de00fca..12b2aca 100644
--- a/scripts/create-parse-qchem.py
+++ b/scripts/create-parse-qchem.py
@@ -37,11 +37,11 @@
     "tddft-create": False,
     "tddft-parse": False,
     "mom-parse": False,
-    "mom-xas-parse": False,
+    "mom-xas-parse": True,
     "sp-parse-5.4": False,
     "tddft-uks-parse-5.4": False,
     "mom-sp-parse-5.4": False,
-    "mom-xas-parse-5.4": True,
+    "mom-xas-parse-5.4": False,
 }
 
 if run["sp-create"]:
@@ -124,6 +124,7 @@
     print(mom_pc2.job2)
     print(mom_pc2.job2.scf)
     print(mom_pc2.job2.tddft)
+    breakpoint()
 
 if run["sp-parse-5.4"]:
     sp_sto = parse_qchem_sp_output((clc_54_folder / "sp-sto-smd.out").read_text())
diff --git a/src/calcflow/inputs/qchem.py b/src/calcflow/inputs/qchem.py
index 38ce92a..a189f13 100644
--- a/src/calcflow/inputs/qchem.py
+++ b/src/calcflow/inputs/qchem.py
@@ -1,4 +1,5 @@
 import re
+from copy import deepcopy
 from dataclasses import dataclass, replace
 from typing import ClassVar, Literal, TypeVar, cast, get_args
 
@@ -46,7 +47,8 @@ class QchemInput(CalculationInput):
         mom_beta_occ: Beta orbital occupation string for MOM.
         mom_transition: Symbolic transition specification (e.g., "HOMO->LUMO").
         reduced_excitation_space: Flag to enable reduced excitation space for TDDFT calculations.
-        solute_orbitals: List of molecular orbitals from which excitations are allowed when TRNSS is enabled.
+        initial_orbitals: List of molecular orbitals from which excitations are allowed when TRNSS is enabled.
+        mom_job2_charge: Charge for the second job in a MOM calculation
     """
 
     program: ClassVar[str] = "qchem"
@@ -63,7 +65,7 @@ class QchemInput(CalculationInput):
 
     # --- TDDFT Reduced Excitation Space ---
     reduced_excitation_space: bool = False
-    solute_orbitals: list[int] | None = None
+    initial_orbitals: list[int] | None = None
 
     # --- MOM Specific Attributes ---
     run_mom: bool = False
@@ -71,6 +73,7 @@ class QchemInput(CalculationInput):
     mom_alpha_occ: str | None = None
     mom_beta_occ: str | None = None
     mom_transition: str | None = None  # Stores symbolic transition if using that mode
+    mom_job2_charge: int | None = None  # Charge for the second job in a MOM calculation
 
     implicit_solvation_model: QCHEM_ALLOWED_SOLVATION_MODELS | None = None
     solvent: str | None = None
@@ -100,11 +103,11 @@ def __post_init__(self) -> None:
         if self.reduced_excitation_space:
             if not self.run_tddft:
                 raise ConfigurationError("reduced_excitation_space (TRNSS) requires TDDFT (run_tddft) to be enabled.")
-            # Validate solute_orbitals if reduced_excitation_space is enabled
-            if not self.solute_orbitals:
-                raise ValidationError("The solute_orbitals list cannot be empty for reduced excitation space.")
-            if not all(isinstance(orb, int) and orb > 0 for orb in self.solute_orbitals):
-                raise ValidationError("All solute_orbitals must be positive integers.")
+            # Validate initial_orbitals if reduced_excitation_space is enabled
+            if not self.initial_orbitals:
+                raise ValidationError("The initial_orbitals list cannot be empty for reduced excitation space.")
+            if not all(isinstance(orb, int) and orb > 0 for orb in self.initial_orbitals):
+                raise ValidationError("All initial_orbitals must be positive integers.")
 
         # Use the Q-Chem specific literal for validation
         if self.implicit_solvation_model and self.implicit_solvation_model not in get_args(
@@ -114,6 +117,14 @@ def __post_init__(self) -> None:
                 f"Solvation model '{self.implicit_solvation_model}' not recognized. Allowed: {get_args(QCHEM_ALLOWED_SOLVATION_MODELS)}"
             )
 
+    def copy(self: T_QchemInput) -> T_QchemInput:
+        """Create a deep copy of the QchemInput instance.
+
+        Returns:
+            A new QchemInput instance with the same parameters.
+        """
+        return deepcopy(self)
+
     def enable_mom(self: T_QchemInput, method: str = "IMOM") -> T_QchemInput:
         """Enable MOM calculation with specified method.
 
@@ -295,15 +306,22 @@ def _generate_occupied_block(self, geometry: "Geometry") -> str:
         if not self.unrestricted:
             raise ConfigurationError("MOM requires unrestricted=True")
 
+        # Determine the charge to use for electron counting in this block
+        # If mom_job2_charge is set, it means we are generating the $occupied block
+        # for the *second* job of a MOM calculation, which might have a different charge.
+        effective_charge = self.mom_job2_charge if self.mom_job2_charge is not None else self.charge
+
         # For MOM targeting ground state or excited states, we currently only support
         # closed-shell singlet reference states for determining occupations.
-        if self.charge != 0 or self.spin_multiplicity != 1:
+        # This validation now uses the effective_charge for the current job.
+        if self.mom_transition is not None and (effective_charge != 0 or self.spin_multiplicity != 1):
             raise NotSupportedError(
-                "MOM occupation determination currently only supports closed-shell singlet reference states. "
-                f"Got charge={self.charge}, multiplicity={self.spin_multiplicity}."
+                "MOM occupation determination via symbolic transition or 'GROUND_STATE' marker "
+                f"currently only supports a closed-shell singlet reference state for the job where occupations are determined. "
+                f"Effective charge for this job = {effective_charge}, multiplicity = {self.spin_multiplicity}."
             )
 
-        total_electrons = geometry.total_nuclear_charge - self.charge
+        total_electrons = geometry.total_nuclear_charge - effective_charge
 
         if self.mom_transition == "GROUND_STATE":
             if total_electrons < 0:
@@ -430,16 +448,16 @@ def set_rpa(self: T_QchemInput, enable: bool = True) -> T_QchemInput:
         logger.debug(f"Setting RPA keyword to: {enable}")
         return replace(self, rpa=enable)
 
-    def set_reduced_excitation_space(self: T_QchemInput, solute_orbitals: list[int]) -> T_QchemInput:
+    def set_reduced_excitation_space(self: T_QchemInput, initial_orbitals: list[int]) -> T_QchemInput:
         """Configure a reduced excitation space for TDDFT calculations.
 
         This setting activates Q-Chem's TRNSS capability, restricting excitations
         to originate from a specified subset of molecular orbitals. It sets
-        TRNSS = TRUE, TRTYPE = 3, and N_SOL = len(solute_orbitals) in the $rem block,
+        TRNSS = TRUE, TRTYPE = 3, and N_SOL = len(initial_orbitals) in the $rem block,
         and requires a $solute block specifying the active orbitals.
 
         Args:
-            solute_orbitals: A list of positive integers representing the molecular orbitals
+            initial_orbitals: A list of positive integers representing the molecular orbitals
                              from which excitations are allowed. The list will be
                              stored internally sorted and with duplicates removed.
 
@@ -448,7 +466,7 @@ def set_reduced_excitation_space(self: T_QchemInput, solute_orbitals: list[int])
 
         Raises:
             ConfigurationError: If TDDFT (run_tddft) is not enabled.
-            ValidationError: If the solute_orbitals list is empty or contains
+            ValidationError: If the initial_orbitals list is empty or contains
                              non-positive integers.
         """
         if not self.run_tddft:
@@ -456,13 +474,13 @@ def set_reduced_excitation_space(self: T_QchemInput, solute_orbitals: list[int])
                 "Reduced excitation space (TRNSS) requires TDDFT to be enabled. "
                 "Ensure run_tddft is True before calling this method."
             )
-        if not solute_orbitals:
-            raise ValidationError("The solute_orbitals list cannot be empty for reduced excitation space.")
-        if not all(isinstance(orb, int) and orb > 0 for orb in solute_orbitals):
-            raise ValidationError("All solute_orbitals must be positive integers.")
+        if not initial_orbitals:
+            raise ValidationError("The initial_orbitals list cannot be empty for reduced excitation space.")
+        if not all(isinstance(orb, int) and orb > 0 for orb in initial_orbitals):
+            raise ValidationError("All initial_orbitals must be positive integers.")
 
         # Store unique, sorted orbitals
-        processed_orbitals = sorted(list(set(solute_orbitals)))
+        processed_orbitals = sorted(list(set(initial_orbitals)))
 
         logger.debug(
             f"Setting reduced excitation space with orbitals: {processed_orbitals}. "
@@ -471,7 +489,7 @@ def set_reduced_excitation_space(self: T_QchemInput, solute_orbitals: list[int])
         return replace(
             self,
             reduced_excitation_space=True,
-            solute_orbitals=processed_orbitals,
+            initial_orbitals=processed_orbitals,
         )
 
     def set_basis(self: T_QchemInput, basis: str | dict[str, str]) -> T_QchemInput:
@@ -489,18 +507,20 @@ def set_basis(self: T_QchemInput, basis: str | dict[str, str]) -> T_QchemInput:
         logger.debug(f"Setting basis set to: {basis}")
         return replace(self, basis_set=basis)
 
-    def _get_molecule_block(self, geom: str) -> str:
+    def _get_molecule_block(self, geom: str, charge_override: int | None = None) -> str:
         """Generates the $molecule block.
 
         Args:
             geom: Molecular geometry in XYZ format (multiline string, excluding header lines).
+            charge_override: Optional integer to override the instance's charge for this block.
 
         Returns:
             String containing the formatted $molecule block.
         """
+        charge_to_use = charge_override if charge_override is not None else self.charge
         lines = [
             "$molecule",
-            f"{self.charge} {self.spin_multiplicity}",
+            f"{charge_to_use} {self.spin_multiplicity}",
             geom.strip(),
             "$end",
         ]
@@ -570,11 +590,13 @@ def _get_rem_block(
                 # --- Reduced Excitation Space for TDDFT --- (This logic is now nested)
                 if self.reduced_excitation_space:
                     # The outer condition (mom_start or not self.run_mom) already ensures this is the correct job context.
-                    if not self.solute_orbitals:  # Should be caught by setter, defensive check
-                        raise InputGenerationError("solute_orbitals must be set when reduced_excitation_space is True.")
+                    if not self.initial_orbitals:  # Should be caught by setter, defensive check
+                        raise InputGenerationError(
+                            "initial_orbitals must be set when reduced_excitation_space is True."
+                        )
                     rem_vars["TRNSS"] = True
                     rem_vars["TRTYPE"] = 3
-                    rem_vars["N_SOL"] = len(self.solute_orbitals)
+                    rem_vars["N_SOL"] = len(self.initial_orbitals)
 
         # --- Implicit Solvation --- #
         if self.implicit_solvation_model:
@@ -655,13 +677,13 @@ def _get_solute_block(self) -> str:
 
         Returns:
             String containing the formatted $solute block if reduced_excitation_space
-            is True and solute_orbitals are provided, otherwise an empty string.
+            is True and initial_orbitals are provided, otherwise an empty string.
         """
-        if not self.reduced_excitation_space or not self.solute_orbitals:
+        if not self.reduced_excitation_space or not self.initial_orbitals:
             return ""
 
         lines = ["$solute"]
-        lines.append(" ".join(str(orb) for orb in self.solute_orbitals))
+        lines.append(" ".join(str(orb) for orb in self.initial_orbitals))
         lines.append("$end")
         return "\n".join(lines)
 
@@ -712,8 +734,19 @@ def export_input_file(self, geometry: "Geometry") -> str:
             return first_job.strip() + "\n"
 
         # For MOM, add second job
+        second_job_molecule_block: str
+        if self.mom_job2_charge is not None:
+            # If a specific charge is set for job 2, generate a full $molecule block
+            logger.info(f"Generating $molecule block for MOM job 2 with charge: {self.mom_job2_charge}")
+            second_job_molecule_block = self._get_molecule_block(
+                geometry.get_coordinate_block(), charge_override=self.mom_job2_charge
+            )
+        else:
+            # Default behavior: read geometry from the first job
+            second_job_molecule_block = "$molecule\n    read\n$end"
+
         second_job_blocks = [
-            "$molecule\n    read\n$end",
+            second_job_molecule_block,
             self._get_rem_block(
                 scf_guess="read",
                 mom_start=True,
@@ -730,6 +763,31 @@ def export_input_file(self, geometry: "Geometry") -> str:
 
         return f"{first_job.strip()}\n\n@@@\n\n{second_job.strip()}\n"
 
+    def set_mom_job2_charge(self: T_QchemInput, charge: int) -> T_QchemInput:
+        """Set a specific charge for the second job in a MOM calculation.
+
+        This charge will be used in the $molecule block of the second job,
+        allowing it to differ from the primary charge of the QchemInput object.
+        The spin multiplicity remains the same as the primary setting.
+
+        Args:
+            charge: The integer charge for the second MOM job.
+
+        Returns:
+            A new QchemInput instance with the second job charge set.
+
+        Raises:
+            ConfigurationError: If MOM is not enabled (run_mom=False).
+        """
+        if not self.run_mom:
+            raise ConfigurationError(
+                "MOM must be enabled (run_mom=True) via enable_mom() before setting a specific charge for the second MOM job."
+            )
+        logger.info(
+            f"Setting charge for second MOM job to: {charge}. This will affect the $molecule block and electron count for job 2."
+        )
+        return replace(self, mom_job2_charge=charge)
+
 
 def _convert_transition_to_occupations(transition: str, n_electrons: int) -> tuple[str, str]:
     """Converts a symbolic transition to Q-Chem occupation strings.
diff --git a/tests/inputs/qchem/test_input_qchem_init.py b/tests/inputs/qchem/test_input_qchem_init.py
index 156084c..e91d713 100644
--- a/tests/inputs/qchem/test_input_qchem_init.py
+++ b/tests/inputs/qchem/test_input_qchem_init.py
@@ -146,21 +146,21 @@ def test_qchem_input_post_init_trnss_without_tddft() -> None:
             basis_set="sto-3g",
             reduced_excitation_space=True,
             run_tddft=False,  # Explicitly set to False
-            solute_orbitals=[1, 2],  # Provide valid orbitals to isolate the TDDFT dependency check
+            initial_orbitals=[1, 2],  # Provide valid orbitals to isolate the TDDFT dependency check
         )
 
 
 @pytest.mark.parametrize(
-    "solute_orbitals, expected_error_match",
+    "initial_orbitals, expected_error_match",
     [
-        ([], "The solute_orbitals list cannot be empty for reduced excitation space."),
-        ([1, 0], "All solute_orbitals must be positive integers."),
-        ([1, -2], "All solute_orbitals must be positive integers."),
-        ([1.5], "All solute_orbitals must be positive integers."),  # Test non-integer
+        ([], "The initial_orbitals list cannot be empty for reduced excitation space."),
+        ([1, 0], "All initial_orbitals must be positive integers."),
+        ([1, -2], "All initial_orbitals must be positive integers."),
+        ([1.5], "All initial_orbitals must be positive integers."),  # Test non-integer
     ],
 )
-def test_qchem_input_post_init_trnss_invalid_solute_orbitals(solute_orbitals, expected_error_match) -> None:
-    """Test validation error if reduced_excitation_space is True but solute_orbitals are invalid."""
+def test_qchem_input_post_init_trnss_invalid_solute_orbitals(initial_orbitals, expected_error_match) -> None:
+    """Test validation error if reduced_excitation_space is True but initial_orbitals are invalid."""
     # TDDFT must be enabled for TRNSS check to be reached
     with pytest.raises(ValidationError, match=expected_error_match):
         QchemInput(
@@ -169,8 +169,56 @@ def test_qchem_input_post_init_trnss_invalid_solute_orbitals(solute_orbitals, ex
             task="energy",
             level_of_theory="hf",
             basis_set="sto-3g",
-            run_tddft=True,  # TDDFT enabled to reach solute_orbitals validation
+            run_tddft=True,  # TDDFT enabled to reach initial_orbitals validation
             tddft_nroots=5,
             reduced_excitation_space=True,
-            solute_orbitals=solute_orbitals,
+            initial_orbitals=initial_orbitals,
         )
+
+
+def test_qchem_input_copy_method(default_qchem_input: QchemInput) -> None:
+    """Test the copy() method for QchemInput to ensure deep copies."""
+    from dataclasses import replace
+
+    # Make the original input a bit more complex
+    original_input = replace(
+        default_qchem_input,
+        n_cores=2,
+        run_tddft=True,
+        tddft_nroots=3,
+        reduced_excitation_space=True,
+        initial_orbitals=[10, 20, 30],
+        basis_set={"C": "6-31g*", "H": "sto-3g"},  # Use a dict basis for deepcopy check
+    )
+
+    copied_input = original_input.copy()
+
+    # 1. Check they are different instances
+    assert copied_input is not original_input, "Copied input should be a new instance."
+
+    # 2. Check they are equal in value (dataclasses implement __eq__)
+    assert copied_input == original_input, "Copied input should be equal in value to the original."
+
+    # 3. Test deep copy behavior for mutable attributes
+
+    # Test with initial_orbitals (list)
+    assert copied_input.initial_orbitals is not None
+    assert original_input.initial_orbitals is not None
+    copied_input.initial_orbitals.append(40)
+    assert original_input.initial_orbitals == [10, 20, 30], (
+        "Modifying initial_orbitals in copied input should not affect original."
+    )
+    assert copied_input.initial_orbitals == [10, 20, 30, 40]
+
+    # Test with basis_set (dict)
+    assert isinstance(copied_input.basis_set, dict)
+    assert isinstance(original_input.basis_set, dict)
+    # The type ignore is because basis_set can be str | dict, but we've ensured it's dict here.
+    copied_input.basis_set["O"] = "def2-svp"  # type: ignore
+
+    assert "O" not in original_input.basis_set, "Modifying basis_set in copied input should not affect original."
+    assert copied_input.basis_set["O"] == "def2-svp"  # type: ignore
+
+    # Ensure other attributes are still the same after modifications to mutable fields of the copy
+    assert copied_input.n_cores == original_input.n_cores
+    assert copied_input.run_tddft == original_input.run_tddft
diff --git a/tests/inputs/qchem/test_input_qchem_mom.py b/tests/inputs/qchem/test_input_qchem_mom.py
index 06e7276..b9dd97e 100644
--- a/tests/inputs/qchem/test_input_qchem_mom.py
+++ b/tests/inputs/qchem/test_input_qchem_mom.py
@@ -129,14 +129,14 @@ def test_generate_occupied_block_non_singlet(mom_enabled_input: QchemInput, h2o_
     inp_charged = replace(mom_enabled_input, charge=1, unrestricted=True).set_mom_transition("HOMO->LUMO")
     with pytest.raises(
         NotSupportedError,
-        match="MOM occupation determination currently only supports closed-shell singlet reference states",
+        match="MOM occupation determination via symbolic transition or 'GROUND_STATE' marker currently only supports a closed-shell singlet reference state for the job where occupations are determined.",
     ):
         inp_charged._generate_occupied_block(h2o_geometry)
 
     inp_triplet = replace(mom_enabled_input, spin_multiplicity=3, unrestricted=True).set_mom_transition("HOMO->LUMO")
     with pytest.raises(
         NotSupportedError,
-        match="MOM occupation determination currently only supports closed-shell singlet reference states",
+        match="MOM occupation determination via symbolic transition or 'GROUND_STATE' marker currently only supports a closed-shell singlet reference state for the job where occupations are determined.",
     ):
         inp_triplet._generate_occupied_block(h2o_geometry)
 
@@ -427,6 +427,7 @@ def test_set_mom_ground_state_before_enable(default_qchem_input: QchemInput) ->
         default_qchem_input.set_mom_ground_state()
 
 
+# fmt:off
 @pytest.mark.parametrize(
     "geometry_elements_coords, charge, multiplicity, expected_alpha, expected_beta, expected_error, error_match",
     [
@@ -434,59 +435,19 @@ def test_set_mom_ground_state_before_enable(default_qchem_input: QchemInput) ->
         ((["O", "H", "H"], [[0, 0, 0], [0, 0, 1], [0, 1, 0]]), 0, 1, "1:5", "1:5", None, None),  # H2O, 10e-
         ((["He"], [[0, 0, 0]]), 0, 1, "1", "1", None, None),  # He, 2e-
         # Invalid system: non-closed-shell singlet for _generate_occupied_block check
-        (
-            (["O", "H", "H"], [[0, 0, 0], [0, 0, 1], [0, 1, 0]]),
-            1,
-            1,
-            None,
-            None,
-            NotSupportedError,
-            "MOM occupation determination currently only supports closed-shell singlet reference states",
-        ),
-        (
-            (["O", "H", "H"], [[0, 0, 0], [0, 0, 1], [0, 1, 0]]),
-            0,
-            3,
-            None,
-            None,
-            NotSupportedError,
-            "MOM occupation determination currently only supports closed-shell singlet reference states",
-        ),
-        # Invalid system: odd total electrons for GROUND_STATE processing (QchemInput charge=0, mult=1)
-        (
-            (["Li"], [[0, 0, 0]]),
-            0,
-            1,
-            None,
-            None,
-            ConfigurationError,
-            "Expected an even number of total electrons",
-        ),  # Li (Z=3), total_e = 3.
+        ((["O", "H", "H"], [[0, 0, 0], [0, 0, 1], [0, 1, 0]]), 1, 1, None, None, NotSupportedError, "MOM occupation determination via symbolic transition or 'GROUND_STATE' marker currently only supports a closed-shell singlet reference state for the job where occupations are determined."),
+        ((["O", "H", "H"], [[0, 0, 0], [0, 0, 1], [0, 1, 0]]), 0, 3, None, None, NotSupportedError, "MOM occupation determination via symbolic transition or 'GROUND_STATE' marker currently only supports a closed-shell singlet reference state for the job where occupations are determined."),
+        # Invalid system: odd total electrons for GROUND_STATE processing (QchemInput charge=0, mult=1)   # Li (Z=3), total_e = 3.
+        ((["Li"], [[0, 0, 0]]), 0, 1, None, None, ConfigurationError, "Expected an even number of total electrons"),
         # Corrected test for QchemInput(charge=1) for a system that would otherwise have 0 electrons.
-        # This should be caught by the initial NotSupportedError due to QchemInput.charge != 0.
-        (
-            (["H"], [[0, 0, 0]]),
-            1,
-            1,
-            None,
-            None,
-            NotSupportedError,
-            "MOM occupation determination currently only supports closed-shell singlet reference states",
-        ),  # H(Z=1), QchemInput(charge=1). total_e = 0.
-        # New test for insufficient occupied orbitals (QchemInput charge=0, mult=1, but 0 total electrons from geometry)
-        (
-            ([], []),
-            0,
-            1,
-            None,
-            None,
-            ConfigurationError,
-            "System with 0 electrons .* insufficient occupied orbitals for 'GROUND_STATE' MOM",
-        ),  # No atoms -> total_nuclear_charge=0. total_e = 0.
+        # This should be caught by the initial NotSupportedError due to QchemInput.charge != 0. # H(Z=1), QchemInput(charge=1). total_e = 0.
+        ((["H"], [[0, 0, 0]]), 1, 1, None, None, NotSupportedError, "MOM occupation determination via symbolic transition or 'GROUND_STATE' marker currently only supports a closed-shell singlet reference state for the job where occupations are determined."),
+        # New test for insufficient occupied orbitals (QchemInput charge=0, mult=1, but 0 total electrons from geometry).   # No atoms -> total_nuclear_charge=0. total_e = 0.
+        (([], []), 0, 1, None, None, ConfigurationError, "System with 0 electrons .* insufficient occupied orbitals for 'GROUND_STATE' MOM"),
         # N atom (total_nuclear_charge=7) QchemInput(charge=0, mult=1) -> 7e-. Expect odd electron error.
         ((["N"], [[0, 0, 0]]), 0, 1, None, None, ConfigurationError, "Expected an even number of total electrons"),
     ],
-)
+) # fmt:on
 def test_generate_occupied_block_mom_ground_state(
     unrestricted_input: QchemInput,
     geometry_elements_coords: tuple[list[str], list[list[float]]],
diff --git a/tests/inputs/qchem/test_input_qchem_setters.py b/tests/inputs/qchem/test_input_qchem_setters.py
index 34f0440..3300a15 100644
--- a/tests/inputs/qchem/test_input_qchem_setters.py
+++ b/tests/inputs/qchem/test_input_qchem_setters.py
@@ -99,7 +99,7 @@ def test_set_basis_dict(default_qchem_input: QchemInput) -> None:
 
 
 @pytest.mark.parametrize(
-    "solute_orbitals, expected_orbitals, expected_error",
+    "initial_orbitals, expected_orbitals, expected_error",
     [
         ([3, 1, 2, 3], [1, 2, 3], None),
         ([], None, ValidationError),
@@ -107,22 +107,22 @@ def test_set_basis_dict(default_qchem_input: QchemInput) -> None:
     ],
 )
 def test_set_reduced_excitation_space(
-    default_qchem_input: QchemInput, solute_orbitals, expected_orbitals, expected_error
+    default_qchem_input: QchemInput, initial_orbitals, expected_orbitals, expected_error
 ) -> None:
     """Test configuring reduced excitation space for TDDFT."""
 
     # Test case where TDDFT is NOT enabled first (should raise ConfigurationError)
     with pytest.raises(ConfigurationError, match="requires TDDFT"):
-        default_qchem_input.set_reduced_excitation_space(solute_orbitals=[1])  # Use a dummy list
+        default_qchem_input.set_reduced_excitation_space(initial_orbitals=[1])  # Use a dummy list
 
     # Enable TDDFT first
     tddft_input = default_qchem_input.set_tddft(nroots=5)
 
     if expected_error:
         with pytest.raises(expected_error):
-            tddft_input.set_reduced_excitation_space(solute_orbitals=solute_orbitals)
+            tddft_input.set_reduced_excitation_space(initial_orbitals=initial_orbitals)
     else:
-        inp = tddft_input.set_reduced_excitation_space(solute_orbitals=solute_orbitals)
+        inp = tddft_input.set_reduced_excitation_space(initial_orbitals=initial_orbitals)
         assert inp.reduced_excitation_space is True
-        assert inp.solute_orbitals == expected_orbitals
+        assert inp.initial_orbitals == expected_orbitals
         assert inp is not tddft_input  # Ensure immutability
diff --git a/tests/inputs/qchem/test_input_qchem_xas.py b/tests/inputs/qchem/test_input_qchem_xas.py
index febd665..508534a 100644
--- a/tests/inputs/qchem/test_input_qchem_xas.py
+++ b/tests/inputs/qchem/test_input_qchem_xas.py
@@ -22,48 +22,48 @@ def test_set_reduced_excitation_space_valid(default_qchem_input: QchemInput) ->
     orbitals = [10, 8, 12, 8]  # Unsorted, with duplicates
     expected_orbitals = [8, 10, 12]
 
-    inp_reduced = inp_tddft.set_reduced_excitation_space(solute_orbitals=orbitals)
+    inp_reduced = inp_tddft.set_reduced_excitation_space(initial_orbitals=orbitals)
 
     assert inp_reduced.run_tddft
     assert inp_reduced.reduced_excitation_space
-    assert inp_reduced.solute_orbitals == expected_orbitals
+    assert inp_reduced.initial_orbitals == expected_orbitals
     assert inp_reduced is not inp_tddft  # Immutability
 
 
 def test_set_reduced_excitation_space_tddft_not_enabled(default_qchem_input: QchemInput) -> None:
     """Test error if setting reduced excitation space before TDDFT is enabled."""
     with pytest.raises(ConfigurationError, match="Reduced excitation space .TRNSS. requires TDDFT to be enabled"):
-        default_qchem_input.set_reduced_excitation_space(solute_orbitals=[1, 2, 3])
+        default_qchem_input.set_reduced_excitation_space(initial_orbitals=[1, 2, 3])
 
 
 def test_set_reduced_excitation_space_empty_orbitals(default_qchem_input: QchemInput) -> None:
-    """Test error if solute_orbitals list is empty."""
+    """Test error if initial_orbitals list is empty."""
     inp_tddft = default_qchem_input.set_tddft(nroots=3)
-    with pytest.raises(ValidationError, match="The solute_orbitals list cannot be empty"):
-        inp_tddft.set_reduced_excitation_space(solute_orbitals=[])
+    with pytest.raises(ValidationError, match="The initial_orbitals list cannot be empty"):
+        inp_tddft.set_reduced_excitation_space(initial_orbitals=[])
 
 
 @pytest.mark.parametrize(
     "invalid_orbitals, error_msg",
     [
-        ([1, 0, 2], "All solute_orbitals must be positive integers."),
-        ([1, -5, 2], "All solute_orbitals must be positive integers."),
-        ([1, 2.5, 3], "All solute_orbitals must be positive integers."),
-        (["a", 2, 3], "All solute_orbitals must be positive integers."),
+        ([1, 0, 2], "All initial_orbitals must be positive integers."),
+        ([1, -5, 2], "All initial_orbitals must be positive integers."),
+        ([1, 2.5, 3], "All initial_orbitals must be positive integers."),
+        (["a", 2, 3], "All initial_orbitals must be positive integers."),
     ],
 )
 def test_set_reduced_excitation_space_invalid_orbital_values(
     default_qchem_input: QchemInput, invalid_orbitals: list, error_msg: str
 ) -> None:
-    """Test error if solute_orbitals list contains non-positive or non-integer values."""
+    """Test error if initial_orbitals list contains non-positive or non-integer values."""
     inp_tddft = default_qchem_input.set_tddft(nroots=3)
     with pytest.raises(ValidationError, match=error_msg):
-        inp_tddft.set_reduced_excitation_space(solute_orbitals=invalid_orbitals)
+        inp_tddft.set_reduced_excitation_space(initial_orbitals=invalid_orbitals)
 
 
 def test_get_rem_block_tddft_reduced_excitation(helpers, default_qchem_input: QchemInput) -> None:
     """Test $rem block with TDDFT and reduced excitation space."""
-    inp = default_qchem_input.set_tddft(nroots=5).set_reduced_excitation_space(solute_orbitals=[8, 10, 11])
+    inp = default_qchem_input.set_tddft(nroots=5).set_reduced_excitation_space(initial_orbitals=[8, 10, 11])
 
     expected_rem_dict = {
         # Base from default_qchem_input
@@ -97,7 +97,7 @@ def test_get_rem_block_tddft_reduced_excitation(helpers, default_qchem_input: Qc
 
 def test_get_solute_block_active(default_qchem_input: QchemInput) -> None:
     """Test $solute block generation when TRNSS is active."""
-    inp = default_qchem_input.set_tddft(nroots=3).set_reduced_excitation_space(solute_orbitals=[7, 8, 9])
+    inp = default_qchem_input.set_tddft(nroots=3).set_reduced_excitation_space(initial_orbitals=[7, 8, 9])
     expected_block = """$solute
 7 8 9
 $end"""
@@ -117,7 +117,7 @@ def test_export_input_file_tddft_reduced_excitation(
     """Test exporting an input file with TDDFT and reduced excitation space."""
     inp = (
         default_qchem_input.set_tddft(nroots=3)
-        .set_reduced_excitation_space(solute_orbitals=[4, 5])
+        .set_reduced_excitation_space(initial_orbitals=[4, 5])
         .set_basis("def2-svp")  # Use a slightly more realistic basis
     )
 
@@ -173,7 +173,7 @@ def test_export_input_file_mom_tddft_reduced_excitation(
         unrestricted_input.enable_mom()
         .set_mom_transition("HOMO->LUMO")  # Sets up MOM for H2O (5 alpha, 5 beta)
         .set_tddft(nroots=2)  # Enable TDDFT for the second job
-        .set_reduced_excitation_space(solute_orbitals=[4, 5])  # Reduce excitation space for TDDFT
+        .set_reduced_excitation_space(initial_orbitals=[4, 5])  # Reduce excitation space for TDDFT
         .set_basis("def2-tzvp")  # Use a different basis for clarity
     )
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>scripts/create-parse-qchem.py (1)</summary><blockquote>

`121-128`: _⚠️ Potential issue_

**Remove stray `breakpoint()`**

A hardcoded `breakpoint()` halts execution in non-interactive environments (CI, batch runs).  Replace with a debug log or guard it with an explicit `--debug` flag.

```diff
-    print(mom_pc2.job2.tddft)
-    breakpoint()
+    print(mom_pc2.job2.tddft)
+    # breakpoint()   # Uncomment for interactive debugging
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>scripts/create-parse-qchem.py (1)</summary><blockquote>

`40-45`: **Default run-flags trigger heavy parsing unconditionally**

Turning `"mom-xas-parse"` on by default will parse large output files whenever the script is executed, which may be surprising for users who simply import or run this helper. Consider leaving all tasks disabled by default or adding a CLI/`argparse` switch instead of hard-coding boolean literals.

</blockquote></details>
<details>
<summary>README.md (2)</summary><blockquote>

`17-32`: **Well-structured Key Features section**

The Key Features section is effectively organized into user-facing benefits and internal design qualities, making it easy for different audiences to find relevant information.

Consider adding missing commas in line 22: "...how you think about them, and calcflow will handle..."

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~22-~22: Possible missing comma found.
Context: ...culation parameters how you think about them and calcflow will handle the translatio...

(AI_HYDRA_LEO_MISSING_COMMA)

---

[typographical] ~26-~26: If the word ‘what’ starts a question, add a question mark at the end of the sentence.
Context: ... (analysis).  what's nice under the hood:  - **Immutable & Fluent API**: ensures ...

(WP_VB_QUESTION_MARK)

</details>

</details>

---

`94-104`: **Missing language specifier in fenced code block**

The fenced code block should specify a language for proper syntax highlighting.

```diff
-```
+```qchem
 $rem
     TRNSS = TRUE
     TRTYPE = 3
     N_SOL = 1
 $end
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

94-94: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4d2167ab22f46d2e0ae20b2dfc6749e643837f62 and c76a7eea71f3039f59db7b7d181e58de7cdf70fe.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `README.md` (1 hunks)
* `scripts/create-parse-qchem.py` (2 hunks)
* `src/calcflow/inputs/qchem.py` (14 hunks)
* `tests/inputs/qchem/test_input_qchem_init.py` (2 hunks)
* `tests/inputs/qchem/test_input_qchem_mom.py` (2 hunks)
* `tests/inputs/qchem/test_input_qchem_setters.py` (1 hunks)
* `tests/inputs/qchem/test_input_qchem_xas.py` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>tests/inputs/qchem/test_input_qchem_setters.py (3)</summary><blockquote>

<details>
<summary>src/calcflow/exceptions.py (2)</summary>

* `ValidationError` (13-16)
* `ConfigurationError` (25-28)

</details>
<details>
<summary>tests/inputs/qchem/conftest.py (1)</summary>

* `default_qchem_input` (26-34)

</details>
<details>
<summary>src/calcflow/inputs/qchem.py (2)</summary>

* `QchemInput` (28-789)
* `set_reduced_excitation_space` (451-493)

</details>

</blockquote></details>
<details>
<summary>tests/inputs/qchem/test_input_qchem_mom.py (3)</summary><blockquote>

<details>
<summary>src/calcflow/inputs/qchem.py (2)</summary>

* `_generate_occupied_block` (290-366)
* `set_mom_transition` (179-234)

</details>
<details>
<summary>tests/inputs/qchem/conftest.py (2)</summary>

* `h2o_geometry` (15-22)
* `mom_enabled_input` (38-40)

</details>
<details>
<summary>src/calcflow/exceptions.py (2)</summary>

* `NotSupportedError` (7-10)
* `ConfigurationError` (25-28)

</details>

</blockquote></details>
<details>
<summary>tests/inputs/qchem/test_input_qchem_init.py (2)</summary><blockquote>

<details>
<summary>tests/inputs/qchem/conftest.py (1)</summary>

* `default_qchem_input` (26-34)

</details>
<details>
<summary>src/calcflow/inputs/qchem.py (2)</summary>

* `QchemInput` (28-789)
* `copy` (120-126)

</details>

</blockquote></details>
<details>
<summary>src/calcflow/inputs/qchem.py (3)</summary><blockquote>

<details>
<summary>src/calcflow/inputs/orca.py (1)</summary>

* `copy` (275-281)

</details>
<details>
<summary>src/calcflow/exceptions.py (4)</summary>

* `ValidationError` (13-16)
* `NotSupportedError` (7-10)
* `ConfigurationError` (25-28)
* `InputGenerationError` (19-22)

</details>
<details>
<summary>src/calcflow/geometry/static.py (2)</summary>

* `total_nuclear_charge` (124-145)
* `get_coordinate_block` (156-163)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>README.md</summary>

[uncategorized] ~22-~22: Possible missing comma found.
Context: ...culation parameters how you think about them and calcflow will handle the translatio...

(AI_HYDRA_LEO_MISSING_COMMA)

---

[typographical] ~26-~26: If the word ‘what’ starts a question, add a question mark at the end of the sentence.
Context: ... (analysis).  what's nice under the hood:  - **Immutable & Fluent API**: ensures ...

(WP_VB_QUESTION_MARK)

---

[style] ~121-~121: ‘on top of that’ might be wordy. Consider a shorter alternative.
Context: ... moved to LUMO and TDDFT from orbital 1 on top of that.  fun fact: because water_molecule is a...

(EN_WORDINESS_PREMIUM_ON_TOP_OF_THAT)

---

[uncategorized] ~123-~123: You might be missing the article “the” here.
Context: ...HOMO and LUMO orbitals. You can specify same transition numerically `5->6` or even s...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~143-~143: A comma might be missing here.
Context: ...s.out").read_text()) ```  And just like that you have access to all relevant results...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[uncategorized] ~143-~143: You might be missing the article “the” here.
Context: ...d just like that you have access to all relevant results.  ```py > mom_pc2.job2 Calculat...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>README.md</summary>

94-94: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

</details>

</details>
<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>src/calcflow/inputs/qchem.py</summary>

[warning] 594-594: src/calcflow/inputs/qchem.py#L594
Added line #L594 was not covered by tests

---

[warning] 741-741: src/calcflow/inputs/qchem.py#L741
Added line #L741 was not covered by tests

---

[warning] 782-783: src/calcflow/inputs/qchem.py#L782-L783
Added lines #L782 - L783 were not covered by tests

---

[warning] 789-789: src/calcflow/inputs/qchem.py#L789
Added line #L789 was not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>src/calcflow/inputs/qchem.py (1)</summary>

`120-127`: **`copy()` duplicates correctly but is redundant with dataclass utilities**

`deepcopy` works, yet `dataclasses.replace(self)` achieves the same for an immutable (frozen) dataclass and avoids the performance overhead of recursively copying immutable primitives.

This is optional, but switching keeps implementation minimal.

[ suggest_optional_refactor ]

</details>
<details>
<summary>tests/inputs/qchem/test_input_qchem_setters.py (1)</summary>

`101-108`: **Renaming looks consistent**

Parameterisation now uses `initial_orbitals`; assertions and error expectations were updated accordingly.  Tests still cover happy-path and error branches—good job!

</details>
<details>
<summary>tests/inputs/qchem/test_input_qchem_mom.py (3)</summary>

`132-133`: **Updated error message for better clarity**

The error message has been improved to more precisely describe the limitation of MOM occupation determination when using symbolic transitions or the 'GROUND_STATE' marker. This is consistent with the effective_charge handling in the implementation.

---

`139-140`: **Consistent error message for the triplet case**

The error message matches the one used for the charged system case, ensuring consistency across different validation scenarios.

---

`430-450`: **Test parameters updated for clarity and new error handling**

The parameterized test cases have been updated to include more descriptive error messages and handle new scenarios including:
- Non-closed-shell singlet references (lines 438-439)
- Systems with odd total electrons (line 441)
- Systems with insufficient electrons for MOM calculations (line 446)

The formatting directives (`fmt:off/on`) provide better readability for the parameter table.

</details>
<details>
<summary>tests/inputs/qchem/test_input_qchem_xas.py (3)</summary>

`25-29`: **Attribute renamed from `solute_orbitals` to `initial_orbitals`**

The parameter name has been changed from `solute_orbitals` to `initial_orbitals` to better represent its purpose in reduced excitation space calculations. This is part of a consistent renaming throughout the codebase.

---

`42-43`: **Updated error messages to use the new attribute name**

Error message strings have been updated to use `initial_orbitals` instead of `solute_orbitals`, maintaining consistency with the renamed attribute in the `QchemInput` class.



Also applies to: 49-52

---

`66-66`: **Method call parameters updated to use the new attribute name**

All method calls using `solute_orbitals` have been updated to use `initial_orbitals`, ensuring consistent use of the renamed attribute throughout the tests.



Also applies to: 100-100, 120-120, 176-176

</details>
<details>
<summary>tests/inputs/qchem/test_input_qchem_init.py (2)</summary>

`149-176`: **Renamed attribute in validation tests**

The attribute has been consistently renamed from `solute_orbitals` to `initial_orbitals` in all test parameters, function names, docstrings, and initializations for validation tests. This maintains consistency with the changes in the `QchemInput` class.

---

`179-225`: **Added test for new `copy()` method**

A comprehensive test has been added to verify the behavior of the new `copy()` method for `QchemInput`. The test confirms:
1. The copy is a distinct instance from the original
2. The copy is equal in value to the original
3. Modifications to mutable attributes (lists, dictionaries) in the copy don't affect the original

This properly tests the deep copy behavior for a class with mutable attributes.

</details>
<details>
<summary>README.md (4)</summary>

`11-16`: **Strong introduction with clear value proposition**

The README now starts with a clear tagline that immediately communicates the purpose of the library. The introduction emphasizes key differentiators (zero dependencies, Pythonic interface) and includes an appropriate warning about pre-release status.

---

`33-64`: **Clear Quick Start example with step-by-step instructions**

The Quick Start guide provides a complete, working example that demonstrates the core functionality. The code is well-commented with clear sections for geometry setup, input configuration, and file export.

---

`66-130`: **Progressive examples demonstrate increasing complexity**

The examples build on each other to show more advanced features like:
1. Modifying TDDFT settings
2. Using element-specific basis sets
3. Setting up reduced excitation space
4. Configuring MOM calculations

These examples effectively demonstrate how the API abstracts complex quantum chemistry concepts into intuitive method calls.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~121-~121: ‘on top of that’ might be wordy. Consider a shorter alternative.
Context: ... moved to LUMO and TDDFT from orbital 1 on top of that.  fun fact: because water_molecule is a...

(EN_WORDINESS_PREMIUM_ON_TOP_OF_THAT)

---

[uncategorized] ~123-~123: You might be missing the article “the” here.
Context: ...HOMO and LUMO orbitals. You can specify same transition numerically `5->6` or even s...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

94-94: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

</details>

</details>

---

`131-175`: **Parsing examples with concrete results**

The parsing examples show both how to extract data and what the output structures look like, which is valuable for users. The inclusion of specific use cases (getting excitation energies, oscillator strengths, Mulliken populations) demonstrates practical applications.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~143-~143: A comma might be missing here.
Context: ...s.out").read_text()) ```  And just like that you have access to all relevant results...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[uncategorized] ~143-~143: You might be missing the article “the” here.
Context: ...d just like that you have access to all relevant results.  ```py > mom_pc2.job2 Calculat...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-22T01:42:36Z

Review comments:
diff_hunk: @@ -295,15 +306,22 @@
         if not self.unrestricted:
             raise ConfigurationError("MOM requires unrestricted=True")
 
+        # Determine the charge to use for electron counting in this block
+        # If mom_job2_charge is set, it means we are generating the $occupied block
+        # for the *second* job of a MOM calculation, which might have a different charge.
+        effective_charge = self.mom_job2_charge if self.mom_job2_charge is not None else self.charge
+
         # For MOM targeting ground state or excited states, we currently only support
         # closed-shell singlet reference states for determining occupations.
-        if self.charge != 0 or self.spin_multiplicity != 1:
+        # This validation now uses the effective_charge for the current job.
+        if self.mom_transition is not None and (effective_charge != 0 or self.spin_multiplicity != 1):
             raise NotSupportedError(
-                "MOM occupation determination currently only supports closed-shell singlet reference states. "
-                f"Got charge={self.charge}, multiplicity={self.spin_multiplicity}."
+                "MOM occupation determination via symbolic transition or 'GROUND_STATE' marker "
+                f"currently only supports a closed-shell singlet reference state for the job where occupations are determined. "
+                f"Effective charge for this job = {effective_charge}, multiplicity = {self.spin_multiplicity}."
             )
 
-        total_electrons = geometry.total_nuclear_charge - self.charge
+        total_electrons = geometry.total_nuclear_charge - effective_charge
 
path: src/calcflow/inputs/qchem.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Bug: `total_nuclear_charge` is used as an attribute instead of a method**

`Geometry.total_nuclear_charge` is defined as a regular method (see `src/calcflow/geometry/static.py`).  
Subtracting an `int` from a *method object* will raise `TypeError` at runtime and break MOM input generation.

In the same block, the error message prints `self.charge`, which may differ from the `effective_charge` used for the calculation, potentially confusing users when `mom_job2_charge` is set.

```diff
-        effective_charge = self.mom_job2_charge if self.mom_job2_charge is not None else self.charge
+        effective_charge = (
+            self.mom_job2_charge if self.mom_job2_charge is not None else self.charge
+        )

-        total_electrons = geometry.total_nuclear_charge - effective_charge
+        total_nuclear_charge = geometry.total_nuclear_charge()  # call the method
+        total_electrons = total_nuclear_charge - effective_charge
...
-                    f"(Nuclear charge: {geometry.total_nuclear_charge}, System charge: {self.charge})."
+                    f"(Nuclear charge: {total_nuclear_charge}, System charge: {effective_charge})."
```

Please update every occurrence of `geometry.total_nuclear_charge` in this file (there are a few in the surrounding lines) to invoke the method, and adjust error-message values accordingly.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        # Determine the charge to use for electron counting in this block
        # If mom_job2_charge is set, it means we are generating the $occupied block
        # for the *second* job of a MOM calculation, which might have a different charge.
        effective_charge = (
            self.mom_job2_charge if self.mom_job2_charge is not None else self.charge
        )

        # For MOM targeting ground state or excited states, we currently only support
        # closed-shell singlet reference states for determining occupations.
        # This validation now uses the effective_charge for the current job.
        if self.mom_transition is not None and (effective_charge != 0 or self.spin_multiplicity != 1):
            raise NotSupportedError(
                "MOM occupation determination via symbolic transition or 'GROUND_STATE' marker "
                f"currently only supports a closed-shell singlet reference state for the job where occupations are determined. "
                f"Effective charge for this job = {effective_charge}, multiplicity = {self.spin_multiplicity}."
            )

        total_nuclear_charge = geometry.total_nuclear_charge()  # call the method
        total_electrons = total_nuclear_charge - effective_charge
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/calcflow/inputs/qchem.py around lines 309 to 325, the code incorrectly
uses geometry.total_nuclear_charge as an attribute instead of calling it as a
method, which causes a TypeError. Update all instances of
geometry.total_nuclear_charge to geometry.total_nuclear_charge() to properly
invoke the method. Also, in the error message, replace self.charge with
effective_charge to accurately reflect the charge used in the calculation and
avoid user confusion.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T01:42:35Z
author_association: NONE
diff_hunk: @@ -730,6 +763,31 @@
 
         return f"{first_job.strip()}\n\n@@@\n\n{second_job.strip()}\n"
 
+    def set_mom_job2_charge(self: T_QchemInput, charge: int) -> T_QchemInput:
+        """Set a specific charge for the second job in a MOM calculation.
+
+        This charge will be used in the $molecule block of the second job,
+        allowing it to differ from the primary charge of the QchemInput object.
+        The spin multiplicity remains the same as the primary setting.
+
+        Args:
+            charge: The integer charge for the second MOM job.
+
+        Returns:
+            A new QchemInput instance with the second job charge set.
+
+        Raises:
+            ConfigurationError: If MOM is not enabled (run_mom=False).
+        """
+        if not self.run_mom:
+            raise ConfigurationError(
+                "MOM must be enabled (run_mom=True) via enable_mom() before setting a specific charge for the second MOM job."
+            )
+        logger.info(
+            f"Setting charge for second MOM job to: {charge}. This will affect the $molecule block and electron count for job 2."
+        )
+        return replace(self, mom_job2_charge=charge)
path: src/calcflow/inputs/qchem.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Input validation is too lenient for `set_mom_job2_charge`**

`charge` is accepted blindly; passing a non-integer (e.g. `"2"` or `None`) will slip through until a much later stage and yield cryptic errors.  A tiny defensive check keeps the public API self-consistent.

```diff
-    def set_mom_job2_charge(self: T_QchemInput, charge: int) -> T_QchemInput:
+    def set_mom_job2_charge(self: T_QchemInput, charge: int) -> T_QchemInput:
...
-        logger.info(
-            f"Setting charge for second MOM job to: {charge}. This will affect the $molecule block and electron count for job 2."
-        )
+        if not isinstance(charge, int):
+            raise ValidationError(f"Charge must be an integer, got {type(charge).__name__}.")
+
+        logger.info(
+            "Setting charge for second MOM job to: %s. "
+            "This will affect the $molecule block and electron count for job 2.",
+            charge,
+        )
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    def set_mom_job2_charge(self: T_QchemInput, charge: int) -> T_QchemInput:
        """Set a specific charge for the second job in a MOM calculation.

        This charge will be used in the $molecule block of the second job,
        allowing it to differ from the primary charge of the QchemInput object.
        The spin multiplicity remains the same as the primary setting.

        Args:
            charge: The integer charge for the second MOM job.

        Returns:
            A new QchemInput instance with the second job charge set.

        Raises:
            ConfigurationError: If MOM is not enabled (run_mom=False).
        """
        if not self.run_mom:
            raise ConfigurationError(
                "MOM must be enabled (run_mom=True) via enable_mom() before setting a specific charge for the second MOM job."
            )
        if not isinstance(charge, int):
            raise ValidationError(f"Charge must be an integer, got {type(charge).__name__}.")
        logger.info(
            "Setting charge for second MOM job to: %s. "
            "This will affect the $molecule block and electron count for job 2.",
            charge,
        )
        return replace(self, mom_job2_charge=charge)
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 782-783: src/calcflow/inputs/qchem.py#L782-L783
Added lines #L782 - L783 were not covered by tests

---

[warning] 789-789: src/calcflow/inputs/qchem.py#L789
Added line #L789 was not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/calcflow/inputs/qchem.py around lines 766 to 789, the set_mom_job2_charge
method accepts the charge parameter without validating its type, which can lead
to confusing errors later. Add a type check at the start of the method to ensure
charge is an integer, and raise a clear TypeError if not. This will enforce
input consistency and provide immediate feedback to users of the API.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T01:42:35Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-22T01:38:23Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce a new `mom_job2_charge` attribute and related setter in the `QchemInput` class, update logic for charge handling in MOM calculations, and rename the `solute_orbitals` attribute to `initial_orbitals` throughout the codebase and related tests. The README is extensively expanded into a detailed user guide, and test cases are updated for naming consistency and new features.

## Changes

| File(s)                                                                                   | Change Summary |
|-------------------------------------------------------------------------------------------|---------------|
| README.md                                                                                 | Rewritten and expanded with a detailed introduction, feature list, quick start guide, code examples, parsing instructions, contribution guidelines, and licensing information. |
| scripts/create-parse-qchem.py                                                            | Changed default parsing flags for MOM XAS routines and added a `breakpoint()` for debugging. |
| src/calcflow/inputs/qchem.py                                                             | Added `mom_job2_charge` attribute and setter, renamed `solute_orbitals` to `initial_orbitals`, updated charge logic for MOM, added a `copy()` method, and adjusted input generation methods accordingly. |
| tests/inputs/qchem/test_input_qchem_init.py                                              | Renamed test parameters from `solute_orbitals` to `initial_orbitals`; added a test for the new `copy()` method ensuring deep copy behavior. |
| tests/inputs/qchem/test_input_qchem_mom.py                                               | Updated error message expectations for MOM occupation determination in non-singlet references. |
| tests/inputs/qchem/test_input_qchem_setters.py                                           | Renamed test parameters and assertions from `solute_orbitals` to `initial_orbitals`. |
| tests/inputs/qchem/test_input_qchem_xas.py                                               | Consistently renamed all references from `solute_orbitals` to `initial_orbitals` in tests and assertions. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant QchemInput
    participant FileSystem

    User->>QchemInput: Create instance (with charge, initial_orbitals, etc.)
    User->>QchemInput: Optionally call set_mom_job2_charge(charge)
    User->>QchemInput: Optionally call set_reduced_excitation_space(initial_orbitals)
    User->>QchemInput: Call export_input_file()
    QchemInput->>FileSystem: Write Q-Chem input file (handles mom_job2_charge if set)
```

## Possibly related PRs

- batistagroup/calcflow#16: Introduces and tests the `"mom-xas-parse"` parsing block in `scripts/create-parse-qchem.py`, directly related to the default flag changes and debugging additions in this PR.
- batistagroup/calcflow#14: Adds parsing support for MOM-enabled Q-Chem outputs and enhances SCF parsing with MOM metadata, closely related to the input handling and MOM charge management changes here.

## Suggested labels

`tests`

## Poem

> 🐇  
> In the garden of code, new seeds are sown,  
> "Solute" becomes "initial," as clarity has grown.  
> Charges now may differ, MOM jobs set apart,  
> The README tells the story, a guide to every heart.  
> With tests that copy deeply, our bunnies leap with glee—  
> CalcFlow’s future blossoms, as tidy as can be!  
> 🌱

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEejqANiS4kAHmmbcryfADNI2RCRRYKJNLRsADQK3PJsuLCK/FjwGNx4iCFoFhb4AO6MsJhEcURZaBSkkK74fF5MGPTMLJBCggYActjMApRcAEwAjAYAql4UXJg1RdgY+BJG+sbgUGT0bjgExGTKNPRMrOxcvPzCouJSMvJMSlSq6lo60yZQcKiomEuEpORU6wpbGJyQVJmILWYhXkcgUZxUak02l0YEMM1MBgASgBRACCABEALLIjTMWgcAwAImJBgAxKTIKiAJIrN7UOiQAGsYH8dwMbIYUiIMywbwojHY3H0dJoZCbJwkGgWeR+dIUdQ0LCYeh2bjKhmuCi1NCQATykjuRB5KxgCxxbxKRAMeXccT4WLffDoT68Ei8jBGqQhRC4CjYMTYPz0TyUSBEbDwJQlMqQSLeADCKQYADE0pk1QwANZoUgaGC8yDkTKVRW4Hy+xT+6TOgT4Cz0XA5s3kdBVZ1KRvwKwLKQUCTwEiZEiObJGgBeeUgY8oTrsNAoGBSkCU3HmZAYA6SzoACrJIvb4AxW/RXBZsOxKduqd6K5zY7JuHkQrx8GgxJJvHEEmWJClI9R4HtZ8tSIKhmDAHNxh9Q9l2keAiAwEI5zII01DNXBZGSNtxT8d1PW8NUKCNO8GDQNU0PUTc81RRgrEKSARQXScVHwPBYwLFJuGyHg/DAPw6K8RlG1wTwGNFdBaCUWgNCMGjCQAaRIeRk38ES/EQQlGQOQCsBFZAAikkJaEkSNJ1cVTA2rOICA8AYwFcN9JzachXHUZAAAppy1WDVyqddNxCayI0OT9vkoByGBIEJfzNWgAKAnhCmIogAEpj3LShFwsWCjQQyAAEdsD/cRq3c+BWDwFQu3UTD71XVtxmEnSt2Qj14AojCQhoaDORS6jYM7btIEJABFCMs0gABlRsKFwTTw0jC1h3tH13mrLxcHEO9sG4SARrAeNeWYGB0XRZNoEYJNsAseKPQY9RYEgOwHAlSBd33LBTiinwGDPYy71IFhJQoeQlFcjBKISr82MqVzw3eHSQlPc9vkvKlbJzb7lR8b8Si7bxVTKTRIExa7xFe57HGcFBUk8X16Qk38MEi4NBNIrxkABdl0GQX14AlMsfXpFqrDYb4wEQVcN1co8BFFVANq3IMqxVWwNya+1GQzaQQkxAB5TFLosBhrturdscl0R4FcWRJzKSEl3ptq7Q9GSDDgPk0SxZFCwyJ7bCcQ9LHkRAonSZAw/Y7xkYvVEr1jKgPRu7ryydfbDuHHG2NcqxdTSLNEDzKkyxSRAnSUGoPXplPCOSvaDqO/g8FxnPq3SB7/Ze3OvvSt9IsQOva4ZdSyeQM1MwJtX1Fup63lyHX+CtLtk5jVayCISJzbbUnUngCesG4fAEmT5q8wANUoI17QlqXrZg2vJ2M1xzL8Znq1KPh08bgBWDQABZ0oADYNAdHQH4f2ThtDkGFB3Smr0/CkFsIlTamVC6yUkpDLK0pkhaXfJrTWrpCKTkmmeCgx0AQCGYEgK+WArQ2lwMgdukQdx7iiBDI81lwpvk/MgOIv1sCGUZGHSckcbImx9CweA05IA1FoNdbwaQAjMTbGQfsWoMBix/IUeAKhqYf2XNbF+F4y6uFwIxEgeYPaQH5N7BQzM/ptw7nEVa/oXbIH0SWeUAg8DKPoDqI0VNvCYipBdM0kUPTeCFjQTRbs9a9hSBYLqBZ2Q5GrE7RAH9jpxmsV7bEJQtTHR1FQiGQJsqWmtPzF2qdnT7i1NgIgsBgJvnEKRbKIY+ALSjEwx6rV8Kd0CfpFRAcbqLjcQoXsk5oZlkIYUW6IRWK4Fxg/TkWF6D1AEIybAlDqE6UZBU20eQ3ZGHJJSCw84zaxidNkpQv1ZljMWITWaDIYwJAEGE2e4gSrciaPaCx+ZvApM5NWQoCjyrqAZDZbJNjBR4mXPgE2miZ6t3SrQJ0jV0DPwOK2E4iglq3IRitfgfBXnvLjujXKozLJoOJISKYCI6GVMQAAemtKpEgYAh5gHyuyYcGgwgEhpWSCk1JaRrAZEyIEINWQFCBd8qxYM0Bk11PgOs/gsCnhzGKe0FZd53nSAgbmyz8h1K2mkp0fpdKUG8LIu+dAuDZIAAaEhqOBewiAOVJRIISB1JQbr5D0jK0gJ4CmQAdcmUuJAfU2QddAP0kakKLjeZOSI1BEpEScvnTMupQYGkVWckI+r8ZR1Dc6lgYA3UeqIuy3+f9vW+pzGJMUHIg35NqDGuNUanRhojQ6oySBdHJuyNMpKGb4VZtBAqsm1EMEuwSbVHUDq9T+EzIfay7kUo+tadlANzjKAfFTdk+Y0rHWltdaKStXg61vLHYFLYxl6TSgxfOHi1lJxeHidlIeazBDoE2l4vAC8bJqhDP7UQPjNb6KUN4oguQ7wJAoIfDmeZGhOnwHGPgaRchHhjJ4usvq/aAq5AxK1MiAgWKMMc4VZy1jNUucWm5N0CV3QeQHImzziVbPeewSi0gjAofIDJQVBgIBgCMIgCgDAWVJlPBkJlUzmXcqOny2QAqSQnJFa8MV9AJUskWIR3j7sCxP3cNZLUcj+7OiLPwA59olzUD5t4mgoaXUAH11kdBcykooka6OOpGjy5gVJ4h4B9XFRstyB4hD8K6Lw3xB0AuyN56MfBskVHtN+jZcRnT60Nq0k2J97SWILK6fsrFECPsXGwegDqy5nhoC5+208LCIB9fZ/9Tnsb8XpPQQiDhJSXzAd4V+fWGxdriJRFIDWKAO2a1G2AdSGkLOLRFwZ1RJRRFoFuPhf1JwxX/FUrLDqXMucQ7gFz42zsud7cWjaz6IgbdDRtFzysWYubsOrW6LnJbcOu9jKZYZVhMZketxQHkjukDO34ZgLnr1Zmu+DyUX26wAZh5mh1fVIBnz/GF3ZlAtR8DYAPTGq24UMFWnkfS4CdphYZH3Mo/0iDSjdjRKzDqmBhB9fd6IjxJIQqdKyhmOolAkF2uz+QiwF3+aOkF78PrnGNjfm7ILxaEd0nq/Ck2j46Co7HZzkHtAkkKPwFh5LA1KDFPiwUJLIZ6BZZIFYMQ6iFBjC2vkVANrXJ89st4B1rn3OecS6QOXhpJQAG5+Dofbl4QKZYHKpBkG+LNkKCzy8wJFAA5MgNngfI1FceJisQyA9s481tje3BwndMBd0bfLFz9E5f4AwTXM8Ozm7iLdN2ViEdnZqA7+ROu4fA/3PQIgUDBnWdndlbPhRSANd7PKJQPresRFDDZBJftNMIzvAugAJL3sDVgfWw6zd050XnijGatajTUtRsmp7fpn0jWXz/kYMMrh1jyztTJc63a7+qyAA5q6W5pZtjrKm4N55amxuKG6hp76qr5Y+bH4oBapVCYJzqAFab3TMLZITB46RhKCfQ57OYsBuaCAeYv7B5aS4Dh5oa8gUBR7fTqCwQOSjy+bFZ+ClaiRtDZClZ8CLB+BKIAwkBAy+jyA35ZIFiuRERljrLM6FiDhD4PY1aI5+5kEB4z4+Y85SR0YbTLZEH6KpaiDpYkwGx1CCAJofwbh3gpplgN6oBkC6IMhtAfyRKSiu7sSoAv557IBc70B+BqR3Q6hWb36RRYGPTZLU7dbEHQ7+4UHM6pCBrVhAjWRQL+xIDuHF4zxl4UD46QAci0DNj5BqgoILgtS2CKgM5RxZJOh06sCKAMzZKs6qECDkE54+rmTUCWTpTZLDbFL5CLA1bI7q7TZNYtZ0YOoXY6IWBTYzYtZHJCqnLnJjLJ54qMYXIsaHxPILAcZJpHjcZfJTCUi85+J/rtQAbRGkEtHqHeZcDWSQAAA+kA/G3gAAvE8b8p2rRKKFnlLsODLiFj4I9hJlJsbDJukHJsFgwkyopryhzjJFAIiA4VVr+g5ucYMXViQDMaMVwGaD6AANrWQAC6Dx7x5AnxExEM4gk2jWjYzWOJ6RBJ3wxJjxzxcun0N0A8oavxgWkJDq8JRx2hvhoaYMoQsg7kXgFgrgXA0ALm3J/xuAaUsIMAspAW8pbJXxnJDqcpvJ/JmIigtqa2w+jI8ElK4CDqIpT2L22u7208LsX22s4p9uUpyp2p343oQxmJNJpc9J+JRJipegLpqpvJ4xFpiOVptAb2U8GsGA9p3Cjpkp0pKp0ukJgUlJUxWJtJiAPpuAjJuAhJ/pgZyZsugJK2XJQZsuup+pnuhpD2FKnRZpIpLmEOLm++CBA+mY8ZrgIQgMzAXAq0BZq05JjZzZrZ/ex+nZ3ZwhvZQkFAIQL+c+eBSgtxqMLJvykAbxzxA5vo6ppZWp5ZIW/JqIxxih0Q5pBoVBLZJBsROenZiZrpeAc5Oey5CpugAZMp95uAO5HJPx+5n5gmJIwmCI3UUJ8m0JAWTKwF52kJLmMJ0OF2ymqmtK6mNIm+UROmUqemzaBmViwFYI3gAakRHwNk0WN0oRCSjeJsOR641Yiw2Sy+/WfAlWPutWKOXps2YRoakx1JIxmZ6p2SJYc40q4Z/sH2VS32oRmRVSBhBYe5RZAJYWb4353hP0DiyAvRlu9Fz6WWuFrgYweCtCJp9ZC8mllAUiEK0gZY8mSEORMYhOiAmMECBwZsqyYC4YmiiUA8kyWAuF7M0gRWrhQIcWR4kluymGMEUOY+N2fWuobE4wmQM0EOWeXF0xbFYx8u/gCw7g6JrFPFpcfJ6Cxkk+2ClmChOlelVSDqkFsFUF34nmR8sgLZ+uPqAaBkXuEyNsxa3BaAvBx6MlYu66euRpmsslfxwZIRfl+YqAuFKEVK7EB6yS9VyB7Y6RfCllVcaeBMhUS4WWMU54bB3gZQ8E7eiS3RQ6MilUby3gbWZxKcXMj04kFJE2KVuV7F7kOouJL52Mi68siAX2koPqb1BiYgaUIKsEIuj67OA40k/ysYFldiUhzAvMZ1Huh4FyKxgkzAF1ucnu9YvC3lC1Wutu61b8cKvsJcBeZYOB8osG21xNkUBaCAuctBoY5UmNjYl1RKeyKQ9E11jm1YEVduW1Fgbs6m1GTGvMVyRmogax9y7gn+7GPAnGME+xm4hxiJTFPWsyK+fA2lcNulzMLsXAlVFlMFAWNVeAJ2+APoUF6gLmvoHov1cQxeSOGJGZpc4pHpbtzWSEAcBw1pNlFALZ1A7IG6kAgASYShpVVm3f6nY21nb20DxQXO0sXDGzFlRpncWzE+1SzrBvYB1B24Ah3qnG0+jMqgWwUQUm3f7VXwUc7CYCkMhWZlUG06RG0ilR1HTm1nZi6NXD7uSTpnKm2d1TJcAfkFmsmAkl0gWQkKbgWQXV3R2UnKYOpEgAUiZGDAVl0z1gVHSV3W0L2d0uoIWr1IXCooWA5oWAi6ZshYVyoFjTUB3A5E7FDk6cjuIxgOooa4CTQ7SbHrDIgB0+pUBIAMhyLyh3gN4a47Qt4MXFIzxZbjAYBgC/RW10ASy8gJHJRWBlh+BGIk1RJtwkaEVe5FJlBLSDS05tiWzSwwR2UOVxDGSkTuE2HFpmhULRnoDcCBxmqMiyCtB1gwTpKYIuApYFjp4ADiiIesvQjQ6ILmk00AqI0AyI6epGFAE8fA2M2SYB/+4CUDxRtGoNreZC5o0N8YZQ6kh8qB1hcNvleNkdJtqF6uTeO0UN7ZLmiDX2xo/1p1BEWtDFZlDYFl9jU9TZF9zjmubjx+l50OoErEVQX2wkPm6QJGKNXuqoWKkQqAVmeOtl0g9luYApaBqQtUxSMYmS9mk4+mjCqTZG+1aafW84gTsNPol0HMpu+KNUqZ/ClRlQtW+2VTLAmiJOf4CEluuTBO+TDlr9RAhc7xiRNT4CQIUYKxkAYV2GfAuG2UYJvV3gm9VREpRw8xotSxtGqzDGdytGGxbG2xituxHyPG3yzx/5tKgFomBgm9EJ34s9u9890F1Vt2l8x9gqyFoqa02mV9GFN9qSd9ezcNJljF0V2MTAVFks6WzEow7lTF9jjqkFlpdAKskZolOksZkU7R5VuyKT4C+mwabaKdnpL1Yx0ayVXtcxk1alSJk4LVXDZoDIfT6R7Aj62lxW/jTTY4MDKL1AZQMBzd+lxpCERlLl5FW6Etj2YZBLr2Np0ZpLyTD0wrcLPo06hVOkc6MB3xe6uyPKWYluvN5xmsPRkogY0CewIgYgjaHg3ANOtLx02Vqdox5JLLqVfJMNuF6z1luR+RhRLlWz+GmQAtWAelWFtANBGAj6d+BB4gnuiLfR7raTo2vwBoDuJcBimKr8w6YEDFhYfWxzVGpzhK5z0tlzhK1zWxnNJKytcWBx9dKGCz9TFz4t0q8ttz7bexnbm4xGSzZGLzdK7znz5dc9Vd/zZtbqwLamZ9YLl9zIULCzPIBrgswU0cMY2SH5Bb5mDIWrM84l0cFLtm6E8gBFnrURxFSJezIrFbz62MtrTm4hj2ntQb4xgbjLm6K0Ar3wj6b4WonJ5Fm9yl22Ai1Y1TgJ+tcrKr4beTz91qwdvI5sA8FrK0LlqKZOfMb9/lLTDCGK5kYgfLuBfAxeVB7h/BGr57UZl72saHfAkbXL7oJ0Z0F02TaGs8jhBuT0jgGERKM2az6RkAIVUMGAdHbF0nKQ54Re2OcyvjGBQOiwX8mc/2x+YOO+UOrWbYDqO+9L6ODE3HwlF7Yl2sS1zSH4pH6ld4qK1Y6KKQz62S6znNMbOzjmHrNO+kCRuDV+Fmz7p4mTBYVmCLVbbAyG/Ot9E7mHKzkte7azxuh47Hdixr+HnNGTVHCwzciQuoSkxhWTHLlWhyFGCxqIYtaNKXsE+K6xctrGrbLySto7nyqtPyAmJ9dKcItws8mVzwG7HwmwmiXAfwmyW7IIOK4IFwUI1wsIBgA3Y3ttkYv1HBA4KTEZQss0MI/XswjAAA7IAmgEdyQP4Ed10K4AAMwAAMN3AAnK4N/I97QAIEdx97QF0AABxdAkDfw/dKBHcMC0CuBHd3fmT7fwhQCrdf6bbPYkD9iDjWltj6BAA= -->

<!-- internal state end -->
<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c76a7eea71f3039f59db7b7d181e58de7cdf70fe and 2d01cd4fe84ff1685f5cb26dd8730c8a1daa1432.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `README.md` (1 hunks)

</details>

</details>
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=batistagroup/calcflow&utm_content=21):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-22T01:39:05Z
body: ## [Codecov](https://app.codecov.io/gh/batistagroup/calcflow/pull/21?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup) Report
Attention: Patch coverage is `84.37500%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 94.28%. Comparing base [(`4d2167a`)](https://app.codecov.io/gh/batistagroup/calcflow/commit/4d2167ab22f46d2e0ae20b2dfc6749e643837f62?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup) to head [(`2d01cd4`)](https://app.codecov.io/gh/batistagroup/calcflow/commit/2d01cd4fe84ff1685f5cb26dd8730c8a1daa1432?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/batistagroup/calcflow/pull/21?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup) | Patch % | Lines |
|---|---|---|
| [src/calcflow/inputs/qchem.py](https://app.codecov.io/gh/batistagroup/calcflow/pull/21?src=pr&el=tree&filepath=src%2Fcalcflow%2Finputs%2Fqchem.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup#diff-c3JjL2NhbGNmbG93L2lucHV0cy9xY2hlbS5weQ==) | 84.37% | [5 Missing :warning: ](https://app.codecov.io/gh/batistagroup/calcflow/pull/21?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master      #21      +/-   ##
==========================================
- Coverage   94.36%   94.28%   -0.09%     
==========================================
  Files          41       41              
  Lines        4063     4076      +13     
==========================================
+ Hits         3834     3843       +9     
- Misses        229      233       +4     
```

| [Files with missing lines](https://app.codecov.io/gh/batistagroup/calcflow/pull/21?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup) | Coverage Δ | |
|---|---|---|
| [src/calcflow/inputs/qchem.py](https://app.codecov.io/gh/batistagroup/calcflow/pull/21?src=pr&el=tree&filepath=src%2Fcalcflow%2Finputs%2Fqchem.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=batistagroup#diff-c3JjL2NhbGNmbG93L2lucHV0cy9xY2hlbS5weQ==) | `96.10% <84.37%> (-1.09%)` | :arrow_down: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE