From 0af0d7c4ca89fef3c41e77ecd8dcd6918d20e62f Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 09:46:15 -0400 Subject: [PATCH 01/12] Support external-potential MBE reporting --- qcmanybody/core.py | 7 +++++++ qcmanybody/tests/test_utils.py | 24 ++++++++++++++++++++++++ qcmanybody/utils.py | 31 ++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/qcmanybody/core.py b/qcmanybody/core.py index a89af67..30892f1 100644 --- a/qcmanybody/core.py +++ b/qcmanybody/core.py @@ -526,6 +526,8 @@ def _analyze( def analyze( self, component_results: Dict[str, Dict[str, Union[float, np.ndarray]]], + *, + external_potential: bool = False, ): """ @@ -548,6 +550,9 @@ def analyze( '["mp2", [2], [2]]': {'energy': -2.86, 'gradient': array([[0., 0., 0.]])}, '["mp2", [1, 2], [1, 2]]': {'energy': -5.73, 'gradient': array([[ 0., 0., 0.0053], [ 0., 0., -0.0053]])}, } + external_potential + Whether the component calculations use fragment-scoped external potentials. Interaction energies are + unavailable in the printed energy summary because the component Hamiltonians differ. """ @@ -602,6 +607,7 @@ def analyze( is_embedded, self.supersystem_ie_only, self.max_nbody if self.has_supersystem else None, + external_potential=external_potential, ) for property_label in available_properties: @@ -615,6 +621,7 @@ def analyze( is_embedded, self.supersystem_ie_only, self.has_supersystem, + external_potential=external_potential, ) ) diff --git a/qcmanybody/tests/test_utils.py b/qcmanybody/tests/test_utils.py index 75cdd06..5003503 100644 --- a/qcmanybody/tests/test_utils.py +++ b/qcmanybody/tests/test_utils.py @@ -136,6 +136,30 @@ def test_resize_gradient(gin, bas, reverse, gans): assert compare_values(gans, gout, atol=1e-5, label="resize_gradient") +def test_print_nbody_energy_external_potential_without_total_energy(): + stdout = qcmb.utils.print_nbody_energy( + {1: 0.0, 2: 0.005150000059}, + "Counterpoise Corrected (CP)", + 2, + {1: ("model", "§A"), 2: ("model", "§A")}, + embedding=False, + supersystem_ie_only=False, + supersystem_beyond=None, + external_potential=True, + ) + + rows = [line.split() for line in stdout.splitlines() if "§A" in line and "N/A" in line] + assert len(rows) == 2 + assert all(row[-5:-2] == ["N/A", "N/A", "N/A"] for row in rows) + assert "nan" not in stdout.lower() + + properties = qcmb.utils.collect_vars( + "cp", "energy", {1: 0.0, 2: 0.005150000059}, 2, external_potential=True + ) + assert "cp_corrected_2_body_contribution_to_energy" in properties + assert not any("interaction" in name for name in properties) + + @pytest.mark.parametrize("hin,bas,reverse,hans", [ pytest.param(f3hesses["full"], [1, 2, 3], False, f3hesses["full"]), # idempotent pytest.param(f3hesses["full"], [1, 2, 3], True, f3hesses["full"]), # idempotent diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index 41677f2..bb3ed41 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -322,6 +322,7 @@ def print_nbody_energy( embedding: bool, supersystem_ie_only: bool, supersystem_beyond: Optional[int], + external_potential: bool = False, ) -> str: """Format summary string for energies of a single bsse_type. Logs and returns output. @@ -338,11 +339,13 @@ def print_nbody_energy( full model chemistry key and second element a short label. A suitable dictionary is `modelchem_labels(manybodycore_instance.nbodies_per_mc_level)`. embedding - Whether charge embedding present suppress printing, usually False + Whether charge embedding is present and suppresses interaction energies, usually False. supersystem_ie_only Whether only 1-body and nfragments-body levels are available, usually False. supersystem_beyond If not None, the number of nbody-levels computed by MBE explicitly. Beyond this gets supersystem SS label. + external_potential + Whether fragment-scoped external potentials are present and suppress interaction energies, usually False. Returns ------- @@ -385,7 +388,8 @@ def print_nbody_energy( if nb in nbody_range: delta_e = energy_body_dict[nb] - previous_e delta_e_kcal = delta_e * constants.hartree2kcalmol - if embedding: + interaction_data_unavailable = embedding or external_potential + if interaction_data_unavailable: int_e = np.nan int_e_kcal = np.nan else: @@ -393,17 +397,26 @@ def print_nbody_energy( int_e_kcal = int_e * constants.hartree2kcalmol if supersystem_ie_only and nb == nfragments: if tot_e: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":20} {"N/A":14} {"N/A":20} {"N/A":20}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" else: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":20} {"N/A":14} {"N/A":20} {"N/A":20}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" else: if tot_e: - if embedding: + if interaction_data_unavailable: info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":20} {"N/A":14} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" else: info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" else: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":20} {"N/A":14} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" previous_e = energy_body_dict[nb] else: info += f""" {lbl:>11} {"":2} {nb:2} {"N/A":20} {"N/A":20} {"N/A":20} {"N/A":20} {"N/A":20}\n""" @@ -422,6 +435,7 @@ def collect_vars( embedding: bool = False, supersystem_ie_only: bool = False, has_supersystem: bool = False, + external_potential: bool = False, ) -> Dict: """From *body_dict*, construct data for ManyBodyResultProperties/ManyBodyProperties. @@ -445,6 +459,9 @@ def collect_vars( By default False: data is available for consecutive levels, up to max_nbody-body. has_supersystem Whether contributions higher than max_nbody are a summary correction. + external_potential + Whether fragment-scoped external potentials are present. Interaction properties are unavailable because the + component Hamiltonians differ. Returns ------- @@ -495,7 +512,7 @@ def collect_vars( for nb in nbody_range: res[f"{bsse}_corrected_total_{prop}_through_{nb}_body"] = body_dict[nb] - if embedding: + if embedding or external_potential: res = {k: v for k, v in res.items() if "interaction" not in k} return res From 70fa81dee2568ce763ed889b456f76ee38dd50ce Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 10:14:14 -0400 Subject: [PATCH 02/12] Address external-potential review findings --- qcmanybody/core.py | 5 +-- qcmanybody/tests/test_core_singlelevel.py | 20 +++++++++++ qcmanybody/utils.py | 41 +++++++++-------------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/qcmanybody/core.py b/qcmanybody/core.py index 30892f1..91353b2 100644 --- a/qcmanybody/core.py +++ b/qcmanybody/core.py @@ -551,8 +551,9 @@ def analyze( '["mp2", [1, 2], [1, 2]]': {'energy': -5.73, 'gradient': array([[ 0., 0., 0.0053], [ 0., 0., -0.0053]])}, } external_potential - Whether the component calculations use fragment-scoped external potentials. Interaction energies are - unavailable in the printed energy summary because the component Hamiltonians differ. + Whether the component calculations use fragment-scoped external potentials. Because the component + Hamiltonians differ, interaction energies are unavailable in the printed summary and interaction-energy + properties are omitted from the returned results. This classification is distinct from charge embedding. """ diff --git a/qcmanybody/tests/test_core_singlelevel.py b/qcmanybody/tests/test_core_singlelevel.py index b5a93a2..b6d4299 100644 --- a/qcmanybody/tests/test_core_singlelevel.py +++ b/qcmanybody/tests/test_core_singlelevel.py @@ -33,6 +33,26 @@ def test_h2o_trimer_single(levels, component_file, ref_file): compare_results(nbody_results, ref_data, levels) +def test_external_potential_reporting(): + levels = {1: "e_scf", 2: "e_scf", 3: "e_scf"} + component_results = load_component_data("h2o_trimer_single_energy_1") + mc = ManyBodyCore( + mol_h2o_3_dict, + [BsseEnum.cp], + levels, + embedding_charges=None, + supersystem_ie_only=False, + return_total_data=True, + ) + + nbody_results = mc.analyze(component_results, external_potential=True) + + assert "cp_corrected_2_body_contribution_to_energy" in nbody_results["results"] + assert not any("interaction" in name for name in nbody_results["results"]) + assert "N/A" in nbody_results["stdout"] + assert "nan" not in nbody_results["stdout"].lower() + + def test_core_mol_error(): # check sensible error from internal Molecule construction odd_mol = mol_h2o_3_dict.copy() diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index bb3ed41..e1d92ca 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -369,6 +369,7 @@ def print_nbody_energy( info += f""" [Eh] [Eh] [kcal/mol] [Eh] [kcal/mol]\n""" previous_e = energy_body_dict[1] tot_e = previous_e != 0.0 + interaction_data_unavailable = embedding or external_potential nbody_range = list(energy_body_dict) if supersystem_ie_only: nbody_range = [1, nfragments] @@ -387,36 +388,24 @@ def print_nbody_energy( if nb in nbody_range: delta_e = energy_body_dict[nb] - previous_e - delta_e_kcal = delta_e * constants.hartree2kcalmol - interaction_data_unavailable = embedding or external_potential + total_e = f"{energy_body_dict[nb]:20.12f}" if tot_e else f'{"N/A":>20}' + if interaction_data_unavailable: - int_e = np.nan - int_e_kcal = np.nan + int_e = f'{"N/A":>20}' + int_e_kcal = f'{"N/A":>20}' else: - int_e = energy_body_dict[nb] - energy_body_dict[1] - int_e_kcal = int_e * constants.hartree2kcalmol + interaction_energy = energy_body_dict[nb] - energy_body_dict[1] + int_e = f"{interaction_energy:20.12f}" + int_e_kcal = f"{interaction_energy * constants.hartree2kcalmol:20.12f}" + if supersystem_ie_only and nb == nfragments: - if tot_e: - if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":20} {"N/A":14} {"N/A":20} {"N/A":20}\n""" - else: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" - else: - if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":20} {"N/A":14} {"N/A":20} {"N/A":20}\n""" - else: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" + delta_e = f'{"N/A":>20}' + delta_e_kcal = f'{"N/A":>20}' else: - if tot_e: - if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":20} {"N/A":14} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" - else: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" - else: - if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":20} {"N/A":14} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" - else: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + delta_e_kcal = f"{delta_e * constants.hartree2kcalmol:20.12f}" + delta_e = f"{delta_e:20.12f}" + + info += f" {lbl:>11} {mclbl:2} {nb:2} {total_e} {int_e} {int_e_kcal} {delta_e} {delta_e_kcal}\n" previous_e = energy_body_dict[nb] else: info += f""" {lbl:>11} {"":2} {nb:2} {"N/A":20} {"N/A":20} {"N/A":20} {"N/A":20} {"N/A":20}\n""" From 35a329e3a1824215ccd1de6c651814c27673aef0 Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 10:36:11 -0400 Subject: [PATCH 03/12] no-mistakes(document): Correct collect_vars result documentation --- qcmanybody/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index e1d92ca..0957146 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -455,7 +455,8 @@ def collect_vars( Returns ------- dict - _description_. Empty return if *embedding* enabled. + Many-body result properties. Interaction properties are omitted when *embedding* or *external_potential* is + enabled; valid total and n-body contribution properties remain. """ bsse = bsse.lower() prop = prop.lower() From 070b8978ca62a07609a2c876c8a66561e132129a Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 10:41:20 -0400 Subject: [PATCH 04/12] no-mistakes: apply CI fixes --- qcmanybody/tests/test_utils.py | 10 +++++++--- qcmanybody/utils.py | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/qcmanybody/tests/test_utils.py b/qcmanybody/tests/test_utils.py index 5003503..6a8a6aa 100644 --- a/qcmanybody/tests/test_utils.py +++ b/qcmanybody/tests/test_utils.py @@ -148,9 +148,13 @@ def test_print_nbody_energy_external_potential_without_total_energy(): external_potential=True, ) - rows = [line.split() for line in stdout.splitlines() if "§A" in line and "N/A" in line] - assert len(rows) == 2 - assert all(row[-5:-2] == ["N/A", "N/A", "N/A"] for row in rows) + rows = [line for line in stdout.splitlines() if "§A" in line and "N/A" in line] + assert rows == [ + " §A 1 N/A N/A N/A" + " 0.000000000000 0.000000000000", + " FULL/RTN §A 2 N/A N/A N/A" + " 0.005150000059 3.231673826977", + ] assert "nan" not in stdout.lower() properties = qcmb.utils.collect_vars( diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index 0957146..006a2dd 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -391,8 +391,9 @@ def print_nbody_energy( total_e = f"{energy_body_dict[nb]:20.12f}" if tot_e else f'{"N/A":>20}' if interaction_data_unavailable: - int_e = f'{"N/A":>20}' - int_e_kcal = f'{"N/A":>20}' + # Retain the established table alignment for unavailable interaction energies. + int_e = f' {"N/A":<20}' + int_e_kcal = f' {"N/A":<14}' else: interaction_energy = energy_body_dict[nb] - energy_body_dict[1] int_e = f"{interaction_energy:20.12f}" From 3fe722388d9d2aeb4ff70793dd50ff9147908a5a Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 10:47:07 -0400 Subject: [PATCH 05/12] no-mistakes: apply CI fixes --- qcmanybody/tests/test_utils.py | 4 ++-- qcmanybody/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qcmanybody/tests/test_utils.py b/qcmanybody/tests/test_utils.py index 6a8a6aa..e8744b5 100644 --- a/qcmanybody/tests/test_utils.py +++ b/qcmanybody/tests/test_utils.py @@ -150,9 +150,9 @@ def test_print_nbody_energy_external_potential_without_total_energy(): rows = [line for line in stdout.splitlines() if "§A" in line and "N/A" in line] assert rows == [ - " §A 1 N/A N/A N/A" + " §A 1 N/A N/A N/A" " 0.000000000000 0.000000000000", - " FULL/RTN §A 2 N/A N/A N/A" + " FULL/RTN §A 2 N/A N/A N/A" " 0.005150000059 3.231673826977", ] assert "nan" not in stdout.lower() diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index 006a2dd..c4945a8 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -393,7 +393,7 @@ def print_nbody_energy( if interaction_data_unavailable: # Retain the established table alignment for unavailable interaction energies. int_e = f' {"N/A":<20}' - int_e_kcal = f' {"N/A":<14}' + int_e_kcal = f'{"N/A":<14}' else: interaction_energy = energy_body_dict[nb] - energy_body_dict[1] int_e = f"{interaction_energy:20.12f}" From cce0220e34b60b41081c0dc59f67f20ad0a6f2dc Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 10:52:10 -0400 Subject: [PATCH 06/12] no-mistakes: apply CI fixes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9c05dec..6e41924 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ pythonpath = [ "tests", ] testpaths = [ - "tests", + "qcmanybody/tests", ] markers = [ "addon: tests require external non-required software", From e6dad35cdd1aa9553dba10f7934f060a07dcd8ac Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 11:17:08 -0400 Subject: [PATCH 07/12] no-mistakes: apply CI fixes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 518a445..937503d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,7 @@ jobs: - name: PyTest run: | - pytest -rws -v ${{ matrix.cfg.pytest }} \ + python -m pytest qcmanybody/tests -rws -v ${{ matrix.cfg.pytest }} \ --cov=qcmanybody --cov-report=xml \ --color=yes --durations 50 --durations-min 20 From feea7706675de689c7a80bb4f0233d5eb9251070 Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 11:21:39 -0400 Subject: [PATCH 08/12] no-mistakes: apply CI fixes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 937503d..518a445 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,7 @@ jobs: - name: PyTest run: | - python -m pytest qcmanybody/tests -rws -v ${{ matrix.cfg.pytest }} \ + pytest -rws -v ${{ matrix.cfg.pytest }} \ --cov=qcmanybody --cov-report=xml \ --color=yes --durations 50 --durations-min 20 From b18d5319c2a65052ee25a380cc92165081cd860a Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 11:26:12 -0400 Subject: [PATCH 09/12] Preserve legacy energy table spacing --- pyproject.toml | 2 +- qcmanybody/tests/test_utils.py | 46 +++++++++++++++++++++++++++++++--- qcmanybody/utils.py | 41 +++++++++++++++++------------- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e41924..9c05dec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ pythonpath = [ "tests", ] testpaths = [ - "qcmanybody/tests", + "tests", ] markers = [ "addon: tests require external non-required software", diff --git a/qcmanybody/tests/test_utils.py b/qcmanybody/tests/test_utils.py index e8744b5..9b093e2 100644 --- a/qcmanybody/tests/test_utils.py +++ b/qcmanybody/tests/test_utils.py @@ -150,10 +150,10 @@ def test_print_nbody_energy_external_potential_without_total_energy(): rows = [line for line in stdout.splitlines() if "§A" in line and "N/A" in line] assert rows == [ - " §A 1 N/A N/A N/A" - " 0.000000000000 0.000000000000", - " FULL/RTN §A 2 N/A N/A N/A" - " 0.005150000059 3.231673826977", + " §A 1 N/A N/A N/A" + " 0.000000000000 0.000000000000", + " FULL/RTN §A 2 N/A N/A N/A" + " 0.005150000059 3.231673826977", ] assert "nan" not in stdout.lower() @@ -164,6 +164,44 @@ def test_print_nbody_energy_external_potential_without_total_energy(): assert not any("interaction" in name for name in properties) +def test_print_nbody_energy_preserves_legacy_spacing(): + labels = {1: ("model", "§A"), 2: ("model", "§A")} + + interaction_only = qcmb.utils.print_nbody_energy( + {1: 0.0, 2: 0.005150000059}, + "Counterpoise Corrected (CP)", + 2, + labels, + embedding=False, + supersystem_ie_only=False, + supersystem_beyond=None, + ) + interaction_rows = [line for line in interaction_only.splitlines() if "§A" in line and "Legend" not in line] + assert interaction_rows == [ + " §A 1 N/A 0.000000000000 0.000000000000" + " 0.000000000000 0.000000000000", + " FULL/RTN §A 2 N/A 0.005150000059 3.231673826977" + " 0.005150000059 3.231673826977", + ] + + supersystem = qcmb.utils.print_nbody_energy( + {1: -1.0, 2: -0.9}, + "Counterpoise Corrected (CP)", + 2, + labels, + embedding=False, + supersystem_ie_only=True, + supersystem_beyond=None, + ) + supersystem_rows = [line for line in supersystem.splitlines() if "§A" in line and "Legend" not in line] + assert supersystem_rows == [ + " §A 1 -1.000000000000 0.000000000000 0.000000000000" + " 0.000000000000 0.000000000000", + " FULL/RTN §A 2 -0.900000000000 0.100000000000 62.750947377754" + " N/A N/A ", + ] + + @pytest.mark.parametrize("hin,bas,reverse,hans", [ pytest.param(f3hesses["full"], [1, 2, 3], False, f3hesses["full"]), # idempotent pytest.param(f3hesses["full"], [1, 2, 3], True, f3hesses["full"]), # idempotent diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index c4945a8..ccbcf64 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -388,25 +388,32 @@ def print_nbody_energy( if nb in nbody_range: delta_e = energy_body_dict[nb] - previous_e - total_e = f"{energy_body_dict[nb]:20.12f}" if tot_e else f'{"N/A":>20}' - - if interaction_data_unavailable: - # Retain the established table alignment for unavailable interaction energies. - int_e = f' {"N/A":<20}' - int_e_kcal = f'{"N/A":<14}' - else: - interaction_energy = energy_body_dict[nb] - energy_body_dict[1] - int_e = f"{interaction_energy:20.12f}" - int_e_kcal = f"{interaction_energy * constants.hartree2kcalmol:20.12f}" - + delta_e_kcal = delta_e * constants.hartree2kcalmol + if not interaction_data_unavailable: + int_e = energy_body_dict[nb] - energy_body_dict[1] + int_e_kcal = int_e * constants.hartree2kcalmol if supersystem_ie_only and nb == nfragments: - delta_e = f'{"N/A":>20}' - delta_e_kcal = f'{"N/A":>20}' + if tot_e: + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":>20} {"N/A":>20} {"N/A":20} {"N/A":20}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" + else: + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":>20} {"N/A":>20} {"N/A":20} {"N/A":20}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" else: - delta_e_kcal = f"{delta_e * constants.hartree2kcalmol:20.12f}" - delta_e = f"{delta_e:20.12f}" - - info += f" {lbl:>11} {mclbl:2} {nb:2} {total_e} {int_e} {int_e_kcal} {delta_e} {delta_e_kcal}\n" + if tot_e: + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":20} {"N/A":14} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + else: + if interaction_data_unavailable: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":>20} {"N/A":>20} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + else: + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" previous_e = energy_body_dict[nb] else: info += f""" {lbl:>11} {"":2} {nb:2} {"N/A":20} {"N/A":20} {"N/A":20} {"N/A":20} {"N/A":20}\n""" From 495d2485856464510f4e28423f16f80bb7268fa9 Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 11:39:33 -0400 Subject: [PATCH 10/12] no-mistakes: apply CI fixes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9c05dec..6e41924 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ pythonpath = [ "tests", ] testpaths = [ - "tests", + "qcmanybody/tests", ] markers = [ "addon: tests require external non-required software", From 7e05e0a9eb0e67e9e66148cc506f08c2132407e5 Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 11:43:53 -0400 Subject: [PATCH 11/12] no-mistakes: apply CI fixes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6e41924..ea6f23c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ default-tag = "0.0.99" # useful for CI/shallow clones minversion = "7.0" addopts = "--import-mode=importlib" pythonpath = [ - "tests", + "qcmanybody/tests", ] testpaths = [ "qcmanybody/tests", From eeb5b4959287bd6b5d4a96df4b04b728408e44ee Mon Sep 17 00:00:00 2001 From: "Wallace, Austin" Date: Mon, 31 Aug 2026 11:51:25 -0400 Subject: [PATCH 12/12] no-mistakes: apply CI fixes --- qcmanybody/tests/test_utils.py | 20 ++++++++++++++++++++ qcmanybody/utils.py | 9 ++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/qcmanybody/tests/test_utils.py b/qcmanybody/tests/test_utils.py index 9b093e2..5d1e1b3 100644 --- a/qcmanybody/tests/test_utils.py +++ b/qcmanybody/tests/test_utils.py @@ -201,6 +201,26 @@ def test_print_nbody_energy_preserves_legacy_spacing(): " N/A N/A ", ] + external_supersystem = qcmb.utils.print_nbody_energy( + {1: -1.0, 2: -0.9}, + "Counterpoise Corrected (CP)", + 2, + labels, + embedding=False, + supersystem_ie_only=True, + supersystem_beyond=None, + external_potential=True, + ) + external_supersystem_rows = [ + line for line in external_supersystem.splitlines() if "§A" in line and "Legend" not in line + ] + assert external_supersystem_rows == [ + " §A 1 -1.000000000000 N/A N/A" + " 0.000000000000 0.000000000000", + " FULL/RTN §A 2 -0.900000000000 N/A N/A" + " N/A N/A ", + ] + @pytest.mark.parametrize("hin,bas,reverse,hans", [ pytest.param(f3hesses["full"], [1, 2, 3], False, f3hesses["full"]), # idempotent diff --git a/qcmanybody/utils.py b/qcmanybody/utils.py index ccbcf64..18afd93 100644 --- a/qcmanybody/utils.py +++ b/qcmanybody/utils.py @@ -370,6 +370,9 @@ def print_nbody_energy( previous_e = energy_body_dict[1] tot_e = previous_e != 0.0 interaction_data_unavailable = embedding or external_potential + # Preserve the historical numeric-field offsets while replacing only the + # literal unavailable ``nan`` token with ``N/A``. + unavailable_interaction = f"{np.nan:20.12f}".replace("nan", "N/A") nbody_range = list(energy_body_dict) if supersystem_ie_only: nbody_range = [1, nfragments] @@ -395,12 +398,12 @@ def print_nbody_energy( if supersystem_ie_only and nb == nfragments: if tot_e: if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {"N/A":>20} {"N/A":>20} {"N/A":20} {"N/A":20}\n""" + info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {unavailable_interaction} {unavailable_interaction} {"N/A":20} {"N/A":20}\n""" else: info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" else: if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":>20} {"N/A":>20} {"N/A":20} {"N/A":20}\n""" + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {unavailable_interaction} {unavailable_interaction} {"N/A":20} {"N/A":20}\n""" else: info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {"N/A":20} {"N/A":20}\n""" else: @@ -411,7 +414,7 @@ def print_nbody_energy( info += f""" {lbl:>11} {mclbl:2} {nb:2} {energy_body_dict[nb]:20.12f} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" else: if interaction_data_unavailable: - info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {"N/A":>20} {"N/A":>20} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" + info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {unavailable_interaction} {unavailable_interaction} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" else: info += f""" {lbl:>11} {mclbl:2} {nb:2} {"N/A":14} {int_e:20.12f} {int_e_kcal:20.12f} {delta_e:20.12f} {delta_e_kcal:20.12f}\n""" previous_e = energy_body_dict[nb]