diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index 5c43b6c..7b9bbf0 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -3,7 +3,7 @@ channels: - conda-forge dependencies: - python - - xtb + - xtb>6.5 - ase - numpy - openbabel diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 991cc15..e33b289 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,29 +4,30 @@ name: PyPi Release on: - push: - pull_request: - workflow_dispatch: + push: + pull_request: + workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - - name: Install dependencies - run: >- - python -m pip install --user --upgrade setuptools wheel - - name: Build - run: >- - python setup.py sdist bdist_wheel - - name: Publish distribution 📦 to PyPI - if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_password }} + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build dependencies + run: python -m pip install --upgrade build + + - name: Build package + run: python -m build + + - name: Publish distribution to PyPI + if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/test-tutorials.yml b/.github/workflows/test-tutorials.yml new file mode 100644 index 0000000..e96eedd --- /dev/null +++ b/.github/workflows/test-tutorials.yml @@ -0,0 +1,100 @@ +name: Tutorial Tests + +on: + push: + branches: [main, Secondary_Solvation_Shell] + pull_request: + branches: [main, Secondary_Solvation_Shell] + +jobs: + find-tutorials: + runs-on: ubuntu-latest + outputs: + tutorials: ${{ steps.set-matrix.outputs.tutorials }} + steps: + - name: Check out repo + uses: actions/checkout@v5 + + - name: Find tutorial notebooks + id: set-matrix + run: | + # Tutorials to skip + SKIP_TUTORIALS=( + "10-Conformer_Searching.ipynb" # CREST sampling takes 6+ minutes + "12-Using_View_Structures.ipynb" # Visualization-focused, pymol renders + ) + + # Find all notebooks and filter out skipped ones + TUTORIALS=$(find documentation/tutorials -name "*.ipynb" | while read notebook; do + basename=$(basename "$notebook") + skip=false + for skip_tutorial in "${SKIP_TUTORIALS[@]}"; do + if [[ "$basename" == "$skip_tutorial" ]]; then + skip=true + break + fi + done + if [[ "$skip" == "false" ]]; then + echo "$notebook" + fi + done | jq -R -s -c 'split("\n")[:-1]') + + echo "tutorials=$TUTORIALS" >> $GITHUB_OUTPUT + + run-tutorial: + needs: find-tutorials + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + tutorial: ${{ fromJson(needs.find-tutorials.outputs.tutorials) }} + defaults: + run: + shell: bash -el {0} + + steps: + - name: Check out repo + uses: actions/checkout@v5 + + - name: Setup micromamba + uses: mamba-org/setup-micromamba@v2 + with: + micromamba-version: 'latest' + environment-name: tutorial-env + create-args: >- + python=3.12 + openbabel + crest + xtb>6.5 + tblite-python + jupytext + init-shell: bash + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + + - name: Install package with optional dependencies + run: | + uv pip install -e ".[mace,dftd,tutorials]" + + - name: Convert notebook to Python script + run: | + jupytext --to py:percent "${{ matrix.tutorial }}" + + - name: Run tutorial + run: | + cd $(dirname "${{ matrix.tutorial }}") + SCRIPT=$(basename "${{ matrix.tutorial }}" .ipynb).py + + echo "Running $SCRIPT...!" + timeout 600 python "$SCRIPT" + + - name: Cleanup + run: | + rm -rf documentation/tutorials/pymol_renders + rm -f documentation/tutorials/*.xyz + rm -f documentation/tutorials/*.py + if: always() diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e85dfb4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Tests + +on: + push: + branches: [main, Secondary_Solvation_Shell] + pull_request: + branches: [main, Secondary_Solvation_Shell] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Python 3.13 blocked by openbabel not yet available on conda-forge + python-version: ['3.11', '3.12'] + defaults: + run: + shell: bash -el {0} + + steps: + - name: Check out repo + uses: actions/checkout@v5 + + - name: Setup micromamba + uses: mamba-org/setup-micromamba@v2 + with: + micromamba-version: 'latest' + environment-name: test-env + create-args: >- + python=${{ matrix.python-version }} + xtb>6.5 + openbabel + crest + tblite-python + init-shell: bash + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + + - name: Install package + run: | + uv pip install -e . + + - name: Run tests + run: | + python -m unittest discover tests diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml deleted file mode 100644 index f9617a9..0000000 --- a/.github/workflows/unittests.yml +++ /dev/null @@ -1,37 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python package - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.8', '3.9', '3.10'] - - steps: - - uses: actions/checkout@v2 - - uses: conda-incubator/setup-miniconda@v2.2.0 - with: - python-version: ${{ matrix.python-version }} - mamba-version: "*" - channels: conda-forge - channel-priority: strict - auto-update-conda: true - environment-file: .ci_support/environment.yml - miniforge-variant: Mambaforge - - name: Setup - shell: bash -l {0} - run: | - pip install --no-deps . - - name: Test - shell: bash -l {0} - run: python -m unittest discover tests diff --git a/.gitignore b/.gitignore index 86adcd8..c77ceb1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ meta_production_sample.pkl development/**/*.csv development/**/*.mol2 -pymol_renders/ \ No newline at end of file +pymol_renders/ +build/ \ No newline at end of file diff --git a/README.md b/README.md index ffbd993..09778cf 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ pip install -e . ## XTB (backend) Potentially Useful References: * [Available Solvents](https://xtb-docs.readthedocs.io/en/latest/gbsa.html) -* [Available Methods](https://xtb-python.readthedocs.io/en/latest/general-api.html) -* [ASE Calculator](https://xtb-python.readthedocs.io/en/latest/ase-calculator.html) +* [Available Methods](https://xtb-docs.readthedocs.io/en/latest/commandline.html) +* [ASE Calculator](https://tblite.readthedocs.io/en/latest/users/ase.html#tblite.ase.TBLite) * [XTB Documentation](https://xtb-docs.readthedocs.io/en/latest/contents.html) ## Basic Use of complex construction functionality: diff --git a/architector/io_calc.py b/architector/io_calc.py index 58bd999..590944c 100644 --- a/architector/io_calc.py +++ b/architector/io_calc.py @@ -21,15 +21,6 @@ from ase.optimize import LBFGS from ase.constraints import FixAtoms, FixBondLengths, FixInternals -# Add any other ASE calculator here. -# To extend to other methods. -has_xtb_python = False -try: - from xtb.ase.calculator import XTB - has_xtb_python = True -except ImportError: - pass - from architector.arch_xtb_text_ase_calc import XTB_Calculator from tblite.ase import TBLite @@ -455,7 +446,7 @@ def calculate(self): electronic_temperature=self.xtb_electronic_temperature, verbosity=-1, ) - elif (not has_xtb_python) or (self.xtb_relax): + else: calc = XTB_Calculator( xtb_method=self.method, xtb_accuracy=self.xtb_accuracy, @@ -464,15 +455,6 @@ def calculate(self): xtb_solvent=self.xtb_solvent, xtb_relax=self.xtb_relax, ) - else: # legacy xtb-python - calc = XTB( - method=self.method, - solvent=self.xtb_solvent, - max_iterations=self.xtb_max_iterations, - electronic_temperature=self.xtb_electronic_temperature, - accuracy=self.xtb_accuracy, - ) - # verbosity=0) # Difference of more than 1. Still perform a ff_preoptimization if requested. if np.abs(self.mol.xtb_charge - self.mol.charge) > 1: if len(self.trans_oxo_triples) > 0: diff --git a/architector/io_lig.py b/architector/io_lig.py index b3c6922..e6cbc0e 100644 --- a/architector/io_lig.py +++ b/architector/io_lig.py @@ -38,14 +38,6 @@ from ase.optimize.bfgslinesearch import BFGSLineSearch import ase.constraints as ase_con -has_xtb_python = False -try: - from xtb.ase.calculator import XTB - - has_xtb_python = True -except: - pass - from architector.arch_xtb_text_ase_calc import XTB_Calculator # from tblite.ase import TBLite -> No GFN-FF support yet. @@ -119,10 +111,7 @@ def set_XTB_calc(ase_atoms): """ ase_atoms.set_initial_charges(np.zeros(len(ase_atoms))) ase_atoms.set_initial_magnetic_moments(np.zeros(len(ase_atoms))) - if has_xtb_python: - calc = XTB(method="GFN-FF") - else: - calc = XTB_Calculator(xtb_method="GFN-FF") + calc = XTB_Calculator(xtb_method="GFN-FF") # Default to only GFN-FF for ligand conformer relaxation. ase_atoms.calc = calc return ase_atoms diff --git a/architector/io_xtb_calc.py b/architector/io_xtb_calc.py deleted file mode 100644 index 797a532..0000000 --- a/architector/io_xtb_calc.py +++ /dev/null @@ -1,609 +0,0 @@ -# """ -# Assign XTB calculator to ase_atoms object following defualts or user input! -# Written by Michael Taylor - -# This module is largely depricated in architector in favor of io_calc.py -# It is useful still for xtb evalulations away from architector's main codebase. -# """ - -import numpy as np -from xtb.ase.calculator import XTB -from architector import arch_context_manage, io_molecule -import architector.io_ptable as io_ptable -import ase -from ase.optimize import BFGSLineSearch -from ase.atoms import Atom, Atoms - - -def set_XTB_calc(ase_atoms, parameters=dict(), assembly=False, isCp_lig=False): - """set_XTB_calc - assign xtb calculator to atoms instance! - - Parameters - ---------- - ase_atoms : ase.atoms.Atoms - atoms to assign calculator to - parameters : dict, optional - parameters from input/ io_process_input! - assembly : bool, optional - whether or not this is assembly or final relaxation, by default False - isCP_lig : bool, optional - whether this is cP ligand evaluation or not, by default False - """ - if isCp_lig: - ase_atoms.set_initial_charges(np.zeros(len(ase_atoms))) - ase_atoms.set_initial_magnetic_moments(np.zeros(len(ase_atoms))) - calc = XTB( - method="GFN-FF" - ) # ,verbosity=0) # Defaul to only GFN-FF for ligand conformer relaxation. - else: - # Charge -> charges already assigned to components during assembly - if (parameters["full_charge"] is not None) and (not assembly): - charge_vect = np.zeros(len(ase_atoms)) - charge_vect[0] = parameters["full_charge"] - ase_atoms.set_initial_charges(charge_vect) - else: - charge_vect = ase_atoms.get_initial_charges() - - mol_charge = np.sum(charge_vect) - symbols = ase_atoms.get_chemical_symbols() - metals = [x for x in symbols if x in io_ptable.all_metals] - - f_in_core = False - - if len(metals) == 1: - if metals[0] in io_ptable.heavy_metals: - f_in_core = True - else: - print( - "No metals - continuing anyway with obviously no f in core elements." - ) - - # Handle spin / magnetism - even_odd_electrons = ( - np.sum([atom.number for atom in ase_atoms]) - mol_charge - ) % 2 - if (parameters["full_spin"] is not None) and (not assembly): - uhf = parameters["full_spin"] - uhf_start = np.zeros(len(ase_atoms)) - uhf_start[0] = uhf - ase_atoms.set_initial_magnetic_moments(uhf_start) - else: - uhf = parameters[ - "metal_spin" - ] # Metal spin set by io_process_input to defaults. - if (even_odd_electrons == 1) and (uhf == 0): - uhf = 1 - elif (even_odd_electrons == 1) and (uhf < 7) and (uhf % 2 == 0): - uhf += 1 - elif (even_odd_electrons == 1) and (uhf >= 7) and (uhf % 2 == 0): - uhf -= 1 - if (even_odd_electrons == 0) and (uhf % 2 == 1): - uhf = uhf - 1 - elif (even_odd_electrons == 1) and (uhf % 2 == 0): - uhf = uhf + 1 - uhf_start = np.zeros(len(ase_atoms)) - if not f_in_core: - uhf_start[0] = uhf - else: # F in core assumes for a 3+ lanthanide there are 11 valence electrons (8 once the 3+ is taken into account) - even_odd_electrons = np.sum( - [atom.number for atom in ase_atoms] - ) - even_odd_electrons = ( - even_odd_electrons - - io_ptable.elements.index(metals[0]) - + 11 - - mol_charge - ) - even_odd_electrons = even_odd_electrons % 2 - if even_odd_electrons == 0: - uhf_start[0] = 0 - else: - uhf_start[0] = 1 - ase_atoms.set_initial_magnetic_moments(uhf_start) - - if assembly: - if ( - parameters["assemble_method"] == "GFN-FF" - ): # Need to turn off charges for GFN-FF evaluation. Probably an XTB-end bug. - ase_atoms.set_initial_charges(np.zeros(len(ase_atoms))) - ase_atoms.set_initial_magnetic_moments( - np.zeros(len(ase_atoms)) - ) - calc = XTB( - method=parameters["assemble_method"], - solvent=parameters["solvent"], - ) - # verbosity=0) - else: - if ( - parameters["full_method"] == "GFN-FF" - ): # Need to turn off charges for GFN-FF evaluation. Probably an XTB-end bug. - ase_atoms.set_initial_charges(np.zeros(len(ase_atoms))) - ase_atoms.set_initial_magnetic_moments( - np.zeros(len(ase_atoms)) - ) - calc = XTB( - method=parameters["full_method"], solvent=parameters["solvent"] - ) - # verbosity=0) - - ######################################################### - ########### Calculator Now Set! ######################### - ######################################################### - ase_atoms.calc = calc - return ase_atoms - - -def set_XTB_calc_lig( - ase_atoms, charge=None, uhf=None, method="GFN2-xTB", solvent="none" -): - """set_XTB_calc - assign xtb calculator to ligand atoms instance! - - Parameters - ---------- - ase_atoms : ase.atoms.Atoms - atoms to assign calculator to - charge : int, optional - charge of the species, default to initial charges set on ase_atoms - uhf : int, optional - number of unpaired electrons in the system, default to 0 - method : str, optional - which gfn family method to use, default GFN2-xTB - solvent: str, optional - use a solvent?, default 'none' - """ - if charge: - mol_charge = charge - else: - mol_charge = np.sum(ase_atoms.get_initial_charges()) - - charge_vect = np.zeros(len(ase_atoms)) - charge_vect[0] = mol_charge - ase_atoms.set_initial_charges(charge_vect) - - # Handle spin / magnetism - even_odd_electrons = ( - np.sum([atom.number for atom in ase_atoms]) - mol_charge - ) % 2 - if uhf is not None: - uhf = uhf - if (even_odd_electrons == 1) and (uhf == 0): - uhf = 1 - elif (even_odd_electrons == 1) and (uhf < 7) and (uhf % 2 == 0): - uhf += 1 - elif (even_odd_electrons == 1) and (uhf >= 7) and (uhf % 2 == 0): - uhf -= 1 - if (even_odd_electrons == 0) and (uhf % 2 == 1): - uhf = uhf - 1 - elif (even_odd_electrons == 1) and (uhf % 2 == 0): - uhf = uhf + 1 - uhf_start = np.zeros(len(ase_atoms)) - uhf_start[0] = uhf - ase_atoms.set_initial_magnetic_moments(uhf_start) - else: - uhf = 0 # Set spin to LS by default - if (even_odd_electrons == 1) and (uhf == 0): - uhf = 1 - elif (even_odd_electrons == 1) and (uhf < 7) and (uhf % 2 == 0): - uhf += 1 - elif (even_odd_electrons == 1) and (uhf >= 7) and (uhf % 2 == 0): - uhf -= 1 - if (even_odd_electrons == 0) and (uhf % 2 == 1): - uhf = uhf - 1 - elif (even_odd_electrons == 1) and (uhf % 2 == 0): - uhf = uhf + 1 - uhf_start = np.zeros(len(ase_atoms)) - uhf_start[0] = uhf - ase_atoms.set_initial_magnetic_moments(uhf_start) - - if ( - method == "GFN-FF" - ): # Need to turn off charges for GFN-FF evaluation. Probably an XTB-end bug. - ase_atoms.set_initial_charges(np.zeros(len(ase_atoms))) - ase_atoms.set_initial_magnetic_moments(np.zeros(len(ase_atoms))) - calc = XTB(method=method, solvent=solvent, verbosity=0) - - ######################################################### - ########### Calculator Now Set! ######################### - ######################################################### - ase_atoms.calc = calc - - -def set_XTB_calc_straight( - ase_atoms, charge=None, uhf=None, method="GFN2-xTB", solvent="none" -): - """set_XTB_calc_straight - assign xtb calculator atoms object with exaclty spin/charge requeste - - Parameters - ---------- - ase_atoms : ase.atoms.Atoms - atoms to assign calculator to - charge : int, optional - charge of the species, default to initial charges set on ase_atoms - uhf : int, optional - number of unpaired electrons in the system, default to 0 - method : str, optional - which gfn family method to use, default GFN2-xTB - solvent: str, optional - use a solvent?, default 'none' - """ - if charge: - mol_charge = charge - else: - mol_charge = np.sum(ase_atoms.get_initial_charges()) - - charge_vect = np.zeros(len(ase_atoms)) - charge_vect[0] = mol_charge - ase_atoms.set_initial_charges(charge_vect) - - uhf_start = np.zeros(len(ase_atoms)) - uhf_start[0] = uhf - ase_atoms.set_initial_magnetic_moments(uhf_start) - - if ( - method == "GFN-FF" - ): # Need to turn off charges for GFN-FF evaluation. Probably an XTB-end bug. - ase_atoms.set_initial_charges(np.zeros(len(ase_atoms))) - ase_atoms.set_initial_magnetic_moments(np.zeros(len(ase_atoms))) - calc = XTB(method=method, solvent=solvent) - # verbosity=0) - - ######################################################### - ########### Calculator Now Set! ######################### - ######################################################### - ase_atoms.calc = calc - - -def xtb_relax( - structure, - charge=None, - uhf=None, - method="GFN2-xTB", - solvent="none", - fmax=0.05, - detect_charge_spin=False, -): - """xtb_relax relax the structure with xtb - - Parameters - ---------- - structure : any 3D structure - xyz, mol2string, ase atoms ... - charge :int, optional - total charge on the system, by default None - uhf : int, optional - number of unpaired electrons in the system, by default None - method : str, optional - which method to use, by default 'GFN2-xTB' - solvent : str, optional - any name xtb solvent, by default 'none' - fmax : float, optional - default 0.05 eV/Angstrom - detect_charge_spin : bool, optional - Use obmol and io_ptable metal defaults to assign charges and spins?, default False. - - Returns - ------- - ase_atoms : ase.atoms.Atoms - relaxed structure - good : bool - whether the relaxation was succesful! - """ - if isinstance(structure, ase.atoms.Atoms): - ase_atoms = structure - if ase_atoms.calc is None: - set_XTB_calc_straight( - ase_atoms, - charge=charge, - uhf=uhf, - method=method, - solvent=solvent, - ) - else: - mol = io_molecule.convert_io_molecule( - structure, detect_charge_spin=detect_charge_spin - ) - ase_atoms = mol.ase_atoms - if detect_charge_spin: - set_XTB_calc_straight( - ase_atoms, - charge=mol.charge, - uhf=mol.xtb_uhf, - method=method, - solvent=solvent, - ) - else: - set_XTB_calc_straight( - ase_atoms, - charge=charge, - uhf=uhf, - method=method, - solvent=solvent, - ) - good = True - with arch_context_manage.make_temp_directory() as _: - try: - dyn = BFGSLineSearch(ase_atoms) - dyn.run(fmax=fmax) - except: - good = False - return ase_atoms, good - - -def xtb_sp( - structure, - charge=None, - uhf=None, - method="GFN2-xTB", - solvent="none", - detect_charge_spin=False, -): - """xtb_sp singlepoint on the structure with xtb - - Parameters - ---------- - structure : any 3D structure - xyz, mol2string, ase atoms ... - charge :int, optional - total charge on the system, by default None - uhf : int, optional - number of unpaired electrons in the system, by default None - method : str, optional - which method to use, by default 'GFN2-xTB' - solvent : str, optional - any name xtb solvent, by default 'none' - - Returns - ------- - ase_atoms : ase.atoms.Atoms - structure with calculator/energy calculated - good : bool - whether the relaxation was succesful! - """ - if isinstance(structure, ase.atoms.Atoms): - ase_atoms = structure - if ase_atoms.calc is None: - set_XTB_calc_straight( - ase_atoms, - charge=charge, - uhf=uhf, - method=method, - solvent=solvent, - ) - else: - mol = io_molecule.convert_io_molecule( - structure, detect_charge_spin=detect_charge_spin - ) - ase_atoms = mol.ase_atoms - if detect_charge_spin: - set_XTB_calc_straight( - ase_atoms, - charge=mol.charge, - uhf=mol.xtb_uhf, - method=method, - solvent=solvent, - ) - else: - set_XTB_calc_straight( - ase_atoms, - charge=charge, - uhf=uhf, - method=method, - solvent=solvent, - ) - good = True - with arch_context_manage.make_temp_directory() as _: - try: - ase_atoms.get_total_energy() - except: - good = False - return ase_atoms, good - - -eV2Hartree = 1 / 27.2114 - - -def get_rxyz_string(ase_atoms): - """dump the ase_atoms to rxyz file string""" - natom = len(ase_atoms) - ss = "" - # write an xyz file first - ss += "%d\n\n" % natom - for symb, coord in zip( - ase_atoms.get_chemical_symbols(), ase_atoms.positions - ): - ss += "%2s %12.6f %12.6f %12.6f\n" % (symb, *coord[:3]) - ss += "\n" - # then force - ss += "FORCES\n" - for symb, force in zip( - ase_atoms.get_chemical_symbols(), ase_atoms.get_forces() - ): - ss += "%3s %22.14e %22.14e %22.14e\n" % (symb, *(force * eV2Hartree)) - ss += "\n" - # then pbc, if needed - # Potentially usefull - ase_atoms.cell, ase_atoms.get_pbc(), ase_atoms.get_cell_lengths_and_angles(), get_celldisp(), get_cell() - # if ase_atoms.cell is not None: - # ss += "PBC\n" - # ss += "%12.6f %12.6f %12.6f\n" % (chemical.pbc.boxhi[0] - - # chemical.pbc.boxlo[0], 0.0, 0.0)) - # ss += "%12.6f %12.6f %12.6f\n" % (chemical.pbc.xy, - # chemical.pbc.boxhi[1] - chemical.pbc.boxlo[1], 0.0)) - # fp.write("%12.6f %12.6f %12.6f\n" % (chemical.pbc.xz, - # chemical.pbc.yz, - # chemical.pbc.boxhi[2] - chemical.pbc.boxlo[2])) - # fp.write('\n') - # then charge and spin - ss += "ATOM-CHARGE-SPIN\n" - for symb, charge, spin in zip( - ase_atoms.get_chemical_symbols(), - ase_atoms.get_charges(), - ase_atoms.get_initial_magnetic_moments(), - ): - ss += "%2s %12.6f %12.6f\n" % (symb, charge, spin) - ss += "\n" - # then different properties - ss += "ENERGY %22.14f\n" % (ase_atoms.get_total_energy() * eV2Hartree) - ss += "PA_BINDING_ENERGY %22.14f\n" % ( - ase_atoms.get_total_energy() * eV2Hartree / len(ase_atoms) - ) - dipole_vect = ase_atoms.get_dipole_moment() - ss += "DIPOLE_VEC %12.6f %12.6f %12.6f\n" % ( - dipole_vect[0], - dipole_vect[1], - dipole_vect[2], - ) - ss += "DIPOLE %12.6f\n" % np.linalg.norm(dipole_vect) - ss += "CHARGE %d\n" % ase_atoms.get_initial_charges().sum() - ss += "MULTIPLICITY %d\n" % round( - sum(ase_atoms.get_initial_magnetic_moments) + 1 - ) - # fp.write("DIFFAB %f\n" % sum(result['atomspin'])) - # fp.write('BAND_GAP %12.6f\n' % (result["egap"] * eV2Hartree)) - # fp.write('CONVERGED %s\n' % str(result["qconverged"])) - # for prop in more_props: - # fp.write('%s %f\n' % (prop.upper(), result[prop])) - ss += "SOURCE %s\n" % ase_atoms.get_calculator().name - return ss - - -def calc_xtb_ref_dict(): - energydict = dict() - print("--------1---------") - for i, elem in enumerate(io_ptable.elements): - if i > 0 and i < 87: - atoms = Atoms([Atom(elem, (0, 0, 0))]) - if elem in io_ptable.all_metals: - if elem in io_ptable.lanthanides: - spin = 0 - charge = 3 - else: - spin = io_ptable.metal_spin_dict[elem] - charge = io_ptable.metal_charge_dict[elem] - else: - if i % 2 == 0: - spin = 0 - else: - spin = 1 - charge = 0 - set_XTB_calc_straight(atoms, charge=charge, uhf=spin) - try: - energy = atoms.get_total_energy() - energydict[elem] = energy - except: - energydict[elem] = None - print("--------2---------") - for elem, spin in io_ptable.second_choice_metal_spin_dict.items(): - print(elem) - if io_ptable.elements.index(elem) < 87: - atoms = Atoms([Atom(elem, (0, 0, 0))]) - charge = io_ptable.metal_charge_dict[elem] - set_XTB_calc_straight(atoms, charge=charge, uhf=spin) - try: - energy = atoms.get_total_energy() - except: - energy = None - failed = False - if (energydict[elem] is None) and isinstance(energy, float): - energydict[elem] = energy - print("Fixed {} with alternate spin".format(elem)) - elif (energydict[elem] is None) and (energy is None): - failed = True - print("{}".format(elem) + " STILL FAILED") - if (not failed) and (energy is not None): - if energydict[elem] > energy: - energydict[elem] = energy - print("replaced {} with alternate spin".format(elem)) - print("--------3---------") - for elem, spin in io_ptable.metal_spin_dict.items(): - print(elem) - if io_ptable.elements.index(elem) < 87: - atoms = Atoms([Atom(elem, (0, 0, 0))]) - charge = 0 - if (i % 2 == 0) or (elem not in io_ptable.lanthanides): - spin = 0 - else: - spin = 1 - set_XTB_calc_straight(atoms, charge=charge, uhf=spin) - try: - energy = atoms.get_total_energy() - except: - energy = None - failed = False - if (energydict[elem] is None) and isinstance(energy, float): - energydict[elem] = energy - print("Fixed {} with 0 charge low spin".format(elem)) - elif (energydict[elem] is None) and (energy is None): - failed = True - print("{}".format(elem) + " STILL FAILED") - if (not failed) and (energy is not None): - if energydict[elem] > (energy): - energydict[elem] = energy - print( - "replaced {} with 0 charge alternate spin".format(elem) - ) - print("--------4---------") - for elem, spin in io_ptable.metal_spin_dict.items(): - print(elem) - if io_ptable.elements.index(elem) < 87: - atoms = Atoms([Atom(elem, (0, 0, 0))]) - charge = 0 - if (i % 2 == 0) or (elem not in io_ptable.lanthanides): - spin = 2 - else: - spin = 3 - set_XTB_calc_straight(atoms, charge=charge, uhf=spin) - try: - energy = atoms.get_total_energy() - except: - energy = None - failed = False - if (energydict[elem] is None) and isinstance(energy, float): - energydict[elem] = energy - print("Fixed {} with 0 charge 2 spin".format(elem)) - elif (energydict[elem] is None) and (energy is None): - failed = True - print("{}".format(elem) + " STILL FAILED") - if (not failed) and (energy is not None): - if energydict[elem] > energy: - energydict[elem] = energy - print("replaced {} with 0 charge 2 spin".format(elem)) - print("--------5---------") - for elem, spin in io_ptable.metal_spin_dict.items(): - print(elem) - if io_ptable.elements.index(elem) < 87: - atoms = Atoms([Atom(elem, (0, 0, 0))]) - charge = 0 - if (i % 2 == 0) or (elem not in io_ptable.lanthanides): - spin = 4 - else: - spin = 5 - set_XTB_calc_straight(atoms, charge=charge, uhf=spin) - try: - energy = atoms.get_total_energy() - except: - print("Failed 5 spin", elem) - energy = None - failed = False - if (energydict[elem] is None) and isinstance(energy, float): - energydict[elem] = energy - print("Fixed {} with 0 charge 4 spin".format(elem)) - elif (energydict[elem] is None) and (energy is None): - failed = True - print("{}".format(elem) + " STILL FAILED") - if (not failed) and (energy is not None): - if energydict[elem] > energy: - energydict[elem] = energy - print("replaced {} with 0 charge 5 spin".format(elem)) - - # Fix Mo - atoms = Atoms([Atom("Mo", (0, 0, 0))]) - charge = 0 - set_XTB_calc_straight(atoms, charge=charge, uhf=6) - e = atoms.get_total_energy() - energydict["Mo"] = e - # energydict_scaled_ha = {sym:val/Hartree for sym,val in energydict.items()} - return energydict diff --git a/architector/visualization.py b/architector/visualization.py index 2e25646..ebcda16 100644 --- a/architector/visualization.py +++ b/architector/visualization.py @@ -168,10 +168,10 @@ def add_bonds( for i, row in bondsdf.iterrows(): # Allow for multiple different colors of interatomic distances. if (row["atom_pair"][0] in visited) and ( - isinstance(distcolor, (list, np.ndarray)) + hasattr(distcolor, "__len__") ): tcolor = distcolor[visited.index(row["atom_pair"][0])] - elif isinstance(distcolor, (list, np.ndarray)): + elif hasattr(distcolor, "__len__"): tcolor = distcolor[count] visited.append(row["atom_pair"][0]) count += 1 @@ -442,7 +442,7 @@ def view_structures( ] else: labels = [str(i) for i in range(len(mols))] - elif isinstance(labels, list) or isinstance(labels, np.ndarray): + elif hasattr(labels, "__len__"): if len(labels) != len(mols): print( "Wrong amount of labels passed, defaulting to chemical formulas." @@ -742,7 +742,7 @@ def view_structures( label = [x.ase_atoms.get_chemical_formula() for x in mols] else: label = [] - elif isinstance(labels, list) or isinstance(labels, np.ndarray): + elif hasattr(labels, "__len__"): if len(labels) != len(mols): print( "Wrong amount of labels passed, defaulting to chemical formulas." diff --git a/documentation/tutorials/11-Distance_Analysis.ipynb b/documentation/tutorials/11-Distance_Analysis.ipynb index 957cdff..db8688a 100644 --- a/documentation/tutorials/11-Distance_Analysis.ipynb +++ b/documentation/tutorials/11-Distance_Analysis.ipynb @@ -331,8 +331,9 @@ "import numpy as np\n", "g = df.groupby('atom_symbols')['distance']\n", "axes = g.plot(kind='hist',bins=np.arange(0.5,7.5,0.5),alpha=0.5,align='left')\n", - "for i, (groupname, group) in enumerate(g):\n", - " axes[i].set_label(groupname)\n", + "# axes is keyed by group name; iterate key,value\n", + "for groupname, ax in axes.items():\n", + " ax.set_label(groupname)\n", "plt.legend()\n", "plt.xlabel('Interatomic Distance ($\\AA$)')\n", "plt.title('Distance Plot')" diff --git a/environment.yml b/environment.yml index 8d8e532..11cc471 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: dependencies: - python - crest - - xtb + - xtb>6.5 - ase - numpy - py3Dmol diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..950f768 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "architector" +version = "0.1.0" +description = "The architector python package - for 3D Coordination Complex Design." +readme = "README.md" +license = { file = "LICENSE.txt" } +authors = [{ name = "Michael G. Taylor et al." }] +keywords = [ + "chemistry", + "computational-chemistry", + "coordination-complexes", + "inorganic-chemistry", + "molecular-design", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Chemistry", +] +requires-python = ">=3.11" +dependencies = [ + "ase", + "mendeleev", + "numba", + "numpy", + "pandas", + "py3Dmol", + "pynauty", + "scipy", + "tqdm", +] + +[project.optional-dependencies] +dftd = ["torch-dftd"] +mace = ["mace-torch"] +tutorials = ["ipython", "matplotlib"] + +[project.urls] +Homepage = "https://github.com/lanl/Architector" +Repository = "https://github.com/lanl/Architector" +Documentation = "https://lanl.github.io/Architector/" + +[tool.setuptools] +packages = ["architector"] + +[tool.setuptools.package-data] +"*" = ["data/*.csv"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 57af8c6..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -from setuptools import setup - -with open("README.md", "r") as fh: - long_description = fh.read() - -# 'xtb', # Xtb as install requirement is broken, installs correctly through conda. -setup( - name='architector', - version='0.1.0', - author='Michael G. Taylor et al.', - packages=['architector'], - package_data={"": ["data/*.csv"]}, - install_requires=[ - 'ase', - 'numpy', - 'py3Dmol', - 'pynauty', - 'scipy', - 'pandas', - 'mendeleev' - ], - license="BSD 3-Clause License", - classifiers=["Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering :: Chemistry"], - description="The architector python package - for 3D Coordination Complex Design.", - long_description=long_description, - long_description_content_type='text/markdown', -)