├── .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 |
--------------------------------------------------------------------------------