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 }