├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── analysis ├── __init__.py └── islands.py ├── config.yaml ├── examples ├── GO_flake.py ├── GO_rect.py ├── GO_sheet.py ├── GO_stack.py ├── carboxyl_ions_with_counterions.py ├── charged_carboxyl_groups.py ├── config.yaml ├── graphene_flake.py ├── graphene_rect.py ├── graphene_sheet.py ├── graphene_stack.py ├── graphite.py ├── peel_simulation.py ├── sliding_flake_sim.py └── tree.py ├── graphene-env.yml ├── makegraphitics ├── __init__.py ├── combine.py ├── connector.py ├── crystal.py ├── data_to_xyz.py ├── lattice.py ├── molecules │ ├── __init__.py │ ├── base.py │ ├── graphene_cell.py │ ├── graphite_cell.py │ ├── graphite_periodic_strip.py │ ├── hexagon_graphene.py │ └── rectangle_graphene.py ├── opls_reader.py ├── params.py ├── params │ ├── config.yaml │ ├── oplsaa.prm │ ├── oxidise.data │ └── oxidise_types.yaml ├── reactors │ ├── __init__.py │ ├── base.py │ ├── oxidise_rf.py │ └── oxidiser.py ├── read_lammpsdata.py ├── shifty.py ├── sim.py └── write_coords.py ├── setup.py └── tests └── test_examples.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.dat 2 | *.pyc 3 | *.xyz 4 | in.* 5 | *.in 6 | data.* 7 | *.data 8 | *.swp 9 | *.lammpstrj 10 | *.c 11 | *.so 12 | *.png 13 | *.tga 14 | build/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include makegraphitics/params/oxidise.data 2 | include makegraphitics/params/oplsaa.prm 3 | include makegraphitics/params/config.yaml 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graphite, Graphene and Graphene Oxide Builder 2 | 3 | MakeGraphitics is a library to ceate various atomistic graphitic structures for molecular dynamics. 4 | 5 | Available structures: 6 | - Hexagonal graphene flake 7 | - Rectangular graphene flake 8 | - Rectangular perodic graphene sheet (no edges) 9 | - Periodic graphite 10 | - Graphene and graphite oxide 11 | 12 | Output: 13 | - .xyz 14 | - lammps data file 15 | 16 | Automatically parameterise by forcefields: 17 | - OPLS 18 | - GraFF 19 | - ReaxFF 20 | 21 | ## Install 22 | 23 | Clone this repository. Install using Python2.7. Run the tests to check the installation has worked. 24 | ``` 25 | git clone https://github.com/velocirobbie/make-graphitics 26 | cd make-graphitics 27 | python setup.py install 28 | pytest 29 | ``` 30 | 31 | A conda environment is provided if you do not have the right packages. If you have conda set up, execute these commands to create a working python environment before the install setp. 32 | ``` 33 | conda env create --file graphene-env.yml 34 | conda activate graphene 35 | ``` 36 | 37 | Running `pytest` will create a bunch of unwanted output files. Sorry about this, I will try and tidy up the outputs soon. In the mean time you can remove with `rm *xyz *data`. 38 | 39 | ## Examples 40 | 41 | See the scripts in the `examples/` directory for a number of sample structures. 42 | 43 | 1) Make a rectangular graphene sheet that extends through periodic boundaries. Parameterised with OPLS and outputs to .xyz for easy veiwing with VMD and a LAMMPS data file. 44 | ``` 45 | python2.7 graphene_sheet.py 46 | ``` 47 | Size of the sheet can be specified in `graphene_sheet.py`. 48 | 49 | 2) Make a hexagonal flake of graphene oxide. Parameterised with OPLS and outputs to .xyz for easy veiwing with VMD and a LAMMPS data file. 50 | ``` 51 | python2.7 GO_flake.py 52 | ``` 53 | There are several tunable parameters in `GO_flake.py` that you may be interested in. Including: 54 | - flake radius 55 | - C/O target ratio, `ratio` 56 | - Rate at which new nodes are added, `new_island_freq` 57 | - output snapshots of the oxidation process every N steps with `video_xyz=N`. Viewed in VMD with `topo readvarxyz out.xyz` 58 | 59 | ## Notes on the Oxidiser 60 | 61 | The Oxidiser takes a graphitic structure and attempts to oxidise it by the process described in (Yang, Angewandte Chemie, 2014; Sinclair, 2019). The algorithm proceeds as follows: 62 | 63 | 1) If hydrogens exist (i.e. edge of a flake), 1/4 are changed to alcohol groups and 1/4 to carboxyl groups (Lerf and Klinowski model). These values can be changed by passing the Oxidiser object the optional arguments: ` edge_OHratio = 0.25, edge_carboxyl_ratio = 0.5`. 64 | 65 | 2) The reactivity of every possible site is calculated. This is done by using a ranodom forest approach to extend the data set of GO reactivites given by Yang et al. We do not take into account the reactivity of the edges. 66 | 67 | 3) A site is oxidised at random weighted by each site's reactivity. The chance of an oxidisation producing an alcohol or epoxy group on the surface is by default 50:50, but can be specified by passing Oxidiser the optional argument: `surface_OHratio = 0.5` 68 | 69 | 4) The time elapsed between oxidations is estimated from the reactivity of the site that has been oxidised. 70 | 71 | 5) New nodes are added proportionally to `time_elapsed * new_island_freq`. Note this can be 0. The reasons for doing this are outlined in (Sinclair 2019). 72 | 73 | 6) Steps 2-5 are repeated until the target C/O ratio is reached or no new sites are available, usually C/O ~ 1.7 . We recommend setting the target, `ratio`, to over 2 as this is what is seen experimentally. 74 | 75 | ## Notes on Parameterisation 76 | 77 | Not all the bonded interactions that can occur in graphene oxide are included in the OPLS parameterisation. We make some neccesary like for like atom-type substitutions to get around this problem. It is not ideal but common practice in molecular dynamics. The substitutions used are outputed after a parameterisation step. Each substitution line outputs the origional atom types, the atom types used to parameterise them, and a summary string that you can use to find in the script `makegraphitics/params.py`. Substitutions keep atom types as close to the origional as possible e.g. replaces an aromatic C with an alkene C, whcih are both sp2 carbon atoms. 78 | 79 | ## More structure examples 80 | 81 | More examples of building structures with this script are in the `examples` directory. 82 | 83 | Note that differenct structures can be combined into one simulation object with `Combine`. Also coordinates can be manipulated before writing to a lammps file. An examploe of this is shown in `peel_sim.py`. 84 | 85 | # Citing 86 | 87 | The work contained here has been published in some of my own papers e.g. 88 | 89 | - Graphene–Graphene Interactions: Friction, Superlubricity, and Exfoliation https://doi.org/10.1002/adma.201705791 90 | 91 | - Modeling Nanostructure in Graphene Oxide: Inhomogeneity and the Percolation Threshold https://doi.org/10.1021/acs.jcim.9b00114 92 | 93 | - The Role of Graphene in Enhancing the Material Properties of Thermosetting Polymers https://doi.org/10.1002/adts.201800168 94 | 95 | I would appreciate a citation if you any of the code in any published work :) You could cite the graphene oxide structure paper, this github page (if the journal allows), or the latest release on the zenodo repository 96 | 97 | ``` 98 | @article{sinclair2019modelling, 99 | title={Modelling nanostructure in graphene oxide: inhomogeneity and the percolation threshold}, 100 | author={Sinclair, Robert Callum and Coveney, Peter Vivian}, 101 | journal = {Journal of Chemical Information and Modeling}, 102 | volume = {59}, 103 | number = {6}, 104 | pages = {2741-2745}, 105 | year = {2019}, 106 | doi = {10.1021/acs.jcim.9b00114}, 107 | } 108 | @misc{make-graphitics-github, 109 | url = {https://github.com/velocirobbie/make-graphitics}, 110 | howpublished = {\url{https://github.com/velocirobbie/make-graphitics}}, 111 | note = {Accessed: \today}, 112 | author = {Sinclair, Robert C.}, 113 | year = {2019} 114 | } 115 | @misc{make-graphitics_zenodo, 116 | author = {Sinclair, Robert. C. }, 117 | title = {make-graphitics}, 118 | version = {0.1.0}, 119 | publisher = {Zenodo}, 120 | year = {2019}, 121 | doi = {10.5281/zenodo.2548538} 122 | } 123 | 124 | -------------------------------------------------------------------------------- /analysis/__init__.py: -------------------------------------------------------------------------------- 1 | from islands import calc_island_sizes 2 | -------------------------------------------------------------------------------- /analysis/islands.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from math import pi 3 | 4 | 5 | class Island(object): 6 | def __init__(self): 7 | self.atoms = [] 8 | self.coords = [] 9 | self.area_boundary = None 10 | self.area_simple = None 11 | 12 | def populate_coords(self, all_coords): 13 | N = len(self.atoms) 14 | self.coords = np.empty((N, 3)) 15 | for i, atom in enumerate(self.atoms): 16 | self.coords[i] = all_coords[atom - 1] 17 | 18 | def calc_boundary(self, alpha): 19 | self.boundary = outer_polygon(self.coords[:, 0:2], alpha) 20 | 21 | def calc_area(self): 22 | self.area_boundary = self.boundary.area 23 | self.area_simple = simple_area(self.coords) 24 | 25 | def com(self): 26 | return self.coords[:, 0:2].mean(0) 27 | 28 | def natoms(self): 29 | return len(self.atoms) 30 | 31 | def diameter(self, method): 32 | return 2 * np.sqrt(getattr(self, method) / pi) 33 | 34 | 35 | def build_bond_network(bonds, atom_types): 36 | N = len(atom_types) 37 | bond_network = { 38 | i + 1: {"type": type_, "bonded_to": []} for i, type_ in enumerate(atom_types) 39 | } 40 | for bond in bonds: 41 | bond_network[bond[0]]["bonded_to"] += [bond[1]] 42 | bond_network[bond[1]]["bonded_to"] += [bond[0]] 43 | return bond_network 44 | 45 | 46 | def flood_island( 47 | index, bond_network, island_labels, atom_types, island_index, coords, x, y 48 | ): 49 | island = Island() 50 | 51 | def unwrap_coord(coord, ref): 52 | dx = coord[0] - ref[0] 53 | while (dx > x / 2) or (dx < -x / 2): 54 | if dx > x / 2: 55 | coord[0] -= x 56 | dx -= x 57 | elif dx < -x / 2: 58 | coord[0] += x 59 | dx += x 60 | dy = coord[1] - ref[1] 61 | while (dy > y / 2) or (dy < -y / 2): 62 | if dy > y / 2: 63 | coord[1] -= y 64 | dy -= y 65 | elif dy < -y / 2: 66 | coord[1] += y 67 | dy += y 68 | return coord 69 | 70 | x_min = 0 71 | x_max = 0 72 | y_min = 0 73 | y_max = 0 74 | 75 | def check_island_range(coord, x_min, x_max, y_min, y_max): 76 | if coord[0] < x_min: 77 | x_min = coord[0] 78 | if coord[0] > x_max: 79 | x_max = coord[0] 80 | if coord[1] < y_min: 81 | y_min = coord[1] 82 | if coord[1] > y_min: 83 | y_max = coord[1] 84 | check = True 85 | if (x_max - x_min > 5 * x) or (y_max - y_min > 5 * y): 86 | check = False 87 | return check 88 | 89 | q = [index + 1] 90 | refs = [coords.mean(0)] 91 | check = True 92 | while q: 93 | v = q.pop() # pop removes and returns last element of array 94 | ref = refs.pop() 95 | island_labels[v - 1] = island_index 96 | island.atoms += [v] 97 | atom_coord = unwrap_coord(coords[v - 1], ref) 98 | check = check_island_range(atom_coord, x_min, x_max, y_min, y_max) 99 | if not check: 100 | raise Exception("GO is below percolation threshold") 101 | island.coords += [atom_coord] 102 | neighbours = bond_network[v]["bonded_to"] 103 | 104 | for neighbour in neighbours: 105 | atom_type = atom_types[neighbour - 1] 106 | already_included = island_labels[neighbour - 1] 107 | if (atom_type == 1) and (not already_included): 108 | q.append(neighbour) 109 | refs.append(atom_coord) 110 | island.is_an_island = check 111 | return island, island_labels 112 | 113 | 114 | def find_islands_by_flood(sim): 115 | bonds = sim.bonds # 2xN array 116 | atom_types = sim.atom_labels 117 | 118 | x, y = [ 119 | sim.box_dimensions[0, 1] - sim.box_dimensions[0, 0], 120 | sim.box_dimensions[1, 1] - sim.box_dimensions[1, 0], 121 | ] 122 | 123 | N = len(atom_types) 124 | bond_network = build_bond_network(bonds, atom_types) 125 | islands = [] 126 | 127 | # array recording if atoms are in an island 128 | # 0=not an island (graphene); 1,2,3.. = in island N 129 | island_labels = np.zeros(N) 130 | 131 | for i in range(N): 132 | # if not already counted in an island, and a aromatic carbon 133 | if (island_labels[i] == 0) and (atom_types[i] == 1): 134 | # found new island 135 | island_index = len(islands) + 1 136 | island, island_labels = flood_island( 137 | i, 138 | bond_network, 139 | island_labels, 140 | atom_types, 141 | island_index, 142 | sim.coords, 143 | x, 144 | y, 145 | ) 146 | islands += [island] 147 | 148 | map(lambda island: island.populate_coords(sim.coords), islands) 149 | 150 | return islands 151 | 152 | 153 | def outer_polygon(coords, alpha): 154 | if len(coords) < 3: 155 | return 0 156 | from scipy.spatial import Delaunay 157 | from shapely.ops import cascaded_union, polygonize 158 | import shapely.geometry as geometry 159 | 160 | def alpha_shape(coords, alpha): 161 | def add_edge(edges, edge_points, coords, i, j): 162 | """ 163 | Add a line between the i-th and j-th points, 164 | if not in the list already 165 | """ 166 | if (i, j) in edges or (j, i) in edges: 167 | # already added 168 | return 169 | edges.add((i, j)) 170 | edge_points.append(coords[[i, j]]) 171 | 172 | tri = Delaunay(coords) 173 | edges = set() 174 | edge_points = [] 175 | # loop over triangles: 176 | # ia, ib, ic = indices of corner points of the 177 | # triangle 178 | for ia, ib, ic in tri.vertices: 179 | pa = coords[ia] 180 | pb = coords[ib] 181 | pc = coords[ic] 182 | # Lengths of sides of triangle 183 | a = np.sqrt((pa[0] - pb[0]) ** 2 + (pa[1] - pb[1]) ** 2) 184 | b = np.sqrt((pb[0] - pc[0]) ** 2 + (pb[1] - pc[1]) ** 2) 185 | c = np.sqrt((pc[0] - pa[0]) ** 2 + (pc[1] - pa[1]) ** 2) 186 | if a > alpha or b > alpha or c > alpha: 187 | pass 188 | else: 189 | add_edge(edges, edge_points, coords, ia, ib) 190 | add_edge(edges, edge_points, coords, ib, ic) 191 | add_edge(edges, edge_points, coords, ic, ia) 192 | m = geometry.MultiLineString(edge_points) 193 | triangles = list(polygonize(m)) 194 | return cascaded_union(triangles), edge_points 195 | 196 | if len(coords) < 4: 197 | # When you have a triangle, there is no sense 198 | # in computing an alpha shape. 199 | return geometry.MultiPoint(list(coords)).convex_hull 200 | else: 201 | concave_hull, edge_points = alpha_shape(coords, alpha=alpha) 202 | return concave_hull 203 | 204 | 205 | def simple_area(coords): 206 | atom_area = ( 207 | 1.414 ** 2 * 3 * np.sqrt(3) / 4 208 | ) # 2.60, number density of graphene atoms 209 | return len(coords) * atom_area 210 | 211 | 212 | def strip_small_islands(islands, min_atoms): 213 | # min_atmos in island to be worth counting 214 | new_islands = [] 215 | for island in islands: 216 | if island.natoms() >= min_atoms: 217 | new_islands += [island] 218 | return new_islands 219 | 220 | 221 | def write_islands_xyz(islands): 222 | N = sum([island.natoms() for island in islands]) 223 | 224 | with open("islands.xyz", "w") as f: 225 | f.write(str(N) + "\n") 226 | f.write("islands\n") 227 | for i, island in enumerate(islands): 228 | for coord in island.coords: 229 | f.write(str((i + 1) % 10) + "\t") 230 | for axis in coord: 231 | f.write(str(axis) + "\t") 232 | f.write("\n") 233 | 234 | 235 | def write_gnuplot(islands): 236 | # write object file for polygons 237 | with open("island_objects.sh", "w") as f: 238 | for i, island in enumerate(islands): 239 | if not island.is_an_island: 240 | break 241 | f.write("set object " + str(i + 1) + " polygon from \\\n") 242 | coords = list(island.boundary.exterior.coords) 243 | for point in coords: 244 | f.write("\t" + str(point[0]) + "," + str(point[1]) + " to \\\n") 245 | f.write("\t" + str(coords[-1][0]) + "," + str(coords[-1][1]) + " \n") 246 | f.write( 247 | "set object " 248 | + str(i + 1) 249 | + " fc rgb '#000000' fillstyle solid lw 0\n\n" 250 | ) 251 | # write coords of all island atoms 252 | with open("island_coords.dat", "w") as f: 253 | for island in islands: 254 | for coord in island.coords: 255 | f.write(str(coord[0]) + "\t " + str(coord[1]) + "\n") 256 | # draw circles of an island's approximate area 257 | with open("island_area_circle.dat", "w") as f: 258 | for island in islands: 259 | f.write( 260 | str(island.com()[0]) 261 | + "\t " 262 | + str(island.com()[1]) 263 | + "\t " 264 | + str(island.diameter("area_simple") / 2) 265 | + "\t" 266 | + str(island.diameter("area_boundary") / 2) 267 | + "\n" 268 | ) 269 | # plot with: 270 | # gnuplot> load 'island_objects.sh' 271 | # gnuplot> pl 'island_coords.dat' ,'island_area_circle.dat' w circ 272 | 273 | 274 | def calc_island_sizes(sim): 275 | islands = find_islands_by_flood(sim) 276 | islands = strip_small_islands(islands, 6) 277 | 278 | map(lambda island: island.calc_boundary(3), islands) 279 | map(lambda island: island.calc_area(), islands) 280 | write_islands_xyz(islands) 281 | write_gnuplot(islands) 282 | 283 | sizes_b = [island.diameter("area_boundary") for island in islands] 284 | sizes_s = [island.diameter("area_simple") for island in islands] 285 | return sizes_b, sizes_s 286 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | Crystal: 2 | CC: 1.421 3 | layer_gap: 3.354 4 | 5 | GraFF_5: 6 | CC: 1.42845 7 | layer_gap: 3.4827 8 | CH: 1.077 9 | dq: 0.115 10 | 11 | GraFF_77: 12 | CC: 1.42827 13 | layer_gap: 3.4827 14 | CH: 1.077 15 | dq: 0.115 16 | 17 | OPLS: 18 | CC: 1.4148 # OPLS 19 | # layer_gap: 3.3827 20 | layer_gap: 3.4827 21 | CH: 1.077 22 | dq: 0.115 23 | 24 | AMBER: 25 | CC: 1.4293 # AMBER 26 | layer_gap: 3.3693 27 | CH: 1.088 28 | dq: 0.115 29 | 30 | COMPASS: 31 | CC: 1.3913 # COMPASS 32 | layer_gap: 3.3548 33 | CH: 1.070 34 | dq: 0.1268 35 | 36 | Driedling: 37 | CC: 1.3838 # Driedling 38 | layer_gap: 3.3955 39 | CH: 1.020 40 | dq: 0.062 41 | 42 | system: 43 | vdw_cutoff: 12.0 44 | N_layers: 2 45 | -------------------------------------------------------------------------------- /examples/GO_flake.py: -------------------------------------------------------------------------------- 1 | import makegraphitics as mg 2 | 3 | flake_radius = 25 4 | layout = [1, 1, 1] # make a 1x1x1 array of flakes 5 | 6 | motif = mg.molecules.Hexagon_Graphene(flake_radius) 7 | flake = mg.Crystal(motif, layout) 8 | 9 | oxidiser = mg.reactors.Oxidiser( 10 | ratio=2.5, video_xyz=20, new_island_freq=1e14, method="rf" 11 | ) 12 | flake = oxidiser.react(flake) 13 | 14 | mg.Parameterise(flake) 15 | 16 | name = "graphene" 17 | output = mg.Writer(flake, name) 18 | output.write_xyz(name + ".xyz") 19 | output.write_lammps(name + ".data") 20 | -------------------------------------------------------------------------------- /examples/GO_rect.py: -------------------------------------------------------------------------------- 1 | import makegraphitics as mg 2 | 3 | motif = mg.molecules.Rectangle_Graphene(50, 50) 4 | flake = mg.Crystal(motif, [1, 1, 1]) 5 | 6 | oxidiser = mg.reactors.Oxidiser( 7 | ratio=2.5, video_xyz=20, new_island_freq=1e14, method="rf" 8 | ) 9 | flake = oxidiser.react(flake) 10 | 11 | mg.Parameterise(flake, flake.vdw_defs) 12 | 13 | name = "graphene" 14 | output = mg.Writer(flake, name) 15 | output.write_xyz(name + ".xyz") 16 | output.write_lammps(name + ".data") 17 | output.write_reaxff(name + "reax.data") 18 | -------------------------------------------------------------------------------- /examples/GO_sheet.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from math import pi, cos 3 | import makegraphitics as mg 4 | 5 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 6 | forcefield = "OPLS" 7 | x_length = 20 8 | y_length = 20 9 | 10 | # calculate array of unit cells to make sheet 11 | # unit cell is the orthorombic unit cell of graphene 12 | unit_cell_x = 2.0 * config[forcefield]["CC"] * cos(pi / 6.0) 13 | unit_cell_y = 3.0 * config[forcefield]["CC"] 14 | x_cells = int(x_length / unit_cell_x) 15 | y_cells = int(y_length / unit_cell_y) 16 | layout = [x_cells, y_cells, 1] # make an array of unit cells with this dimension 17 | 18 | motif = mg.molecules.Graphene(forcefield=forcefield) 19 | sheet = mg.Crystal(motif, layout) 20 | 21 | oxidiser = mg.reactors.Oxidiser( 22 | ratio=2.5, video_xyz=20, new_island_freq=1e14, method="rf" 23 | ) 24 | sheet = oxidiser.react(sheet) 25 | 26 | mg.Parameterise(sheet, sheet.vdw_defs) 27 | 28 | name = "GO_sheet" 29 | output = mg.Writer(sheet, name) 30 | output.write_xyz(name + ".xyz") 31 | output.write_lammps(name + ".data") 32 | -------------------------------------------------------------------------------- /examples/GO_stack.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import makegraphitics as mg 3 | 4 | vdw_defs = {1: 90, 2: 91} 5 | R = 15 6 | GO_separation = 10 # approx 1 nm in experiment (with water!) 7 | 8 | oxidiser = mg.reactors.Oxidiser( 9 | ratio=2.5, video_xyz=20, new_island_freq=1e14, method="rf" 10 | ) 11 | 12 | for i in range(3): 13 | motif = mg.molecules.Hexagon_Graphene(R) 14 | new_layer = mg.Crystal(motif, [1, 1, 1]) 15 | new_layer = oxidiser.react(new_layer) 16 | 17 | new_layer.coords = new_layer.coords + np.array((0, 0, i * GO_separation)) 18 | if i == 0: 19 | sim = new_layer 20 | else: 21 | sim = mg.Combine(sim, new_layer) 22 | 23 | mg.Parameterise(sim, new_layer.vdw_defs) 24 | 25 | name = "GO_stack" 26 | output = mg.Writer(sim, name) 27 | output.write_xyz(name + ".xyz") 28 | output.write_lammps(name + ".data") 29 | -------------------------------------------------------------------------------- /examples/carboxyl_ions_with_counterions.py: -------------------------------------------------------------------------------- 1 | import makegraphitics as mg 2 | 3 | flake_radius = 25 4 | layout = [1, 1, 1] # make a 1x1x1 array of flakes 5 | 6 | motif = mg.molecules.Hexagon_Graphene(flake_radius) 7 | flake = mg.Crystal(motif, layout) 8 | 9 | oxidiser = mg.reactors.Oxidiser( 10 | ratio=2.5, new_island_freq=1e14, method="rf", 11 | carboxyl_charged_ratio=0.5, 12 | counterion="Ca" 13 | #counterion="Na" 14 | ) 15 | flake = oxidiser.react(flake) 16 | 17 | mg.Parameterise(flake) 18 | 19 | flake.validate() 20 | 21 | out = mg.Writer(flake) 22 | out.write_xyz() 23 | -------------------------------------------------------------------------------- /examples/charged_carboxyl_groups.py: -------------------------------------------------------------------------------- 1 | import makegraphitics as mg 2 | 3 | flake_radius = 25 4 | layout = [1, 1, 1] # make a 1x1x1 array of flakes 5 | 6 | motif = mg.molecules.Hexagon_Graphene(flake_radius) 7 | flake = mg.Crystal(motif, layout) 8 | 9 | oxidiser = mg.reactors.Oxidiser( 10 | ratio=2.5, new_island_freq=1e14, method="rf", 11 | carboxyl_charged_ratio=0.5 12 | ) 13 | flake = oxidiser.react(flake) 14 | 15 | mg.Parameterise(flake) 16 | 17 | flake.validate() 18 | -------------------------------------------------------------------------------- /examples/config.yaml: -------------------------------------------------------------------------------- 1 | Crystal: 2 | CC: 1.421 3 | layer_gap: 3.354 4 | 5 | GraFF_5: 6 | CC: 1.42845 7 | layer_gap: 3.4827 8 | CH: 1.077 9 | dq: 0.115 10 | 11 | GraFF_77: 12 | CC: 1.42827 13 | layer_gap: 3.4827 14 | CH: 1.077 15 | dq: 0.115 16 | 17 | OPLS: 18 | CC: 1.4148 # OPLS 19 | # layer_gap: 3.3827 20 | layer_gap: 3.4827 21 | CH: 1.077 22 | dq: 0.115 23 | 24 | AMBER: 25 | CC: 1.4293 # AMBER 26 | layer_gap: 3.3693 27 | CH: 1.088 28 | dq: 0.115 29 | 30 | COMPASS: 31 | CC: 1.3913 # COMPASS 32 | layer_gap: 3.3548 33 | CH: 1.070 34 | dq: 0.1268 35 | 36 | Driedling: 37 | CC: 1.3838 # Driedling 38 | layer_gap: 3.3955 39 | CH: 1.020 40 | dq: 0.062 41 | 42 | system: 43 | vdw_cutoff: 12.0 44 | N_layers: 2 45 | -------------------------------------------------------------------------------- /examples/graphene_flake.py: -------------------------------------------------------------------------------- 1 | import makegraphitics as mg 2 | 3 | R = 40 4 | motif = mg.molecules.Hexagon_Graphene(R) 5 | flake = mg.Crystal(motif, [1, 1, 1]) 6 | vdw_defs = {1: 90, 2: 91} 7 | 8 | mg.Parameterise(flake, vdw_defs) 9 | 10 | name = "graphene" 11 | output = mg.Writer(flake, name) 12 | output.write_xyz(name + ".xyz") 13 | output.write_lammps(name + ".data") 14 | -------------------------------------------------------------------------------- /examples/graphene_rect.py: -------------------------------------------------------------------------------- 1 | import makegraphitics as mg 2 | 3 | # makes a graphene flake that is 50x50 Angstroms 4 | motif = mg.molecules.Rectangle_Graphene(50, 50) 5 | flake = mg.Crystal(motif, [1, 1, 1]) 6 | vdw_defs = {1: 90, 2: 91} 7 | 8 | mg.Parameterise(flake, vdw_defs) 9 | 10 | name = "graphene" 11 | output = mg.Writer(flake, name) 12 | output.write_xyz(name + ".xyz") 13 | output.write_lammps(name + ".data") 14 | -------------------------------------------------------------------------------- /examples/graphene_sheet.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from math import cos, pi 3 | import makegraphitics as mg 4 | 5 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 6 | forcefield = "OPLS" 7 | 8 | x_length = 20 9 | y_length = 20 10 | 11 | # calculate array of unit cells to make sheet 12 | # unit cell is the orthorombic unit cell of graphene 13 | unit_cell_x = 2.0 * config[forcefield]["CC"] * cos(pi / 6.0) 14 | unit_cell_y = 3.0 * config[forcefield]["CC"] 15 | x_cells = int(x_length / unit_cell_x) 16 | y_cells = int(y_length / unit_cell_y) 17 | layout = [x_cells, y_cells, 1] # make an array of unit cells with this dimension 18 | 19 | motif = mg.molecules.Graphene() 20 | graphene = mg.Crystal(motif, layout) 21 | vdw_defs = {1: 90} 22 | 23 | mg.Parameterise(graphene, vdw_defs) 24 | 25 | name = "graphene" 26 | output = mg.Writer(graphene, name) 27 | output.write_xyz(name + ".xyz") 28 | output.write_lammps(name + ".data") 29 | -------------------------------------------------------------------------------- /examples/graphene_stack.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import numpy as np 3 | import makegraphitics as mg 4 | 5 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 6 | forcefield = "OPLS" 7 | 8 | vdw_defs = {1: 90, 2: 91} 9 | R = 25 10 | for i in range(5): 11 | motif = mg.molecules.Hexagon_Graphene(R) 12 | new_layer = mg.Crystal(motif, [1, 1, 1]) 13 | 14 | new_layer.coords = new_layer.coords + np.array( 15 | (0, 0, i * config[forcefield]["layer_gap"]) 16 | ) 17 | new_layer.vdw_defs = {1: 90, 2: 91} 18 | if i == 0: 19 | sim = new_layer 20 | else: 21 | sim = mg.Combine(sim, new_layer) 22 | 23 | mg.Parameterise(sim) 24 | 25 | name = "graphene_stack" 26 | output = mg.Writer(sim, name) 27 | output.write_xyz(name + ".xyz") 28 | output.write_lammps(name + ".data") 29 | -------------------------------------------------------------------------------- /examples/graphite.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from math import cos, pi 3 | import makegraphitics as mg 4 | 5 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 6 | forcefield = "OPLS" 7 | 8 | x_length = 20 9 | y_length = 20 10 | layers = 10 11 | 12 | # calculate array of unit cells to make sheet 13 | # unit cell is the orthorombic unit cell of graphene 14 | unit_cell_x = 2.0 * config[forcefield]["CC"] * cos(pi / 6.0) 15 | unit_cell_y = 3.0 * config[forcefield]["CC"] 16 | x_cells = int(x_length / unit_cell_x) 17 | y_cells = int(y_length / unit_cell_y) 18 | layout = [ 19 | x_cells, 20 | y_cells, 21 | int(layers / 2), 22 | ] # make an array of unit cells with this dimension 23 | 24 | motif = mg.molecules.Graphite() 25 | graphite = mg.Crystal(motif, layout) 26 | vdw_defs = {1: 90} 27 | 28 | mg.Parameterise(graphite, vdw_defs) 29 | 30 | name = "graphite" 31 | output = mg.Writer(graphite, name) 32 | output.write_xyz(name + ".xyz") 33 | output.write_lammps(name + ".data") 34 | -------------------------------------------------------------------------------- /examples/peel_simulation.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import numpy as np 3 | import math 4 | import makegraphitics as mg 5 | 6 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 7 | forcefield = "GraFF_5" 8 | graphite = mg.molecules.Graphite() 9 | bulk = mg.Crystal(graphite, [21, 12, 1]) 10 | bulk.coords = bulk.coords + np.array((0, 0, 1 * config[forcefield]["layer_gap"])) 11 | 12 | 13 | molecule1 = mg.molecules.Hexagon_Graphene(15) 14 | flake1 = mg.Crystal(molecule1, [1, 1, 1]) 15 | 16 | flake1.coords = flake1.coords + np.array( 17 | ( 18 | 10 * 2 * math.cos(math.pi / 6) * config[forcefield]["CC"], 19 | 6 * 3 * config[forcefield]["CC"], 20 | 3 * config[forcefield]["layer_gap"], 21 | ) 22 | ) 23 | 24 | bulk.vdw_defs = {1: 90} 25 | flake1.vdw_defs = {1: 90, 2: 91} 26 | sim = mg.Combine(bulk, flake1) 27 | 28 | output = mg.Writer(sim, "flake on graphite") 29 | output.write_xyz("graphene" + str(1) + ".xyz") 30 | output.write_lammps("flake" + str(1) + ".data") 31 | -------------------------------------------------------------------------------- /examples/sliding_flake_sim.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import numpy as np 3 | import makegraphitics as mg 4 | 5 | forcefield = "GraFF_5" 6 | 7 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 8 | 9 | graphite = mg.molecules.Graphite() 10 | bulk = mg.Crystal(graphite, [122, 71, 2]) 11 | 12 | 13 | molecule1 = mg.molecules.Hexagon_Graphene(50) 14 | flake1 = mg.Crystal(molecule1, [1, 1, 1]) 15 | # make flake carbons different to bulk 16 | # for atom in range(molecule.natoms): 17 | # if flake.atom_labels[atom] == 1: 18 | # flake.atom_labels[atom] = 3 19 | 20 | flake1.coords = flake1.coords + np.array( 21 | ( 22 | 20 * 2 * (3 ** 0.5) * config[forcefield]["CC"], 23 | 72 * config[forcefield]["CC"], 24 | 3.7 + (4) * config[forcefield]["layer_gap"], 25 | ) 26 | ) 27 | bulk.coords = bulk.coords + np.array((0, 0, 3.7)) 28 | 29 | bulk.vdw_defs = {1: 90} 30 | flake1.vdw_defs = {1: 90, 2: 91} 31 | 32 | 33 | sim = mg.Combine(bulk, flake1) 34 | sim.box_dimensions[2] = 30 35 | # output = Shifter(sim,'lammps') 36 | # output.rotate(180,1) 37 | output = mg.Writer(sim, "flake on graphite") 38 | output.write_xyz("graphene.xyz") 39 | output.write_lammps("flake.data") 40 | -------------------------------------------------------------------------------- /examples/tree.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import numpy as np 3 | import makegraphitics as mg 4 | 5 | config = yaml.load(open("config.yaml"), Loader=yaml.FullLoader) 6 | forcefield = "OPLS" 7 | 8 | vdw_defs = {1: 90, 2: 91} 9 | 10 | graphite = mg.molecules.Graphene() 11 | sim = mg.Crystal(graphite, [40, 30, 1]) 12 | sim.vdw_defs = vdw_defs 13 | mg.Parameterise(sim, vdw_defs) 14 | 15 | j = 0 16 | for i in [ 17 | 3, 18 | 3, 19 | 4, 20 | 4, 21 | 5, 22 | 5, 23 | 0, 24 | 0, 25 | 0, 26 | 15, 27 | 13, 28 | 11, 29 | 9, 30 | 7, 31 | 12, 32 | 10, 33 | 8, 34 | 6, 35 | 9, 36 | 7, 37 | 6, 38 | 5, 39 | 6, 40 | 5, 41 | 4, 42 | 3, 43 | 4, 44 | 2, 45 | 1, 46 | 0, 47 | -1, 48 | ]: 49 | print j 50 | j += 1 51 | motif = mg.molecules.Hexagon_Graphene(5 + i * 2.44) 52 | next_layer = mg.Crystal(motif, [1, 1, 1]) 53 | 54 | next_layer.coords = next_layer.coords + np.array( 55 | ( 56 | 10 * 2 * (3 ** 0.5) * config[forcefield]["CC"], 57 | 30 * config[forcefield]["CC"], 58 | j * config[forcefield]["layer_gap"], 59 | ) 60 | ) 61 | mg.Parameterise(next_layer, vdw_defs) 62 | sim = mg.Combine(sim, next_layer) 63 | """ 64 | for vector in [[4,10,10],[6,65,22],[15,40,10],[11,14,25],[10,70,10],[19,67,13]]: 65 | motif = Hexagon_Graphene(config,forcefield,8) 66 | new_flake = Crystal(motif,config,forcefield,[1,1,1]) 67 | new_flake.coords = new_flake.coords + np.array(( 68 | vector[0] * 2*(3**0.5) * config[forcefield]['CC'], 69 | vector[1] * config[forcefield]['CC'], 70 | vector[2] * config[forcefield]['layer_gap'])) 71 | Parameterise(new_flake,vdw_defs) 72 | sim = Combine(sim,new_flake) 73 | """ 74 | 75 | 76 | name = "graphene" 77 | output = mg.Writer(sim, name) 78 | output.write_xyz(name + ".xyz") 79 | output.write_lammps(name + ".data") 80 | -------------------------------------------------------------------------------- /graphene-env.yml: -------------------------------------------------------------------------------- 1 | name: graphene 2 | channels: 3 | - anaconda 4 | - defaults 5 | - conda 6 | - conda-forge 7 | dependencies: 8 | - numpy=1.16.5=py27hacdab7b_0 9 | - pytest=4.6.2=py27_0 10 | - python=2.7.17=h97142e2_0 11 | - pyyaml=5.1.1=py27h1de35cc_0 12 | - scikit-learn=0.20.3=py27h27c97d8_0 13 | - setuptools=41.6.0=py27_0 14 | 15 | -------------------------------------------------------------------------------- /makegraphitics/__init__.py: -------------------------------------------------------------------------------- 1 | from read_lammpsdata import ReadLammpsData 2 | from params import Parameterise 3 | from combine import Combine 4 | from connector import Connector 5 | from crystal import Crystal 6 | from lattice import Lattice 7 | from write_coords import Writer 8 | from sim import Sim 9 | from . import reactors 10 | from . import molecules 11 | -------------------------------------------------------------------------------- /makegraphitics/combine.py: -------------------------------------------------------------------------------- 1 | from sim import Sim 2 | import numpy as np 3 | import copy 4 | 5 | 6 | class Combine(Sim): 7 | def __init__(self, sim1, sim2): 8 | # combine two Crystal objects 9 | # keep cell size from sim1 10 | natoms1 = len(sim1.coords) 11 | nmols1 = np.amax(sim1.molecule_labels) 12 | 13 | self.vdw_defs = copy.deepcopy(sim1.vdw_defs) 14 | # self.pair_coeffs = sim1.pair_coeffs 15 | # self.masses = sim1.masses 16 | for i in sim2.vdw_defs: 17 | exists_in_sim1 = 0 18 | for j in sim1.vdw_defs: 19 | if sim1.vdw_defs[j] == sim2.vdw_defs[i]: 20 | exists_in_sim1 += 1 21 | sim2.atom_labels = self.replace_labels(sim2.atom_labels, i, j) 22 | if not exists_in_sim1: 23 | new_label = max(self.vdw_defs.keys()) + 1 24 | # self.pair_coeffs[new_label] = sim2.pair_coeffs[i] 25 | # self.masses[new_label] = sim2.masses[i] 26 | sim2.atom_labels = self.replace_labels(sim2.atom_labels, i, new_label) 27 | self.vdw_defs[new_label] = sim2.vdw_defs[i] 28 | 29 | elif exists_in_sim1 > 1: 30 | raise Exception(exists_in_sim1) 31 | 32 | self.box_dimensions = sim1.box_dimensions 33 | 34 | # for thing in ['bond','angle','improper']: 35 | # coeff = thing+'_coeffs' 36 | # types = thing+'_types' 37 | # labels= thing+'_labels' 38 | # N = 'N'+thing+'_types' 39 | # self.combine_coeff(sim1, sim2, coeff,types,labels) 40 | # self.combine_coeff(sim1, sim2,'dihedral_coeffs','dihedral_types','dihedral_labels') 41 | 42 | setattr( 43 | self, "coords", self.stack(getattr(sim1, "coords"), getattr(sim2, "coords")) 44 | ) 45 | for attr in ["bonds", "angles", "dihedrals", "impropers"]: 46 | attr1 = getattr(sim1, attr) 47 | attr2 = getattr(sim2, attr) + len(getattr(sim1, "coords")) 48 | # print attr,len(attr1),len(attr2) 49 | setattr(self, attr, self.stack(attr1, attr2)) 50 | # print len(getattr(self,attr)) 51 | 52 | for attr in [ 53 | "atom_charges", 54 | "atom_labels", 55 | "bond_labels", 56 | "angle_labels", 57 | "dihedral_labels", 58 | "improper_labels", 59 | ]: 60 | attr1 = getattr(sim1, attr) 61 | attr2 = getattr(sim2, attr) 62 | setattr(self, attr, self.join(attr1, attr2)) 63 | 64 | self.molecule_labels = self.join( 65 | sim1.molecule_labels, list(np.array(sim2.molecule_labels) + nmols1) 66 | ) 67 | 68 | def combine_coeff(self, sim1, sim2, coeff, types, labels): 69 | new_types = copy.deepcopy(getattr(sim1, types)) 70 | new_coeffs = copy.deepcopy(getattr(sim1, coeff)) 71 | types1 = getattr(sim1, types) 72 | types2 = getattr(sim2, types) 73 | for i in range(len(types2)): 74 | def2 = [sim2.vdw_defs[a] for a in types2[i]] 75 | exists_in_sim1 = 0 76 | for j in range(len(types1)): 77 | def1 = [sim1.vdw_defs[a] for a in types1[j]] 78 | if (def1 == def2) or (def1 == list(reversed(def2))): 79 | print "matched", coeff, i + 1, j + 1, def1, def2 80 | exists_in_sim1 += 1 81 | new_labels = self.replace_labels( 82 | getattr(sim2, labels), i + 1, j + 1 83 | ) 84 | setattr(sim2, labels, new_labels) 85 | if not exists_in_sim1: 86 | new_label = max(new_coeffs.keys()) + 1 87 | new_coeff = getattr(sim2, coeff)[i + 1] 88 | new_coeffs[new_label] = new_coeff 89 | new_labels = self.replace_labels( 90 | getattr(sim2, labels), i + 1, new_label 91 | ) 92 | setattr(sim2, labels, new_labels) 93 | new_types += [getattr(sim2, types)[i]] 94 | elif exists_in_sim1 > 1: 95 | raise IndexError(exists_in_sim1) 96 | setattr(self, coeff, new_coeffs) 97 | setattr(self, types, new_types) 98 | 99 | def replace_labels(self, labels, a, b): 100 | count = 0 101 | for label in range(len(labels)): 102 | if labels[label] == a: 103 | labels[label] = b 104 | count += 1 105 | return labels 106 | 107 | def stack(self, array1, array2): 108 | return np.vstack((array1, array2)) 109 | 110 | def join(self, list1, list2): 111 | return list1 + list2 112 | 113 | def reduce(self, sim1, sim2, labels, coeffs): 114 | pass 115 | -------------------------------------------------------------------------------- /makegraphitics/connector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Connector(object): 5 | def bond_labels(self, atom_labels, bonds, bond_types): 6 | bond_labels = [] 7 | for bond in bonds: 8 | atoms = [atom_labels[bond[0] - 1], atom_labels[bond[1] - 1]] 9 | for i in range(len(bond_types)): 10 | if bond_types[i] == atoms: 11 | bond_labels.append(i + 1) 12 | break 13 | if bond_types[i] == list(reversed(atoms)): 14 | bond_labels.append(i + 1) 15 | break 16 | if len(bond_labels) != len(bonds): 17 | raise ValueError("bond assignment went wrong") 18 | return bond_labels 19 | 20 | def angles(self, bonds, bond_graph): 21 | N = int(np.amax(bonds)) # Number of atoms 22 | estimate_n_angles = N * 6 23 | angles = np.empty((estimate_n_angles, 3), dtype=int) 24 | 25 | counter = 0 26 | for centre in range(1, N + 1): 27 | neighbours = list(bond_graph[centre - 1]) 28 | neighbours = [x + 1 for x in neighbours] 29 | for i in range(len(neighbours)): 30 | for j in range(i + 1, len(neighbours)): 31 | angle = [neighbours[i], centre, neighbours[j]] 32 | angles[counter] = angle 33 | counter += 1 34 | # remove excess rows in angle array 35 | angles = angles[:counter] 36 | return angles 37 | 38 | def angle_labels(self, atom_labels, angles, angle_types): 39 | angle_labels = [] 40 | for angle in angles: 41 | atoms = [ 42 | atom_labels[angle[0] - 1], 43 | atom_labels[angle[1] - 1], 44 | atom_labels[angle[2] - 1], 45 | ] 46 | for i in range(len(angle_types)): 47 | if angle_types[i] == atoms: 48 | angle_labels.append(i + 1) 49 | break 50 | if angle_types[i] == list(reversed(atoms)): 51 | angle_labels.append(i + 1) 52 | break 53 | if len(angle_labels) != len(angles): 54 | raise ValueError("angle assignment went wrong") 55 | return angle_labels 56 | 57 | def dihedrals(self, bonds, bond_graph): 58 | estimate_n_dihedrals = len(bonds) * 9 59 | dihedrals = np.empty((estimate_n_dihedrals, 4), dtype=int) 60 | 61 | counter = 0 62 | for bond in bonds: 63 | neighbours1 = list(bond_graph[bond[0] - 1]) 64 | neighbours1.remove(bond[1] - 1) 65 | neighbours1 = [x + 1 for x in neighbours1] 66 | neighbours2 = list(bond_graph[bond[1] - 1]) 67 | neighbours2.remove(bond[0] - 1) 68 | neighbours2 = [x + 1 for x in neighbours2] 69 | 70 | if len(neighbours1) and len(neighbours2): 71 | for neighbour1 in neighbours1: 72 | for neighbour2 in neighbours2: 73 | # check it is not a three member ring 74 | if neighbour1 != neighbour2: 75 | dihedral = [neighbour1, bond[0], bond[1], neighbour2] 76 | dihedrals[counter] = dihedral 77 | counter += 1 78 | # remove excess rows in dihedral array 79 | dihedrals = dihedrals[:counter] 80 | return dihedrals 81 | 82 | def dihedral_labels(self, atom_labels, dihedrals, dihedral_types): 83 | dihedral_labels = [] 84 | for dihedral in dihedrals: 85 | atoms = [ 86 | atom_labels[dihedral[0] - 1], 87 | atom_labels[dihedral[1] - 1], 88 | atom_labels[dihedral[2] - 1], 89 | atom_labels[dihedral[3] - 1], 90 | ] 91 | for i in range(len(dihedral_types)): 92 | if dihedral_types[i] == atoms: 93 | dihedral_labels.append(i + 1) 94 | break 95 | if dihedral_types[i] == list(reversed(atoms)): 96 | dihedral_labels.append(i + 1) 97 | break 98 | if len(dihedral_labels) != len(dihedrals): 99 | raise ValueError("dihedral assignment went wrong") 100 | return dihedral_labels 101 | 102 | def impropers(self, bonds, bond_graph): 103 | N = int(np.amax(bonds)) # Number of atoms 104 | estimate_n_impropers = N 105 | impropers = np.empty((estimate_n_impropers, 4), dtype=int) 106 | 107 | counter = 0 108 | for centre in range(1, N + 1): 109 | neighbours = list(bond_graph[centre - 1]) 110 | neighbours = [x + 1 for x in neighbours] 111 | if len(neighbours) == 3: 112 | improper = [centre, neighbours[0], neighbours[1], neighbours[2]] 113 | impropers[counter] = improper 114 | counter += 1 115 | # remove excess rows in improper array 116 | impropers = impropers[:counter] 117 | return impropers 118 | 119 | def improper_labels(self, atom_labels, impropers, improper_types): 120 | improper_labels = [] 121 | for improper in impropers: 122 | atoms = [ 123 | atom_labels[improper[0] - 1], 124 | atom_labels[improper[1] - 1], 125 | atom_labels[improper[2] - 1], 126 | atom_labels[improper[3] - 1], 127 | ] 128 | for i in range(len(improper_types)): 129 | flag1 = improper_types[i][0] == atoms[0] 130 | flag2 = set(improper_types[i][1:]) == set(atoms[1:]) 131 | if flag1 and flag2: 132 | improper_labels.append(i + 1) 133 | if len(improper_labels) != len(impropers): 134 | print len(improper_labels), len(impropers) 135 | raise ValueError("improper assignment went wrong", len(improper_labels)) 136 | return improper_labels 137 | 138 | def find_connections(self, bonds, centre): 139 | connections = np.where(bonds == centre) 140 | connections = np.vstack((connections[0], connections[1])) 141 | return connections.transpose() 142 | 143 | def find_neighbours(self, bonds, centre): 144 | connections = self.find_connections(bonds, centre) 145 | neighbours = [] 146 | for connection in connections: 147 | # Find atom connected to centre 148 | neighbour = bonds[connection[0]][connection[1] - 1] 149 | neighbours.append(neighbour) 150 | return neighbours 151 | 152 | def find_dihedral_types(self, atom_labels, dihedrals): 153 | dihedral_types = [] 154 | for dihedral in dihedrals: 155 | atoms = [ 156 | atom_labels[dihedral[0] - 1], 157 | atom_labels[dihedral[1] - 1], 158 | atom_labels[dihedral[2] - 1], 159 | atom_labels[dihedral[3] - 1], 160 | ] 161 | found = False 162 | for i in range(len(dihedral_types)): 163 | if dihedral_types[i] == atoms: 164 | found = True 165 | break 166 | if dihedral_types[i] == list(reversed(atoms)): 167 | found = True 168 | break 169 | if not found: 170 | dihedral_types += [atoms] 171 | return dihedral_types 172 | 173 | def find_bond_types(self, atom_labels, bonds): 174 | bond_types = [] 175 | for bond in bonds: 176 | atoms = [atom_labels[bond[0] - 1], atom_labels[bond[1] - 1]] 177 | found = False 178 | for i in range(len(bond_types)): 179 | if bond_types[i] == atoms: 180 | found = True 181 | break 182 | if bond_types[i] == list(reversed(atoms)): 183 | found = True 184 | break 185 | if not found: 186 | bond_types += [atoms] 187 | return bond_types 188 | 189 | def find_angle_types(self, atom_labels, angles): 190 | angle_types = [] 191 | for angle in angles: 192 | atoms = [ 193 | atom_labels[angle[0] - 1], 194 | atom_labels[angle[1] - 1], 195 | atom_labels[angle[2] - 1], 196 | ] 197 | found = False 198 | for i in range(len(angle_types)): 199 | if angle_types[i] == atoms: 200 | found = True 201 | break 202 | if angle_types[i] == list(reversed(atoms)): 203 | found = True 204 | break 205 | if not found: 206 | angle_types += [atoms] 207 | return angle_types 208 | 209 | def find_improper_types(self, atom_labels, impropers): 210 | improper_types = [] 211 | for improper in impropers: 212 | atoms = [ 213 | atom_labels[improper[0] - 1], 214 | atom_labels[improper[1] - 1], 215 | atom_labels[improper[2] - 1], 216 | atom_labels[improper[3] - 1], 217 | ] 218 | found = False 219 | for improper_type in improper_types: 220 | flag1 = improper_type[0] == atoms[0] 221 | flag2 = set(atoms[1:4]) == set(improper_type[1:4]) 222 | if flag1 and flag2: 223 | found = True 224 | break 225 | if not found: 226 | improper_types += [atoms] 227 | return improper_types 228 | -------------------------------------------------------------------------------- /makegraphitics/crystal.py: -------------------------------------------------------------------------------- 1 | from lattice import Lattice 2 | from connector import Connector 3 | from sim import Sim 4 | 5 | 6 | class Crystal(Sim): 7 | def __init__(self, molecule, lattice_dimensions, forcefield="OPLS"): 8 | 9 | self.molecule = molecule 10 | self.config = self.crystal_params() 11 | if forcefield: 12 | self.forcefield = forcefield 13 | else: 14 | self.forcefield = "OPLS" 15 | self.lattice = self.init_lattice() 16 | self.lattice_dimensions = self.determine_lattice(lattice_dimensions) 17 | self.generate_structure() 18 | self.generate_bonds() 19 | self.generate_connections() 20 | 21 | def init_lattice(self): 22 | self.cell_coords = self.molecule.cell_coords() 23 | return Lattice(self.molecule.cell_shape()) 24 | 25 | def determine_lattice(self, lattice_dimensions): 26 | if not lattice_dimensions: 27 | return [1, 1, 1] 28 | elif lattice_dimensions == "vdw": 29 | return self.lattice.lattice_size_vdw(self.config["system"]["vdw_cutoff"]) 30 | elif lattice_dimensions == "layers": 31 | return self.lattice.lattice_size_layers( 32 | self.config["system"]["vdw_cutoff"], self.config["system"]["N_layers"] 33 | ) 34 | else: 35 | return lattice_dimensions 36 | 37 | def generate_structure(self): 38 | self.lattice_points = self.lattice.create_lattice_points( 39 | self.lattice_dimensions 40 | ) 41 | self.coords = self.lattice.cell_onto_lattice( 42 | self.cell_coords, self.lattice_points 43 | ) 44 | self.box_dimensions = self.lattice.system_size(self.lattice_dimensions) 45 | self.molecule_labels = self.molecule.assign_molecules(self.lattice_dimensions) 46 | self.atom_labels = self.molecule.assign_atom_labels(self.lattice_dimensions) 47 | self.atom_charges = self.molecule.assign_atom_charges( 48 | self.lattice_dimensions, self.config[self.forcefield]["dq"] 49 | ) 50 | 51 | def generate_bonds(self): 52 | connect = Connector() 53 | self.bonds = self.molecule.assign_bonds(self.lattice_dimensions) 54 | -------------------------------------------------------------------------------- /makegraphitics/data_to_xyz.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from scripts.molecules import * 3 | from scripts import * 4 | import numpy as np 5 | import sys 6 | 7 | sim = ReadLammpsData(sys.argv[1]) 8 | 9 | 10 | name = "out" 11 | output = Writer(sim, name) 12 | output.write_xyz(name + ".xyz") 13 | output.write_lammps(name + ".data") 14 | -------------------------------------------------------------------------------- /makegraphitics/lattice.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Lattice(object): 5 | def __init__(self, cell_dimensions): 6 | self.cell_dimensions = cell_dimensions 7 | 8 | def lattice_size_vdw(self, vdw_cutoff): 9 | # Will result in a cell at least as big as 2 * vdw_cutoff 10 | # in all directions 11 | Nlattice_points = [] 12 | for axis in self.cell_dimensions: 13 | Npoints_on_axis = int(np.ceil((2 * vdw_cutoff) / axis)) 14 | Nlattice_points.append(Npoints_on_axis) 15 | return Nlattice_points 16 | 17 | def lattice_size_layers(self, vdw_cutoff, N_layers): 18 | # Define a cell as big as 2 * vdw_cutoff in x y direction 19 | # and a specified number of cells in z 20 | Nlattice_points = [] 21 | Nlattice_points.append(int(np.ceil(2 * vdw_cutoff / self.cell_dimensions[0]))) 22 | Nlattice_points.append(int(np.ceil(2 * vdw_cutoff / self.cell_dimensions[1]))) 23 | Nlattice_points.append(N_layers) 24 | return Nlattice_points 25 | 26 | def create_lattice_points(self, Nlattice_points): 27 | a, b, c = self.cell_dimensions 28 | lattice_points = [] 29 | for x in range(Nlattice_points[0]): 30 | for y in range(Nlattice_points[1]): 31 | for z in range(Nlattice_points[2]): 32 | point = [x * a, y * b, z * c] 33 | lattice_points.append(point) 34 | lattice_points = np.array(lattice_points) 35 | return lattice_points 36 | 37 | def cell_onto_lattice(self, cell_coords, lattice_points): 38 | N_atoms = len(cell_coords) * len(lattice_points) 39 | atoms = np.empty([N_atoms, 3], float) 40 | # counter = 0 41 | for i, lattice_point in enumerate(lattice_points): 42 | start = i * len(cell_coords) 43 | end = (i + 1) * len(cell_coords) 44 | atoms[start:end] = np.array(cell_coords) + np.array(lattice_point) 45 | # for atom in cell_coords: 46 | # new_atom = np.array(atom) + np.array(lattice_point) 47 | # atoms[counter] = new_atom 48 | # counter += 1 49 | return atoms 50 | 51 | def system_size(self, Nlattice_points): 52 | box_vectors = np.array(Nlattice_points) * np.array(self.cell_dimensions) 53 | box_dimensions = np.vstack((np.zeros(3), box_vectors)).transpose() 54 | return box_dimensions 55 | -------------------------------------------------------------------------------- /makegraphitics/molecules/__init__.py: -------------------------------------------------------------------------------- 1 | from graphite_cell import Graphite 2 | from base import Molecule 3 | from hexagon_graphene import Hexagon_Graphene 4 | from graphene_cell import Graphene 5 | from rectangle_graphene import Rectangle_Graphene 6 | -------------------------------------------------------------------------------- /makegraphitics/molecules/base.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | 4 | 5 | class Molecule(object): 6 | """A molecule or motif to be projected onto lattice points 7 | Structure and bonding is defined within a derivative of this class""" 8 | 9 | def crystal_params(self): 10 | path = os.path.dirname(__file__) + "/../params/" 11 | return yaml.load(open(path + "config.yaml"), Loader=yaml.FullLoader) 12 | 13 | def cell_shape(self): 14 | raise NotImplementedError("cell_shape not defined for this motif") 15 | 16 | def cell_coords(self): 17 | raise NotImplementedError("cell_coords not defined for this motif") 18 | 19 | def assign_molecules(self): 20 | raise NotImplementedError("assing_molecules not defined for this motif") 21 | 22 | def assign_atom_labels(self): 23 | raise NotImplementedError("assign_atom_labels not defined for this motif") 24 | 25 | def assign_atom_charges(self): 26 | raise NotImplementedError("assing_atom_charges not defined for this motif") 27 | 28 | def assign_bonds(self): 29 | raise NotImplementedError("assign_bonds not defined for this motif") 30 | 31 | def connection_types(self): 32 | raise NotImplementedError("connection_types not defined for this motif") 33 | -------------------------------------------------------------------------------- /makegraphitics/molecules/graphene_cell.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from math import pi, cos 3 | from base import Molecule 4 | 5 | 6 | class Graphene(Molecule): 7 | def __init__(self, forcefield="OPLS"): 8 | config = self.crystal_params() 9 | 10 | self.CC = config[forcefield]["CC"] 11 | self.layer_gap = config[forcefield]["layer_gap"] 12 | 13 | def cell_shape(self): 14 | a = 2.0 * self.CC * cos(pi / 6.0) 15 | b = 3.0 * self.CC 16 | c = self.layer_gap 17 | cell_dimensions = [a, b, c] 18 | return cell_dimensions 19 | 20 | def cell_coords(self): 21 | CC = self.CC 22 | cos_CC = cos(pi / 6.0) * CC 23 | sin_CC = 0.5 * CC 24 | C1 = [0, 0, 0] 25 | C2 = [0, CC, 0] 26 | C3 = [cos_CC, CC + sin_CC, 0] 27 | C4 = [cos_CC, 2 * CC + sin_CC, 0] 28 | cell_coords = np.array([C1, C2, C3, C4]) 29 | return cell_coords 30 | 31 | def assign_molecules(self, lattice_dimensions): 32 | unit_cell_molecule_label = [1, 1, 1, 1] 33 | molecule_labels = [] 34 | for x in range(lattice_dimensions[0]): 35 | for y in range(lattice_dimensions[1]): 36 | for z in range(lattice_dimensions[2]): 37 | labels = np.array(unit_cell_molecule_label) + (z * 2) 38 | molecule_labels.extend(list(labels)) 39 | return molecule_labels 40 | 41 | def assign_atom_labels(self, lattice_dimensions): 42 | atom_labels = [] 43 | cell_labels = [1, 1, 1, 1] 44 | for x in range(lattice_dimensions[0]): 45 | for y in range(lattice_dimensions[1]): 46 | for z in range(lattice_dimensions[2]): 47 | atom_labels.extend(list(cell_labels)) 48 | return atom_labels 49 | 50 | def assign_atom_charges(self, lattice_dimensions, q): 51 | atom_charges = [] 52 | cell_charges = [0, 0, 0, 0] 53 | for x in range(lattice_dimensions[0]): 54 | for y in range(lattice_dimensions[1]): 55 | for z in range(lattice_dimensions[2]): 56 | atom_charges.extend(list(cell_charges)) 57 | return atom_charges 58 | 59 | def assign_bonds(self, lattice_dimensions): 60 | internal_bonds = np.array([[1, 2], [2, 3], [3, 4]]) 61 | bonds = np.empty((0, 2), dtype=int) 62 | # loop through all cells 63 | for x in range(lattice_dimensions[0]): 64 | for y in range(lattice_dimensions[1]): 65 | for z in range(lattice_dimensions[2]): 66 | cell_position = [x, y, z] 67 | ( 68 | xcell_position, 69 | ycell_position, 70 | xycell_position, 71 | ) = find_adjacent_cells(cell_position, lattice_dimensions) 72 | 73 | # Add bonds within cell 74 | i = self.index_cell([x, y, z], lattice_dimensions, 4) 75 | bonds = np.vstack((bonds, internal_bonds + i)) 76 | # Add bonds that cross cell boundries 77 | bonds = self.add_cross_bond( 78 | lattice_dimensions, xcell_position, [3, 2], bonds, i 79 | ) 80 | bonds = self.add_cross_bond( 81 | lattice_dimensions, xycell_position, [4, 1], bonds, i 82 | ) 83 | bonds = self.add_cross_bond( 84 | lattice_dimensions, ycell_position, [4, 1], bonds, i 85 | ) 86 | 87 | return bonds 88 | 89 | def index_cell(self, cell_position, lattice_dimensions, atoms_per_cell): 90 | N = atoms_per_cell 91 | total = cell_position[2] * N 92 | total += cell_position[1] * lattice_dimensions[2] * N 93 | total += cell_position[0] * lattice_dimensions[2] * lattice_dimensions[1] * N 94 | return total 95 | 96 | def add_cross_bond(self, lattice_dimensions, cell_position, atoms, bonds, i): 97 | # cell_position : the adjoining cell 98 | # atoms [i,j]: ith atom in cell, jth atom in adjoining 99 | atoms[0] += i 100 | atoms[1] += self.index_cell(cell_position, lattice_dimensions, 4) 101 | return np.vstack((bonds, atoms)) 102 | 103 | def connection_types(self): 104 | bond_types = [[1, 1]] 105 | angle_types = [[1, 1, 1]] 106 | dihedral_types = [[1, 1, 1, 1]] 107 | improper_types = [[1, 1, 1, 1]] 108 | return bond_types, angle_types, dihedral_types, improper_types 109 | 110 | 111 | def find_connections(bonds, centre): 112 | connections = np.where(bonds == centre) 113 | connections = np.vstack((connections[0], connections[1])) 114 | return connections.transpose() 115 | 116 | 117 | def find_neighbours(bonds, centre): 118 | connections = find_connections(bonds, centre) 119 | neighbours = [] 120 | for connection in connections: 121 | # Find atom connected to centre 122 | neighbour = bonds[connection[0]][connection[1] - 1] 123 | neighbours.append(neighbour) 124 | return neighbours 125 | 126 | 127 | def find_adjacent_cells(cell_position, lattice_dimensions): 128 | x, y, z = cell_position 129 | # Account for periodic system 130 | if x == lattice_dimensions[0] - 1: 131 | xcell_position = [0, y, z] 132 | else: 133 | xcell_position = [x + 1, y, z] 134 | 135 | if y == lattice_dimensions[1] - 1: 136 | ycell_position = [x, 0, z] 137 | else: 138 | ycell_position = [x, y + 1, z] 139 | 140 | xycell_position = [xcell_position[0], ycell_position[1], z] 141 | 142 | return xcell_position, ycell_position, xycell_position 143 | -------------------------------------------------------------------------------- /makegraphitics/molecules/graphite_cell.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from math import pi, cos 3 | from base import Molecule 4 | 5 | 6 | class Graphite(Molecule): 7 | # Handling AB graphite with orthorhombic unit cell (8 atom) 8 | def __init__(self, forcefield="OPLS"): 9 | config = self.crystal_params() 10 | 11 | self.CC = config[forcefield]["CC"] 12 | self.layer_gap = config[forcefield]["layer_gap"] 13 | 14 | # orthorhombic unitcell lattice parameters 15 | def cell_shape(self): 16 | a = 2.0 * self.CC * cos(pi / 6.0) 17 | b = 3.0 * self.CC 18 | c = 2.0 * self.layer_gap 19 | cell_dimensions = [a, b, c] 20 | return cell_dimensions 21 | 22 | def cell_coords(self): 23 | CC = self.CC 24 | layer_gap = self.layer_gap 25 | cos_CC = cos(pi / 6.0) * CC 26 | sin_CC = 0.5 * CC 27 | C1 = [0, 0, 0] 28 | C2 = [0, CC, 0] 29 | C3 = [cos_CC, CC + sin_CC, 0] 30 | C4 = [cos_CC, 2 * CC + sin_CC, 0] 31 | C5 = [0, CC, layer_gap] 32 | C6 = [0, CC * 2, layer_gap] 33 | C7 = [cos_CC, 2 * CC + sin_CC, layer_gap] 34 | C8 = [cos_CC, sin_CC, layer_gap] 35 | cell_coords = np.array([C1, C2, C3, C4, C5, C6, C7, C8]) 36 | return cell_coords 37 | 38 | def assign_molecules(self, lattice_dimensions): 39 | unit_cell_molecule_label = [1, 1, 1, 1, 2, 2, 2, 2] 40 | molecule_labels = [] 41 | for x in range(lattice_dimensions[0]): 42 | for y in range(lattice_dimensions[1]): 43 | for z in range(lattice_dimensions[2]): 44 | labels = np.array(unit_cell_molecule_label) + (z * 2) 45 | molecule_labels.extend(list(labels)) 46 | return molecule_labels 47 | 48 | def assign_atom_labels(self, lattice_dimensions): 49 | atom_labels = [] 50 | cell_labels = [1, 1, 1, 1, 1, 1, 1, 1] 51 | for x in range(lattice_dimensions[0]): 52 | for y in range(lattice_dimensions[1]): 53 | for z in range(lattice_dimensions[2]): 54 | atom_labels.extend(list(cell_labels)) 55 | return atom_labels 56 | 57 | def assign_atom_charges(self, lattice_dimensions, q): 58 | atom_charges = [] 59 | cell_charges = [0, 0, 0, 0, 0, 0, 0, 0] 60 | for x in range(lattice_dimensions[0]): 61 | for y in range(lattice_dimensions[1]): 62 | for z in range(lattice_dimensions[2]): 63 | atom_charges.extend(list(cell_charges)) 64 | return atom_charges 65 | 66 | def assign_bonds(self, lattice_dimensions): 67 | internal_bonds = np.array([[1, 2], [2, 3], [3, 4], [5, 6], [6, 7], [8, 5]]) 68 | bonds = np.empty((0, 2), dtype=int) 69 | # loop through all cells 70 | for x in range(lattice_dimensions[0]): 71 | for y in range(lattice_dimensions[1]): 72 | for z in range(lattice_dimensions[2]): 73 | cell_position = [x, y, z] 74 | ( 75 | xcell_position, 76 | ycell_position, 77 | xycell_position, 78 | ) = find_adjacent_cells(cell_position, lattice_dimensions) 79 | 80 | # Add bonds within cell 81 | i = self.index_cell([x, y, z], lattice_dimensions, 8) 82 | bonds = np.vstack((bonds, internal_bonds + i)) 83 | # Add bonds that cross cell boundries 84 | bonds = self.add_cross_bond( 85 | lattice_dimensions, xcell_position, [3, 2], bonds, i 86 | ) 87 | bonds = self.add_cross_bond( 88 | lattice_dimensions, xycell_position, [4, 1], bonds, i 89 | ) 90 | bonds = self.add_cross_bond( 91 | lattice_dimensions, ycell_position, [4, 1], bonds, i 92 | ) 93 | 94 | bonds = self.add_cross_bond( 95 | lattice_dimensions, xcell_position, [7, 6], bonds, i 96 | ) 97 | bonds = self.add_cross_bond( 98 | lattice_dimensions, xcell_position, [8, 5], bonds, i 99 | ) 100 | bonds = self.add_cross_bond( 101 | lattice_dimensions, ycell_position, [7, 8], bonds, i 102 | ) 103 | 104 | return bonds 105 | 106 | def index_cell(self, cell_position, lattice_dimensions, atoms_per_cell): 107 | N = atoms_per_cell 108 | total = cell_position[2] * N 109 | total += cell_position[1] * lattice_dimensions[2] * N 110 | total += cell_position[0] * lattice_dimensions[2] * lattice_dimensions[1] * N 111 | return total 112 | 113 | def add_cross_bond(self, lattice_dimensions, cell_position, atoms, bonds, i): 114 | # cell_position : the adjoining cell 115 | # atoms [i,j]: ith atom in cell, jth atom in adjoining 116 | atoms[0] += i 117 | atoms[1] += self.index_cell(cell_position, lattice_dimensions, 8) 118 | return np.vstack((bonds, atoms)) 119 | 120 | def connection_types(self): 121 | bond_types = [[1, 1]] 122 | angle_types = [[1, 1, 1]] 123 | dihedral_types = [[1, 1, 1, 1]] 124 | improper_types = [[1, 1, 1, 1]] 125 | return bond_types, angle_types, dihedral_types, improper_types 126 | 127 | 128 | def find_connections(bonds, centre): 129 | connections = np.where(bonds == centre) 130 | connections = np.vstack((connections[0], connections[1])) 131 | return connections.transpose() 132 | 133 | 134 | def find_neighbours(bonds, centre): 135 | connections = find_connections(bonds, centre) 136 | neighbours = [] 137 | for connection in connections: 138 | # Find atom connected to centre 139 | neighbour = bonds[connection[0]][connection[1] - 1] 140 | neighbours.append(neighbour) 141 | return neighbours 142 | 143 | 144 | def find_adjacent_cells(cell_position, lattice_dimensions): 145 | x, y, z = cell_position 146 | # Account for periodic system 147 | if x == lattice_dimensions[0] - 1: 148 | xcell_position = [0, y, z] 149 | else: 150 | xcell_position = [x + 1, y, z] 151 | 152 | if y == lattice_dimensions[1] - 1: 153 | ycell_position = [x, 0, z] 154 | else: 155 | ycell_position = [x, y + 1, z] 156 | 157 | xycell_position = [xcell_position[0], ycell_position[1], z] 158 | 159 | return xcell_position, ycell_position, xycell_position 160 | -------------------------------------------------------------------------------- /makegraphitics/molecules/graphite_periodic_strip.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from math import pi, cos 3 | from base import Molecule 4 | 5 | 6 | class GraphiteStrip(Molecule): 7 | # Handling AB graphite with orthorhombic unit cell (8 atom) 8 | # zig zag edges 9 | def __init__(self, config, forcefield, length): 10 | self.CC = config[forcefield]["CC"] 11 | self.CH = config[forcefield]["CH"] 12 | self.layer_gap = config[forcefield]["layer_gap"] 13 | self.length = length 14 | 15 | # orthorhombic unitcell lattice parameters 16 | def cell_shape(self): 17 | a = 2.0 * self.CC * cos(pi / 6.0) 18 | c = 2.0 * self.layer_gap 19 | 20 | b = 2.0 * self.CC + 2.0 * self.CH 21 | units_per_cell = 1 22 | while b < self.length: 23 | b += 3.0 * self.CC 24 | units_per_cell += 1 25 | 26 | if units_per_cell < 3: 27 | raise ValueError("make it bigger") 28 | self.units_per_cell = units_per_cell 29 | 30 | cell_dimensions = [a, b, c] 31 | return cell_dimensions 32 | 33 | def cell_coords(self): 34 | CC = self.CC 35 | CH = self.CH 36 | layer_gap = self.layer_gap 37 | cos_CC = cos(pi / 6.0) * CC 38 | sin_CC = 0.5 * CC 39 | 40 | def simple_unit(): 41 | C1 = [0, 0, 0] 42 | C2 = [0, CC, 0] 43 | C3 = [cos_CC, CC + sin_CC, 0] 44 | C4 = [cos_CC, 2 * CC + sin_CC, 0] 45 | C5 = [0, CC, layer_gap] 46 | C6 = [0, CC * 2, layer_gap] 47 | C7 = [cos_CC, 2 * CC + sin_CC, layer_gap] 48 | C8 = [cos_CC, sin_CC, layer_gap] 49 | unit_coords = np.array([C1, C2, C3, C4, C5, C6, C7, C8]) 50 | return unit_coords 51 | 52 | def bottom_unit(): 53 | H1 = [CC - CH, 0, 0] 54 | C2 = [0, CC, 0] 55 | C3 = [cos_CC, CC + sin_CC, 0] 56 | C4 = [cos_CC, 2 * CC + sin_CC, 0] 57 | H5 = [0, 2 * CC - CH, layer_gap] 58 | C6 = [0, CC * 2, layer_gap] 59 | C7 = [cos_CC, 2 * CC + sin_CC, layer_gap] 60 | unit_coords = np.array([H1, C2, C3, C4, H5, C6, C7]) 61 | return unit_coords 62 | 63 | def top_unit(): 64 | C1 = [0, 0, 0] 65 | C2 = [0, CC, 0] 66 | C3 = [cos_CC, CC + sin_CC, 0] 67 | H4 = [cos_CC, CH + CC + sin_CC, 0] 68 | C5 = [0, CC, layer_gap] 69 | H6 = [0, CC + CH, layer_gap] 70 | C8 = [cos_CC, sin_CC, layer_gap] 71 | unit_coords = np.array([C1, C2, C3, H4, C5, H6, C7]) 72 | return unit_coords 73 | 74 | cell_coords = bottom_unit() 75 | for i in range(self.units_per_cell - 2): 76 | unit = simple_unit() 77 | for j in unit: 78 | j[2] += (i + 1) * 3 * CC 79 | cell_coords = np.vstack(cell_cords, unit) 80 | cell_coords = np.vstack(cell_coords, top_unit()) 81 | return cell_coords 82 | 83 | def assign_molecules(self, lattice_dimensions): 84 | l = [1, 1, 1, 1, 2, 2, 2] # bottom 85 | for i in range(self.units_per_cell - 2): 86 | l += [1, 1, 1, 1, 2, 2, 2, 2] # simple 87 | l += [1, 1, 1, 1, 2, 2, 2] # top 88 | unit_cell_molecule_label = l 89 | 90 | molecule_labels = [] 91 | for x in range(lattice_dimensions[0]): 92 | for y in range(lattice_dimensions[1]): 93 | for z in range(lattice_dimensions[2]): 94 | labels = np.array(unit_cell_molecule_label) + (z * 2) 95 | molecule_labels.extend(list(labels)) 96 | return molecule_labels 97 | 98 | def assign_atom_labels(self, lattice_dimensions): 99 | l = [2, 1, 1, 1, 2, 1, 1] # bottom 100 | for i in range(self.units_per_cell - 2): 101 | l += [1, 1, 1, 1, 1, 1, 1, 1] # simple 102 | l += [1, 1, 1, 2, 1, 1, 2] # top 103 | cell_label = l 104 | 105 | atom_labels = [] 106 | for x in range(lattice_dimensions[0]): 107 | for y in range(lattice_dimensions[1]): 108 | for z in range(lattice_dimensions[2]): 109 | atom_labels.extend(list(cell_labels)) 110 | return atom_labels 111 | 112 | def assign_atom_charges(self, lattice_dimensions, q): 113 | l = [2, 1, 1, 1, 2, 1, 1] # bottom 114 | for i in range(self.units_per_cell - 2): 115 | l += [1, 1, 1, 1, 1, 1, 1, 1] # simple 116 | l += [1, 1, 1, 2, 1, 1, 2] # top 117 | cell_label = l 118 | 119 | atom_charges = [] 120 | cell_charges = [0, 0, 0, 0, 0, 0, 0, 0] 121 | for x in range(lattice_dimensions[0]): 122 | for y in range(lattice_dimensions[1]): 123 | for z in range(lattice_dimensions[2]): 124 | atom_charges.extend(list(cell_charges)) 125 | return atom_charges 126 | 127 | def assign_bonds(self, lattice_dimensions): 128 | internal_bonds = np.array([[1, 2], [2, 3], [3, 4], [5, 6], [6, 7], [8, 5]]) 129 | bonds = np.empty((0, 2), dtype=int) 130 | # loop through all cells 131 | for x in range(lattice_dimensions[0]): 132 | for y in range(lattice_dimensions[1]): 133 | for z in range(lattice_dimensions[2]): 134 | cell_position = [x, y, z] 135 | ( 136 | xcell_position, 137 | ycell_position, 138 | xycell_position, 139 | ) = find_adjacent_cells(cell_position, lattice_dimensions) 140 | 141 | # Add bonds within cell 142 | i = self.index_cell([x, y, z], lattice_dimensions, 8) 143 | bonds = np.vstack((bonds, internal_bonds + i)) 144 | # Add bonds that cross cell boundries 145 | bonds = self.add_cross_bond( 146 | lattice_dimensions, xcell_position, [3, 2], bonds, i 147 | ) 148 | bonds = self.add_cross_bond( 149 | lattice_dimensions, xycell_position, [4, 1], bonds, i 150 | ) 151 | bonds = self.add_cross_bond( 152 | lattice_dimensions, ycell_position, [4, 1], bonds, i 153 | ) 154 | 155 | bonds = self.add_cross_bond( 156 | lattice_dimensions, xcell_position, [7, 6], bonds, i 157 | ) 158 | bonds = self.add_cross_bond( 159 | lattice_dimensions, xcell_position, [8, 5], bonds, i 160 | ) 161 | bonds = self.add_cross_bond( 162 | lattice_dimensions, ycell_position, [7, 8], bonds, i 163 | ) 164 | 165 | return bonds 166 | 167 | def index_cell(self, cell_position, lattice_dimensions, atoms_per_cell): 168 | N = atoms_per_cell 169 | total = cell_position[2] * N 170 | total += cell_position[1] * lattice_dimensions[2] * N 171 | total += cell_position[0] * lattice_dimensions[2] * lattice_dimensions[1] * N 172 | return total 173 | 174 | def add_cross_bond(self, lattice_dimensions, cell_position, atoms, bonds, i): 175 | # cell_position : the adjoining cell 176 | # atoms [i,j]: ith atom in cell, jth atom in adjoining 177 | atoms[0] += i 178 | atoms[1] += self.index_cell(cell_position, lattice_dimensions, 8) 179 | return np.vstack((bonds, atoms)) 180 | 181 | def connection_types(self): 182 | bond_types = [[1, 1]] 183 | angle_types = [[1, 1, 1]] 184 | dihedral_types = [[1, 1, 1, 1]] 185 | improper_types = [[1, 1, 1, 1]] 186 | return bond_types, angle_types, dihedral_types, improper_types 187 | 188 | 189 | def find_connections(bonds, centre): 190 | connections = np.where(bonds == centre) 191 | connections = np.vstack((connections[0], connections[1])) 192 | return connections.transpose() 193 | 194 | 195 | def find_neighbours(bonds, centre): 196 | connections = find_connections(bonds, centre) 197 | neighbours = [] 198 | for connection in connections: 199 | # Find atom connected to centre 200 | neighbour = bonds[connection[0]][connection[1] - 1] 201 | neighbours.append(neighbour) 202 | return neighbours 203 | 204 | 205 | def find_adjacent_cells(cell_position, lattice_dimensions): 206 | x, y, z = cell_position 207 | # Account for periodic system 208 | if x == lattice_dimensions[0] - 1: 209 | xcell_position = [0, y, z] 210 | else: 211 | xcell_position = [x + 1, y, z] 212 | 213 | if y == lattice_dimensions[1] - 1: 214 | ycell_position = [x, 0, z] 215 | else: 216 | ycell_position = [x, y + 1, z] 217 | 218 | xycell_position = [xcell_position[0], ycell_position[1], z] 219 | 220 | return xcell_position, ycell_position, xycell_position 221 | -------------------------------------------------------------------------------- /makegraphitics/molecules/hexagon_graphene.py: -------------------------------------------------------------------------------- 1 | from math import pi, cos, sin, sqrt 2 | import numpy as np 3 | from base import Molecule 4 | 5 | 6 | class Hexagon_Graphene(Molecule): 7 | def __init__(self, radius, forcefield="OPLS"): 8 | config = self.crystal_params() 9 | 10 | self.CC = config[forcefield]["CC"] 11 | self.CH = config[forcefield]["CH"] 12 | self.layer_gap = config[forcefield]["layer_gap"] 13 | self.order = int( 14 | (radius + (sqrt(3) / 2) * (self.CC - self.CH)) / (self.CC * sqrt(3)) 15 | ) 16 | self.radius = self.order * self.CC * sqrt(3) 17 | self.natoms = 0 18 | for ring in range(self.order): 19 | self.natoms += ((ring + 1) * 2 - 1) * 6 20 | self.natoms += self.order * 6 # Hydrogens 21 | 22 | def cell_shape(self): 23 | a = 100 + 2 * self.radius 24 | b = 100 + 2 * self.radius 25 | c = self.layer_gap 26 | return [a, b, c] 27 | 28 | def cell_coords(self): 29 | CC = self.CC 30 | CH = self.CH 31 | cos_CC = cos(pi / 6.0) * CC 32 | sin_CC = 0.5 * CC 33 | 34 | atoms_per_section = self.natoms / 6 35 | section = np.empty((atoms_per_section, 3)) 36 | 37 | global atom 38 | atom = 0 39 | 40 | def add_atom(coord): 41 | global atom 42 | section[atom] = coord 43 | atom += 1 44 | 45 | add_atom([0, CC, 0]) 46 | 47 | for ring in range(1, self.order): 48 | add_atom([-ring * cos_CC, CC + ring * (CC + sin_CC), 0]) 49 | for branch in range(ring): 50 | add_atom( 51 | [ 52 | -(ring - 1) * cos_CC + branch * 2 * cos_CC, 53 | sin_CC + ring * (CC + sin_CC), 54 | 0, 55 | ] 56 | ) 57 | add_atom( 58 | [ 59 | -(ring - 2) * cos_CC + branch * 2 * cos_CC, 60 | CC + ring * (CC + sin_CC), 61 | 0, 62 | ] 63 | ) 64 | 65 | # Add Hydrogens round edge 66 | for branch in range(self.order): 67 | add_atom( 68 | [ 69 | -(self.order - 1) * cos_CC + branch * 2 * cos_CC, 70 | CC + CH + (self.order - 1) * (sin_CC + CC), 71 | 0, 72 | ] 73 | ) 74 | 75 | def rotate_vector(section, theta): 76 | sint = sin(-theta) 77 | cost = cos(theta) 78 | x = cost * section[:, 0] - sint * section[:, 1] 79 | y = sint * section[:, 0] + cost * section[:, 1] 80 | z = section[:, 2] 81 | return np.array([x, y, z]).T 82 | 83 | coords = np.empty((self.natoms, 3)) 84 | for theta in range(6): 85 | rotated_section = rotate_vector(section, theta * pi / 3.0) 86 | start = theta * atoms_per_section 87 | end = (theta + 1) * atoms_per_section 88 | coords[start:end] = rotated_section 89 | 90 | return coords 91 | 92 | def assign_molecules(self, lattice_dimensions): 93 | molecule_labels = [] 94 | for z in range(lattice_dimensions[2]): 95 | labels = np.ones(self.natoms, dtype=int) 96 | molecule_labels.extend(list(labels + z)) 97 | return molecule_labels 98 | 99 | def assign_atom_labels(self, lattice_dimensions): 100 | atom_labels = [] 101 | segment_carbons = [1] * ((self.natoms - (self.order * 6)) / 6) 102 | segment_hydrogens = [2] * (self.order) 103 | for z in range(lattice_dimensions[2]): 104 | for i in range(6): 105 | atom_labels += segment_carbons + segment_hydrogens 106 | return atom_labels 107 | 108 | def assign_atom_charges(self, lattice_dimensions, q): 109 | atom_charges = [] 110 | number_of_inner_carbons = 0 111 | for ring in range(self.order - 1): 112 | number_of_inner_carbons += (ring + 1) * 2 - 1 113 | segment_inner_carbons = [0] * number_of_inner_carbons 114 | segment_outer_carbons = [-q] + [0, -q] * (self.order - 1) 115 | segment_hydrogens = [q] * self.order 116 | segment_charges = ( 117 | segment_inner_carbons + segment_outer_carbons + segment_hydrogens 118 | ) 119 | for z in range(lattice_dimensions[2]): 120 | for i in range(6): 121 | atom_charges += segment_charges 122 | return atom_charges 123 | 124 | def assign_bonds(self, lattice_dimensions): 125 | segment_bonds = [] 126 | natoms_section = self.natoms / 6 127 | natoms_passed = 0 128 | for ring in range(1, self.order): 129 | natoms_ring = (ring + 1) * 2 - 1 130 | for branch in range(ring): 131 | b1 = natoms_passed + 1 + branch * 2 132 | b2 = b1 + natoms_ring - 1 133 | segment_bonds += [[b1, b2]] 134 | segment_bonds += [[b2, b2 - 1]] 135 | segment_bonds += [[b2, b2 + 1]] 136 | natoms_passed += ring * 2 - 1 137 | 138 | for i in range(self.order): 139 | H = natoms_section - i 140 | C = natoms_section - self.order - (i * 2) 141 | segment_bonds += [[C, H]] 142 | 143 | last_on_ring = 0 144 | for i in range(self.order): 145 | last_on_ring += i * 2 + 1 146 | first_on_ring = last_on_ring - (i * 2) 147 | segment_bonds += [[last_on_ring, first_on_ring + natoms_section]] 148 | 149 | segment_bonds = np.array(segment_bonds) 150 | molecule_bonds = np.empty((0, 2), dtype=int) 151 | for i in range(6): 152 | molecule_bonds = np.vstack( 153 | (molecule_bonds, segment_bonds + natoms_section * i) 154 | ) 155 | 156 | for connection in range(self.order): 157 | molecule_bonds[-(connection + 1), 1] -= self.natoms 158 | 159 | bonds = np.empty((0, 2), dtype=int) 160 | for z in range(lattice_dimensions[2]): 161 | bonds = np.vstack((bonds, molecule_bonds + (z * self.natoms))) 162 | 163 | return bonds 164 | 165 | def connection_types(self): 166 | bond_types = [[1, 1], [1, 2]] 167 | angle_types = [[1, 1, 1], [1, 1, 2]] 168 | dihedral_types = [[1, 1, 1, 1], [1, 1, 1, 2], [2, 1, 1, 2]] 169 | improper_types = [[1, 1, 1, 1], [1, 1, 1, 2]] 170 | return bond_types, angle_types, dihedral_types, improper_types 171 | -------------------------------------------------------------------------------- /makegraphitics/molecules/rectangle_graphene.py: -------------------------------------------------------------------------------- 1 | from math import pi, cos, sin 2 | import numpy as np 3 | from base import Molecule 4 | 5 | 6 | class Rectangle_Graphene(Molecule): 7 | def __init__(self, x_length, y_length, forcefield="OPLS"): 8 | # zigzag edges run along the x axis 9 | # armchair edges run along the y axis 10 | config = self.crystal_params() 11 | 12 | self.CC = config[forcefield]["CC"] 13 | self.CH = config[forcefield]["CH"] 14 | self.layer_gap = config[forcefield]["layer_gap"] 15 | 16 | self.x_length = x_length 17 | self.y_length = y_length 18 | x_unit_length = 2.0 * self.CC * cos(pi / 6.0) 19 | y_unit_length = 1.5 * self.CC 20 | 21 | # number of rows of hexagons along each axis 22 | self.x = int((x_length - self.CC * cos(pi / 6.0)) / x_unit_length) 23 | self.y = int((y_length - self.CC * sin(pi / 6.0)) / y_unit_length) 24 | # mandate odd number of rows of hexagons for symmetry 25 | if not self.y % 2: 26 | self.y -= 1 27 | 28 | if (self.x <= 0) or (self.y <= 0): 29 | raise Exception("Dimensions too small") 30 | 31 | carbons_per_row = 1 + self.x * 2 32 | self.n_Cs = carbons_per_row * (self.y + 1) 33 | 34 | hydrogens_x = 2 * self.x 35 | hydrogens_y = 2 * (self.y + 1) 36 | self.n_Hs = hydrogens_x + hydrogens_y 37 | 38 | self.natoms = self.n_Cs + self.n_Hs 39 | 40 | def cell_shape(self): 41 | a = 100 + self.x_length 42 | b = 100 + self.y_length 43 | c = self.layer_gap 44 | return [a, b, c] 45 | 46 | def cell_coords(self): 47 | CC = self.CC 48 | CH = self.CH 49 | cos_CC = cos(pi / 6.0) * CC 50 | sin_CC = 0.5 * CC 51 | 52 | coords = np.zeros((self.natoms, 3)) 53 | 54 | global atom 55 | atom = 0 56 | 57 | def add_coord(coord): 58 | global atom 59 | coords[atom] = coord 60 | atom += 1 61 | 62 | # first row of carbons 63 | add_coord([0, 0, 0]) 64 | for col in range(self.x): 65 | add_coord([cos_CC + col * (2 * cos_CC), -sin_CC, 0]) 66 | add_coord([2 * cos_CC + col * (2 * cos_CC), 0, 0]) 67 | 68 | # other rows of carbons 69 | for row in range(self.y): 70 | if not row % 2: # even (0 is even) 71 | y_offset = CC + (row * 3 / 2) * CC 72 | add_coord([0, y_offset, 0]) 73 | for col in range(self.x): 74 | add_coord([cos_CC + col * (2 * cos_CC), y_offset + sin_CC, 0]) 75 | add_coord([2 * cos_CC + col * (2 * cos_CC), y_offset, 0]) 76 | else: # odd 77 | y_offset = (row + 1) * CC * 3 / 2 78 | add_coord([0, y_offset, 0]) 79 | for col in range(self.x): 80 | add_coord([cos_CC + col * (2 * cos_CC), y_offset - sin_CC, 0]) 81 | add_coord([2 * cos_CC + col * (2 * cos_CC), y_offset, 0]) 82 | 83 | # zig-zag Hs 84 | for col in range(self.x): 85 | add_coord([cos_CC + 2 * col * cos_CC, -sin_CC - CH, 0]) 86 | for col in range(self.x): 87 | add_coord( 88 | [cos_CC + 2 * col * cos_CC, CC + (row * 3 / 2) * CC + sin_CC + CH, 0] 89 | ) 90 | 91 | # armchair Hs 92 | cos_CH = cos(pi / 6.0) * CH 93 | sin_CH = 0.5 * CH 94 | for row in range(self.y): 95 | if row % 2: 96 | continue # odd rows of hexagons have no Hs on ends 97 | y_offset = row * 3 / 2 * CC 98 | add_coord([-cos_CH, y_offset - sin_CH, 0]) 99 | add_coord([-cos_CH, y_offset + CC + sin_CH, 0]) 100 | add_coord([2 * self.x * cos_CC + cos_CH, y_offset - sin_CH, 0]) 101 | add_coord([2 * self.x * cos_CC + cos_CH, y_offset + CC + sin_CH, 0]) 102 | 103 | return coords 104 | 105 | def assign_molecules(self, lattice_dimensions): 106 | molecule_labels = [] 107 | for z in range(lattice_dimensions[2]): 108 | labels = np.ones(self.natoms, dtype=int) 109 | molecule_labels.extend(list(labels + z)) 110 | return molecule_labels 111 | 112 | def assign_atom_labels(self, lattice_dimensions): 113 | atom_labels = np.zeros(self.natoms, dtype=int) 114 | atom_labels[: self.n_Cs] = 1 115 | atom_labels[self.n_Cs :] = 2 116 | return list(atom_labels) 117 | 118 | def assign_atom_charges(self, lattice_dimensions, q): 119 | atom_charges = np.zeros(self.natoms) 120 | # zigzag carbons 121 | for col in range(self.x): 122 | # bottom row 123 | atom = 1 + 2 * col 124 | atom_charges[atom] = -q 125 | # top row 126 | atom = self.n_Cs - 2 - 2 * col 127 | atom_charges[atom] = -q 128 | 129 | # armchair carbons 130 | carbons_per_row = 1 + self.x * 2 131 | for row in range(self.y + 1): 132 | atom = carbons_per_row * row 133 | atom_charges[atom] = -q 134 | atom = carbons_per_row * row + carbons_per_row - 1 135 | atom_charges[atom] = -q 136 | 137 | # hydrogens 138 | atom_charges[self.n_Cs :] = q 139 | 140 | return list(atom_charges) 141 | 142 | def assign_bonds(self, lattice_dimensions): 143 | firstrow = 3 + 4 * self.x 144 | a = 3 + 3 * self.x # even rows 145 | b = 2 + 3 * self.x # odd rows 146 | nbonds = firstrow + a * ((self.y - 1) / 2) + b * ((self.y + 1) / 2) 147 | bonds = np.empty((nbonds, 2), dtype=int) 148 | 149 | global bond 150 | bond = 0 151 | 152 | def add_bond(a, b): 153 | global bond 154 | bonds[bond] = [a + 1, b + 1] 155 | bond += 1 156 | 157 | # along each row 158 | carbons_per_row = 1 + self.x * 2 159 | for row in range(self.y + 1): 160 | for atom in range(self.x * 2): 161 | atom1 = carbons_per_row * row + atom 162 | atom2 = carbons_per_row * row + atom + 1 163 | add_bond(atom1, atom2) 164 | 165 | # between rows of carbons 166 | for row in range(self.y): 167 | if row % 2: 168 | for col in range(self.x): 169 | atom1 = carbons_per_row * row + col * 2 + 1 170 | atom2 = carbons_per_row * (row + 1) + col * 2 + 1 171 | add_bond(atom1, atom2) 172 | else: 173 | for col in range(self.x + 1): 174 | atom1 = carbons_per_row * row + col * 2 175 | atom2 = carbons_per_row * (row + 1) + col * 2 176 | add_bond(atom1, atom2) 177 | 178 | # hydrogen bonds 179 | # bottom row 180 | for col in range(self.x): 181 | atom_c = col * 2 + 1 182 | atom_h = self.n_Cs + col 183 | add_bond(atom_c, atom_h) 184 | # top row 185 | for col in range(self.x): 186 | atom_c = col * 2 + 1 + self.y * carbons_per_row 187 | atom_h = self.n_Cs + col + self.x 188 | add_bond(atom_c, atom_h) 189 | # armchair hydrogens 190 | h_offset = self.n_Cs + 2 * self.x 191 | for row in range(self.y + 1): 192 | if row % 2: 193 | continue # odd rows of hexagons have no Hs on ends 194 | atom_c = carbons_per_row * row 195 | atom_h = h_offset + row * 2 196 | add_bond(atom_c, atom_h) 197 | 198 | atom_c = carbons_per_row * (row + 1) 199 | atom_h = h_offset + 1 + row * 2 200 | add_bond(atom_c, atom_h) 201 | 202 | atom_c = carbons_per_row * (row + 1) - 1 203 | atom_h = h_offset + 2 + row * 2 204 | add_bond(atom_c, atom_h) 205 | 206 | atom_c = carbons_per_row * (row + 2) - 1 207 | atom_h = h_offset + 3 + row * 2 208 | add_bond(atom_c, atom_h) 209 | return bonds 210 | 211 | def connection_types(self): 212 | bond_types = [[1, 1], [1, 2]] 213 | angle_types = [[1, 1, 1], [1, 1, 2]] 214 | dihedral_types = [[1, 1, 1, 1], [1, 1, 1, 2], [2, 1, 1, 2]] 215 | improper_types = [[1, 1, 1, 1], [1, 1, 1, 2]] 216 | return bond_types, angle_types, dihedral_types, improper_types 217 | -------------------------------------------------------------------------------- /makegraphitics/opls_reader.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class OPLS_Reader(object): 5 | def __init__(self, datafile): 6 | self.setup_dicts() 7 | self.main = { 8 | "atom ": self.add_mass, 9 | "vdw ": self.add_pair, 10 | "bond ": self.add_bond, 11 | "angle ": self.add_angle, 12 | "torsion ": self.add_dihedral, 13 | "charge ": self.add_charge, 14 | "imptors ": self.add_improper, 15 | } 16 | 17 | with open(datafile) as f: 18 | for line in f: 19 | if len(line) > 7: 20 | self.readline(line) 21 | else: 22 | pass 23 | # blank line 24 | # print 'BLANK',line 25 | 26 | def readline(self, line): 27 | func = self.main.get(line[0:8], False) 28 | if not func: 29 | pass 30 | # data we can't parse 31 | # print 'WRONG',line 32 | else: 33 | func(line) 34 | 35 | def add_mass(self, line): 36 | self.vdw_type["vdw"] += [int(line[12:15])] 37 | self.vdw_type["type"] += [int(line[15:20])] 38 | self.mass["a"] += [int(line[12:15])] 39 | self.mass["m"] += [float(line[65:72])] 40 | 41 | def add_pair(self, line): 42 | line = line.split() 43 | self.pair["a"] += [int(line[1])] 44 | self.pair["s"] += [float(line[2])] 45 | self.pair["e"] += [float(line[3])] 46 | 47 | def add_bond(self, line): 48 | line = line.split() 49 | self.bond["a1"] += [int(line[1])] 50 | self.bond["a2"] += [int(line[2])] 51 | self.bond["k"] += [float(line[3])] 52 | self.bond["r"] += [float(line[4])] 53 | 54 | def add_angle(self, line): 55 | line = line.split() 56 | self.angle["a1"] += [int(line[1])] 57 | self.angle["a2"] += [int(line[2])] 58 | self.angle["a3"] += [int(line[3])] 59 | self.angle["k"] += [float(line[4])] 60 | self.angle["r"] += [float(line[5])] 61 | 62 | def add_dihedral(self, line): 63 | line = line.split() 64 | self.dihedral["a1"] += [int(line[1])] 65 | self.dihedral["a2"] += [int(line[2])] 66 | self.dihedral["a3"] += [int(line[3])] 67 | self.dihedral["a4"] += [int(line[4])] 68 | self.dihedral["k1"] += [float(line[5])] 69 | self.dihedral["k2"] += [float(line[8])] 70 | self.dihedral["k3"] += [float(line[11])] 71 | if len(line) == 17: 72 | self.dihedral["k4"] += [float(line[14])] 73 | else: 74 | self.dihedral["k4"] += [0.0] 75 | 76 | def add_charge(self, line): 77 | line = line.split() 78 | self.charge["a"] += [int(line[1])] 79 | self.charge["q"] += [float(line[2])] 80 | 81 | def add_improper(self, line): 82 | line = line.split() 83 | self.improper["a1"] += [int(line[1])] 84 | self.improper["a2"] += [int(line[2])] 85 | self.improper["centre"] += [int(line[3])] 86 | self.improper["a3"] += [int(line[4])] 87 | self.improper["k"] += [float(line[5])] 88 | self.improper["r"] += [float(line[6])] 89 | 90 | def setup_dicts(self): 91 | self.vdw_type = {"vdw": [], "type": []} 92 | self.pair = {"a": [], "s": [], "e": []} 93 | self.bond = {"a1": [], "a2": [], "k": [], "r": []} 94 | self.angle = {"a1": [], "a2": [], "a3": [], "k": [], "r": []} 95 | self.dihedral = { 96 | "a1": [], 97 | "a2": [], 98 | "a3": [], 99 | "a4": [], 100 | "k1": [], 101 | "k2": [], 102 | "k3": [], 103 | "k4": [], 104 | } 105 | self.improper = {"a1": [], "a2": [], "centre": [], "a3": [], "k": [], "r": []} 106 | self.charge = {"a": [], "q": []} 107 | self.mass = {"a": [], "m": []} 108 | 109 | 110 | # a = OPLS_Reader(sys.argv[1]) 111 | -------------------------------------------------------------------------------- /makegraphitics/params.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from opls_reader import OPLS_Reader 4 | 5 | 6 | class Parameterise(object): 7 | def __init__(self, crystal, vdw_defs=None, forcefield="OPLS", 8 | assign_charge=False): 9 | # use vdw_defs of atoms to paramterise the bonded and nonbonded coefficients 10 | if not vdw_defs: 11 | try: 12 | self.vdw_defs = crystal.vdw_defs 13 | except AttributeError: 14 | raise Exception( 15 | "The simulation you want to parameterise requires vdw_defs ," 16 | + "provide as an attribute to the simulation or as a keyword arg" 17 | ) 18 | else: 19 | self.vdw_defs = vdw_defs 20 | crystal.vdw_defs = vdw_defs 21 | 22 | self.retrieve_ff_data(forcefield) 23 | 24 | self.type_defs = {} 25 | for label in self.vdw_defs: 26 | vdw = self.vdw_defs[label] 27 | self.type_defs[label] = self.vdw_type["type"][ 28 | self.vdw_type["vdw"].index(vdw) 29 | ] 30 | print "Atom label -> OPLS vdw definitions: \t", self.vdw_defs 31 | print "Atom label -> OPLS type definitions: \t", self.type_defs 32 | 33 | try: 34 | crystal.bond_types, crystal.angle_types, crystal.dihedral_types, crystal.improper_types 35 | except AttributeError: 36 | crystal.generate_connections() 37 | crystal.bond_coeffs = self.match_bonds(crystal.bond_types) 38 | crystal.angle_coeffs = self.match_angles(crystal.angle_types) 39 | crystal.dihedral_coeffs = self.match_dihedrals(crystal.dihedral_types) 40 | crystal.improper_coeffs = self.match_impropers(crystal.improper_types) 41 | crystal.pair_coeffs = self.match_pairs() 42 | crystal.masses = self.match_masses() 43 | 44 | if assign_charge: 45 | crystal.atom_charges = self.match_charges(crystal.atom_labels) 46 | 47 | def match_charges(self, atom_labels): 48 | charge_data = self.charge_data 49 | charge_coeffs = {} 50 | for label in self.vdw_defs: 51 | found = 0 52 | for j in range(len(charge_data["a"])): 53 | atom_data = charge_data["a"][j] 54 | if self.vdw_defs[label] == atom_data: 55 | found += 1 56 | charge_coeffs[label] = charge_data["q"][j] 57 | if found != 1: 58 | raise ValueError("WRONG", label, "\t found ", found, " entries") 59 | 60 | charges = np.empty(len(atom_labels)) 61 | for i, label in enumerate(atom_labels): 62 | charges[i] = charge_coeffs[label] 63 | return charges 64 | 65 | def match_masses(self): 66 | mass_data = self.mass_data 67 | mass_coeffs = {} 68 | for label in self.vdw_defs: 69 | found = 0 70 | for j in range(len(mass_data["a"])): 71 | atom_data = mass_data["a"][j] 72 | if self.vdw_defs[label] == atom_data: 73 | found += 1 74 | mass_coeffs[label] = mass_data["m"][j] 75 | if found != 1: 76 | raise ValueError("WRONG", label, "\t found ", found, " entries") 77 | return mass_coeffs 78 | 79 | def match_pairs(self): 80 | pair_data = self.pair_data 81 | pair_coeffs = {} 82 | for label in self.vdw_defs: 83 | found = 0 84 | for j in range(len(pair_data["a"])): 85 | atom_data = pair_data["a"][j] 86 | if self.vdw_defs[label] == atom_data: 87 | found += 1 88 | pair_coeffs[label] = {} 89 | pair_coeffs[label][1] = pair_data["e"][j] 90 | pair_coeffs[label][2] = pair_data["s"][j] 91 | if found != 1: 92 | raise ValueError("WRONG", label, "\t found ", found, " entries") 93 | return pair_coeffs 94 | 95 | def match_bonds(self, bond_types): 96 | bond_data = self.bond_data 97 | bond_coeffs = {} 98 | for i in range(len(bond_types)): 99 | a1 = self.type_defs[bond_types[i][0]] 100 | a2 = self.type_defs[bond_types[i][1]] 101 | atoms = [a1, a2] 102 | found = 0 103 | for j in range(len(bond_data["k"])): 104 | atom_data = [bond_data["a1"][j], bond_data["a2"][j]] 105 | flag1 = atoms == atom_data 106 | flag2 = atoms == list(reversed(atom_data)) 107 | if flag1 or flag2: 108 | found += 1 109 | bond_coeffs[i + 1] = {} 110 | bond_coeffs[i + 1][1] = bond_data["k"][j] 111 | bond_coeffs[i + 1][2] = bond_data["r"][j] 112 | if found != 1: 113 | raise ValueError("WRONG", atoms, "\t found ", found, " entries") 114 | return bond_coeffs 115 | 116 | def match_angles(self, angle_types): 117 | def search_angles(angle_data, atoms, angle_coeffs): 118 | found = 0 119 | for j in range(len(angle_data["k"])): 120 | atom_data = [ 121 | angle_data["a1"][j], 122 | angle_data["a2"][j], 123 | angle_data["a3"][j], 124 | ] 125 | flag1 = atoms == atom_data 126 | flag2 = atoms == list(reversed(atom_data)) 127 | if flag1 or flag2: 128 | found += 1 129 | angle_coeffs[i + 1] = {} 130 | angle_coeffs[i + 1][1] = angle_data["k"][j] 131 | angle_coeffs[i + 1][2] = angle_data["r"][j] 132 | return angle_coeffs, found 133 | 134 | angle_data = self.angle_data 135 | angle_coeffs = {} 136 | questionable_substitutions = 0 137 | for i in range(len(angle_types)): 138 | a1 = self.type_defs[angle_types[i][0]] 139 | a2 = self.type_defs[angle_types[i][1]] 140 | a3 = self.type_defs[angle_types[i][2]] 141 | atoms = [a1, a2, a3] 142 | # print atoms, angle_types[i] 143 | angle_coeffs, found = search_angles(angle_data, atoms, angle_coeffs) 144 | 145 | if found == 0: 146 | for j in range(3): 147 | if atoms[j] == 48: 148 | atoms[j] = 47 # Try alkene carbon instead of aromatic 149 | if atoms[j] == 49: 150 | atoms[j] = 46 # Try alkene H 151 | angle_coeffs, found = search_angles(angle_data, atoms, angle_coeffs) 152 | if found == 1: 153 | print found, [a1, a2, a3], atoms, "sub aromatic -> alkene" 154 | if found == 0: 155 | if atoms[1] == 47: 156 | if atoms[0] == 13: 157 | atoms[0] = 47 158 | elif atoms[2] == 13: 159 | atoms[0] = 47 160 | angle_coeffs, found = search_angles(angle_data, atoms, angle_coeffs) 161 | if found == 0: 162 | print found, [a1, a2, a3], atoms, "made 13 -> 47 sub" 163 | if found != 1: 164 | raise ValueError("WRONG", atoms, "\t found ", found, " entries") 165 | # print 'WRONG',atoms,'\t found ',found,' entries' 166 | if questionable_substitutions != 0: 167 | print "made ", questionable_substitutions, " questionable angle subs" 168 | return angle_coeffs 169 | 170 | def match_dihedrals(self, dihedral_types): 171 | def add_coeff(dihedral_data, dihedral_coeffs, j): 172 | N = len(dihedral_coeffs) 173 | dihedral_coeffs[N + 1] = {} 174 | dihedral_coeffs[N + 1][1] = dihedral_data["k1"][j] 175 | dihedral_coeffs[N + 1][2] = dihedral_data["k2"][j] 176 | dihedral_coeffs[N + 1][3] = dihedral_data["k3"][j] 177 | dihedral_coeffs[N + 1][4] = dihedral_data["k4"][j] 178 | return dihedral_coeffs 179 | 180 | def check_wildcards(atoms, dihedral_data, dihedral_coeffs): 181 | found = 0 182 | t = len(dihedral_data["k1"]) 183 | for i in range(t): 184 | a1 = dihedral_data["a1"][i] 185 | a2 = dihedral_data["a2"][i] 186 | a3 = dihedral_data["a3"][i] 187 | a4 = dihedral_data["a4"][i] 188 | if a1 == 0: 189 | flag1 = atoms[1:4] == [a2, a3, a4] 190 | flag2 = atoms[0:3] == [a4, a3, a2] 191 | if flag1 or flag2: 192 | found += 1 193 | dihedral_coeffs = add_coeff(dihedral_data, dihedral_coeffs, i) 194 | break 195 | if a4 == 0: 196 | flag1 = atoms[0:3] == [a1, a2, a3] 197 | flag2 = atoms[1:4] == [a4, a2, a1] 198 | if flag1 or flag2: 199 | foudn += 1 200 | dihedral_coeffs = add_coeff(dihedral_data, dihedral_coeffs, i) 201 | break 202 | if a1 == 0 and a4 == 0: 203 | flag1 = [atoms[1], atoms[2]] == [a2, a3] 204 | flag2 = [atoms[2], atoms[1]] == [a3, a2] 205 | if flag1 or flag2: 206 | found += 1 207 | dihedral_coeffs = add_coeff(dihedral_data, dihedral_coeffs, i) 208 | break 209 | return dihedral_coeffs, found 210 | 211 | def search_dihedrals(dihedral_data, atoms, dihedral_coeffs): 212 | found = 0 213 | for j in range(len(dihedral_data["k1"])): 214 | atom_data = [ 215 | dihedral_data["a1"][j], 216 | dihedral_data["a2"][j], 217 | dihedral_data["a3"][j], 218 | dihedral_data["a4"][j], 219 | ] 220 | flag1 = atoms == atom_data 221 | flag2 = atoms == list(reversed(atom_data)) 222 | if flag1 or flag2: 223 | found += 1 224 | dihedral_coeffs = add_coeff(dihedral_data, dihedral_coeffs, j) 225 | if found == 0: 226 | dihedral_coeffs, found = check_wildcards( 227 | atoms, dihedral_data, dihedral_coeffs 228 | ) 229 | return dihedral_coeffs, found 230 | 231 | dihedral_data = self.dihedral_data 232 | dihedral_coeffs = {} 233 | questionable = 0 234 | for i in range(len(dihedral_types)): 235 | a1 = self.type_defs[dihedral_types[i][0]] 236 | a2 = self.type_defs[dihedral_types[i][1]] 237 | a3 = self.type_defs[dihedral_types[i][2]] 238 | a4 = self.type_defs[dihedral_types[i][3]] 239 | atoms = [a1, a2, a3, a4] 240 | # print atoms 241 | 242 | dihedral_coeffs, found = search_dihedrals( 243 | dihedral_data, atoms, dihedral_coeffs 244 | ) 245 | 246 | if found == 0: 247 | for j in range(4): 248 | if atoms[j] == 48: 249 | atoms[j] = 47 # Try alkene carbon instead of aromatic 250 | if atoms[j] == 49: 251 | atoms[j] = 46 # Try alkene H 252 | dihedral_coeffs, found = search_dihedrals( 253 | dihedral_data, atoms, dihedral_coeffs 254 | ) 255 | if found == 0: 256 | change = False 257 | if atoms[0] == 5: 258 | atoms[0] = 46 259 | change = True 260 | if atoms[3] == 5: 261 | atoms[3] = 46 262 | dihedral_coeffs, found = search_dihedrals( 263 | dihedral_data, atoms, dihedral_coeffs 264 | ) 265 | if found != 0: 266 | print found, [a1, a2, a3, a4], atoms, "made 5 -> 46 sub" 267 | 268 | if found == 0: 269 | if atoms[1:4] == [13, 47, 3] or atoms[0:3] == [3, 47, 13]: 270 | atoms = [0, 13, 47, 47] 271 | dihedral_coeffs, found = search_dihedrals( 272 | dihedral_data, atoms, dihedral_coeffs 273 | ) 274 | if found != 0: 275 | print found, [a1, a2, a3, a4], atoms, "made 13,47,47 sub" 276 | 277 | if found == 0: 278 | if atoms[1] == 13 and atoms[2] == 13: 279 | if atoms[0] == 47: 280 | atoms[0] = 3 281 | elif atoms[3] == 47: 282 | atoms[0] = 3 283 | dihedral_coeffs, found = search_dihedrals( 284 | dihedral_data, atoms, dihedral_coeffs 285 | ) 286 | if found != 0: 287 | print found, [a1, a2, a3, a4], atoms, "sub 47 -> 3" 288 | 289 | if found == 0: 290 | if atoms == [13, 47, 5, 7] or atoms == [7, 5, 47, 13]: 291 | atoms = [7, 5, 47, 47] 292 | dihedral_coeffs, found = search_dihedrals( 293 | dihedral_data, atoms, dihedral_coeffs 294 | ) 295 | if found != 0: 296 | print found, [a1, a2, a3, a4], atoms, "made phenol sub" 297 | 298 | if found == 0: 299 | if atoms == [47, 47, 3, 52] or atoms == [52, 3, 47, 47]: 300 | atoms = [48, 48, 3, 4] 301 | dihedral_coeffs, found = search_dihedrals( 302 | dihedral_data, atoms, dihedral_coeffs 303 | ) 304 | if found != 0: 305 | print found, [a1, a2, a3, a4], atoms, "made carboxylate sub" 306 | 307 | 308 | if found != 1: 309 | raise ValueError( 310 | "Torsion", [a1, a2, a3, a4], "found ", found, " entries" 311 | ) 312 | # print 'WRONG',[a1,a2,a3,a4],'\t found ',found,' entries' 313 | if questionable != 0: 314 | print "made ", questionable, " questionable dihedral substitutions" 315 | return dihedral_coeffs 316 | 317 | def match_impropers(self, improper_types): 318 | def search_impropers(improper_data, centre, neighbours, improper_coeffs): 319 | found = 0 320 | for j in range(len(improper_data["k"])): 321 | data_neighbours = [ 322 | improper_data["a1"][j], 323 | improper_data["a2"][j], 324 | improper_data["a3"][j], 325 | ] 326 | data_centre = improper_data["centre"][j] 327 | flag1 = data_centre == centre 328 | # Test if other improper atom in connected to centre 329 | flag2 = set(neighbours) >= {data_neighbours[2]} 330 | flag3 = data_neighbours == [0, 0, 0] 331 | if flag1 and (flag2 or flag3): 332 | found += 1 333 | improper_coeffs[i + 1] = {} 334 | improper_coeffs[i + 1][1] = improper_data["k"][j] 335 | improper_coeffs[i + 1][2] = improper_data["r"][j] 336 | return improper_coeffs, found 337 | 338 | improper_data = self.improper_data 339 | improper_coeffs = {} 340 | questionable_substitutions = 0 341 | for i in range(len(improper_types)): 342 | centre = self.type_defs[improper_types[i][0]] 343 | a1 = self.type_defs[improper_types[i][1]] 344 | a2 = self.type_defs[improper_types[i][2]] 345 | a3 = self.type_defs[improper_types[i][3]] 346 | neighbours = [a1, a2, a3] 347 | 348 | improper_coeffs, found = search_impropers( 349 | improper_data, centre, neighbours, improper_coeffs 350 | ) 351 | if found != 1: 352 | raise ValueError( 353 | "Improper", centre, neighbours, "found ", found, " entries" 354 | ) 355 | # print 'WRONG',centre,neighbours,'\t found ',found,' entries' 356 | return improper_coeffs 357 | 358 | def retrieve_ff_data(self, forcefield): 359 | if forcefield == "OPLS": 360 | paramfile = os.path.dirname(__file__) + "/params/oplsaa.prm" 361 | data = OPLS_Reader(paramfile) 362 | elif forcefield == "ReaxFF": 363 | # paramters can be found https://github.com/lammps/lammps/tree/master/potentials 364 | raise Exception("No need to generate paramters for this forcefield") 365 | else: 366 | raise Exception(forcefield, "forcefield not implemented") 367 | self.vdw_type = data.vdw_type 368 | self.bond_data = data.bond 369 | self.angle_data = data.angle 370 | self.dihedral_data = data.dihedral 371 | self.improper_data = data.improper 372 | self.pair_data = data.pair 373 | self.mass_data = data.mass 374 | self.charge_data = data.charge 375 | -------------------------------------------------------------------------------- /makegraphitics/params/config.yaml: -------------------------------------------------------------------------------- 1 | Crystal: 2 | CC: 1.421 3 | layer_gap: 3.354 4 | 5 | GraFF_5: 6 | CC: 1.42845 7 | layer_gap: 3.4827 8 | CH: 1.077 9 | dq: 0.115 10 | 11 | GraFF_77: 12 | CC: 1.42827 13 | layer_gap: 3.4827 14 | CH: 1.077 15 | dq: 0.115 16 | 17 | OPLS: 18 | CC: 1.4148 # OPLS 19 | # layer_gap: 3.3827 20 | layer_gap: 3.4827 21 | CH: 1.077 22 | dq: 0.115 23 | 24 | AMBER: 25 | CC: 1.4293 # AMBER 26 | layer_gap: 3.3693 27 | CH: 1.088 28 | dq: 0.115 29 | 30 | COMPASS: 31 | CC: 1.3913 # COMPASS 32 | layer_gap: 3.3548 33 | CH: 1.070 34 | dq: 0.1268 35 | 36 | Driedling: 37 | CC: 1.3838 # Driedling 38 | layer_gap: 3.3955 39 | CH: 1.020 40 | dq: 0.062 41 | 42 | system: 43 | vdw_cutoff: 12.0 44 | N_layers: 2 45 | -------------------------------------------------------------------------------- /makegraphitics/params/oxidise.data: -------------------------------------------------------------------------------- 1 | 1.5e24 2 | -1 3 | 4 | 1.3e24 5 | -1 -1 6 | 7 | 2.0e25 8 | 1 -1 9 | 10 | 1.9e23 11 | 1 -1 12 | 13 | 7.8e24 14 | 1 15 | 1 16 | 3.0e24 17 | -2 -2 -2 18 | -2 -2 -2 19 | 1.3e23 20 | -1 2 21 | 2 22 | 1.4e23 23 | -2 24 | 1 -2 25 | 1.5e23 26 | -1 -2 27 | 1 -2 28 | 8.5e24 29 | 2 30 | 2 1 31 | 1.9e22 32 | 1 -2 33 | -2 34 | 8.5e23 35 | -1 -2 36 | -2 37 | 4.3e15 38 | 1 1 39 | 40 | 2.4e16 41 | 1 1 42 | 43 | 3.8e17 44 | 1 45 | 1 46 | 1.4e19 47 | 48 | 1 49 | 4.1e20 50 | 1 51 | 52 | 3.0e16 53 | -2 54 | -2 55 | 2.9e18 56 | -2 -2 57 | -2 -2 58 | 2.3e19 59 | -2 2 60 | -2 2 61 | 5.6e17 62 | -2 63 | -2 1 64 | 4.2e19 65 | -2 66 | -2 -1 67 | 1.4e16 68 | 1 2 69 | 2 70 | 4.6e20 71 | 2 72 | 2 1 73 | 4.3e20 74 | -1 2 75 | 2 76 | 2e17 77 | 1 2 78 | 2 79 | 1.9e19 80 | 1 1 81 | -2 -2 82 | 2.2e12 83 | 1 1 1 84 | 85 | 5.9e12 86 | 2 2 2 2 87 | 2 2 2 2 88 | 5.0e13 89 | 2 2 90 | 2 2 91 | 4.5e11 92 | 2 -2 93 | 2 -2 94 | 8.7e11 95 | 2 96 | 2 97 | 1.0e13 98 | -2 99 | -2 100 | 7.7e14 101 | 1 102 | -2 -2 103 | 8.7e13 104 | 2 105 | 2 -1 106 | 9.0e13 107 | 108 | 2 2 -1 109 | 1.1e11 110 | 2 111 | 2 1 112 | 1.8e11 113 | 2 114 | -1 2 115 | 3.2e12 116 | 2 117 | 2 -1 118 | 4.4e5 119 | 120 | 1 1 121 | 7.3e-1 122 | 1 1 1 1 123 | 124 | 6.0e1 125 | 126 | -1 127 | 3.0e2 128 | 129 | -1 130 | 8.1e-9 131 | 132 | -2 -2 133 | 2.2e4 134 | 2 135 | 2 136 | 5.0e1 137 | 138 | -2 139 | 1.0e1 140 | 141 | 2 142 | 8.8e9 143 | 144 | 2 2 145 | 6.0e1 146 | 2 147 | 2 -1 148 | 1.6e-5 149 | 150 | -2 -2 -1 151 | 5.9e3 152 | 2 153 | 2 1 154 | 1e0 155 | 156 | 157 | -------------------------------------------------------------------------------- /makegraphitics/params/oxidise_types.yaml: -------------------------------------------------------------------------------- 1 | 1: 48 2 | 2: 49 3 | 3: 13 4 | 4: 5 5 | 5: 7 6 | 6: 20 7 | 7: 5 8 | 8: 3 9 | 9: 4 10 | 10: 5 11 | 11: 48 12 | -------------------------------------------------------------------------------- /makegraphitics/reactors/__init__.py: -------------------------------------------------------------------------------- 1 | from base import Reactor 2 | from oxidiser import Oxidiser 3 | -------------------------------------------------------------------------------- /makegraphitics/reactors/base.py: -------------------------------------------------------------------------------- 1 | from .. import Writer 2 | from .. import Parameterise 3 | 4 | 5 | class Reactor(object): 6 | """ 7 | Base class that performs a reaction on sim object 8 | """ 9 | 10 | def react(self, sim): 11 | """ 12 | Perform reaction on sim, return with changes 13 | """ 14 | raise NotImplementedError 15 | 16 | def output_snapshot(self, sim, format_="xyz", filename="out"): 17 | if format_ == "xyz": 18 | out = Writer(sim) 19 | out.write_xyz(filename=filename + ".xyz", option="a") 20 | elif format_ == "lammps": 21 | sim.generate_connections() 22 | Parameterise(sim) 23 | out = Writer(sim) 24 | out.write_lammps(filename=filename + ".data") 25 | else: 26 | raise NotImplementedError 27 | -------------------------------------------------------------------------------- /makegraphitics/reactors/oxidise_rf.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os 3 | 4 | 5 | class Reaction(object): 6 | def __init__(self, rate, first, second): 7 | self.rate = rate 8 | self.first = first 9 | self.second = second 10 | 11 | 12 | oxidise_data = os.path.dirname(__file__) + "/../params/oxidise.data" 13 | reactions = [] 14 | with open(oxidise_data, "r") as f: 15 | count = 0 16 | for line in f: 17 | count += 1 18 | N = count / 3 19 | f.seek(0) 20 | for i in range(N): 21 | rate = float(f.readline()) 22 | first = [int(j) for j in f.readline().split()] 23 | second = [int(j) for j in f.readline().split()] 24 | 25 | a = Reaction(rate, first, second) 26 | a.lograte = np.log10(rate) 27 | a.first_alc_above = sum([i == 1 for i in a.first]) 28 | a.first_alc_below = sum([i == -1 for i in a.first]) 29 | a.first_epo_above = sum([i == 2 for i in a.first]) 30 | a.first_epo_below = sum([i == -2 for i in a.first]) 31 | a.second_alc_above = sum([i == 1 for i in a.second]) 32 | a.second_alc_below = sum([i == -1 for i in a.second]) 33 | a.second_epo_above = sum([i == 2 for i in a.second]) 34 | a.second_epo_below = sum([i == -2 for i in a.second]) 35 | reactions += [a] 36 | 37 | attributes = [ 38 | "first_alc_above", 39 | "first_alc_below", 40 | "first_epo_above", 41 | "first_epo_below", 42 | "second_alc_above", 43 | "second_alc_below", 44 | "second_epo_above", 45 | "second_epo_below", 46 | ] 47 | 48 | X = np.zeros((len(reactions), len(attributes))) 49 | for ri, r in enumerate(reactions): 50 | for ai, a in enumerate(attributes): 51 | X[ri, ai] = getattr(r, a) 52 | Y = np.zeros(len(reactions)) 53 | for ri, r in enumerate(reactions): 54 | Y[ri] = getattr(r, "lograte") 55 | 56 | 57 | def init_random_forest(): 58 | from sklearn.ensemble import RandomForestRegressor 59 | 60 | rf_reg = RandomForestRegressor(n_estimators=10, max_depth=4) 61 | rf_reg.fit(X, Y) 62 | return rf_reg 63 | 64 | 65 | def fit_empirical(reactions, a, b, c, d, e, f, g, h): 66 | m = [a, b, c, d, e, f, g, h] 67 | rates = [] 68 | try: 69 | len(reactions) 70 | except TypeError: 71 | reactions = [reactions] 72 | for reaction in reactions: 73 | steric = 0 74 | polar = 0 75 | hbond = 0 76 | edge = 0 77 | 78 | for state in reaction.first: 79 | if state == 1: 80 | steric += 1 81 | elif state == 2: 82 | steric += m[6] 83 | if abs(state) == 1: 84 | polar += 1 85 | if abs(state) == 2: 86 | polar += m[7] 87 | 88 | # if state == 1: hbond = 89 | 90 | for state in reaction.second: 91 | if state == 1: 92 | hbond += 1 93 | if state == 3: 94 | edge = 1 95 | 96 | steric = m[0] * steric + m[1] * steric * steric 97 | polar = m[2] * polar + m[3] * polar * polar 98 | hbond = m[4] * hbond + m[5] * hbond * hbond 99 | 100 | if edge: 101 | rate = 1 102 | else: 103 | # rate = 10 ** (steric + polar + hbond) 104 | rate = steric + polar + hbond 105 | rates += [rate] 106 | return rates 107 | 108 | 109 | if __name__ == "__main__": 110 | 111 | rf = init_random_forest() 112 | 113 | p0 = [-3.867, 0.185, 23.169, -5.138, 11.648, -4.413, 1, 0.633] 114 | 115 | from scipy.optimize import curve_fit 116 | 117 | print p0 118 | popt, pcov = curve_fit(fit_empirical, reactions, Y) 119 | print popt 120 | print pcov 121 | 122 | for i in range(len(reactions)): 123 | print Y[i], fit_empirical(reactions[i], *popt)[0], rf.predict([X[i]])[0] 124 | 125 | fi = rf.feature_importances_ 126 | for i, j in zip(attributes, fi): 127 | print i, j 128 | -------------------------------------------------------------------------------- /makegraphitics/reactors/oxidiser.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from oxidise_rf import init_random_forest 3 | from base import Reactor 4 | 5 | 6 | class Oxidiser(Reactor): 7 | def __init__( 8 | self, 9 | ratio=2.5, # Target overall C/O ratio 10 | surface_OHratio=0.5, # Surface OH/epoxy fraction 11 | edge_OHratio=0.25, # edge H:OH:carboxyl ratio 12 | edge_carboxyl_ratio=0.25, # edge H:OH:carboxyl ratio 13 | carboxyl_charged_ratio=0, # proportion of deprotonated carboxyls 14 | counterion=None, # Include counterion with charged carboxyl groups 15 | method="rf", # which method to calculate a site's affinity 16 | # empirical / rf (random forest) 17 | new_island_freq=0, # Freq s-1 attempt to add new island 18 | n_partitions=None, # find_site speed up, for flakes > 100 nm 19 | video_xyz=False, 20 | video_lammps=False, 21 | stats=False, 22 | ): 23 | 24 | assert type(ratio) in [int, float] and ratio > 0 25 | self.target_ratio = ratio 26 | 27 | assert method in ["rf", "empirical"] 28 | self.method = method 29 | if self.method == "rf": 30 | # Read in data from Yang2014, generate random forest regressor 31 | self.rf = init_random_forest() 32 | 33 | assert type(new_island_freq) in [int, float] 34 | self.new_island_freq = new_island_freq 35 | 36 | assert ( 37 | (n_partitions is None) 38 | or (n_partitions is False) # default, will estimate a good partition 39 | or (type(n_partitions) == int and n_partitions > 0) # no partitioning 40 | ) # specify 41 | self.n_partitions = n_partitions 42 | 43 | assert video_xyz == False or (type(video_xyz) == int and video_xyz > 0) 44 | self.video_xyz = video_xyz 45 | assert video_lammps == False or (type(video_lammps) == int and video_lammps > 0) 46 | self.video_lammps = video_lammps 47 | assert type(stats) == bool 48 | self.stats = stats 49 | 50 | assert 0 <= surface_OHratio <= 1 51 | assert 0 <= edge_OHratio <= 1 52 | assert 0 <= edge_carboxyl_ratio <= 1 53 | assert 0 <= carboxyl_charged_ratio <= 1 54 | assert edge_OHratio + edge_carboxyl_ratio <= 1 55 | 56 | assert counterion in [None, 'Na', 'Ca'] 57 | if (counterion is not None) and (carboxyl_charged_ratio == 0): 58 | print("WARNING: you have specified a counterion without " + 59 | "specifying carboxyl_charged_ratio, no counter ions " + 60 | "will included") 61 | 62 | self.surface_OHratio = surface_OHratio 63 | self.edge_OHratio = edge_OHratio 64 | self.edge_carboxyl_ratio = edge_carboxyl_ratio 65 | self.carboxyl_charged_ratio = carboxyl_charged_ratio 66 | self.counterion = counterion 67 | 68 | def react(self, sim): 69 | # check sim is suitible for oxidation reaction implemented here 70 | self.validate_system(sim) 71 | 72 | # initialise data structures and reactivity information 73 | self.prepare_system(sim) 74 | 75 | self.Ncarbons = np.sum(np.array(sim.atom_labels) == 1) 76 | self.Nhydrogens = np.sum(np.array(sim.atom_labels) == 2) 77 | self.Noxygens = 0 78 | 79 | if self.Nhydrogens: 80 | self.oxidise_edges(sim) 81 | 82 | self.oxidise(sim) 83 | 84 | sim.generate_connections() 85 | return sim 86 | 87 | def validate_system(self, sim): 88 | # Check that this sim has only graphitic carbons and hydrogens 89 | assert set(np.unique(sim.atom_labels)).issubset({1, 2}) 90 | 91 | def prepare_system(self, sim): 92 | sim.bond_graph = sim.generate_bond_graph(sim.bonds) 93 | 94 | ( 95 | self.CCbonds, 96 | self.neighbours, 97 | self.CCbonds_next_to_atom, 98 | ) = self.neighbour_matrix(sim) 99 | self.NCCbonds = len(self.CCbonds) 100 | 101 | self.affinities_above, self.affinities_below = self.init_affinity_matrix(sim) 102 | self.atom_states = self.init_atom_states(sim) 103 | 104 | # lists to record oxidisation process 105 | self.time_order = [] 106 | self.time_elapsed_list = [] 107 | self.node_order = [] 108 | 109 | self.partitions = self.set_partitions(self.n_partitions, self.NCCbonds) 110 | 111 | # all OPLS atom types that are introduced by oxidation 112 | sim.vdw_defs = { 113 | 1: 90, # Cg, graphitic (aromatic) 114 | 2: 91, # Hg, graphitic edge 115 | 3: 101, # Ct, tertiary C-OH 116 | 4: 96, # Oa, C-OH 117 | 5: 97, # Ha, C-OH 118 | 6: 122, # Oe, epoxy 119 | 11: 108, # Cb, Benzyl 120 | 7: 109, # Oa, C-OH 121 | 8: 209, # Cc, Carboxylic carbon 122 | 9: 210, # Oc, Ketone oxygen 123 | 10: 211, # Oa, alcohol 124 | 12: 213, # C, carboxylate -COO 125 | 13: 214, # O, carboxylate -COO 126 | 349: 349, # Na+ 127 | 354: 354, # Ca 2+ 128 | } # OPLS definitions 129 | 130 | def set_partitions(self, n_partitions, NCCbonds): 131 | if n_partitions is None: 132 | n_partitions = int(NCCbonds / 10000) 133 | if n_partitions == 0: 134 | n_partitions = 1 135 | elif n_partitions is False: 136 | n_partitions = 1 137 | self.n_partitions = n_partitions 138 | 139 | partition_size = int(NCCbonds / n_partitions) 140 | partitions = np.empty((n_partitions, 2), dtype=int) 141 | for i in range(n_partitions): 142 | partitions[i, 0] = i * partition_size 143 | partitions[i, 1] = (i + 1) * partition_size 144 | # in case NCCBonds is not evenly divisible, 145 | # include remainder in the last partition 146 | partitions[-1][1] = NCCbonds 147 | return partitions 148 | 149 | def oxidise_edges(self, sim): 150 | print "Oxidising edges" 151 | edge_OH = 0 152 | carboxyl = 0 153 | charge_carboxyl_sites = [] 154 | charged_carboxyls = 0 155 | n_counterions = 0 156 | 157 | for i in range(len(sim.atom_labels)): 158 | if sim.atom_labels[i] == 2: 159 | r = np.random.random() 160 | if r < self.edge_OHratio: 161 | self.add_edge_OH(sim, i) 162 | self.Noxygens += 1 163 | edge_OH += 1 164 | elif r > 1 - self.edge_carboxyl_ratio: 165 | r2 = np.random.random() 166 | if r2 > self.carboxyl_charged_ratio: 167 | self.add_carboxyl(sim, i) 168 | else: 169 | charge_carboxyl_sites += [i] 170 | self.Noxygens += 2 171 | self.Ncarbons += 1 172 | carboxyl += 1 173 | else: 174 | pass # leave as H 175 | 176 | # Ca counterions are 2+ charge 177 | # if odd number in charge_carboxyl_sites remove one 178 | if ((len(charge_carboxyl_sites) % 2) and 179 | (self.counterion == 'Ca')): 180 | self.add_carboxyl(sim, charge_carboxyl_sites[0]) 181 | charge_carboxyl_sites = charge_carboxyl_sites[1:] 182 | 183 | for i, site in enumerate(charge_carboxyl_sites): 184 | counterion = self.counterion 185 | # Ca is a 2+ ion, add every other ion 186 | if counterion == "Ca" and i % 2: 187 | counterion = None 188 | if counterion: 189 | n_counterions += 1 190 | self.add_charged_carboxyl(sim, site, counterion) 191 | charged_carboxyls += 1 192 | 193 | print "added:" 194 | print edge_OH, "\tOH" 195 | print carboxyl - charged_carboxyls, "\tCOOH" 196 | print charged_carboxyls,"\tCOO-" 197 | if self.counterion: 198 | print n_counterions, self.counterion, "counterions" 199 | print "----------------------" 200 | print 201 | 202 | return edge_OH, carboxyl 203 | 204 | def oxidise(self, crystal): 205 | print "Oxidising basal plane" 206 | OH_added = 0 207 | epoxy_added = 0 208 | time_elapsed = 0 # since last new island 209 | dt = 0 210 | nodes = 0 211 | new_island = 1 212 | while self.ratio() > self.target_ratio: 213 | if not new_island: 214 | available_CC_bonds = np.sum(np.array(self.affinities_above != 0)) 215 | new_island = np.random.poisson( 216 | float(dt) * self.new_island_freq * available_CC_bonds 217 | ) 218 | self.node_order += [new_island] 219 | self.time_elapsed_list += [time_elapsed] 220 | 221 | # choose site 222 | if new_island: 223 | dt = 0 224 | new_island -= 1 225 | time_elapsed = 0 226 | site, above, dt = self.find_site(crystal, new_island=True) 227 | nodes += 1 228 | print "new_island accepted,", nodes, "nodes (", new_island, ")" 229 | else: 230 | site, above, dt = self.find_site(crystal) 231 | time_elapsed += dt 232 | if above == 0: 233 | print "Could not reach C/O ratio:", self.target_ratio 234 | break 235 | 236 | # oxygenate at site,above 237 | r = np.random.random() # between 0,1 238 | if r < self.surface_OHratio: 239 | # add OH 240 | r2 = np.random.randint(2) 241 | atom1 = self.CCbonds[site][r2] - 1 242 | self.add_OH(crystal, above, atom1) 243 | self.atom_states[atom1] = 1 * above 244 | self.update_affinity(atom1 + 1) 245 | OH_added += 1 246 | else: 247 | # add epoxy 248 | atom1, atom2 = self.CCbonds[site] 249 | atom1, atom2 = atom1 - 1, atom2 - 1 250 | self.add_epoxy(crystal, above, atom1, atom2) 251 | self.atom_states[atom1] = 2 * above 252 | self.atom_states[atom2] = 2 * above 253 | self.update_affinity(atom1 + 1) 254 | self.update_affinity(atom2 + 1) 255 | epoxy_added += 1 256 | self.Noxygens += 1 257 | 258 | # outputs 259 | if not self.Noxygens % 20: 260 | oxygens_to_add = int(self.Ncarbons / self.target_ratio) 261 | print self.Noxygens, "/", oxygens_to_add, "\toxygens added\t", nodes, "nodes" 262 | if self.video_xyz and not self.Noxygens % self.video_xyz: 263 | self.output_snapshot(crystal) 264 | if self.video_lammps and not self.Noxygens % self.video_lammps: 265 | self.output_snapshot( 266 | crystal, format_="lammps", filename=str(self.Noxygens) 267 | ) 268 | 269 | print OH_added, "\tOH were added" 270 | print epoxy_added, "\tepoxy were added" 271 | if epoxy_added != 0: 272 | print "OH/epoxy = ", float(OH_added) / (epoxy_added) 273 | else: 274 | print "OH/epoxy = inf" 275 | print nodes, "nodes" 276 | print "==========" 277 | print "C/O = ", self.Ncarbons, "/", self.Noxygens, "=", self.ratio() 278 | 279 | def ratio(self): 280 | if self.Noxygens == 0: 281 | ratio = float("inf") 282 | else: 283 | ratio = float(self.Ncarbons) / self.Noxygens 284 | return ratio 285 | 286 | def find_12_neighbours(self, crystal, i, j): 287 | expected_first_neighbours = 4 288 | expected_second_neighbours = 8 289 | 290 | first_neighbours = crystal.bonded_to(i - 1) 291 | first_neighbours += crystal.bonded_to(j - 1) 292 | first_neighbours = [n + 1 for n in first_neighbours] 293 | first_neighbours = set(first_neighbours) - {i, j} 294 | if len(first_neighbours) != expected_first_neighbours: 295 | raise ValueError("Not enough first neighbours", i, j, first_neighbours) 296 | 297 | second_neighbours = set() 298 | for atom in first_neighbours: 299 | if crystal.atom_labels[atom - 1] == 2: 300 | expected_second_neighbours -= 2 301 | for n in first_neighbours: 302 | second_neighbours = second_neighbours | set(crystal.bonded_to(n - 1)) 303 | second_neighbours = {n + 1 for n in second_neighbours} 304 | second_neighbours = second_neighbours - first_neighbours - {i, j} 305 | if len(second_neighbours) != expected_second_neighbours: 306 | raise ValueError("Not enough second neighbours", i, j, second_neighbours) 307 | 308 | return list(first_neighbours) + list(second_neighbours) 309 | 310 | def init_affinity_matrix(self, crystal): 311 | affinities_above = np.ones(self.NCCbonds) 312 | affinities_below = np.ones(self.NCCbonds) 313 | for i in range(self.NCCbonds): 314 | first_neighbours = self.neighbours[i][0:4] 315 | for atom in first_neighbours: 316 | if crystal.atom_labels[atom - 1] == 2: 317 | affinities_above[i] = 0 318 | affinities_below[i] = 0 319 | return affinities_above, affinities_below 320 | 321 | def neighbour_matrix(self, crystal): 322 | Nbonds = len(crystal.bonds) 323 | CCbonds = [] 324 | neighbours = [] 325 | CCbonds_next_to_atom = {i + 1: set() for i in range(len(crystal.coords))} 326 | 327 | count = 0 328 | for i in range(Nbonds): 329 | c1 = crystal.bonds[i][0] 330 | c2 = crystal.bonds[i][1] 331 | label1 = crystal.atom_labels[c1 - 1] 332 | label2 = crystal.atom_labels[c2 - 1] 333 | 334 | if label1 == 1 and label2 == 1: 335 | CCbonds += [[c1, c2]] 336 | c1c2_neighbours = self.find_12_neighbours(crystal, c1, c2) 337 | neighbours += [c1c2_neighbours] 338 | 339 | CCbonds_next_to_atom[c1] |= {count} 340 | CCbonds_next_to_atom[c2] |= {count} 341 | for neighbour in c1c2_neighbours: 342 | CCbonds_next_to_atom[neighbour] |= {count} 343 | count += 1 344 | 345 | return np.array(CCbonds), neighbours, CCbonds_next_to_atom 346 | 347 | def update_affinity(self, atom): 348 | for bond in self.CCbonds_next_to_atom[atom]: 349 | if atom in self.CCbonds[bond]: 350 | self.affinities_above[bond] = 0 351 | self.affinities_below[bond] = 0 352 | elif self.affinities_above[bond] != 0: 353 | self.calc_affinities(bond) 354 | 355 | def calc_affinities(self, site): 356 | calc_affinity = getattr(self, "calc_affinity_" + self.method) 357 | n = [] 358 | for i in self.neighbours[site]: 359 | n += [self.atom_states[i - 1]] 360 | first = n[0:5] 361 | second = n[5:] 362 | above = calc_affinity(first, second) 363 | self.affinities_above[site] = above 364 | first = -np.array(first) 365 | second = -np.array(second) 366 | below = calc_affinity(first, second) 367 | self.affinities_below[site] = below 368 | 369 | def calc_affinity_rf(self, first, second): 370 | edge = False 371 | X = [0] * 8 372 | for state in first: 373 | if state == 1: 374 | X[0] += 1 375 | if state == -1: 376 | X[1] += 1 377 | if state == 2: 378 | X[2] += 1 379 | if state == -2: 380 | X[3] += 1 381 | for state in second: 382 | if state == 1: 383 | X[4] += 1 384 | if state == -1: 385 | X[5] += 1 386 | if state == 2: 387 | X[6] += 1 388 | if state == -2: 389 | X[7] += 1 390 | if state == 3: 391 | edge = True 392 | if edge: 393 | rate = 1 394 | else: 395 | exponent = self.rf.predict([X]) 396 | rate = 10 ** exponent[0] 397 | return rate 398 | 399 | def calc_affinity_empirical(self, first, second): 400 | steric = 0 401 | polar = 0 402 | hbond = 0 403 | edge = 0 404 | m = [-3.867, 0.185, 23.169, -5.138, 11.648, -4.413] 405 | for state in first: 406 | if state == 1: 407 | steric += 1 408 | elif state == 2: 409 | steric += 1 410 | if abs(state) == 1: 411 | polar += 1 412 | if abs(state) == 2: 413 | polar += 0.633 414 | 415 | # if state == 1: hbond = 416 | 417 | for state in second: 418 | if state == 1: 419 | hbond += 1 420 | if state == 3: 421 | edge = 1 422 | 423 | steric = m[0] * steric + m[1] * steric * steric 424 | polar = m[2] * polar + m[3] * polar * polar 425 | hbond = m[4] * hbond + m[5] * hbond * hbond 426 | 427 | if edge: 428 | rate = 1 429 | else: 430 | rate = 10 ** (steric + polar + hbond) 431 | return rate 432 | 433 | def find_new_island(self): 434 | # number of sites that are not CH and can react 435 | bool_affinity = np.array(self.affinities_above != 0) 436 | total = np.sum(bool_affinity) * 2 437 | if total == 0: 438 | # no reactions possible 439 | return 0, 0 440 | 441 | r = np.random.random() * total 442 | R = 0 443 | above = 0 444 | for i, affinity in enumerate(bool_affinity): 445 | R += affinity 446 | if R > r: 447 | above = 1 448 | break 449 | if not above: 450 | for i, affinity in enumerate(bool_affinity): 451 | R += affinity 452 | if R > r: 453 | above = -1 454 | break 455 | if above == 0: 456 | # no possible oxidation sites 457 | raise Exception("Couldnt find a new island site") 458 | 459 | return i, above 460 | 461 | def find_site(self, sim, new_island=False): 462 | if new_island: 463 | reactivity_above = np.array(self.affinities_above != 0, dtype=float) 464 | reactivity_below = np.array(self.affinities_below != 0, dtype=float) 465 | else: 466 | reactivity_above = self.affinities_above 467 | reactivity_below = self.affinities_below 468 | 469 | totals_above = np.zeros(self.n_partitions) 470 | totals_below = np.zeros(self.n_partitions) 471 | for i in xrange(self.n_partitions): 472 | totals_above[i] = np.sum( 473 | reactivity_above[self.partitions[i][0] : self.partitions[i][1]] 474 | ) 475 | totals_below[i] = np.sum( 476 | reactivity_below[self.partitions[i][0] : self.partitions[i][1]] 477 | ) 478 | 479 | total_above = np.sum(totals_above) 480 | total_below = np.sum(totals_below) 481 | total = total_above + total_below 482 | if total == 0: 483 | # no reactions possible 484 | return 0, 0, 0 485 | 486 | r = np.random.random() * total 487 | 488 | def search(running_total, r, reactivity, totals): 489 | found = False 490 | for partition in range(self.n_partitions): 491 | running_total += totals[partition] 492 | if running_total > r: 493 | running_total -= totals[partition] 494 | for site in range(*self.partitions[partition]): 495 | running_total += reactivity[site] 496 | if running_total > r: 497 | found = True 498 | break 499 | if found: 500 | break 501 | assert found 502 | return site 503 | 504 | if r < total_above: 505 | site = search(0, r, reactivity_above, totals_above) 506 | above = 1 507 | else: 508 | site = search(total_above, r, reactivity_below, totals_below) 509 | above = -1 510 | 511 | # check its a valid site 512 | first_neighbours = self.neighbours[site][0:4] 513 | for atom in first_neighbours: 514 | if sim.atom_labels[atom - 1] == 2: 515 | raise Exception("i've picked an unallowed oxidation site...") 516 | 517 | if new_island: 518 | time = 0 519 | else: 520 | time = 1 / (total) 521 | self.time_order += [time] 522 | return site, above, time 523 | 524 | def add_edge_OH(self, crystal, H_at): 525 | bonded_to = crystal.bonded_to(H_at) 526 | C_at = bonded_to[0] 527 | if len(bonded_to) != 1: 528 | raise ValueError 529 | 530 | C_coord = crystal.coords[C_at] 531 | H_coord = crystal.coords[H_at] 532 | CO = 1.4 533 | OH = 0.7 534 | bond_vector = H_coord - C_coord 535 | bond_vector = bond_vector / np.linalg.norm(bond_vector) 536 | o_coord = C_coord + bond_vector * CO 537 | above = np.random.randint(2) 538 | h_coord = o_coord + bond_vector * OH + np.array(([0, 0, OH * (-1) ** above])) 539 | 540 | molecule = crystal.molecule_labels[C_at] 541 | O_at = H_at # H becomes O to preserve bond already there 542 | H_at = len(crystal.atom_labels) 543 | crystal.atom_labels[C_at] = 11 # 3 is a C-OH carbon 544 | crystal.atom_charges[C_at] = 0.15 545 | crystal.coords[O_at] = o_coord 546 | crystal.atom_labels[O_at] = 7 547 | crystal.atom_charges[O_at] = -0.585 548 | 549 | crystal.coords = np.vstack((crystal.coords, h_coord)) 550 | crystal.atom_labels += [5] # H 551 | crystal.atom_charges += [0.435] 552 | crystal.molecule_labels += [molecule] 553 | 554 | new_bond = np.array(([O_at + 1, H_at + 1])) 555 | crystal.bonds = np.vstack((crystal.bonds, new_bond)) 556 | 557 | def add_charged_carboxyl(self, crystal, H_at, counterion): 558 | bonded_to = crystal.bonded_to(H_at) 559 | C_at = bonded_to[0] 560 | if len(bonded_to) != 1: 561 | raise ValueError 562 | 563 | C_coord = crystal.coords[C_at] 564 | H_coord = crystal.coords[H_at] 565 | CC = 1.4 566 | CO = 1.4 567 | C_ion = 4.0 568 | angle = np.pi / 3 569 | sangle = np.sin(angle) * CO 570 | cangle = np.cos(angle) * CO 571 | bond_vector = H_coord - C_coord 572 | bond_vector = bond_vector / np.linalg.norm(bond_vector) 573 | 574 | C1_coord = C_coord + bond_vector * CC 575 | above = (-1) ** (np.random.randint(2)) 576 | O1_coord = C1_coord + bond_vector * cangle + np.array([0, 0, sangle * above]) 577 | O2_coord = O1_coord + np.array([0, 0, -2 * sangle * above]) 578 | 579 | molecule = crystal.molecule_labels[C_at] 580 | 581 | C1_at = H_at 582 | O1_at = len(crystal.atom_labels) 583 | O2_at = O1_at + 1 584 | 585 | crystal.atom_labels[C_at] = 11 586 | crystal.atom_charges[C_at] = -0.100 587 | 588 | crystal.coords[C1_at] = C1_coord 589 | crystal.atom_labels[C1_at] = 12 590 | crystal.atom_charges[C1_at] = 0.7 591 | crystal.coords = np.vstack((crystal.coords, O1_coord)) 592 | crystal.coords = np.vstack((crystal.coords, O2_coord)) 593 | crystal.atom_labels += [13, 13] 594 | crystal.atom_charges += [-0.8, -0.8] 595 | crystal.molecule_labels += [molecule] * 2 596 | 597 | if counterion: 598 | counterion_coord = C1_coord + bond_vector * C_ion 599 | crystal.coords = np.vstack((crystal.coords, counterion_coord)) 600 | crystal.molecule_labels += [ max(crystal.molecule_labels) + 1 ] 601 | if counterion == 'Na': 602 | crystal.atom_labels += [349] 603 | crystal.atom_charges += [+1.0] 604 | elif counterion == 'Ca': 605 | crystal.atom_labels += [354] 606 | crystal.atom_charges += [+2.0] 607 | else: 608 | raise Exception('Counterion not implemented:', counterion) 609 | 610 | 611 | new_bonds = np.array( 612 | ([C1_at + 1, O1_at + 1], [C1_at + 1, O2_at + 1]) 613 | ) 614 | crystal.bonds = np.vstack((crystal.bonds, new_bonds)) 615 | 616 | 617 | def add_carboxyl(self, crystal, H_at): 618 | bonded_to = crystal.bonded_to(H_at) 619 | C_at = bonded_to[0] 620 | if len(bonded_to) != 1: 621 | raise ValueError 622 | 623 | C_coord = crystal.coords[C_at] 624 | H_coord = crystal.coords[H_at] 625 | CC = 1.4 626 | CO = 1.4 627 | OH = 1.1 628 | angle = np.pi / 3 629 | sangle = np.sin(angle) * CO 630 | cangle = np.cos(angle) * CO 631 | bond_vector = H_coord - C_coord 632 | bond_vector = bond_vector / np.linalg.norm(bond_vector) 633 | 634 | C1_coord = C_coord + bond_vector * CC 635 | above = (-1) ** (np.random.randint(2)) 636 | O1_coord = C1_coord + bond_vector * cangle + np.array([0, 0, sangle * above]) 637 | O2_coord = O1_coord + np.array([0, 0, -2 * sangle * above]) 638 | 639 | H_coord = O2_coord + bond_vector * OH 640 | 641 | molecule = crystal.molecule_labels[C_at] 642 | 643 | C1_at = H_at 644 | O1_at = len(crystal.atom_labels) 645 | O2_at = O1_at + 1 646 | H_at = O1_at + 2 647 | 648 | crystal.atom_labels[C_at] = 11 649 | crystal.atom_charges[C_at] = -0.115 650 | 651 | crystal.coords[C1_at] = C1_coord 652 | crystal.atom_labels[C1_at] = 8 653 | crystal.atom_charges[C1_at] = 0.635 654 | crystal.coords = np.vstack((crystal.coords, O1_coord)) 655 | crystal.coords = np.vstack((crystal.coords, O2_coord)) 656 | crystal.coords = np.vstack((crystal.coords, H_coord)) 657 | crystal.atom_labels += [9, 10, 5] 658 | crystal.atom_charges += [-0.44, -0.53, 0.45] 659 | crystal.molecule_labels += [molecule] * 3 660 | 661 | new_bonds = np.array( 662 | ([C1_at + 1, O1_at + 1], [C1_at + 1, O2_at + 1], [O2_at + 1, H_at + 1]) 663 | ) 664 | crystal.bonds = np.vstack((crystal.bonds, new_bonds)) 665 | 666 | def add_OH(self, crystal, above, at): 667 | crystal.atom_labels[at] = 3 # 3 is a C-OH carbon 668 | crystal.atom_charges[at] = 0.265 669 | molecule = crystal.molecule_labels[at] 670 | 671 | CO = 1.4 * above 672 | OH = 1.0 * above 673 | o_coord = crystal.coords[at] + np.array([0, 0, CO]) 674 | h_coord = o_coord + np.array([0, 0, OH]) 675 | crystal.coords = np.vstack((crystal.coords, o_coord)) 676 | crystal.coords = np.vstack((crystal.coords, h_coord)) 677 | crystal.atom_labels += [4, 5] 678 | crystal.atom_charges += [-0.683, 0.418] 679 | crystal.molecule_labels += [molecule, molecule] 680 | 681 | hid = len(crystal.atom_labels) 682 | oid = hid - 1 683 | new_bonds = np.array(([at + 1, oid], [oid, hid])) 684 | crystal.bonds = np.vstack((crystal.bonds, new_bonds)) 685 | 686 | def add_epoxy(self, crystal, above, c1, c2): 687 | crystal.atom_labels[c1] = 3 # 3 is epoxy carbon 688 | crystal.atom_labels[c2] = 3 # 3 is epoxy carbon 689 | crystal.atom_charges[c1] = 0.2 690 | crystal.atom_charges[c2] = 0.2 691 | 692 | molecule = crystal.molecule_labels[c1] 693 | 694 | c1c2 = crystal.coords[c2] - crystal.coords[c1] 695 | if c1c2[0] > 2: 696 | c1c2[0] += crystal.box_dimensions[0, 1] 697 | elif c1c2[0] < -2: 698 | c1c2[0] += crystal.box_dimensions[0, 1] 699 | if c1c2[1] > 2: 700 | c1c2[1] += crystal.box_dimensions[1, 1] 701 | elif c1c2[1] < -2: 702 | c1c2[1] += crystal.box_dimensions[1, 1] 703 | CO = 0.9 * above 704 | o_coord = crystal.coords[c1] + np.array([0, 0, CO]) + 0.5 * c1c2 705 | 706 | crystal.coords = np.vstack((crystal.coords, o_coord)) 707 | crystal.atom_labels += [6] 708 | crystal.atom_charges += [-0.4] 709 | crystal.molecule_labels += [molecule] 710 | oid = len(crystal.atom_labels) 711 | new_bonds = np.array(([c1 + 1, oid], [c2 + 1, oid])) 712 | crystal.bonds = np.vstack((crystal.bonds, new_bonds)) 713 | 714 | def remove_graphitic_bonds(self, crystal, a): 715 | connections = self.find_connections(crystal.bonds, a + 1) 716 | for i in range(len(connections)): 717 | bond = connections[i][0] 718 | if crystal.bond_labels[bond] == 1: 719 | crystal.bond_labels[bond] = 3 720 | 721 | def change_bond_label(self, crystal, a1, a2, label): 722 | connections = self.find_connections(crystal.bonds, a1 + 1) 723 | for i in range(len(connections)): 724 | bond = connections[i][0] 725 | if a2 + 1 in crystal.bonds[bond]: 726 | crystal.bond_labels[bond] = label 727 | 728 | def find_connections(self, bonds, centre): 729 | connections = np.where(bonds == centre) 730 | connections = np.vstack((connections[0], connections[1])) 731 | return connections.transpose() 732 | 733 | def oxidise_islands(self, crystal): 734 | removed = 1 # not 0 735 | sp3 = [3] 736 | epoxy_added = 0 737 | OH_added = 0 738 | while removed != 0: 739 | epoxy_added_cycle = 0 740 | OH_added_cycle = 0 741 | removed = 0 742 | for bond in crystal.bonds: 743 | c1 = bond[0] - 1 744 | c2 = bond[1] - 1 745 | label1 = crystal.atom_labels[c1] 746 | label2 = crystal.atom_labels[c2] 747 | # if i is a graphitic bond 748 | if label1 == 1 and label2 == 1: 749 | # is it near oxidised sections 750 | bonded_to = crystal.bonded_to(c1) + crystal.bonded_to(c2) 751 | sp3_flag = 0 752 | for atom in bonded_to: 753 | if crystal.atom_labels[atom] in sp3: 754 | sp3_flag += 1 755 | 756 | # if bond is surrounded by sp3 carbons 757 | if sp3_flag >= 3: 758 | self.add_epoxy(crystal, c1, c2) 759 | removed += 1 760 | epoxy_added_cycle += 1 761 | 762 | for i in range(len(crystal.atom_labels)): 763 | if crystal.atom_labels[i] == 1: 764 | bonded_to = crystal.bonded_to(i) 765 | sp3_flag = 0 766 | for atom in bonded_to: 767 | if crystal.atom_labels[atom] in sp3: 768 | sp3_flag += 1 769 | # if bond is surrounded by sp3 carbons 770 | if sp3_flag >= 3: 771 | self.add_OH(crystal, i) 772 | removed += 1 773 | OH_added_cycle += 1 774 | 775 | print "cycle: islands removed", removed, " with ", epoxy_added_cycle, " epoxy and ", OH_added_cycle, " OH" 776 | OH_added += OH_added_cycle 777 | epoxy_added += epoxy_added_cycle 778 | 779 | return OH_added, epoxy_added 780 | 781 | def init_atom_states(self, sim): 782 | atom_states = np.zeros(len(sim.atom_labels)) 783 | atom_states[atom_states == 2] = 3 784 | return atom_states 785 | -------------------------------------------------------------------------------- /makegraphitics/read_lammpsdata.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sim import Sim 3 | 4 | 5 | class ReadLammpsData(Sim): 6 | def __init__(self, filename): 7 | """ Given a lammps data file this script will read in all 8 | the available data. 9 | Data that can be read in are listed in self.attributes. 10 | Coefficients will not be read and should be in a separate 11 | input file for lammps. 12 | Copy attributes to another class with: 13 | ReadLammpsData.__dict__ = obj.__dict__.copy() 14 | """ 15 | self.filename = filename 16 | self.attributes = { 17 | "masses", 18 | "coords", 19 | "molecule_labels", 20 | "atom_charges", 21 | "atom_labels", 22 | "atom_ids", 23 | "box_dimensions", 24 | "bonds", 25 | "bond_labels", 26 | "Nbond_types", 27 | "angles", 28 | "angle_labels", 29 | "Nangle_types", 30 | "dihedrals", 31 | "dihedral_labels", 32 | "Ndihedral_types", 33 | "impropers", 34 | "improper_labels", 35 | "Nimproper_types", 36 | "pair_coeffs", 37 | "bond_coeffs", 38 | "angle_coeffs", 39 | "dihedral_coeffs", 40 | "improper_coeffs", 41 | } 42 | 43 | with open(self.filename) as datafile: 44 | number_of_lines = 0 45 | for line in datafile: 46 | number_of_lines += 1 47 | datafile.seek(0) 48 | 49 | self.count = 0 50 | while self.count < number_of_lines: 51 | line = self.read(datafile) 52 | self.analyse(line, datafile) 53 | self.box_dimensions = np.array( 54 | [[self.xlo, self.xhi], [self.ylo, self.yhi], [self.zlo, self.zhi]] 55 | ) 56 | self.validate() 57 | extra_attributes = set(self.__dict__.keys()) - self.attributes 58 | for attribute in extra_attributes: 59 | delattr(self, attribute) 60 | 61 | def read(self, datafile): 62 | self.count += 1 63 | return datafile.readline().split() 64 | 65 | def analyse(self, line, datafile): 66 | def could_not_read(unknown): 67 | print "Could not decipher: ", unknown, " on line ", self.count 68 | 69 | def is_number(s): 70 | try: 71 | float(s) 72 | return True 73 | except ValueError: 74 | return False 75 | 76 | Ncoeff = { 77 | "Pair": "Natom_types", 78 | "Bond": "Nbond_types", 79 | "Angle": "Nangle_types", 80 | "Dihedral": "Ndihedral_types", 81 | "Improper": "Nimproper_types", 82 | } 83 | 84 | main = { 85 | "Masses": self.read_masses, 86 | "Atoms": self.read_atoms, 87 | "Velocities": self.read_velocities, 88 | "Bonds": self.read_bonds, 89 | "Angles": self.read_angles, 90 | "Dihedrals": self.read_dihedrals, 91 | "Impropers": self.read_impropers, 92 | } 93 | 94 | header_numbers = { 95 | "atoms": "Natoms", 96 | "bonds": "Nbonds", 97 | "angles": "Nangles", 98 | "dihedrals": "Ndihedrals", 99 | "impropers": "Nimpropers", 100 | } 101 | 102 | header_types = { 103 | "atom": "Natom_types", 104 | "bond": "Nbond_types", 105 | "angle": "Nangle_types", 106 | "dihedral": "Ndihedral_types", 107 | "improper": "Nimproper_types", 108 | } 109 | 110 | box = [["xlo", "xhi"], ["ylo", "yhi"], ["zlo", "zhi"]] 111 | 112 | if not line: 113 | return # blank line 114 | l = line[0] 115 | if l == "#": 116 | return # catch comment line 117 | elif l[0] == "#": 118 | return # catch comment line 119 | if is_number(l): 120 | if len(line) == 2: 121 | attribute = header_numbers.get(line[1], False) 122 | if not attribute: 123 | could_not_read(line) 124 | else: 125 | setattr(self, attribute, int(l)) 126 | 127 | if len(line) == 3: 128 | attribute = header_types.get(line[1], False) 129 | if not attribute: 130 | could_not_read(line) 131 | else: 132 | setattr(self, attribute, int(l)) 133 | 134 | if len(line) == 4: 135 | if line[2:4] in box: 136 | setattr(self, line[2], float(line[0])) 137 | setattr(self, line[3], float(line[1])) 138 | else: 139 | could_not_read(line) 140 | elif len(line) == 2: 141 | print "reading " + l + " Coeffs" 142 | self.read(datafile) 143 | name = line[0].lower() + "_" + line[1].lower() 144 | N = getattr(self, Ncoeff.get(l)) 145 | self.read_coeffs(datafile, name, N) 146 | 147 | else: 148 | func = main.get(l, False) 149 | if not func: 150 | could_not_read(line) 151 | else: 152 | print "reading " + l 153 | self.read(datafile) 154 | func(datafile) 155 | 156 | def read_masses(self, datafile): 157 | self.masses = {} 158 | for i in range(self.Natom_types): 159 | line = self.read(datafile) 160 | self.masses[int(line[0])] = float(line[1]) 161 | 162 | def read_atoms(self, datafile): 163 | self.atom_ids = np.empty(self.Natoms, dtype=int) 164 | self.coords = np.zeros((self.Natoms, 3)) 165 | self.atom_charges = np.zeros(self.Natoms) 166 | self.molecule_labels = np.zeros(self.Natoms, dtype=int) 167 | self.atom_labels = np.zeros(self.Natoms, dtype=int) 168 | for i in range(self.Natoms): 169 | line = self.read(datafile) 170 | self.atom_ids[i] = line[0] 171 | self.coords[i] = line[4:7] 172 | self.molecule_labels[i] = line[1] 173 | self.atom_labels[i] = line[2] 174 | self.atom_charges[i] = line[3] 175 | 176 | def read_velocities(self, datafile): 177 | print "--- Ignoring Velocities --- noone cares" 178 | for i in range(self.Natoms): 179 | line = self.read(datafile) 180 | 181 | def read_data_line(self, datafile, atom_labels, atoms): 182 | line = self.read(datafile) 183 | index = int(line[0]) - 1 # index line up with numpy array 184 | atom_labels[index] = int(line[1]) 185 | atoms[index] = [int(atom) for atom in line[2:]] 186 | 187 | def read_bonds(self, datafile): 188 | self.bonds = np.zeros((self.Nbonds, 2), int) 189 | self.bond_labels = np.zeros((self.Nbonds), dtype=int) 190 | for i in range(self.Nbonds): 191 | self.read_data_line(datafile, self.bond_labels, self.bonds) 192 | 193 | def read_angles(self, datafile): 194 | self.angles = np.zeros((self.Nangles, 3), int) 195 | self.angle_labels = np.zeros(self.Nangles, dtype=int) 196 | for i in range(self.Nangles): 197 | self.read_data_line(datafile, self.angle_labels, self.angles) 198 | 199 | def read_dihedrals(self, datafile): 200 | self.dihedrals = np.zeros((self.Ndihedrals, 4), int) 201 | self.dihedral_labels = np.zeros(self.Ndihedrals, dtype=int) 202 | for i in range(self.Ndihedrals): 203 | self.read_data_line(datafile, self.dihedral_labels, self.dihedrals) 204 | 205 | def read_impropers(self, datafile): 206 | self.impropers = np.zeros((self.Nimpropers, 4), int) 207 | self.improper_labels = np.zeros(self.Nimpropers, dtype=int) 208 | for i in range(self.Nimpropers): 209 | self.read_data_line(datafile, self.improper_labels, self.impropers) 210 | 211 | def read_coeffs(self, datafile, coeffs, N): 212 | setattr(self, coeffs, {}) 213 | coeffdict = getattr(self, coeffs) 214 | for i in range(N): 215 | line = self.read(datafile) 216 | label = int(line[0]) 217 | coeffdict[label] = {} 218 | for k in range(1, len(line)): 219 | coeffdict[label][k] = float(line[k]) 220 | 221 | def validate(self): 222 | for attribute in self.attributes: 223 | try: 224 | a = getattr(self, attribute) 225 | except AttributeError: 226 | print "WARNING: undefined " + attribute 227 | -------------------------------------------------------------------------------- /makegraphitics/shifty.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from math import cos, sin, pi 3 | from write_coords import Writer 4 | 5 | 6 | class Shifter(object): 7 | def __init__(self, sim, output_style="xyz", target=0): 8 | self.sim = sim 9 | self.coords = sim.coords 10 | self.mol = sim.molecule_labels 11 | self.target = target 12 | self.output_style = output_style 13 | 14 | if not target: 15 | self.target = np.amax(self.mol) 16 | 17 | def rotate(self, end, steps): 18 | rotate_range = np.arange(0, end, steps) 19 | for rotation in rotate_range: 20 | rotated_coords = self.rotate_molecule(rotation * pi / 180.0) 21 | self.write_shifted_coords(rotated_coords, rotation) 22 | 23 | def rotate_molecule(self, theta): 24 | sint = sin(theta) 25 | cost = cos(theta) 26 | new_coords = np.empty(np.shape(self.coords)) 27 | for i in range(len(self.coords)): 28 | if self.mol[i] == self.target: 29 | a = cost * self.coords[i][0] - sint * self.coords[i][1] 30 | b = sint * self.coords[i][0] + cost * self.coords[i][1] 31 | c = self.coords[i][2] 32 | new_coords[i] = [a, b, c] 33 | else: 34 | new_coords[i] = self.coords[i] 35 | return new_coords 36 | 37 | def z_shift(self, start, end, step): 38 | shift_range = np.arange(start, end, step) 39 | for shift in shift_range: 40 | shifted_coords = self.move_molecule(0, 0, shift) 41 | self.write_shifted_coords(shifted_coords, shift) 42 | 43 | def in_plane_shift(self, direction, start, end, step): 44 | # direction form [x,y] - should be a unit vector 45 | shift_range = np.arange(start, end, step) 46 | for shift in shift_range: 47 | shifted_coords = self.move_molecule( 48 | shift * direction[0], shift * direction[1], 0 49 | ) 50 | self.write_shifted_coords(shifted_coords, shift) 51 | 52 | def move_molecule(self, x_shift, y_shift, z_shift): 53 | new_coords = np.empty(np.shape(self.coords)) 54 | for i in range(len(self.coords)): 55 | if self.mol[i] == self.target: 56 | new_coords[i][0] = self.coords[i][0] + x_shift 57 | new_coords[i][1] = self.coords[i][1] + y_shift 58 | new_coords[i][2] = self.coords[i][2] + z_shift 59 | else: 60 | new_coords[i] = self.coords[i] 61 | return new_coords 62 | 63 | def write_shifted_coords(self, shifted_coords, shift): 64 | temp_sim = self.sim 65 | temp_sim.coords = shifted_coords 66 | writer = Writer(temp_sim) 67 | if self.output_style == "xyz": 68 | writer.write_xyz("shift_" + str(shift) + ".xyz") 69 | elif self.output_style == "lammps": 70 | writer.write_lammps("data.shift_" + str(shift)) 71 | -------------------------------------------------------------------------------- /makegraphitics/sim.py: -------------------------------------------------------------------------------- 1 | from lattice import Lattice 2 | from connector import Connector 3 | import numpy as np 4 | import os 5 | import yaml 6 | 7 | 8 | class Sim(object): 9 | def generate_connections(self): 10 | try: 11 | self.atom_labels, self.bonds 12 | except AttributeError: 13 | raise Exception("Simulation has not been assigned atom types and bonds yet") 14 | 15 | connect = Connector() 16 | self.bond_types = connect.find_bond_types(self.atom_labels, self.bonds) 17 | self.bond_labels = connect.bond_labels( 18 | self.atom_labels, self.bonds, self.bond_types 19 | ) 20 | 21 | self.bond_graph = self.generate_bond_graph(self.bonds) 22 | 23 | self.angles = connect.angles(self.bonds, self.bond_graph) 24 | self.angle_types = connect.find_angle_types(self.atom_labels, self.angles) 25 | self.angle_labels = connect.angle_labels( 26 | self.atom_labels, self.angles, self.angle_types 27 | ) 28 | 29 | self.dihedrals = connect.dihedrals(self.bonds, self.bond_graph) 30 | self.dihedral_types = connect.find_dihedral_types( 31 | self.atom_labels, self.dihedrals 32 | ) 33 | self.dihedral_labels = connect.dihedral_labels( 34 | self.atom_labels, self.dihedrals, self.dihedral_types 35 | ) 36 | 37 | self.impropers = connect.impropers(self.bonds, self.bond_graph) 38 | self.improper_types = connect.find_improper_types( 39 | self.atom_labels, self.impropers 40 | ) 41 | self.improper_labels = connect.improper_labels( 42 | self.atom_labels, self.impropers, self.improper_types 43 | ) 44 | 45 | def generate_bond_graph(self, bonds): 46 | N = int(np.amax(bonds)) # Number of atoms 47 | bond_graph = dict() 48 | for i in xrange(N): 49 | bond_graph[i] = set() 50 | 51 | for bond in bonds: 52 | bond_graph[bond[0] - 1].add(bond[1] - 1) 53 | bond_graph[bond[1] - 1].add(bond[0] - 1) 54 | return bond_graph 55 | 56 | def bonded_to(self, centre): 57 | return list(self.bond_graph[centre]) 58 | 59 | def validate(self): 60 | n_atoms = len(self.coords) 61 | for attr in ["molecule_labels", "atom_charges", "atom_labels"]: 62 | if hasattr(self, attr): 63 | assert len(getattr(self, attr)) == n_atoms, attr 64 | if abs(np.sum(self.atom_charges)) > 0.01: 65 | print "WARNING: charges do not sum to zero",\ 66 | np.sum(self.atom_charges) 67 | 68 | def crystal_params(self): 69 | path = os.path.dirname(__file__) + "/params/" 70 | return yaml.load(open(path + "config.yaml"), Loader=yaml.FullLoader) 71 | -------------------------------------------------------------------------------- /makegraphitics/write_coords.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | 5 | class Writer(object): 6 | def __init__(self, sim, system_name="comment line"): 7 | # Takes a numpy 3xN array of atom coordinates and outputs 8 | # them in different formats for viewing/modelling 9 | self.coords = sim.coords 10 | self.atom_labels = sim.atom_labels 11 | self.molecule = sim.molecule_labels 12 | self.charges = sim.atom_charges 13 | 14 | self.bonds = sim.bonds 15 | self.bond_labels = sim.bond_labels 16 | self.angles = sim.angles 17 | self.angle_labels = sim.angle_labels 18 | self.dihedrals = sim.dihedrals 19 | self.dihedral_labels = sim.dihedral_labels 20 | self.impropers = sim.impropers 21 | self.improper_labels = sim.improper_labels 22 | 23 | self.box_dimensions = sim.box_dimensions 24 | self.system_name = system_name 25 | 26 | if hasattr(sim, "masses"): 27 | self.masses = sim.masses 28 | if hasattr(sim, "bond_coeffs"): 29 | self.bond_coeffs = sim.bond_coeffs 30 | if hasattr(sim, "angle_coeffs"): 31 | self.angle_coeffs = sim.angle_coeffs 32 | if hasattr(sim, "dihedral_coeffs"): 33 | self.dihedral_coeffs = sim.dihedral_coeffs 34 | if hasattr(sim, "improper_coeffs"): 35 | self.improper_coeffs = sim.improper_coeffs 36 | if hasattr(sim, "pair_coeffs"): 37 | self.pair_coeffs = sim.pair_coeffs 38 | 39 | type_lists = { 40 | "atom": "pair_coeffs", 41 | "bond": "bond_coeffs", 42 | "angle": "angle_coeffs", 43 | "dihedral": "dihedral_coeffs", 44 | "improper": "improper_coeffs", 45 | } 46 | for type_list in type_lists: 47 | maxlabel = max(getattr(self, type_list + "_labels")) 48 | coeff = type_lists[type_list] 49 | if hasattr(sim, coeff): 50 | maxcoeff = max(getattr(self, coeff)) 51 | else: 52 | maxcoeff = 0 53 | setattr(self, "n" + type_list + "_types", max(maxcoeff, maxlabel)) 54 | 55 | def write_xyz(self, filename="out.xyz", option="w"): 56 | with open(filename, option) as outfile: 57 | outfile.write(str(len(self.coords)) + "\n" + self.system_name + "\n") 58 | for i in range(len(self.coords)): 59 | xyz = ( 60 | str(self.coords[i][0]) 61 | + " " 62 | + str(self.coords[i][1]) 63 | + " " 64 | + str(self.coords[i][2]) 65 | ) 66 | if hasattr(self, "masses"): 67 | try: 68 | mass = self.masses[self.atom_labels[i]] 69 | except KeyError: 70 | mass = 100 71 | if abs(mass - 12.0) < 0.5: 72 | atom_label = "C " 73 | elif abs(mass - 1.0) < 0.5: 74 | atom_label = "H " 75 | elif abs(mass - 14.0) < 0.5: 76 | atom_label = "N " 77 | elif abs(mass - 16.0) < 0.5: 78 | atom_label = "O " 79 | elif abs(mass - 22.9) < 0.5: 80 | atom_label = "Na " 81 | elif abs(mass - 40.1) < 0.5: 82 | atom_label ="Ca " 83 | else: 84 | atom_label = str(self.atom_labels[i]) + " " 85 | else: 86 | atom_label = str(self.atom_labels[i]) + " " 87 | 88 | outfile.write(atom_label + xyz + "\n") 89 | print "Coords written to " + str(filename) 90 | 91 | def write_reaxff(self, filename="data.lammps"): 92 | # atom_type charge 93 | masses = np.unique(self.masses.values()) 94 | nreax_types = len(masses) 95 | # reax_types = {mass:i+1 for i,mass in enumerate(masses)} 96 | reax_types = {12.011: 1, 1.008: 2, 15.999: 3} 97 | with open(filename, "w") as outfile: 98 | outfile.write( 99 | "# " 100 | + self.system_name 101 | + "\n" 102 | + str(len(self.coords)) 103 | + " atoms \n" 104 | + "\n" 105 | + str(nreax_types) 106 | + " atom types \n" 107 | + "\n" 108 | + str(self.box_dimensions[0, 0]) 109 | + "\t" 110 | + str(self.box_dimensions[0, 1]) 111 | + "\t xlo xhi \n" 112 | + str(self.box_dimensions[1, 0]) 113 | + "\t" 114 | + str(self.box_dimensions[1, 1]) 115 | + "\t ylo yhi \n" 116 | + str(self.box_dimensions[2, 0]) 117 | + "\t" 118 | + str(self.box_dimensions[2, 1]) 119 | + "\t zlo zhi \n" 120 | + "\n" 121 | ) 122 | if hasattr(self, "masses"): 123 | outfile.write("\n Masses \n \n") 124 | for mass in reax_types: 125 | outfile.write(str(reax_types[mass]) + "\t" + str(mass) + "\n") 126 | 127 | outfile.write("\n Atoms \n \n") 128 | 129 | for i in range(len(self.coords)): 130 | atom_type = self.atom_labels[i] 131 | reax_type = reax_types[self.masses[atom_type]] 132 | outfile.write( 133 | str(i + 1) 134 | + "\t " 135 | + str(reax_type) # atom ID 136 | + "\t " 137 | + str(self.charges[i]) # atom type 138 | + "\t " 139 | + str(self.coords[i][0]) # atomcharg 140 | + "\t " 141 | + str(self.coords[i][1]) # x 142 | + "\t " 143 | + str(self.coords[i][2]) # y 144 | + "\n " # z 145 | ) 146 | 147 | print "Coords written to " + filename 148 | 149 | def write_lammps(self, filename="data.lammps"): 150 | # atom_type full 151 | with open(filename, "w") as outfile: 152 | outfile.write( 153 | "# " 154 | + self.system_name 155 | + "\n" 156 | + str(len(self.coords)) 157 | + " atoms \n" 158 | + str(len(self.bonds)) 159 | + " bonds \n" 160 | + str(len(self.angles)) 161 | + " angles \n" 162 | + str(len(self.dihedrals)) 163 | + " dihedrals \n" 164 | + str(len(self.impropers)) 165 | + " impropers \n" 166 | "\n" 167 | + str(self.natom_types) 168 | + " atom types \n" 169 | + str(self.nbond_types) 170 | + " bond types \n" 171 | + str(self.nangle_types) 172 | + " angle types \n" 173 | + str(self.ndihedral_types) 174 | + " dihedral types \n" 175 | + str(self.nimproper_types) 176 | + " improper types \n" 177 | + "\n" 178 | + str(self.box_dimensions[0, 0]) 179 | + "\t" 180 | + str(self.box_dimensions[0, 1]) 181 | + "\t xlo xhi \n" 182 | + str(self.box_dimensions[1, 0]) 183 | + "\t" 184 | + str(self.box_dimensions[1, 1]) 185 | + "\t ylo yhi \n" 186 | + str(self.box_dimensions[2, 0]) 187 | + "\t" 188 | + str(self.box_dimensions[2, 1]) 189 | + "\t zlo zhi \n" 190 | + "\n" 191 | ) 192 | if hasattr(self, "masses"): 193 | outfile.write("\n Masses \n \n") 194 | for i in self.masses: 195 | outfile.write(str(i) + "\t" + str(self.masses[i]) + "\n") 196 | 197 | if hasattr(self, "pair_coeffs"): 198 | outfile.write("\n Pair Coeffs \n \n") 199 | for i in self.pair_coeffs: 200 | outfile.write( 201 | str(i) 202 | + "\t" 203 | + str(self.pair_coeffs[i][1]) 204 | + "\t" 205 | + str(self.pair_coeffs[i][2]) 206 | + "\n" 207 | ) 208 | 209 | if hasattr(self, "bond_coeffs"): 210 | outfile.write("\n Bond Coeffs \n \n") 211 | for i in self.bond_coeffs: 212 | outfile.write( 213 | str(i) 214 | + "\t" 215 | + str(self.bond_coeffs[i][1]) 216 | + "\t" 217 | + str(self.bond_coeffs[i][2]) 218 | + "\n" 219 | ) 220 | if hasattr(self, "angle_coeffs"): 221 | outfile.write("\n Angle Coeffs \n \n") 222 | for i in self.angle_coeffs: 223 | outfile.write( 224 | str(i) 225 | + "\t" 226 | + str(self.angle_coeffs[i][1]) 227 | + "\t" 228 | + str(self.angle_coeffs[i][2]) 229 | + "\n" 230 | ) 231 | if hasattr(self, "dihedral_coeffs"): 232 | outfile.write("\n Dihedral Coeffs \n \n") 233 | for i in self.dihedral_coeffs: 234 | outfile.write( 235 | str(i) 236 | + "\t" 237 | + str(self.dihedral_coeffs[i][1]) 238 | + "\t" 239 | + str(self.dihedral_coeffs[i][2]) 240 | + "\t" 241 | + str(self.dihedral_coeffs[i][3]) 242 | + "\t" 243 | + str(self.dihedral_coeffs[i][4]) 244 | + "\n" 245 | ) 246 | 247 | if hasattr(self, "improper_coeffs"): 248 | outfile.write("\n Improper Coeffs \n \n") 249 | for i in self.improper_coeffs: 250 | outfile.write( 251 | str(i) 252 | + "\t" 253 | + str(self.improper_coeffs[i][1]) 254 | + "\t" 255 | + str(self.improper_coeffs[i][2]) 256 | + "\n" 257 | ) 258 | 259 | outfile.write("\n Atoms \n \n") 260 | 261 | for i in range(len(self.coords)): 262 | outfile.write( 263 | str(i + 1) 264 | + "\t " 265 | + str(self.molecule[i]) # atom ID 266 | + "\t " 267 | + str(self.atom_labels[i]) # molecule ID 268 | + "\t " 269 | + str(self.charges[i]) # atom type 270 | + "\t " 271 | + str(self.coords[i][0]) # atomcharg 272 | + "\t " 273 | + str(self.coords[i][1]) # x 274 | + "\t " 275 | + str(self.coords[i][2]) # y 276 | + "\n " # z 277 | ) 278 | 279 | if len(self.bonds): 280 | outfile.write("\n Bonds \n \n") 281 | for i in range(len(self.bonds)): 282 | outfile.write( 283 | str(i + 1) 284 | + "\t " 285 | + str(self.bond_labels[i]) # bond ID 286 | + "\t " 287 | + str(self.bonds[i][0]) 288 | + "\t " 289 | + str(self.bonds[i][1]) # atom 1 290 | + "\n" # atom 2 291 | ) 292 | 293 | if len(self.angles): 294 | outfile.write("\n Angles \n \n") 295 | for i in range(len(self.angles)): 296 | outfile.write( 297 | str(i + 1) 298 | + "\t " 299 | + str(self.angle_labels[i]) # angle ID 300 | + "\t " 301 | + str(self.angles[i][0]) 302 | + "\t " 303 | + str(self.angles[i][1]) 304 | + "\t " 305 | + str(self.angles[i][2]) 306 | + "\n" 307 | ) 308 | 309 | if len(self.dihedrals): 310 | outfile.write("\n Dihedrals \n \n") 311 | for i in range(len(self.dihedrals)): 312 | outfile.write( 313 | str(i + 1) 314 | + "\t" 315 | + str(self.dihedral_labels[i]) 316 | + " \t" 317 | + str(self.dihedrals[i][0]) 318 | + " \t" 319 | + str(self.dihedrals[i][1]) 320 | + " \t" 321 | + str(self.dihedrals[i][2]) 322 | + " \t" 323 | + str(self.dihedrals[i][3]) 324 | + " \n" 325 | ) 326 | 327 | if len(self.impropers): 328 | outfile.write("\n Impropers \n \n") 329 | for i in range(len(self.impropers)): 330 | outfile.write( 331 | str(i + 1) 332 | + "\t" 333 | + str(self.improper_labels[i]) 334 | + "\t" 335 | + str(self.impropers[i][0]) 336 | + " \t" 337 | + str(self.impropers[i][1]) 338 | + " \t" 339 | + str(self.impropers[i][2]) 340 | + " \t" 341 | + str(self.impropers[i][3]) 342 | + " \n" 343 | ) 344 | 345 | print "Coords written to " + filename 346 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="makegraphitics", 5 | version="0.2", 6 | description="""Library to build graphene and graphite based structures 7 | for atomistic simulation""", 8 | url="https://github.com/velocirobbie/make-graphitics", 9 | author="Robert C Sinclair", 10 | packages=find_packages(), 11 | include_package_data=True, 12 | ) 13 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | def test_examples(): 2 | from os import listdir 3 | from os.path import isfile, join 4 | import subprocess 5 | 6 | path = "examples/" 7 | 8 | example_files = [f for f in listdir(path) if isfile(join(path, f))] 9 | example_files = [f for f in example_files if f[-3:] == ".py"] 10 | 11 | for example in example_files: 12 | return_code = subprocess.call(["python", path + example]) 13 | assert not return_code 14 | 15 | 16 | if __name__ == "__main__": 17 | test_examples() 18 | --------------------------------------------------------------------------------