github.com/rmera/gochem@v0.7.1/bonds.go (about)

     1  /*
     2   * atomicdata.go, part of gochem.
     3   *
     4   *
     5   * Copyright 2021 Raul Mera <rmera{at}chemDOThelsinkiDOTfi>
     6   *
     7   * This program is free software; you can redistribute it and/or modify
     8   * it under the terms of the GNU Lesser General Public License as
     9   * published by the Free Software Foundation; either version 2.1 of the
    10   * License, or (at your option) any later version.
    11   *
    12   * This program is distributed in the hope that it will be useful,
    13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    15   * GNU General Public License for more details.
    16   *
    17   * You should have received a copy of the GNU Lesser General
    18   * Public License along with this program.  If not, see
    19   * <http://www.gnu.org/licenses/>.
    20   *
    21   *
    22   * goChem is currently developed at the Universidad de Santiago de Chile
    23   * (USACH)
    24   *
    25   */
    26  
    27  package chem
    28  
    29  import (
    30  	"fmt"
    31  	"sort"
    32  
    33  	v3 "github.com/rmera/gochem/v3"
    34  )
    35  
    36  // constants from DOI:10.1186/1758-2946-3-33
    37  const (
    38  	tooclose = 0.63
    39  	bondtol  = 0.045 //the reference actually says 0.45 A but I strongly suspect that is a typo.
    40  )
    41  
    42  // Bond represents a chemical, covalent bond.
    43  type Bond struct {
    44  	Index  int
    45  	At1    *Atom
    46  	At2    *Atom
    47  	Dist   float64
    48  	Energy float64 //One might prefer to use energy insteaf of order.
    49  	//NOTE
    50  	//I'm not sure if I leave just the order and let the user "Abuse" that field for energy
    51  	//or anything else they want, or if I keep this extra field.
    52  	Order float64 //Order 0 means undetermined
    53  }
    54  
    55  // Cross returns the atom bonded to the origin atom
    56  // bond in the receiver.
    57  func (B *Bond) Cross(origin *Atom) *Atom {
    58  	if origin.index == B.At1.index {
    59  		return B.At2
    60  	}
    61  	if origin.index == B.At2.index {
    62  		return B.At1
    63  	}
    64  	panic("Trying to cross a bond: The origin atom given is not present in the bond!") //I think this got to be a programming error, so a panic is warranted.
    65  
    66  }
    67  
    68  // Remove removes the receiver bond from the the Bond slices in the corresponding atoms
    69  func (b *Bond) Remove() error {
    70  	lenb1 := len(b.At1.Bonds)
    71  	lenb2 := len(b.At2.Bonds)
    72  	b.At1.Bonds = takefromslice(b.At1.Bonds, b.Index)
    73  	b.At2.Bonds = takefromslice(b.At2.Bonds, b.Index)
    74  	err := new(CError)
    75  	errs := 0
    76  	err.msg = fmt.Sprintf("Failed to remove bond Index:%d", b.Index)
    77  	if len(b.At1.Bonds) == lenb1 {
    78  		err.msg = err.msg + fmt.Sprintf("from atom. Index:%d", b.At1.index)
    79  		err.Decorate("RemoveBond")
    80  		errs++
    81  	}
    82  	if len(b.At2.Bonds) == lenb2 {
    83  		err := new(CError)
    84  		if errs > 0 {
    85  			err.msg = err.msg + " and"
    86  		}
    87  		err.msg = err.msg + fmt.Sprintf("from atom. Index:%d", b.At2.index)
    88  		err.Decorate("RemoveBond")
    89  		errs++
    90  	}
    91  	if errs > 0 {
    92  		return err
    93  	}
    94  	return nil
    95  }
    96  
    97  // returns a new *Bond slice with the element id removed
    98  func takefromslice(bonds []*Bond, id int) []*Bond {
    99  	newb := make([]*Bond, len(bonds)-1)
   100  	for _, v := range bonds {
   101  		if v.Index != id {
   102  			newb = append(newb, v)
   103  		}
   104  	}
   105  	return newb
   106  }
   107  
   108  // BondedOptions contains options for the BondePaths function
   109  type BondedOptions struct {
   110  	OnlyShortest bool  //Only return the shortest path between the atoms
   111  	path         []int //
   112  }
   113  
   114  /******
   115  **** The following is commented out and excluded from the API, as I don't think it is needed.
   116  **** In any case, adding this function, or any other, wouldn't break the API, so it's best
   117  **** to exclude when in doubt. Of course this is in itself an API break, but the bond functionality
   118  **** is still very new, so the break is very unlikely to affect anyone.
   119  //SetAlreadyWalkedPath set the unexported field "path" in BondedOptions to p.
   120  //the path field represents the already-walked path, so you almost never
   121  //want to set it. The exception is definidng your own function that calls
   122  //BondedPath recursively, as I do here. This method was added to allow
   123  //for such use.
   124  func (B *BondedOptions)SetAlreadyWalkedPath(p []int){
   125      B.path=p
   126  }
   127  ******/
   128  
   129  // BondedPaths determines the paths between at and the atom with
   130  // Index targetIndex. It returns a slice of slices of int, where each sub-slice contains all the atoms
   131  // in between at and the target (including the index of at)
   132  // If there is no valid path, it returns nil.
   133  // In the options structure BondeOption, OnlyShortest set to true causes only the shortest of the found paths to
   134  // be returned.  All atoms in the molecule need to have the "index" field filled.
   135  // If the targetIndex is the same as that of the current atom, and the path is not given, nil, or
   136  // of len 0, the function will search for a cyclic path back to the initial atom.
   137  // if onlyshortest is true, only the shortest path will be returned (the other elements of the slice will be nil)
   138  // This can be useful if you want to save memory on a very intrincate molecule.
   139  func BondedPaths(at *Atom, targetIndex int, options ...*BondedOptions) [][]int {
   140  	if len(options) == 0 {
   141  		options = []*BondedOptions{&BondedOptions{OnlyShortest: false, path: nil}}
   142  	}
   143  	onlyshortest := options[0].OnlyShortest
   144  	path := [][]int{options[0].path}
   145  	//I am not completely sure about this function signature. It is a candidate for API change.
   146  	if len(path) > 0 && len(path[0]) > 1 && path[0][len(path[0])-2] == at.index {
   147  		return nil //We are back to the atom we just had visited, not a valid path. We have to check this before checking if we completed the "quest"
   148  		//or, by just going back via the same bond, it would seem like we are at the finishing line.
   149  	}
   150  	if len(path) == 0 {
   151  		path = append(path, []int{at.index})
   152  	} else if path[0] == nil {
   153  		path[0] = []int{at.index}
   154  
   155  	} else {
   156  		path[0] = append(path[0], at.index)
   157  	}
   158  
   159  	if at.index == targetIndex && len(path[0]) > 1 {
   160  		return [][]int{path[0]} //We arrived! Note that if the starting node is the same as the target, we will
   161  		//only settle for a "cyclic" path that goes through at least another atom (really, at least 2 more atoms).
   162  		// We will not immediately return success on the first node. This is enforced by the len(path[0]>1 condition.
   163  	}
   164  	//Here we check that we are not back to an atom we previously visited. This checks for loops, and has to be performed
   165  	//after we check if we got to the goal, since the goal could be the same starting atom (if we look for a cyclic path).
   166  	if len(path[0]) > 1 && isInInt(path[0][:len(path[0])-1], at.index) {
   167  		return nil //means we took the same bond back to the previous node, or got trapped in a loop. not a valid path.
   168  	}
   169  	if len(at.Bonds) <= 1 {
   170  		return nil //means that we hit an end of the road. There is only one bond in the atom (i.e. the one leading to the previous node)
   171  	}
   172  	rets := make([][]int, 0, len(at.Bonds))
   173  	for _, v := range at.Bonds {
   174  		path2 := make([]int, len(path[0]))
   175  		copy(path2, path[0])
   176  		rets = append(rets, BondedPaths(v.Cross(at), targetIndex, &BondedOptions{OnlyShortest: onlyshortest, path: path2})...) //scary stuff
   177  	}
   178  	rets2 := make([][]int, 0, len(at.Bonds))
   179  	for _, v := range rets {
   180  		if v != nil {
   181  			rets2 = append(rets2, v)
   182  		}
   183  	}
   184  	if len(rets2) == 0 {
   185  		return nil
   186  	}
   187  	sort.Slice(rets2, func(i, j int) bool { return len(rets2[i]) < len(rets2[j]) })
   188  	if onlyshortest {
   189  		return [][]int{rets2[0]}
   190  	}
   191  	return rets2
   192  
   193  }
   194  
   195  // If this works, we could replace BondedPaths by just a call to this function with f=func(*Bond)bool{return true}
   196  // BondedPaths determines the paths between at and the atom with
   197  // Index targetIndex. It returns a slice of slices of int, where each sub-slice contains all the atoms
   198  // in between at and the target (including the index of at)
   199  // If there is no valid path, it returns nil.
   200  // In the options structure BondeOption, OnlyShortest set to true causes only the shortest of the found paths to
   201  // be returned.  All atoms in the molecule need to have the "index" field filled.
   202  // If the targetIndex is the same as that of the current atom, and the path is not given, nil, or
   203  // of len 0, the function will search for a cyclic path back to the initial atom.
   204  // if onlyshortest is true, only the shortest path will be returned (the other elements of the slice will be nil)
   205  // This can be useful if you want to save memory on a very intrincate molecule.
   206  func BondedPathsFunc(at *Atom, targetIndex int, f func(*Bond) bool, options ...*BondedOptions) [][]int {
   207  	if len(options) == 0 {
   208  		options = []*BondedOptions{&BondedOptions{OnlyShortest: false, path: nil}}
   209  	}
   210  	onlyshortest := options[0].OnlyShortest
   211  	path := [][]int{options[0].path}
   212  	//I am not completely sure about this function signature. It is a candidate for API change.
   213  	if len(path) > 0 && len(path[0]) > 1 && path[0][len(path[0])-2] == at.index {
   214  		return nil //We are back to the atom we just had visited, not a valid path. We have to check this before checking if we completed the "quest"
   215  		//or, by just going back via the same bond, it would seem like we are at the finishing line.
   216  	}
   217  	if len(path) == 0 {
   218  		path = append(path, []int{at.index})
   219  	} else if path[0] == nil {
   220  		path[0] = []int{at.index}
   221  
   222  	} else {
   223  		path[0] = append(path[0], at.index)
   224  	}
   225  
   226  	if at.index == targetIndex && len(path[0]) > 1 {
   227  		return [][]int{path[0]} //We arrived! Note that if the starting node is the same as the target, we will
   228  		//only settle for a "cyclic" path that goes through at least another atom (really, at least 2 more atoms).
   229  		// We will not immediately return success on the first node. This is enforced by the len(path[0]>1 condition.
   230  	}
   231  	//Here we check that we are not back to an atom we previously visited. This checks for loops, and has to be performed
   232  	//after we check if we got to the goal, since the goal could be the same starting atom (if we look for a cyclic path).
   233  	if len(path[0]) > 1 && isInInt(path[0][:len(path[0])-1], at.index) {
   234  		return nil //means we took the same bond back to the previous node, or got trapped in a loop. not a valid path.
   235  	}
   236  	if len(at.Bonds) <= 1 {
   237  		return nil //means that we hit an end of the road. There is only one bond in the atom (i.e. the one leading to the previous node)
   238  	}
   239  	rets := make([][]int, 0, len(at.Bonds))
   240  	for _, v := range at.Bonds {
   241  		if !f(v) {
   242  			continue
   243  		}
   244  		path2 := make([]int, len(path[0]))
   245  		copy(path2, path[0])
   246  		rets = append(rets, BondedPathsFunc(v.Cross(at), targetIndex, f, &BondedOptions{OnlyShortest: onlyshortest, path: path2})...) //scary stuff
   247  	}
   248  	rets2 := make([][]int, 0, len(at.Bonds))
   249  	for _, v := range rets {
   250  		if v != nil {
   251  			rets2 = append(rets2, v)
   252  		}
   253  	}
   254  	if len(rets2) == 0 {
   255  		return nil
   256  	}
   257  	sort.Slice(rets2, func(i, j int) bool { return len(rets2[i]) < len(rets2[j]) })
   258  	if onlyshortest {
   259  		return [][]int{rets2[0]}
   260  	}
   261  	return rets2
   262  
   263  }
   264  
   265  // Ring represents a molecular cycle.
   266  type Ring struct {
   267  	Atoms     []int
   268  	planarity float64
   269  }
   270  
   271  // IsIn returns true or false depending on whether
   272  // the atom with the given index is part of the ring
   273  func (R *Ring) IsIn(index int) bool {
   274  	return isInInt(R.Atoms, index)
   275  }
   276  
   277  // Size returns the number of atoms in the ring
   278  func (R *Ring) Size() int {
   279  	return len(R.Atoms)
   280  }
   281  
   282  // Planarity returns the planarity percentage of the receiver ring
   283  // coords is the set of coordinates for the _entire_ molecule of which
   284  // the ring is part. Planarity does not check that coords indeed corresponds
   285  // to the correct molecule, so, doing so is the user's responsibility.
   286  func (R *Ring) Planarity(coord *v3.Matrix) float64 {
   287  	if R.planarity != 0 {
   288  		return R.planarity
   289  	}
   290  	c := v3.Zeros(coord.NVecs())
   291  	c.SomeVecs(coord, R.Atoms)
   292  	_, plan, err := EasyShape(c, 0.01)
   293  	if err != nil {
   294  		R.planarity = -1
   295  		return -1
   296  	}
   297  	R.planarity = plan
   298  	return plan
   299  
   300  }
   301  
   302  // AddHs Adds to the ring the H atoms bonded to its members
   303  // mol is the _entire_ molecule of which the receiver ring is part.
   304  // It will panic if an atom of the ring is not
   305  // found in mol, or is not bonded to anything
   306  func (R *Ring) AddHs(mol Atomer) {
   307  	newind := make([]int, 0, 6)
   308  	for _, v := range R.Atoms {
   309  		at := mol.Atom(v) //this can panic if you give the wrong mol object
   310  		for _, w := range at.Bonds {
   311  			at2 := w.Cross(at)
   312  			if at2.Symbol == "H" {
   313  				newind = append(newind, at2.index)
   314  			}
   315  
   316  		}
   317  	}
   318  	R.Atoms = append(R.Atoms, newind...)
   319  }
   320  
   321  // InWhichRing returns the index of the first ring found to which the
   322  // at atom belongs, or -1 if the atom is not part of any ring.
   323  func InWhichRing(at *Atom, rings []*Ring) int {
   324  	if len(rings) == 0 {
   325  		return -1
   326  	}
   327  	for i, v := range rings {
   328  		if v.IsIn(at.index) {
   329  			return i
   330  		}
   331  	}
   332  	return -1
   333  
   334  }
   335  
   336  // Identifies and returns all rings in mol, by
   337  // searching for cyclic paths.
   338  func FindRings(coords *v3.Matrix, mol Atomer) []*Ring {
   339  	L := mol.Len()
   340  	var rings []*Ring
   341  	minplanarity := 95.0
   342  	for i := 0; i < L; i++ {
   343  		at := mol.Atom(i)
   344  		if InWhichRing(at, rings) == -1 {
   345  			paths := BondedPaths(at, at.index, &BondedOptions{OnlyShortest: true})
   346  			if len(paths) == 0 || len(paths[0][1:]) > 6 {
   347  				continue
   348  			}
   349  			r := &Ring{Atoms: paths[0]}
   350  			p := r.Planarity(coords)
   351  			if p > minplanarity {
   352  				r.AddHs(mol)
   353  				rings = append(rings, r)
   354  			}
   355  		}
   356  
   357  	}
   358  	return rings
   359  }