diff --git a/README.md b/README.md
index 57d6ad57..3b555bcb 100644
--- a/README.md
+++ b/README.md
@@ -33,9 +33,9 @@ Once you have `micromamba` installed and have already cloned this repo, you can
### Linux
```bash
-micromamba create -n qligfep_new python=3.11
+micromamba create -n qligfep_new python=3.14
micromamba activate qligfep_new
-micromamba install gfortran=11.3.0 openff-toolkit=0.17.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.1.1 openff-nagl=0.5.4 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 michellab::fkcombu konnektor -c conda-forge --yes
+micromamba install gfortran=11.3.0 openff-toolkit=0.18.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.4.0 openff-nagl=0.5.5 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 michellab::fkcombu konnektor -c conda-forge --yes
```
Now that you have the environment ready and activated, [clone the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository), enter the `Q` directory with `cd Q/`, and install qligfep:
@@ -49,7 +49,7 @@ The `qprep` Fortran binary will be automatically compiled during installation.
To install everything in one line...
```bash
-micromamba create -n qligfep_new python=3.11 gfortran=11.3.0 openff-toolkit=0.17.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.1.1 openff-nagl=0.5.4 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 michellab::fkcombu konnektor -c conda-forge --yes && micromamba activate qligfep_new && python -m pip install -e .
+micromamba create -n qligfep_new python=3.14 gfortran=11.3.0 openff-toolkit=0.18.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.4.0 openff-nagl=0.5.5 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 michellab::fkcombu konnektor -c conda-forge --yes && micromamba activate qligfep_new && python -m pip install -e .
```
@@ -58,7 +58,7 @@ micromamba create -n qligfep_new python=3.11 gfortran=11.3.0 openff-toolkit=0.17
Similar to Linux, [clone the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository), enter the `Q` directory with `cd Q/`, create the environment and install:
``` bash
-micromamba create -n qligfep_new python=3.11 gfortran=11.3.0 openff-toolkit=0.17.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.1.1 openff-nagl=0.5.4 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 davidararipe::kcombu_bss konnektor -c conda-forge --yes
+micromamba create -n qligfep_new python=3.14 gfortran=11.3.0 openff-toolkit=0.18.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.4.0 openff-nagl=0.5.5 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 davidararipe::kcombu_bss konnektor -c conda-forge --yes
micromamba activate qligfep_new
python -m pip install joblib scipy tqdm
python -m pip install -e .
@@ -70,7 +70,7 @@ The `qprep` Fortran binary will be automatically compiled during installation.
To install everything in one line...
```bash
-micromamba create -n qligfep_new python=3.11 gfortran=11.3.0 openff-toolkit=0.17.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.1.1 openff-nagl=0.5.4 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 davidararipe::kcombu_bss konnektor -c conda-forge --yes && micromamba activate qligfep_new && python -m pip install joblib scipy tqdm && python -m pip install -e .
+micromamba create -n qligfep_new python=3.14 gfortran=11.3.0 openff-toolkit=0.18.1 "openff-utilities>=0.1.12" openff-forcefields=2026.01.0 openmm=8.4.0 openff-nagl=0.5.5 openff-nagl-models=2025.9.0 lomap2 kartograf=1.0.1 davidararipe::kcombu_bss konnektor -c conda-forge --yes && micromamba activate qligfep_new && python -m pip install joblib scipy tqdm && python -m pip install -e .
```
diff --git a/pyproject.toml b/pyproject.toml
index 9f6df315..126abc9d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "QligFEP"
dynamic = ["version"]
description = "Python CLI designed to facilitate a robust and fast setup of free energy perturbation (FEP)."
readme = { file = "README.md", content-type = "text/markdown" }
-requires-python = ">=3.10"
+requires-python = ">=3.14"
license = { file = "LICENSE" }
# keywords = [""]
authors = [{ name = "Willem Jespers", email = "w.jespers@rug.nl" },
@@ -24,7 +24,7 @@ classifiers = [
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering :: Chemistry",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10" # todo: might have other versions
+ "Programming Language :: Python :: 3.14" # todo: might have other versions
]
dependencies = [
"joblib",
@@ -69,15 +69,16 @@ repository = "https://github.com/qusers/qligfep"
[tool.black]
line-length = 110
-target-version = ['py310']
+target-version = ['py314']
[tool.isort]
profile = "black"
+line_length = 110
[tool.ruff]
line-length = 110
indent-width = 4
-target-version = 'py39'
+target-version = 'py314'
[tool.ruff.lint]
select = [
diff --git a/src/QligFEP/CLI/cog_cli.py b/src/QligFEP/CLI/cog_cli.py
index 89d61d9b..7eb7a734 100644
--- a/src/QligFEP/CLI/cog_cli.py
+++ b/src/QligFEP/CLI/cog_cli.py
@@ -65,7 +65,7 @@ def _cog_sdf(self):
for i, center in enumerate(centers):
logger.debug(f"Ligand {i+1} center: {center}")
- overall_center = [sum(x) / len(centers) for x in zip(*centers)]
+ overall_center = [sum(x) / len(centers) for x in zip(*centers, strict=False)]
return f"[{round(overall_center[0], 3):.3f} {round(overall_center[1], 3):.3f} {round(overall_center[2], 3):.3f}]"
def _calculate_center(self, coordinates):
diff --git a/src/QligFEP/CLI/konnektor_cli.py b/src/QligFEP/CLI/konnektor_cli.py
index 7468ee68..92f8ed97 100644
--- a/src/QligFEP/CLI/konnektor_cli.py
+++ b/src/QligFEP/CLI/konnektor_cli.py
@@ -3,7 +3,6 @@
import argparse
import json
from pathlib import Path
-from typing import Optional
import numpy as np
from kartograf import KartografAtomMapper, SmallMoleculeComponent
@@ -34,18 +33,18 @@ class KonnektorWrap:
def __init__(
self,
inp: str,
- out: Optional[str] = None,
+ out: str | None = None,
network: str = "mst",
scorer: str = "combined",
restraint_method: str = "heavyatom_p",
processes: int = 1,
log_level: str = "info",
- central_ligand: Optional[str] = None,
+ central_ligand: str | None = None,
n_redundancy: int = 2,
connectivity: int = 3,
separate_charges: bool = False,
charge_changes_score: float = 0.0,
- exp_key: Optional[str] = None,
+ exp_key: str | None = None,
self_solve: bool = False,
):
self.inp = inp
@@ -66,7 +65,7 @@ def __init__(
self.out = self._parse_output(out)
self._sdf_dir = self._prepare_input()
- def _parse_output(self, output: Optional[str]) -> str:
+ def _parse_output(self, output: str | None) -> str:
inpath = Path(self.inp)
if output is None:
if inpath.is_dir():
@@ -310,7 +309,7 @@ def run(self) -> dict:
return result
- def _resolve_sdf_path(self) -> Optional[Path]:
+ def _resolve_sdf_path(self) -> Path | None:
"""Find the SDF file used as input."""
inp = Path(self.inp)
if inp.is_file() and inp.suffix == ".sdf":
@@ -354,7 +353,7 @@ def _mcs_rmsd(mol_a, mol_b, timeout=5):
conf_a = ha.GetConformer()
conf_b = hb.GetConformer()
sq_dists = []
- for ia, ib in zip(match_a, match_b):
+ for ia, ib in zip(match_a, match_b, strict=False):
pa = conf_a.GetAtomPosition(ia)
pb = conf_b.GetAtomPosition(ib)
sq_dists.append((pa.x - pb.x) ** 2 + (pa.y - pb.y) ** 2 + (pa.z - pb.z) ** 2)
@@ -423,7 +422,7 @@ def _realign_to_neighbors(outlier_mol, neighbor_mols):
continue
conf_n = hn.GetConformer()
o_to_core = {oa: i for i, oa in enumerate(match_o)}
- for oa, na in zip(match_o_nb, match_n_nb):
+ for oa, na in zip(match_o_nb, match_n_nb, strict=False):
if oa in o_to_core:
p = conf_n.GetAtomPosition(na)
core_positions.setdefault(o_to_core[oa], []).append(
diff --git a/src/QligFEP/CLI/lomap_wrap_cli.py b/src/QligFEP/CLI/lomap_wrap_cli.py
index 5fac38ff..a61a6e19 100644
--- a/src/QligFEP/CLI/lomap_wrap_cli.py
+++ b/src/QligFEP/CLI/lomap_wrap_cli.py
@@ -5,7 +5,6 @@
import re
from multiprocessing import cpu_count
from pathlib import Path
-from typing import Optional
import lomap
import numpy as np
@@ -22,10 +21,10 @@ class LomapWrap:
def __init__(
self,
inp: str,
- out: Optional[str] = None,
+ out: str | None = None,
time=30,
verbose="info",
- exp_key: Optional[str] = None,
+ exp_key: str | None = None,
**kwargs,
):
self.nodes = {}
diff --git a/src/QligFEP/CLI/qligfep_cli.py b/src/QligFEP/CLI/qligfep_cli.py
index 9d67edd8..03408dc3 100644
--- a/src/QligFEP/CLI/qligfep_cli.py
+++ b/src/QligFEP/CLI/qligfep_cli.py
@@ -4,7 +4,6 @@
import datetime
import json
from pathlib import Path
-from typing import Optional
from QligFEP import __version__
@@ -15,7 +14,7 @@
from .parser_base import parse_arguments
-def main(args: Optional[argparse.Namespace] = None, **kwargs) -> None:
+def main(args: argparse.Namespace | None = None, **kwargs) -> None:
"""Main function for qligfep_cli.py. Takes arguments from argparse and passes them
to QligFEP class. If no arguments are given, the function will use the keyword arguments
that are passed to it.
diff --git a/src/QligFEP/CLI/setupFEP.py b/src/QligFEP/CLI/setupFEP.py
index f634b76b..3f422326 100644
--- a/src/QligFEP/CLI/setupFEP.py
+++ b/src/QligFEP/CLI/setupFEP.py
@@ -7,7 +7,6 @@
import shutil
import subprocess
from pathlib import Path
-from typing import Optional
from ..IO import parse_qprep_total_charge
from ..logger import logger, setup_logger
@@ -76,7 +75,7 @@ def submit_command(command: str) -> None:
raise
-def main(args: Optional[argparse.Namespace] = None, **kwargs) -> None:
+def main(args: argparse.Namespace | None = None, **kwargs) -> None:
# setup the logger with the desired log level
setup_logger(level=args.log)
@@ -113,7 +112,7 @@ def main(args: Optional[argparse.Namespace] = None, **kwargs) -> None:
lig_pairs = ligpairs_from_json(args.json_map)
protein_dir = cwd / "2.protein"
- for system, sys_dir in zip(systems, sys_directories):
+ for system, sys_dir in zip(systems, sys_directories, strict=False):
for lig1, lig2, same_charge in lig_pairs:
# For cross-charge water edges, look up the protein leg's total charge
diff --git a/src/QligFEP/IO.py b/src/QligFEP/IO.py
index 6f409e4e..1e11a3fd 100644
--- a/src/QligFEP/IO.py
+++ b/src/QligFEP/IO.py
@@ -4,7 +4,7 @@
import stat
import subprocess
from pathlib import Path
-from typing import NamedTuple, Optional
+from typing import NamedTuple
import numpy as np
import pandas as pd
@@ -34,8 +34,8 @@ class SlurmRunInfo(NamedTuple):
"""Per-replicate run metadata parsed from one ``slurm*.out`` footer."""
runtime: str
- seed: Optional[str]
- replicate: Optional[str]
+ seed: str | None
+ replicate: str | None
status: str
diff --git a/src/QligFEP/README.md b/src/QligFEP/README.md
deleted file mode 100644
index 620419c3..00000000
--- a/src/QligFEP/README.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# QligFEP v2.0 and QresFEP v1.0
-
-This collection of python command line functions is designed with the
-aim to facilitate a robust and fast setup of FEP calculations for the
-software package **Q**. These modules use python 3, python 2 is no
-longer supported, and an old version of the code using python 2
-is now only available in the python2 branch.
-
-This package includes at the moment two main modules:
-- QligFEP.py: module to generate ligand FEP calculations using a
-dual topology approach,
-see Jespers et al. (https://doi.org/10.1186/s13321-019-0348-5).
-
-- QresFEP.py: module to generate protein FEP calculations using a
-single topology approach,
-see Jespers et al. (https://doi.org/10.1021/acs.jctc.9b00538).
-
-Future versions will include QLIE, dual topology QresFEP and several
-translation tools for new forcefields (at the moment we support opls,
-charmm,amber and openFF).
-
-A few toplevel scripts are included in the scripts folder to facilitate
-high throughput setup. Additionally, a tutorials folder is included
-with a detailed description of the setup procedure as published in
-Jespers et al. (QresFEP/QligFEP). This tutorial includes the generation
-of ligand parameters using OPLS, how to prepare a protein system, and
-how to run ligand and protein FEP calculations. These examples are
-based on ligand binding of CDk2 inhibitors.
-
-# Installing QligFEP and QresFEP
-
-- Install a working version of Q, e.g.:
-
-
-
-
-- Clone this repository:
-
- git clone https://github.com/qusers/qligfep.git
-
-- Create the shipped conda environment
-
- conda env create -f environment.yml
-
-In settings.py:
-
-- Change SCHROD_DIR to the Schrodinger location, if you want to be
-able to generate OPLS ligand parameters using ffld_server.
-
-- Change Q_DIR to the location of the q executables. This can be
-particularly useful if you use setupFEP from a local machine on
-a mounted directory. (In which case, the executables of the preparation
-part and running part of Q are at several places).
-
-- You can add slurm specific parameters in the CLUSTER INPUTS section,
-according to the given example.
-
-## Requirements
-- ffld_server
-- cgenff
-- Protein Preparation Wizard
-- Python3.10
-- Q
-
-contact: [Willem Jespers (PhD)](mailto:w.jespers@lacdr.leidenuniv.nl?subject=[QLigFEP]%20[QResFEP])
-
diff --git a/src/QligFEP/analysis_plotting.py b/src/QligFEP/analysis_plotting.py
index 6a651972..c3f39965 100644
--- a/src/QligFEP/analysis_plotting.py
+++ b/src/QligFEP/analysis_plotting.py
@@ -238,7 +238,7 @@ def result_to_latex(res, latexify_each=False): # TODO: move this out of this me
(1.04, hori_height - spacing),
(1.04, hori_height - spacing * 2),
)
- for txt_position, body in zip(txt_positions, text_body):
+ for txt_position, body in zip(txt_positions, text_body, strict=False):
plt.text(
*txt_position,
body,
diff --git a/src/QligFEP/analyze_FEP.py b/src/QligFEP/analyze_FEP.py
index 1deda388..fbca5175 100644
--- a/src/QligFEP/analyze_FEP.py
+++ b/src/QligFEP/analyze_FEP.py
@@ -4,7 +4,6 @@
import json
import os
from pathlib import Path
-from typing import Optional
import numpy as np
import pandas as pd
@@ -31,7 +30,7 @@ def __init__(
system: str,
target_name: str,
mapping_json: str,
- n_lambdas: Optional[int] = None,
+ n_lambdas: int | None = None,
allow_missing_edges: bool = False,
) -> None:
"""Initialize the FEP reader class. This class will store the FEP information inside
diff --git a/src/QligFEP/analyze_neq.py b/src/QligFEP/analyze_neq.py
index f95575aa..8e584103 100644
--- a/src/QligFEP/analyze_neq.py
+++ b/src/QligFEP/analyze_neq.py
@@ -26,7 +26,6 @@
import os
from collections import defaultdict
from pathlib import Path
-from typing import Optional
import numpy as np
import pandas as pd
@@ -75,7 +74,7 @@ def dF_to_kcal(dF: float, work_units: str, temperature: float) -> float:
WORK_TAIL_BYTES = 256 * 1024
-def read_final_work(log_path: str) -> Optional[float]:
+def read_final_work(log_path: str) -> float | None:
"""Read the final accumulated switching work from a qdyn NEQ-mode log.
Each switch prints ``At step N, work accumulated was ...`` every output interval;
@@ -101,7 +100,7 @@ def read_final_work(log_path: str) -> Optional[float]:
if "work accumulated" in line:
try:
return float(line.split()[6])
- except (IndexError, ValueError):
+ except IndexError, ValueError:
logger.warning(f"Could not parse the work value in {log_path}")
return None
return None
@@ -264,7 +263,7 @@ def _dF_by_rep_from_works(by_rep: dict, beta: float, work_units: str, temperatur
continue
try:
result[rep] = dF_to_kcal(bar_delta_f(forward, reverse, beta), work_units, temperature)
- except (ValueError, RuntimeError):
+ except ValueError, RuntimeError:
result[rep] = None
return result
@@ -573,7 +572,7 @@ def populate_mapping_json(df: pd.DataFrame, mapping_json: str, output_file: str)
logger.info(f"Injected NEQ ddG into {matched} edge(s); wrote {output_file}")
-def _nan_to_none(value) -> Optional[float]:
+def _nan_to_none(value) -> float | None:
"""Return ``None`` for a missing/NaN value (so it serializes to JSON null), else a float."""
return None if pd.isna(value) else float(value)
diff --git a/src/QligFEP/chemIO.py b/src/QligFEP/chemIO.py
index 8b0c4e7d..81ae388b 100644
--- a/src/QligFEP/chemIO.py
+++ b/src/QligFEP/chemIO.py
@@ -1,7 +1,7 @@
from io import StringIO
from itertools import product
from pathlib import Path
-from typing import Generator, Optional, Union # noqa: UP035
+from typing import Generator # noqa: UP035
import pandas as pd
import py3Dmol
@@ -55,7 +55,7 @@ def __init__(self, lig, pattern: str = "*.sdf", reindex_hydrogens: bool = True):
self.setup_mols_and_names(self.lig, pattern)
self.parse_sdf_contents() # add the sdf content to the dictionary
- def __getitem__(self, name: str) -> Optional[Molecule]:
+ def __getitem__(self, name: str) -> Molecule | None:
"""Retrieve a molecule by its name.
Args:
@@ -71,12 +71,12 @@ def __getitem__(self, name: str) -> Optional[Molecule]:
logger.warning(f"Molecule with name {name} not found.")
return None
- def __iter__(self) -> Generator[tuple[str, Molecule], None, None]:
+ def __iter__(self) -> Generator[tuple[str, Molecule]]:
"""Iterate over the names and the Molecule objects"""
- yield from zip(self.lig_names, self.molecules)
+ yield from zip(self.lig_names, self.molecules, strict=False)
def display_overlay(
- self, *ligands: Union[str, Chem.Mol, Molecule], size=(800, 600), render=False
+ self, *ligands: str | Chem.Mol | Molecule, size=(800, 600), render=False
) -> py3Dmol.view:
"""Display the overlay of the ligands using py3Dmol.
@@ -138,7 +138,7 @@ def _rdkit_to_openff(self, rdkit_mol: Chem.Mol, hydrogens_are_explicit: bool) ->
rdkit_mol, hydrogens_are_explicit=hydrogens_are_explicit, allow_undefined_stereo=True
)
- def _parse_mol(self, ligpath: Union[Path, str]) -> tuple[list[Molecule], list[str]]:
+ def _parse_mol(self, ligpath: Path | str) -> tuple[list[Molecule], list[str]]:
"""Parse a .sdf file into a list of Molecule objects and their names.
Loads via RDKit first to inspect the original hydrogen state, then converts
@@ -193,7 +193,7 @@ def _parse_mol(self, ligpath: Union[Path, str]) -> tuple[list[Molecule], list[st
if self._reindex_hydrogens:
mols = [self._force_H_reindexing(mol) for mol in mols]
- for mol, name in zip(mols, lig_names):
+ for mol, name in zip(mols, lig_names, strict=False):
mol.name = name
return mols, lig_names
@@ -246,7 +246,7 @@ def parse_sdf_contents(self):
mol.to_file(string_buffer, file_format="sdf")
self.sdf_contents.update({name: string_buffer.getvalue().splitlines()})
- def write_sdf_separate(self, output_dir, molecules: Optional[list[Molecule]] = None) -> None:
+ def write_sdf_separate(self, output_dir, molecules: list[Molecule] | None = None) -> None:
"""Function to write the separate multiple molecules within a sdf file into their own
.sdf, placed under `output_dir`.
@@ -273,7 +273,7 @@ def write_sdf_separate(self, output_dir, molecules: Optional[list[Molecule]] = N
for mol in molecules:
mol.to_file(file_path=f"{mol.name}.sdf", file_format="sdf")
- def write_to_single_sdf(self, output_name: str, molecules: Optional[list[Molecule]] = None) -> None:
+ def write_to_single_sdf(self, output_name: str, molecules: list[Molecule] | None = None) -> None:
"""Writes all `self.molecules` to a single `.sdf` file.
Args:
@@ -304,7 +304,7 @@ def write_to_single_pdb(self, output_name: str, init_offset: int = 0) -> pd.Data
lig_resn = ["LI", "LG", "LH"]
last_lig_resn = [d for d in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
lig_resnames = ["".join(i) for i in product(lig_resn, last_lig_resn)]
- for mol, resn in zip(self.molecules, lig_resnames):
+ for mol, resn in zip(self.molecules, lig_resnames, strict=False):
# write the molecule pdb lines in memory and convert them to pd.DataFrame
output = StringIO()
mol.to_file(output, file_format="pdb")
diff --git a/src/QligFEP/conformer_generator.py b/src/QligFEP/conformer_generator.py
index 7bc50eda..7c8781ae 100644
--- a/src/QligFEP/conformer_generator.py
+++ b/src/QligFEP/conformer_generator.py
@@ -8,7 +8,6 @@
"""
from pathlib import Path
-from typing import Union
from rdkit import Chem
from rdkit.Chem import AllChem, rdFMCS
@@ -38,7 +37,7 @@ class BiasedConformerGenerator(MoleculeIO):
def __init__(
self,
- references: Union[str, Path, MoleculeIO],
+ references: str | Path | MoleculeIO,
reindex_hydrogens: bool = True,
mcs_atom_compare: str = "any",
mcs_bond_compare: str = "any",
diff --git a/src/QligFEP/lig_aligner.py b/src/QligFEP/lig_aligner.py
index b2e1821b..0c66a9a9 100644
--- a/src/QligFEP/lig_aligner.py
+++ b/src/QligFEP/lig_aligner.py
@@ -5,7 +5,7 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial
from pathlib import Path
-from typing import Any, Optional, Union
+from typing import Any
from openff.toolkit import Molecule
from rdkit import Chem
@@ -45,12 +45,12 @@ def __init__(
pattern: str = f"*{SDF_EXTENSION}",
reindex_hydrogens: bool = True,
n_threads: int = 1,
- protein: Optional[str] = None,
+ protein: str | None = None,
energy: str = "a",
search: str = "f",
steep_descend: bool = True,
connectivity: str = "t",
- top_constraint_tol: Optional[int] = None,
+ top_constraint_tol: int | None = None,
atom_type: str = "X",
bond_type: str = "X",
scaffold_lock: bool = False,
@@ -114,10 +114,10 @@ def __init__(
super().__init__(lig, pattern=pattern, reindex_hydrogens=reindex_hydrogens)
self.kcombu_exe = self._set_fkcombu_exe()
self.n_threads = n_threads
- self.reference_mol: Optional[Molecule] = None
+ self.reference_mol: Molecule | None = None
self.aligned_molecules: dict[str, Molecule] = {}
self.alignment_scores: dict[str, dict[str, float]] = {}
- self.temp_dir: Optional[tempfile.TemporaryDirectory] = None
+ self.temp_dir: tempfile.TemporaryDirectory | None = None
self.fkparams = self._process_fkparams(
{
"P": protein,
@@ -373,10 +373,10 @@ def _transfer_charges_metadata(molA, molB):
matchB = molB.GetSubstructMatch(mcs_mol)
logger.trace("Mapping of atoms:")
- for a, b in zip(matchA, matchB): # trace the mapping; used for debugging
+ for a, b in zip(matchA, matchB, strict=False): # trace the mapping; used for debugging
logger.trace(f"MolA atom {a} maps to MolB atom {b}")
- for a, b in zip(matchA, matchB): # iterate atoms and transfer charges
+ for a, b in zip(matchA, matchB, strict=False): # iterate atoms and transfer charges
atomA = molA.GetAtomWithIdx(a)
atomB = molB.GetAtomWithIdx(b)
formal_charge = atomA.GetFormalCharge()
@@ -403,7 +403,7 @@ def _transfer_sdf_metadata(self, original_file: Path, aligned_file: Path):
aligned_supplier = Chem.SDMolSupplier(str(aligned_file), removeHs=True, sanitize=False)
aligned_mols = []
- for original_mol, aligned_mol in zip(original_supplier, aligned_supplier):
+ for original_mol, aligned_mol in zip(original_supplier, aligned_supplier, strict=False):
if original_mol is not None and aligned_mol is not None:
Chem.SanitizeMol(aligned_mol, sanitizeOps=Chem.SanitizeFlags.SANITIZE_SETAROMATICITY)
@@ -423,7 +423,7 @@ def _transfer_sdf_metadata(self, original_file: Path, aligned_file: Path):
aligned_writer.close()
def align_single_molecule(
- self, molecule: Union[str, Molecule], reference: Union[str, Molecule]
+ self, molecule: str | Molecule, reference: str | Molecule
) -> tuple[Molecule, dict[str, float]]:
"""
Align a single molecule to a reference molecule.
@@ -464,7 +464,7 @@ def align_single_molecule(
return aligned_molecule, scores
def kcombu_align(
- self, reference: Union[str, Molecule], molecules_to_align: Optional[list[Union[str, Molecule]]] = None
+ self, reference: str | Molecule, molecules_to_align: list[str | Molecule] | None = None
) -> list[Molecule]:
"""
Aligns the specified molecules to a reference molecule using kcombu. The aligned molecules returned
@@ -531,9 +531,7 @@ def kcombu_align(
self.cleanup()
return aligned_ligands
- def output_aligned_ligands(
- self, output_name: str, ref_names: Optional[Union[str, list[str]]] = None
- ) -> None:
+ def output_aligned_ligands(self, output_name: str, ref_names: str | list[str] | None = None) -> None:
"""
Write the aligned molecules to a single .sdf file, optionally including the original reference ligand(s).
@@ -575,7 +573,7 @@ def output_aligned_ligands(
self.temp_dir.cleanup()
logger.info("Temporary directory cleaned up")
- def get_molecule(self, name: str, aligned: bool = True) -> Optional[Molecule]:
+ def get_molecule(self, name: str, aligned: bool = True) -> Molecule | None:
"""
Retrieve a molecule by name, either aligned or original.
@@ -642,11 +640,11 @@ def __init__(
self.opt_param = opt_param
self.max_preiters = max_preiters
self.max_postiters = max_postiters
- self.reference_mol: Optional[Molecule] = None
+ self.reference_mol: Molecule | None = None
self.aligned_molecules: dict[str, Molecule] = {}
self.alignment_scores: dict[str, tuple[float, float]] = {}
- def _resolve_molecule(self, mol_or_name: Union[str, Molecule]) -> Molecule:
+ def _resolve_molecule(self, mol_or_name: str | Molecule) -> Molecule:
"""Resolve a molecule from a name string or return the Molecule directly.
Args:
@@ -687,7 +685,7 @@ def _align_rdkit_mol(self, ref_rdkit: Chem.Mol, probe_rdkit: Chem.Mol) -> tuple[
return shape_tani, color_tani
def align_single_molecule(
- self, molecule: Union[str, Molecule], reference: Union[str, Molecule]
+ self, molecule: str | Molecule, reference: str | Molecule
) -> tuple[Molecule, float, float]:
"""Align a single molecule to a reference molecule.
@@ -712,7 +710,7 @@ def align_single_molecule(
return aligned_mol, shape_tani, color_tani
def align(
- self, reference: Union[str, Molecule], molecules_to_align: Optional[list[Union[str, Molecule]]] = None
+ self, reference: str | Molecule, molecules_to_align: list[str | Molecule] | None = None
) -> dict[str, Molecule]:
"""Align molecules to a reference using shape+color overlap.
@@ -766,9 +764,7 @@ def align(
return self.aligned_molecules
- def output_aligned_ligands(
- self, output_name: str, ref_names: Optional[Union[str, list[str]]] = None
- ) -> None:
+ def output_aligned_ligands(self, output_name: str, ref_names: str | list[str] | None = None) -> None:
"""Write aligned molecules to a single SDF file, including alignment scores as SD properties.
Args:
@@ -803,7 +799,7 @@ def output_aligned_ligands(
writer.close()
logger.info(f"Aligned molecules written to {output_name}")
- def get_molecule(self, name: str, aligned: bool = True) -> Optional[Molecule]:
+ def get_molecule(self, name: str, aligned: bool = True) -> Molecule | None:
"""Retrieve a molecule by name, either aligned or original.
Args:
diff --git a/src/QligFEP/openff2Q.py b/src/QligFEP/openff2Q.py
index b7d8f453..8779b7d5 100644
--- a/src/QligFEP/openff2Q.py
+++ b/src/QligFEP/openff2Q.py
@@ -2,7 +2,7 @@
from io import StringIO
from pathlib import Path
-from typing import Optional, TextIO
+from typing import TextIO
import numpy as np
from joblib import Parallel, delayed, parallel_config
@@ -104,7 +104,7 @@ def __init__(
self.total_charges = {} # store the total charges
self._set_nagl(nagl=nagl, nagl_model=nagl_model)
- def _set_forcefield(self, ffstring: Optional[str]) -> ForceField:
+ def _set_forcefield(self, ffstring: str | None) -> ForceField:
if ffstring is None:
# why not the constrained: https://docs.openforcefield.org/projects/toolkit/en/stable/faq.html
ffstring = "openff-2.3.0.offxml"
@@ -148,7 +148,7 @@ def _assign_charge(self, molecule: Molecule) -> np.ndarray:
def set_topologies_and_parameters(self):
topologies = {}
parameters = {}
- for lname, mol in zip(self.lig_names, self.molecules):
+ for lname, mol in zip(self.lig_names, self.molecules, strict=False):
topology = Topology.from_molecules(mol)
topologies.update({lname: topology})
parameters.update({lname: self.forcefield.label_molecules(topology)[0]})
@@ -163,7 +163,7 @@ def process_ligands(self) -> None:
charges_magnitudes = Parallel()(delayed(self._assign_charge)(molecule) for molecule in molecules)
logger.info("Done! Writing .lib, .prm and .pdb files for each ligand")
logger.debug(f"Output path: {self.out_dir}")
- for lname, charges in zip(self.lig_names, charges_magnitudes):
+ for lname, charges in zip(self.lig_names, charges_magnitudes, strict=False):
charges = round_charges_preserving_sum(charges)
self.charges_list_magnitude.update({lname: charges})
formatted_sum = f"{charges.sum():.3f}"
@@ -243,8 +243,8 @@ def create_atom_prm_mapping(self, lname):
def write_lib_Q(
self,
lname: str,
- outfile: Optional[TextIO] = None,
- prefix: Optional[str] = None,
+ outfile: TextIO | None = None,
+ prefix: str | None = None,
residue_name: str = "LIG",
):
"""Writes Q's .lib file for a given ligand.
@@ -302,7 +302,7 @@ def write_lib_Q(
if should_close:
outfile.close()
- def write_prm_Q(self, lname: str, outfile: Optional[TextIO] = None, prefix: Optional[str] = None):
+ def write_prm_Q(self, lname: str, outfile: TextIO | None = None, prefix: str | None = None):
"""Writes Q's .prm file for a given ligand.
Args:
@@ -316,7 +316,7 @@ def write_prm_Q(self, lname: str, outfile: Optional[TextIO] = None, prefix: Opti
Defaults to "LIG".
"""
- def insert_prefix(at_name, prefix: Optional[str]):
+ def insert_prefix(at_name, prefix: str | None):
if prefix is not None:
return prefix + at_name
return at_name
@@ -442,7 +442,7 @@ def insert_prefix(at_name, prefix: Optional[str]):
if should_close:
outfile.close()
- def write_PDB(self, lname: str, outfile: Optional[TextIO] = None, residue_name: str = "LIG"):
+ def write_PDB(self, lname: str, outfile: TextIO | None = None, residue_name: str = "LIG"):
"""Writes pdb file for a given ligand.
Args:
@@ -514,7 +514,7 @@ def write_cofactor_plus_ff_files(self, ff: str):
)
lig_prm_contents = {}
- for name, prefix, res in zip(self.lig_names, prefixes, residues):
+ for name, prefix, res in zip(self.lig_names, prefixes, residues, strict=False):
lib_out = StringIO()
self.write_lib_Q(name, outfile=lib_out, prefix=prefix, residue_name=res)
lib_lines = lib_out.getvalue().split("\n")
diff --git a/src/QligFEP/pdb_utils.py b/src/QligFEP/pdb_utils.py
index 7cb227fe..3c50a981 100644
--- a/src/QligFEP/pdb_utils.py
+++ b/src/QligFEP/pdb_utils.py
@@ -5,7 +5,6 @@
import warnings
from pathlib import Path
from string import ascii_uppercase
-from typing import Optional, Union
import MDAnalysis as mda
import numpy as np
@@ -21,10 +20,10 @@ def rm_HOH_clash_NN(
pdb_df_query: pd.DataFrame,
pdb_df_target: pd.DataFrame,
th: float = 2.5,
- output_file: Union[str, Path] = None,
+ output_file: str | Path = None,
heavy_only: bool = True,
ligand_only: bool = False,
- header: Optional[str] = None,
+ header: str | None = None,
save_removed: bool = False,
):
"""Use a NearestNeighbors approach to find water molecules within a distance threshold
@@ -189,11 +188,11 @@ def next_chain_id(existing_ids):
def append_pdb_to_another(
- main_pdb: Union[pd.DataFrame, str, list[str]],
- to_append_pdb: Union[pd.DataFrame, str, list[str]],
- save_pdb: Optional[str] = None,
+ main_pdb: pd.DataFrame | str | list[str],
+ to_append_pdb: pd.DataFrame | str | list[str],
+ save_pdb: str | None = None,
assign_new_chain: bool = False,
- new_ligname: Optional[str] = None,
+ new_ligname: str | None = None,
ignore_waters: bool = False,
) -> pd.DataFrame:
"""Reads the two pdbs as DataFrames, appends the second to the end of the protein
@@ -370,7 +369,9 @@ def disulfide_search(npdb, min_dist=1.8, max_dist_cyx=4.0, max_dist_cys=2.5):
# Find disulfide pairs with appropriate distance cutoffs
for ii, res_i in enumerate(cys_residues):
for res_j in cys_residues[ii + 1 :]:
- distance = math.sqrt(sum((a - b) ** 2 for a, b in zip(res_i["coords"], res_j["coords"])))
+ distance = math.sqrt(
+ sum((a - b) ** 2 for a, b in zip(res_i["coords"], res_j["coords"], strict=False))
+ )
# Use wide range only when both residues are CYX/CYD (confirmed disulfide
# partners). Mixed CYX+CYS pairs use the strict cutoff to avoid false
@@ -407,7 +408,7 @@ def get_coords(atomname, residue):
def calculate_distance(atom_coords, center_coords) -> float:
- return math.sqrt(sum((a - b) ** 2 for a, b in zip(atom_coords, center_coords)))
+ return math.sqrt(sum((a - b) ** 2 for a, b in zip(atom_coords, center_coords, strict=False)))
def _convert_to(value, dtype):
@@ -477,7 +478,7 @@ def read_pdb_to_dataframe(pdb_file):
return df
-def residue_atom_serial_range(pdb_df, residue_names: Union[str, list[str]]) -> Optional[tuple[int, int]]:
+def residue_atom_serial_range(pdb_df, residue_names: str | list[str]) -> tuple[int, int] | None:
"""Return the (first, last) atom serial numbers for the given residue name(s).
Returns None when no atoms match, so callers can skip building restraints
@@ -497,7 +498,7 @@ def residue_atom_serial_range(pdb_df, residue_names: Union[str, list[str]]) -> O
def write_dataframe_to_pdb(
- df, output_file, header: Optional[str] = None, ter_after_indices: Optional[set[int]] = None
+ df, output_file, header: str | None = None, ter_after_indices: set[int] | None = None
):
"""Save a DataFrame object created from read_pdb_to_dataframe function to a PDB file.
diff --git a/src/QligFEP/qligfep.py b/src/QligFEP/qligfep.py
index 2d6c678a..fa63f080 100644
--- a/src/QligFEP/qligfep.py
+++ b/src/QligFEP/qligfep.py
@@ -5,7 +5,7 @@
import shutil
import stat
from pathlib import Path
-from typing import Literal, Optional, Union
+from typing import Literal
import numpy as np
import pandas as pd
@@ -64,7 +64,7 @@
COUNTER_WATER_RESNAME = "CWT"
-def lrf_required_for_edge(same_charge: "bool | None") -> bool:
+def lrf_required_for_edge(same_charge: bool | None) -> bool:
"""Whether this edge must run with LRF on.
A charge-changing edge (``same_charge`` is False) changes the in-sphere net charge, whose
@@ -96,10 +96,10 @@ def __init__(
replicates: str = "10",
sampling: Literal["sigmoidal", "linear", "exponential", "reverse_exponential"] = "sigmoidal",
timestep: Literal["1fs", "2fs"] = "2fs",
- to_clean: Optional[list[str]] = None,
- water_thresh: Union[float, int] = 1.4,
+ to_clean: list[str] | None = None,
+ water_thresh: float | int = 1.4,
dr_force: float = 0.5,
- random_state: Optional[int] = 42,
+ random_state: int | None = 42,
wath_ligand_only: bool = False,
neq: bool = False,
neq_reps: int = 5,
@@ -108,7 +108,7 @@ def __init__(
neq_relax_steps: int = 5000,
neq_L: float = 8.0,
neq_schedule: Literal["sigmoidal", "linear"] = "sigmoidal",
- protein_charge: Optional[int] = None,
+ protein_charge: int | None = None,
charge_method: str = "ion_match",
):
self.timestep = timestep
@@ -159,7 +159,7 @@ def __init__(
raise ValueError(f"charge_method={charge_method!r} not in {valid_charge_methods}")
self.charge_method = charge_method
# Populated by read_files() once formal charges are known.
- self.same_charge: Optional[bool] = None
+ self.same_charge: bool | None = None
# Co-alchemical water state, populated by place_counter_water() when
# charge_method == "coalchemical_water". Each entry is a dict with
# keys "topology_indices" (3 ints) and "qatoms" (3 Q-atom descriptors).
@@ -175,7 +175,7 @@ def __init__(
try:
resnr = int(line[22:26])
atnr = int(line[6:11])
- except (IndexError, ValueError):
+ except IndexError, ValueError:
continue
break
self.residueoffset = resnr
@@ -594,7 +594,7 @@ def place_counter_ions(self, writedir: str) -> int:
try:
last_atnr = max(last_atnr, int(line[6:11]))
last_resnr = max(last_resnr, int(line[22:26]))
- except (ValueError, IndexError):
+ except ValueError, IndexError:
continue
# Append ion ATOM lines to the combined PDB
@@ -1605,7 +1605,7 @@ def write_runfile(self, writedir, file_list):
elif self.start == "0.5":
outfile.write(f"{mpirun} md_0500_0500.inp > md_0500_0500.log\n\n")
- for md1, md2 in zip(md_1, md_2):
+ for md1, md2 in zip(md_1, md_2, strict=False):
outfile.write(f"{mpirun} {md1[:-4]}.inp > {md1[:-4]}.log\n")
outfile.write(f"{mpirun} {md2[:-4]}.inp > {md2[:-4]}.log\n")
outfile.write("\n")
@@ -1629,7 +1629,7 @@ def write_qfep(self, windows, lambdas):
with open(qfep_out, "w") as outfile:
outfile.write(content)
- def avoid_water_protein_clashes(self, writedir, header: Optional[str] = None, save_removed: bool = False):
+ def avoid_water_protein_clashes(self, writedir, header: str | None = None, save_removed: bool = False):
"""Function to remove water molecules too close to protein & ligands | ligands (water leg).
Thresholds are the distances in Ångström from the protein & ligands | ligands atoms
to the nearest heavy atom in the water molecule (HOH).
diff --git a/src/QligFEP/qmapfep.py b/src/QligFEP/qmapfep.py
index 0b84699a..3b14765e 100644
--- a/src/QligFEP/qmapfep.py
+++ b/src/QligFEP/qmapfep.py
@@ -8,7 +8,7 @@
from collections import namedtuple
from functools import cached_property, lru_cache
from pathlib import Path
-from typing import Literal, Optional
+from typing import Literal
import networkx as nx
from networkx.readwrite import json_graph
@@ -345,7 +345,9 @@ def png(self):
self._remove_hs() # Does this three belongs here ??
self._reorient_molecule() #
# Loop over R groups.
- for color, (label, group) in zip(self.palette, self.pool.groups[self.pool_idx].items()):
+ for color, (label, group) in zip(
+ self.palette, self.pool.groups[self.pool_idx].items(), strict=False
+ ):
if label == "Core":
continue
self._highlight_residue(group, color, highlights)
@@ -1020,11 +1022,11 @@ def makeplot_dG(self):
class Init:
def __init__(
self,
- input_sdf: Optional[str] = None,
- input_json: Optional[str] = None,
+ input_sdf: str | None = None,
+ input_json: str | None = None,
metric: Literal["MFP", "Tanimoto", "MCS", "SMILES"] = "Tanimoto",
- output: Optional[str] = None,
- wrkdir: Optional[str] = None,
+ output: str | None = None,
+ wrkdir: str | None = None,
):
metric = metric
o = output
diff --git a/src/QligFEP/restraints/restraint_setter.py b/src/QligFEP/restraints/restraint_setter.py
index 2178ddbd..9226b029 100644
--- a/src/QligFEP/restraints/restraint_setter.py
+++ b/src/QligFEP/restraints/restraint_setter.py
@@ -3,7 +3,6 @@
from copy import deepcopy
from itertools import zip_longest
from pathlib import Path
-from typing import Union
import numpy as np
from kartograf import KartografAtomMapper, SmallMoleculeComponent
@@ -53,7 +52,7 @@ def __init__(self, molA: str, molB: str, kartograf_max_atom_distance: float | in
@staticmethod
def input_to_small_molecule_component(
- input_molecule: Union[Molecule, Chem.Mol, str, Path],
+ input_molecule: Molecule | Chem.Mol | str | Path,
) -> SmallMoleculeComponent:
if isinstance(input_molecule, SmallMoleculeComponent):
mol = input_molecule
@@ -257,7 +256,7 @@ def is_ring_equivalent(
return False
# Further check the atomic number and connectivity
- for a, b in zip(ring_atoms_a_indices, ring_atoms_b_indices):
+ for a, b in zip(ring_atoms_a_indices, ring_atoms_b_indices, strict=False):
logger.trace(f"Comparison method: {compare_method}")
atom_a = mol_a.GetAtomWithIdx(a)
atom_b = mol_b.GetAtomWithIdx(b)
diff --git a/src/QligFEP/visualization.py b/src/QligFEP/visualization.py
index 3341590e..d32b9590 100644
--- a/src/QligFEP/visualization.py
+++ b/src/QligFEP/visualization.py
@@ -1,7 +1,6 @@
"""Module to hold utility visualization functions for QligFEP."""
from pathlib import Path
-from typing import Optional, Union
import matplotlib.pyplot as plt
import py3Dmol
@@ -15,15 +14,15 @@
from .restraints.restraint_setter import RestraintSetter
-def mol_to_molblock(mol: Union[Molecule, Chem.Mol]) -> str:
+def mol_to_molblock(mol: Molecule | Chem.Mol) -> str:
if isinstance(mol, Molecule):
mol = mol.to_rdkit()
return Chem.MolToMolBlock(mol)
def render_system(
- molecules: list[Union[Molecule, Chem.Mol]],
- protein_path: Optional[Union[Path, str]] = None,
+ molecules: list[Molecule | Chem.Mol],
+ protein_path: Path | str | None = None,
protein_style: str = "stick",
size: tuple[int, int] = (600, 500),
) -> None:
@@ -83,8 +82,8 @@ def render_system(
# Credit to: https://github.com/OpenFreeEnergy/openfe/blob/main/openfe/utils/visualization_3D.py
# And to: https://github.com/OpenFreeEnergy/kartograf/blob/main/src/kartograf/utils/mapping_visualization_widget.py
def render_ligand_restraints(
- ligand1: Union[Chem.Mol, Molecule, SmallMoleculeComponent],
- ligand2: Union[Chem.Mol, Molecule, SmallMoleculeComponent],
+ ligand1: Chem.Mol | Molecule | SmallMoleculeComponent,
+ ligand2: Chem.Mol | Molecule | SmallMoleculeComponent,
restraint_mapping: dict[int, int],
show_atom_idxs: bool = True,
size: tuple[int, int] = (900, 500),
@@ -177,8 +176,8 @@ def add_spheres(view, mol, mapping, is_ligand1=True, viewer=(0, 0)):
def apply_and_render_restraint(
- ligand1: Union[Molecule, Chem.Mol, str, Path],
- ligand2: Union[Molecule, Chem.Mol, str, Path],
+ ligand1: Molecule | Chem.Mol | str | Path,
+ ligand2: Molecule | Chem.Mol | str | Path,
restraint_method: str = "hybridization_p",
show_atom_idxs: bool = True,
size: tuple[int, int] = (900, 500),
diff --git a/test/conftest.py b/test/conftest.py
index 73f3d4c1..f4b76d0e 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -300,7 +300,7 @@ def tyk2_tutorial_path(tutorials_path: Path) -> Path:
@pytest.fixture
-def temp_work_dir() -> Generator[Path, None, None]:
+def temp_work_dir() -> Generator[Path]:
"""Create a temporary working directory that is cleaned up after the test."""
temp_dir = Path(tempfile.mkdtemp())
try:
@@ -611,7 +611,7 @@ def compare_inp_files(self, actual: dict, expected: dict, ignore_keys: list[str]
f"[{section}] length mismatch: expected {len(expected_section)}, got {len(actual_section)}"
)
else:
- for i, (exp_line, act_line) in enumerate(zip(expected_section, actual_section)):
+ for i, (exp_line, act_line) in enumerate(zip(expected_section, actual_section, strict=False)):
if exp_line != act_line:
differences.append(f"[{section}][{i}]: expected '{exp_line}', got '{act_line}'")
diff --git a/test/neq/test_analysis_plotting.py b/test/neq/test_analysis_plotting.py
index 4029e0fa..b42c4d24 100644
--- a/test/neq/test_analysis_plotting.py
+++ b/test/neq/test_analysis_plotting.py
@@ -66,7 +66,7 @@ def test_create_ddG_plot_axis_bounds_include_all_points():
fig, ax = create_ddG_plot(df, target_name="test")
xlo, xhi = ax.get_xlim()
ylo, yhi = ax.get_ylim()
- for exp, calc in zip(df["ddg_value"], df["Q_ddG_avg"]):
+ for exp, calc in zip(df["ddg_value"], df["Q_ddG_avg"], strict=False):
assert xlo <= exp <= xhi, f"exp {exp} outside x-range [{xlo}, {xhi}]"
assert ylo <= calc <= yhi, f"calc {calc} outside y-range [{ylo}, {yhi}]"
diff --git a/test/qligfep/test_fep_input_files.py b/test/qligfep/test_fep_input_files.py
index 4e216875..180f23c9 100644
--- a/test/qligfep/test_fep_input_files.py
+++ b/test/qligfep/test_fep_input_files.py
@@ -155,7 +155,9 @@ def test_distance_restraints_match_golden(self, generated_fep_dir: Path):
), f"Distance restraints count mismatch: expected {len(golden_restraints)}, got {len(actual_restraints)}"
# Compare each restraint line
- for i, (golden_line, actual_line) in enumerate(zip(golden_restraints, actual_restraints)):
+ for i, (golden_line, actual_line) in enumerate(
+ zip(golden_restraints, actual_restraints, strict=False)
+ ):
assert (
actual_line == golden_line
), f"Distance restraint {i} mismatch:\n expected: {golden_line}\n actual: {actual_line}"
diff --git a/test/qligfep/test_qprep_cli.py b/test/qligfep/test_qprep_cli.py
index 8858672a..5fffa770 100644
--- a/test/qligfep/test_qprep_cli.py
+++ b/test/qligfep/test_qprep_cli.py
@@ -350,6 +350,7 @@ def test_dna_with_c1_inside_sphere_unchanged(self):
for (aname, elem), (px, py, pz) in zip(
[("P", "P"), ("OP1", "O"), ("OP2", "O"), ("O5'", "O"), ("C1'", "C")],
positions,
+ strict=False,
):
rows.append(_make_atom("ATOM", serial, aname, "DA", "E", 1, px, py, pz, elem))
serial += 1