#!/usr/bin/env python
import logging
import os
import shutil
import tempfile
from pathlib import Path
from typing import Iterable, List, Tuple, Union
import yaml
from parmed import Structure
from bindflow.home import home
from bindflow.utils import tools
logger = logging.getLogger(__name__)
def get_atom_types(top: tools.PathLike) -> dict:
"""
Return the atomtypes section as a dict with key atom type and values the
corresponded line. Include statements are not
take it into account.
"""
atom_types = {}
with open(top, 'r') as f:
lines = f.readlines()
section_found = False
for line in lines:
if line.startswith('[ atomtypes ]'):
section_found = True
continue
if section_found:
if line.startswith(';'):
continue
elif (not line.strip() or line.startswith('[')) and not line.startswith('[ atomtypes ]'):
section_found = False
continue
fields = line.split()
if len(fields) >= 6:
atom_type = fields[0]
atom_types[atom_type] = line
return atom_types
def get_molecule_names(input_topology: tools.PathLike, section: str = 'molecules') -> list:
"""It gets the molecule names specified inside input_topology
Parameters
----------
input_topology : tools.PathLike
The path of the input topology
section : str
The section to extract names from: molecules or moleculetype
Returns
-------
list
A list of the molecules presented in the topology
"""
if section not in ['molecules', 'moleculetype']:
raise ValueError(f"section must be 'molecules', 'moleculetype. {section} was provided.")
with open(input_topology, 'r') as f:
lines = f.readlines()
molecules = []
i = 0
while i < len(lines):
if section in lines[i]:
i += 1
while ("[" not in lines[i]):
if not lines[i].startswith(';'):
split_line = lines[i].split()
if len(split_line) == 2:
molecules.append(split_line[0])
i += 1
if i >= len(lines):
break
i += 1
return molecules
def add_posres_section(input_topology: tools.PathLike, molecules: Iterable[str], out_file: tools.PathLike = None):
"""This will add to the original topology file the corresponded POSRES section to the
provided molecules:
Examples of added lines:
#ifdef POSRES
#include "posres_{molecule}.itp"
#endif
Parameters
----------
input_topology : PathLike
The path of the input topology
molecules : Iterable[str]
The list of name of the molecules for which the topology section will be added
out_file : PathLike, optional
The path to output the modified topology file, by default None. Which means that it
will modify inplace input_topology.
"""
with open(input_topology, "r") as f:
top_lines = f.readlines()
# This is just to be as close as possible to the result of pdb2gmx
add_sol_internally = False
if 'SOL' not in molecules:
molecules.append('SOL')
add_sol_internally = True
look_out_flag = False
out_lines = []
for line in top_lines:
if not line.startswith("[ molecules ]"):
for molecule in molecules:
if molecule in line and " 3\n" in line:
look_out_flag = True
mol_name = line.split()[0]
if look_out_flag and ('[ moleculetype ]' in line or '[ system ]' in line):
if mol_name == 'SOL' and add_sol_internally:
out_lines.append("\n#ifdef POSRES_WATER\n")
out_lines.append("; Position restraint for each water oxygen\n")
out_lines.append("[ position_restraints ]\n")
out_lines.append("; i funct fcx fcy fcz\n")
out_lines.append(" 1 1 1000 1000 1000\n")
out_lines.append("#endif\n\n")
else:
out_lines.append("\n#ifdef POSRES\n")
out_lines.append(f'#include "posres_{mol_name}.itp"\n')
out_lines.append("#endif\n\n")
look_out_flag = False
out_lines.append(line)
if not out_file:
out_file = input_topology
with open(out_file, "w") as w:
w.write("".join(out_lines))
def make_posres(input_topology: tools.PathLike, molecules: Iterable[str], out_dir: tools.PathLike, f_xyz: tuple = (2500, 2500, 2500)):
"""Make a position restraint file out of input_topology for all the molecules specified
on molecules. Taking only the heavy atoms into account
Parameters
----------
input_topology : PathLike
The path of the input topology
molecules : Iterable[str]
The list of name of the molecules for which the posres file will be created
out_dir : PathLike
The path where the posres files will be written
f_xyz : tuple
The x, y, z components of the restraint force to be used. It could
be a float number of a string to be then defined on the mdp file, by default (2500, 2500, 2500)
"""
for molecule in molecules:
atom_flag = False
with open(input_topology, "r") as f:
top_lines = f.readlines()
posres_filename = f"posres_{molecule}.itp"
with open(Path(out_dir)/posres_filename, "w") as posres_file:
posres_file.write("[ position_restraints ]\n")
for i, _ in enumerate(top_lines):
if f"{molecule} " in (top_lines[i]) and " 3\n" in (top_lines[i]):
j = i + 1
while j < len(top_lines):
if '[ atoms ]' in top_lines[j]:
j += 1 # skip this line
atom_flag = True
if top_lines[j].startswith('['): # A new section was reached
break
if atom_flag:
if not top_lines[j].startswith("\n") and not top_lines[j].startswith(";") and not top_lines[j].startswith("#"):
# Check if heavy atom based on the mass. In case of use of HMR, for that reason 3
if float(top_lines[j].split()[7]) > 3:
posres_str = f"{top_lines[j].split()[0]} 1 {f_xyz[0]} {f_xyz[1]} {f_xyz[2]}\n"
posres_file.write(posres_str)
j += 1
break
# Add posre sections to the topology
add_posres_section(input_topology=input_topology, molecules=molecules, out_file=None)
def _tip3p_settles_to_constraints(top: tools.PathLike, molecule: str, out_top: Union[tools.PathLike, None] = None) -> None:
"""Temporal solution to TODO (put the GitHub Issue).
Basically it will change the settles entrance of `molecule`
by:
; https://gromacs.bioexcel.eu/t/how-to-treat-specific-water-molecules-as-ligand/3470/9
'[ constraints ]'
; ai aj funct length
1 2 1 0.09572
1 3 1 0.09572
2 3 1 0.15139
Warning
-------
This is only useful for replacing the settle section of a tip3p-like molecule.
This function its just a workaround and will probably bve removed on the future
Parameters
----------
top : tools.PathLike
The GMX topology file
molecule : str
Name of the molecule where to look for the [ settles ] section
out_top : Union[tools.PathLike, None], optional
Path for a output topology, by default None which means that top will be modify in place.
"""
constraints_section = "; https://gromacs.bioexcel.eu/t/how-to-treat-specific-water-molecules-as-ligand/3470/9\n"\
"[ constraints ]\n"\
"; ai aj funct length\n"\
"1 2 1 0.09572\n"\
"1 3 1 0.09572\n"\
"2 3 1 0.15139\n\n"
with open(top, 'r') as f:
lines = f.readlines()
idx_begins, idx_ends = None, None
section_found = False
i = 0
while not lines[i].startswith('[ molecules ]') and i < len(lines):
if molecule in lines[i] and " 3\n" in lines[i]:
j = i
while not lines[j].startswith('[ moleculetype ]') and j < len(lines):
if lines[j].startswith('[ settles ]'):
section_found = True
idx_begins = j
j += 1
if section_found and lines[j].startswith(('[', '#')):
idx_ends = j
break
j += 1
break
i += 1
if not out_top:
out_top = top
with open(out_top, 'w') as f:
f.write("".join(lines[:idx_begins]) + constraints_section + "".join(lines[idx_ends:]))
[docs]
class Solvate:
[docs]
def __init__(self, water_model_code: str, builder_dir: tools.PathLike = '.solvate', load_dependencies: List[str] = None) -> None:
"""Class to solvate GMX systems.
Force fields were extracted from `GMX topologies <https://gitlab.com/gromacs/gromacs/-/tree/main/share/top?ref_type=heads>`__.
Remember to cite properly the main references if you use any of the water models in your work.
Available water models:
* amber:
* amber/spc
* amber/spce
* amber/tip3p
* amber/tip4p
* amber/tip4pew
* amber/tip5p
* charmm
* charmm/spc
* charmm/spce
* charmm/tip3p
* charmm/tips3p
* charmm/tip4p
* charmm/tip5p
* oplsaa
* oplsaa/spc
* oplsaa/spce
* oplsaa/tip3p
* oplsaa/tip4p
* oplsaa/tip4pew
* oplsaa/tip5p
* oplsaa/tip5pe
Parameters
----------
water_model_code : str
Water model code in the form: "{force field family}/{water model}"
builder_dir : tools.PathLik, optional
Where the temporal files will be written.
load_dependencies : List[str], optional
It is used in case some previous loading steps are needed for GROMACS commands;
e.g: ['source /groups/CBG/opt/spack-0.18.1/shared.bash', 'module load sandybridge/gromacs/2025.4'], by default None
Raises
------
ValueError
Invalid force field family
ValueError
Invalid water model for the selected force field
"""
self.load_dependencies = load_dependencies
self.builder_dir = Path(builder_dir).resolve()
self.builder_dir.mkdir(exist_ok=True, parents=True)
# Make directory to save topologies after solvation.
# This will be cleaned out and created every time the class is called (at the beginning)
# to avoid mismatch.ch between files generated on different calls
self.solvated_dir = self.builder_dir/'solvated_sys'
with open(Path(home(dataDir='gmx_water_models'))/'water_models.yml', 'r') as f:
self.water_models_data = yaml.safe_load(f)
# Check validity of input code
force_field_family, water_model = water_model_code.split('/')
if force_field_family not in self.water_models_data:
raise ValueError(f"Invalid force field family: {force_field_family}. Choose from {self.water_models_data.keys()}")
elif water_model not in self.water_models_data[force_field_family]:
raise ValueError(f"Invalid water model ({water_model}) for {force_field_family}."
f"Choose from {self.water_models_data[force_field_family].keys()}")
self.force_field_family = force_field_family
self.water_model = water_model
self.water_itp, self.ions_itp, self.ffnonbonded_itp, self.water_gro = self._get_gmx_water_model()
self.cwd = os.getcwd()
if load_dependencies:
if isinstance(load_dependencies, List):
self.load_dependencies = tools.HARD_CODE_DEPENDENCIES + ["export GMX_MAXBACKUP=-1"] + load_dependencies
else:
raise ValueError(f"load_dependencies must be a List. Provided: {load_dependencies}")
def _get_gmx_water_model(self) -> Tuple[tools.PathLike]:
"""
Retrieve water model files
Returns
-------
Tuple[PathLike]
A tuple with the absolute path of (in this order):
* water itp file
* ions itp file
* water (configuration) gro file
* atom type itp definition
"""
# Extract water and ions topologies
ff_dir = Path(home(dataDir='gmx_water_models'))
water_itp = (ff_dir/str(self.force_field_family)/f"{self.water_model}.itp").resolve()
ions_itp = (ff_dir/str(self.force_field_family)/"ions.itp").resolve()
ffnonbonded_itp = (ff_dir/str(self.force_field_family)/"ffnonbonded.itp").resolve()
water_gro = (ff_dir/"configurations"/self.water_models_data[self.force_field_family][self.water_model]).resolve()
return water_itp, ions_itp, ffnonbonded_itp, water_gro
def _include_all_atom_types(self, top: tools.PathLike) -> None:
"""Add all the atom types of the corresponded force field family
They will be added to the first [ atomtypes ] section on the file
top.
Parameters
----------
top : top: tools.PathLike
Topology file to be modified.
"""
with open(top, 'r') as f:
lines = f.readlines()
idx_begins, idx_ends = None, None
section_found = False
for i, line in enumerate(lines):
if line.startswith('[ atomtypes ]'):
idx_begins = i + 1
section_found = True
continue
if section_found:
if line.startswith('['):
idx_ends = i
break
# Add missing atom_types for solvation
# Extra atom_types will be removed when parmed write the structure
if idx_begins is not None and idx_ends is not None:
atom_types = get_atom_types(top)
for atom_type_name, atom_type_info in get_atom_types(self.ffnonbonded_itp).items():
if atom_type_name not in atom_types:
atom_types[atom_type_name] = atom_type_info
with open(top, 'w') as f:
f.write("".join(lines[:idx_begins] + list(atom_types.values()) + ["\n\n"] + lines[idx_ends:]))
def _include_water_ions_params(self, top: tools.PathLike) -> None:
"""It add include statements to the corresponded water and ion itp files
Parameters
----------
top : tools.PathLike
The topology file
"""
include_statements = [
f"#include \"{self.water_itp}\"\n",
f"#include \"{self.ions_itp}\"\n",
]
with open(top, 'r') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
if line.startswith('[ system ]'):
break
with open(top, 'w') as f:
f.write("".join(lines[:idx] + include_statements + lines[idx:]))
def _add_water_and_ions(
self,
gro: tools.PathLike,
top: tools.PathLike,
bt: str = "triclinic",
box: list[float] = None,
angles: list[float] = None,
d: float = None,
c: bool = False,
pname: str = "NA",
nname: str = "CL",
ion_conc: float = 150E-3,
rmin: float = 1.0) -> None:
"""Make box, solvate and add ions to the system
Parameters
----------
gro : tools.PathLike
The configuration file.
top: tools.PathLike
The GMX's topology file.
bt : str, optional
Box type for -box and -d: triclinic, cubic, dodecahedron, octahedron, by default triclinic
box : str, optional
Box vector lengths (a,b,c) in nm (remember that PDB are in Angstroms), by default None. Which means that gmx editconf will use (0 0 0)
angles : Iterable[float], optional
This is the angles between the components of the vector in DEGREES.
It is important that the provided vector has the correct units, by default None.
For membrane systems (90,90,60) is advisable.
d : float, optional
Distance between the solute and the box, by default None. Which means that gmx editconf will use 0
c : bool, optional
Center molecule in box (implied by -box and -d), by default False
pname : str, optional
Name of the positive ion, by default NA
nname : str, optional
Name of the negative ion, by default CL
ion_conc : float, optional
Ion concentration used during neutralization of the system, by default 150E-3
rmin : float, optional
Minimum distance between ions and non-solvent, by default 1.0
out_dir : PathLike, optional
Where the files will be written: solvated.gro, solvated.top, by default '.'
"""
# We can change directory because all the path used are already converted to absolute paths
os.chdir(self.solvated_dir)
editconf_kwargs = dict(
f=gro,
o=gro,
bt=bt
)
if box:
editconf_kwargs['box'] = ' '.join([str(i) for i in box])
if angles:
editconf_kwargs['angles'] = ' '.join([str(i) for i in angles])
if d:
editconf_kwargs['d'] = d
if c:
editconf_kwargs['c'] = True
# First write an mdp file.
with open("ions.mdp", "w") as file:
file.write("; Neighbor searching\n"
"cutoff-scheme = Verlet\n"
"rlist = 1.1\n"
"pbc = xyz\n"
"verlet-buffer-tolerance = -1\n"
"\n; Electrostatics\n"
"coulombtype = cut-off\n"
"\n; VdW\n"
"rvdw = 1.0\n")
# It is failing becasue There is not define the atom type for the water molecules
# Define GMX functions
@tools.gmx_command(load_dependencies=self.load_dependencies)
def editconf(**kwargs): ...
@tools.gmx_command(load_dependencies=self.load_dependencies)
def solvate(**kwargs): ...
@tools.gmx_command(load_dependencies=self.load_dependencies)
def grompp(**kwargs): ...
@tools.gmx_command(load_dependencies=self.load_dependencies, stdin_command="echo \"SOL\"")
def genion(**kwargs): ...
# Execute the GMX functions
editconf(**editconf_kwargs)
solvate(cp=gro, p=top, cs=self.water_gro, o=gro)
grompp(f="ions.mdp", c=gro, p=top, o="ions.tpr")
genion(s="ions.tpr", p=top, o=gro, neutral=True, pname=pname, nname=nname, rmin=rmin, conc=ion_conc)
# Just to clean the topology. In this way only the used atom types are written.
# And the include statements are removed
# It builds a monolithic topology
struc = tools.readParmEDMolecule(top_file=top, gro_file=gro)
struc.save(str(top), overwrite=True)
struc.save(str(gro), overwrite=True)
# Change back to cwd
os.chdir(self.cwd)
[docs]
def clean(self, directory: Union[None, tools.PathLike] = None) -> None:
"""Used to delete self.builder_dir or directory if provided
Danger
------
Use it wisely (when directory is provided), you may ended up deleting your computer :-)
Parameters
----------
directory : Union[None, tools.PathLike], optional
Directory to delete, by default None
"""
os.chdir(self.cwd)
if directory:
dir2delete = directory
else:
dir2delete = self.builder_dir
try:
shutil.rmtree(dir2delete)
except FileNotFoundError:
pass
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, exception_traceback):
self.clean()
[docs]
def __call__(
self,
structure: Structure,
bt: str = "triclinic",
box: list[float] = None,
angles: list[float] = None,
d: float = None,
c: bool = False,
pname: str = "NA",
nname: str = "CL",
ion_conc: float = 150E-3,
rmin: float = 1.0,
exclusion_list: list = None,
out_dir: tools.PathLike = '.',
out_name: str = 'solvated',
f_xyz: tuple = (2500, 2500, 2500),
settles_to_constraints_on: Union[tools.PathLike, str] = None) -> None:
if exclusion_list is None:
exclusion_list = ["SOL", "NA", "CL"] # "MG", "ZN"]
# Clean any possible generated files during previous calls
out_dir = Path(out_dir)
self.clean(self.solvated_dir)
self.solvated_dir.mkdir(exist_ok=True, parents=True)
out_dir.mkdir(exist_ok=True, parents=True)
# Set out files
init_top = self.solvated_dir/'init.top'
init_gro = self.solvated_dir/'init.gro'
# Write the top and gro file of the structure
structure.save(str(init_top), overwrite=True)
structure.save(str(init_gro), overwrite=True)
# Add ion and water section to the topology
self._include_water_ions_params(init_top)
# Include all the atom types of the force field
self._include_all_atom_types(init_top)
# Solvate add ions and clean the topology
self._add_water_and_ions(
gro=init_gro,
top=init_top,
bt=bt,
box=box,
angles=angles,
d=d,
c=c,
pname=pname,
nname=nname,
ion_conc=ion_conc,
rmin=rmin)
# Add position restraint section to topology
molecules = list(set(get_molecule_names(init_top)) - set(exclusion_list))
# Here I have to move the files to the final directory with its specified names
make_posres(
input_topology=init_top,
molecules=molecules,
out_dir=out_dir,
f_xyz=f_xyz
)
# Fix conversion for constraints to settles on water-like molecules
if settles_to_constraints_on:
_tip3p_settles_to_constraints(top=init_top, molecule=settles_to_constraints_on, out_top=None)
shutil.copy(init_top, out_dir/f'{out_name}.top')
shutil.copy(init_gro, out_dir/f'{out_name}.gro')
def index_for_membrane_system(
configuration_file: tools.PathLike,
ndxout: tools.PathLike = "index.ndx",
ligand_name: str = "LIG",
host_name: str = "Protein",
cofactor_selection: str = None,
cofactor_on_protein: bool = True,
load_dependencies: List[str] = None):
"""Make the index file for membrane systems with SOLU, MEMB and SOLV. It uses gmx make_ndx and select internally.
One examples selection that can be created with ligand_name = LIG; cofactor_selection = resname COF and cofactor_on_protein = True is:
#. "RECEPTOR" group {host_name};
#. "LIGAND" resname {ligand_name};
#. "SOLU" group {host_name} or resname {ligand_name} or resname COF;
#. "MEMB" ((group System and ! group Water_and_ions) and ! group {host_name}) and ! (resname {ligand_name}) and ! (resname COF);
#. "SOLV" group Water_and_ions;
Parameters
----------
configuration_file : PathLike
PDB or GRO file of the system.
ndxout : PathLike
Path to output the index file.
ligand_name : str
The residue name for the ligand in the configuration file, by default "LIG".
host_name : str
The group name for the host in the configuration file, by default "Protein".
cofactor_selection : str
The selection for the cofactor in the configuration file, by default None
cofactor_on_protein : bool
It only works if cofactor_name is provided. If True, the cofactor will be part of the protein and the lignad
if False will be part of the solvent and ions, bt default True
load_dependencies : List[str], optional
It is used in case some previous loading steps are needed for GROMACS commands;
e.g: ['source /groups/CBG/opt/spack-0.18.1/shared.bash', 'module load sandybridge/gromacs/2025.4'], by default None
"""
tmpopt = tempfile.NamedTemporaryFile(suffix='.opt')
tmpndx = tempfile.NamedTemporaryFile(suffix='.ndx')
# Nice use of gmx select, see the use of the parenthesis
sele_RECEPTOR = f"\"RECEPTOR\" group {host_name}"
sele_LIGAND = f"\"LIGAND\" resname {ligand_name}"
sele_MEMB = f"\"MEMB\" ((group System and ! group Water_and_ions) and ! group {host_name}) and ! (resname {ligand_name})"
sele_SOLU = f"\"SOLU\" group {host_name} or resname {ligand_name}"
sele_SOLV = "\"SOLV\" group Water_and_ions"
if cofactor_selection:
sele_MEMB += f" and ! ({cofactor_selection})"
if cofactor_on_protein:
sele_SOLU += f" or {cofactor_selection}"
else:
sele_SOLV += f" or {cofactor_selection}"
logger.info("Groups in the index.ndx file:")
logger.info(f"\t{sele_RECEPTOR}")
logger.info(f"\t{sele_LIGAND}")
logger.info(f"\t{sele_SOLU}")
logger.info(f"\t{sele_MEMB}")
logger.info(f"\t{sele_SOLV}")
sele_RECEPTOR += ";\n"
sele_LIGAND += ";\n"
sele_SOLU += ";\n"
sele_MEMB += ";\n"
sele_SOLV += ";\n"
with open(tmpopt.name, "w") as opt:
opt.write(sele_RECEPTOR + sele_LIGAND + sele_SOLU + sele_MEMB + sele_SOLV)
@tools.gmx_command(load_dependencies=load_dependencies, stdin_command="echo \"q\"")
def make_ndx(**kwargs): ...
@tools.gmx_command(load_dependencies=load_dependencies)
def select(**kwargs): ...
make_ndx(f=configuration_file, o=tmpndx.name)
select(s=configuration_file, sf=tmpopt.name, n=tmpndx.name, on=ndxout)
# tools.run(f"""
# export GMX_MAXBACKUP=-1
# echo "q" | gmx make_ndx -f {configuration_file} -o {tmpndx.name}
# gmx select -s {configuration_file} -sf {tmpopt.name} -n {tmpndx.name} -on {ndxout}
# """)
# deleting the line _f0_t0.000 in the file
with open(ndxout, "r") as index:
data = index.read()
data = data.replace("_f0_t0.000", "")
with open(ndxout, "w") as index:
index.write(data)
tmpopt.close()
tmpndx.close()
def index_for_soluble_system(
configuration_file: tools.PathLike,
ndxout: tools.PathLike = "index.ndx",
ligand_name: str = "LIG",
host_name: str = "Protein",
load_dependencies: List[str] = None):
"""Make the index file for soluble system. This is only needed in case MMPBSA calculation;
#. "RECEPTOR" group {host_name};
#. "LIGAND" resname {ligand_name};
Parameters
----------
configuration_file : PathLike
PDB or GRO file of the system.
ndxout : PathLike
Path to output the index file.
ligand_name : str
The residue name for the ligand in the configuration file, by default "LIG".
host_name : str
The group name for the host in the configuration file, by default "Protein".
load_dependencies : List[str], optional
It is used in case some previous loading steps are needed for GROMACS commands;
e.g: ['source /groups/CBG/opt/spack-0.18.1/shared.bash', 'module load sandybridge/gromacs/2025.4'], by default None
"""
tmpopt = tempfile.NamedTemporaryFile(suffix='.opt')
tmpndx = tempfile.NamedTemporaryFile(suffix='.ndx')
sele_RECEPTOR = f"\"RECEPTOR\" group {host_name}"
sele_LIGAND = f"\"LIGAND\" resname {ligand_name}"
logger.info("Groups in the index.ndx file:")
logger.info(f"\t{sele_RECEPTOR}")
logger.info(f"\t{sele_LIGAND}")
sele_RECEPTOR += ";\n"
sele_LIGAND += ";\n"
with open(tmpopt.name, "w") as opt:
opt.write(sele_RECEPTOR + sele_LIGAND)
@tools.gmx_command(load_dependencies=load_dependencies, stdin_command="echo \"q\"")
def make_ndx(**kwargs): ...
@tools.gmx_command(load_dependencies=load_dependencies)
def select(**kwargs): ...
make_ndx(f=configuration_file, o=tmpndx.name)
select(s=configuration_file, sf=tmpopt.name, n=tmpndx.name, on=ndxout)
# tools.run(f"""
# export GMX_MAXBACKUP=-1
# echo "q" | gmx make_ndx -f {configuration_file} -o {tmpndx.name}
# gmx select -s {configuration_file} -sf {tmpopt.name} -n {tmpndx.name} -on {ndxout}
# """)
# deleting the line _f0_t0.000 in the file
with open(ndxout, "r") as index:
data = index.read()
data = data.replace("_f0_t0.000", "")
with open(ndxout, "w") as index:
index.write(data)
tmpopt.close()
tmpndx.close()
if __name__ == '__main__':
pass