diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8dd991e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Cache- +*.pyc + +# Pycharm editor- +*idea + +# Distrubtion/Packaging- +build/ +dist/ +*.egg-info/ +pypirc + +# Pytest +.pytest_cache \ No newline at end of file diff --git a/Carbonpy.py b/Carbonpy.py deleted file mode 100644 index b3169bc..0000000 --- a/Carbonpy.py +++ /dev/null @@ -1,186 +0,0 @@ -# An organic chemistry module for Grade 12(mostly)- -# Can name compounds based on their structure, convert the compound from one functional group to another and more... -# - single bond -# = double bond -# ~ triple bond -# TODO: Identify branched chains, functional groups and somehow represent the compound in the same way you would draw it - -import re -from typing import Union - - -class Namer(object): # IUPAC Names for now only - symbol = '\u2261' # The triple bond symbol ≡ - subscripts = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉") # Subscripts for molecular and structural formula - - def __init__(self, structure: str) -> None: - self.processing = self.structure = structure # Processing is a string only for processing - self.carbons = 0 # No. of carbon atoms present - self.hydrogens = 0 - self.bond = "" # Name of bond - # self.final = "" # Name of final compound - - # Counts number of hydrogens and carbons in compound- - self.carbons = self.atom_counter('C') - self.hydrogens = self.atom_counter('H') - - def __str__(self): # If user wants to see structural formula - return f"{self.structure.replace('~', self.symbol).translate(self.subscripts)}" - - def __repr__(self): - return f"{self.__class__.__name__}({self.structure!r})" - - def molecular_formula(self): # If user wants to see molecular formula - return str(f"C{self.carbons if self.carbons > 1 else ''}H{self.hydrogens}").translate(self.subscripts) - - def analyser(self) -> str: - compound_name = "" - many_bonds = "" # Is empty for saturated compounds - - # Checks valencies of atoms in compound- - self.valency_checker() - # Processing and deciding name(s) of the compound- - bond_type = self.suffix_namer() - - if any(suffix in bond_type for suffix in list(multipl_suffixes.values())): - many_bonds += "a-" # This is the 'a' in a compound like butadiene - elif not bond_type == "ane": # If compound has only one unsaturated bond - many_bonds += "-" - compound_name += f"{many_bonds}{bond_type}" # Suffix and position is decided - - return f"{prefixes[self.carbons].capitalize()}{compound_name}" # returns final name - - def valency_checker(self) -> None: - """Checks if valencies of carbon are satisfied and raises error if not satisfied. """ - - valency = 0 - hydros_bonds = {'H': 1, "H2": 1, "H3": 2, "H4": 3, '-': 1, '=': 2, '~': 3} - splitted = re.split('([-=~])', self.structure) # Splits the bonds and elements - - for index, element in enumerate(splitted): # Adds the bonds to the string of atoms - if element == "-" or element == "=" or element == "~": - splitted[index - 1] += element - splitted[index + 1] += element - splitted.pop(index) # Removes those bonds from the list. Final list example: ['CH3-', 'CH2-', 'CH3-'] - - for element in splitted: # Counts the bonds and hydrogens to see if valency is satisfied - for hyd_bonds in hydros_bonds.keys(): # Iterating through dict - if hyd_bonds in element: - valency += hydros_bonds[hyd_bonds] * element.count(hyd_bonds) - if valency != 4: - raise ValencyError("Check valencies of your compound!") - valency = 0 - - def atom_counter(self, element): - if element == "C": - return self.structure.count('C') - - elif element == "H": - count = 0 - hydros = {"H": 1, "H2": 1, "H3": 2, "H4": 3} # Each value is less than 1 of parent since 'H' is in it too. - for hydro, value in hydros.items(): - count += self.structure.count(hydro) * value # Multiplied by its value to get actual value of H - return count - - def suffix_namer(self) -> str: - lowest_db = lowest_tb = db_suffix = tb_suffix = "" # db,tb- double, triple bond - self.processing = self.processing.translate({ord(i): None for i in 'CH23'}) # Removes everything except bonds - - lows_pos = self.lowest_position() - if not isinstance(lows_pos, dict): # If compound is saturated - return f"ane" # Alkane - - for key, value in lows_pos.items(): - if value == '=': - lowest_db += f"{key}," # Adds position of double bond with ',' for more bonds - elif value == '~': - lowest_tb += f"{key}," # Same as above, except this time for triple bond - - lowest_tb = lowest_tb.strip(',') # Removes ',' - lowest_db = lowest_db.strip(',') - - # If many double/triple bonds present, get their suffix(di, tri, tetra, etc.) - if len(lowest_db) >= 3: - db_suffix = f"-{multipl_suffixes[len(lowest_db.replace(',', ''))]}" # Add that '-' too - else: - db_suffix += "-" # else only '-' - - if len(lowest_tb) >= 3: - tb_suffix = f"-{multipl_suffixes[len(lowest_tb.replace(',', ''))]}" - else: - tb_suffix += "-" - - if '=' in self.processing and '~' in self.processing: # If double and triple bond present - return f"{lowest_db}{db_suffix}en-{lowest_tb}{tb_suffix}yne" - - elif '~' in self.processing: # Only triple bond present - return f"{lowest_tb}{tb_suffix}yne" - - elif '=' in self.processing: # Only double bond present - return f"{lowest_db}{db_suffix}ene" # Return with di,tri,etc - - def lowest_position(self) -> Union[None, dict]: - """First point of difference rule used""" - lowest_front = {} - lowest_back = {} - # TODO: Maybe number from front and back simultaneously? (Also made me realize this may not work for isomers) - # Adds all occurrences from front - for index, string in enumerate(self.processing): - if string in ('=', '~'): - lowest_front[index + 1] = string - - # Adds all occurrences from back - for index, string in enumerate(''.join(reversed(self.processing))): - if string in ('=', '~'): - lowest_back[index + 1] = string - - assert (len(lowest_front) == len(lowest_back)) # Make sure they have the same length - for (index, value), (index2, value2) in zip(lowest_front.items(), lowest_back.items()): - # First point of difference- - if index < index2: - return lowest_front - elif index2 < index: - return lowest_back - elif index == index2: # Same index, check for precedence (only = and ~ for now) - # Double bond has more precedence than triple - if value == '=': # Will change into a dict access for func groups priority - return lowest_front - elif value2 == '=': - return lowest_back - - if len(lowest_front) == 0: - return None - else: - return lowest_back # Can also return front(if compound is symmetrical) - - def priority_order(self): - pass - - -class ValencyError(Exception): - pass - - -prefixes = {1: "meth", 2: "eth", 3: "prop", 4: "but", 5: "pent", 6: "hex", 7: "hept", 8: "oct", 9: "non", 10: "dec", - 11: "undec", 12: "dodec", 13: "tridec", 14: "tetradec", 15: "pentadec", 16: "hexadec", 17: "heptadec", - 18: "octadec", 19: "nonadec", 20: "icos"} - -# precedence = {"=": 1, "~": 1} - -multipl_suffixes = {2: "di", 3: "tri", 4: "tetra", 5: "penta", 6: "hexa", 7: "hepta", 8: "octa", 9: "nona"} - -compound1 = Namer('CH3-C~C-CH3') -compound2 = Namer('CH~CH') -compound3 = Namer('CH~C-C~C-CH=C=C=CH2') -compound4 = Namer('CH4') -compound5 = Namer('CH2=CH-CH=CH-CH=CH2') -compound6 = Namer('CH2=CH2') -compound7 = Namer('CH~C-CH=CH2') - -print(f"{compound1}\n{compound1.molecular_formula()}\n{compound1.analyser()}\n") -print(f"{compound2}\n{compound2.molecular_formula()}\n{compound2.analyser()}\n") -print(f"{compound3}\n{compound3.molecular_formula()}\n{compound3.analyser()}\n") -print(f"{compound4}\n{compound4.molecular_formula()}\n{compound4.analyser()}\n") -print(f"{compound5}\n{compound5.molecular_formula()}\n{compound5.analyser()}\n") -print(f"{compound6}\n{compound6.molecular_formula()}\n{compound6.analyser()}\n") -print(f"{compound7}\n{compound7.molecular_formula()}\n{compound7.analyser()}\n") diff --git a/LICENSE b/LICENSE index 261eeb9..29f81d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,201 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 4386812..f928005 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,37 @@ -# Carbonpy -A module which names straight/branched chain organic compounds, suggests conversions from one type to another, etc. - -## Usage- - -### Naming compounds- -Instantiate the class `Namer()` , which takes a string which contains the hydrocarbon (condensed form) and then call it with a method named `analyser()` to get the IUPAC name of the compound. - -Example: -``` -a = Namer('CH~CH') -a.analyser() ->>> Ethyne -``` - -Due to limitations in expressing a hydrocarbon easily, we have selected this path -Single bond:- - -Double bond:- = -Triple bond:- ~ -Branches:- -([compound](more branches...))([another branch from same carbon])- and so on -(Branches support coming soon) - -You can also get the molecular formula of the compound: -``` -compound = Namer('CH~C-C~C-CH=C=C=CH2') -compound.molecular_formula() ->>> C₈H₄ -``` -Example- 2-Methylpropane would be expressed as: -``` -a = Namer('CH3-CH(CH3)-CH3').analyser() ->>> 2-Methylpropane -``` - -Will support naming with functional groups in the future. - -P.S: This project is NOT dead. +# Carbonpy +A module which names straight/branched chain organic compounds, suggests conversions from one type to another, etc. + +## Usage- + +### Naming compounds- +Instantiate the class `Namer()` , which takes a string which contains the hydrocarbon (condensed form) and then call it with a method named `analyser()` to get the IUPAC name of the compound. + +Example: +``` +a = Namer('CH~CH') +a.analyser() +>>> Ethyne +``` + +Due to limitations in expressing a hydrocarbon easily, we have selected this path +Single bond:- - +Double bond:- = +Triple bond:- ~ +Branches:- -([compound](more branches...))([another branch from same carbon])- and so on +(Branches support coming soon) + +You can also get the molecular formula of the compound: +``` +compound = Namer('CH~C-C~C-CH=C=C=CH2') +compound.molecular_formula() +>>> C₈H₄ +``` +Example- 2-Methylpropane would be expressed as: +``` +a = Namer('CH3-CH(CH3)-CH3').analyser() +>>> 2-Methylpropane +``` + +Will support naming with functional groups in the future. + +P.S: This project is NOT dead. diff --git a/carbonpy/__init__.py b/carbonpy/__init__.py new file mode 100644 index 0000000..cf6be27 --- /dev/null +++ b/carbonpy/__init__.py @@ -0,0 +1,9 @@ +from namer import BaseNamer, Branched +from base.compound import CompoundObject +from .base.element import Element +from error import ValencyError +from .version import __version__ + +__author__ = 'Harshil Mehta' + +__all__ = ['BaseNamer', 'ValencyError', 'Branched', 'Element', 'CompoundObject'] diff --git a/carbonpy/base/GraphEdge.py b/carbonpy/base/GraphEdge.py new file mode 100644 index 0000000..e7ce9ce --- /dev/null +++ b/carbonpy/base/GraphEdge.py @@ -0,0 +1,5 @@ +class GraphEdge: + def __init__(self, from_node, to_node, bond_type): + self.from_node = from_node + self.to_node = to_node + self.bond_type = bond_type diff --git a/carbonpy/base/GraphElement.py b/carbonpy/base/GraphElement.py new file mode 100644 index 0000000..06ed20a --- /dev/null +++ b/carbonpy/base/GraphElement.py @@ -0,0 +1,40 @@ +from carbonpy import Element +from ..graphical import forces + +from math import cos, sin + + +class GraphElement(Element): + def __init__(self, value, comp, x, y): + super().__init__(value, comp) + self.x = x + self.y = y + + def get_coords(self): + return self.x, self.y + + def calc_attractive_f_mag(self, other): + dist = forces.calc_dist(other, self) + return 50 * (dist - 65) + + def calc_attractive_f(self, other): + force_mag = self.calc_attractive_f_mag(other) + force_angle = forces.calc_angle(other, self) + + force_x = cos(force_angle) * force_mag + force_y = sin(force_angle) * force_mag + return force_x, force_y + + def calc_repulsive_f_mag(self, other): + dist = forces.calc_dist(other, self) + if dist < 100: + dist = 100 + return 10000 * (10 ** 10) / (dist ** dist) + + def calc_repulsive_f(self, other): + force_mag = self.calc_repulsive_f_mag(other) + force_angle = forces.calc_angle(other, self) + + force_x = cos(force_angle) * force_mag + force_y = sin(force_angle) * force_mag + return force_x, force_y diff --git a/carbonpy/base/__init__.py b/carbonpy/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/carbonpy/base/compound.py b/carbonpy/base/compound.py new file mode 100644 index 0000000..9346f7e --- /dev/null +++ b/carbonpy/base/compound.py @@ -0,0 +1,193 @@ +# An organic chemistry module for Grade 12(mostly)- +# Can name compounds based on their structure, convert the compound from one functional group to another and more... +# - single bond +# = double bond +# ~ triple bond +# TODO: Identify functional groups and somehow represent the compound in the same way you would draw it +# TODO: fix imports by adding '.' before merging + +from collections import deque +from typing import Dict, Deque + +from constants import symbol +from base.element import Element +from error import ValencyError + + +class CompoundObject: # IUPAC Names for now only + subscripts = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉") # Subscripts for molecular and structural formula + + def __init__(self, structure: str) -> None: + self.processing = self.structure = structure.upper() # Processing is a string only for processing + self._carbons = self.atom_counter('C') + + if self._carbons > 10000: + raise ValueError(f"Got {self._carbons} carbon atoms, this version supports only up to 10,000 carbon atoms!") + + self._hydrogens = self.atom_counter('H') + + self._carbon_comps = self.processing.translate({ord(i): ' ' for i in '-=~()'}).split() + self._bonds_only = list(self.processing.translate({ord(i): None for i in 'CH234()'})) + # assert len(self._carbon_comps) - 1 == len(self._bonds_only) + + self._graph: Dict[Element, Deque[str]] = self.make_graph() + + def __str__(self): # If user wants to see structural formula; called from print() + return f"{self.structure.replace('~', symbol).translate(self.subscripts)}" + + def __repr__(self): + return f"{self.__class__.__name__}({self.structure!r})" + + def __len__(self) -> int: + return self.carbons + + def __iter__(self) -> Element: + for element_node in self._graph: + yield element_node + + def __eq__(self, other): + other = other.molar_mass if isinstance(other, self.__class__) else other + return self.molar_mass == float(other) if isinstance(other, (int, float)) else NotImplemented + + def __lt__(self, other): + other = other.molar_mass if isinstance(other, self.__class__) else other + return self.molar_mass < float(other) if isinstance(other, (int, float)) else NotImplemented + + def __le__(self, other): + other = other.molar_mass if isinstance(other, self.__class__) else other + return self.molar_mass <= float(other) if isinstance(other, (int, float)) else NotImplemented + + def __gt__(self, other): + other = other.molar_mass if isinstance(other, self.__class__) else other + return self.molar_mass > float(other) if isinstance(other, (int, float)) else NotImplemented + + def __ge__(self, other): + other = other.molar_mass if isinstance(other, self.__class__) else other + return self.molar_mass >= float(other) if isinstance(other, (int, float)) else NotImplemented + + @property + def graph(self) -> Dict[Element, Deque[str]]: + return self._graph + + @property + def molar_mass(self) -> float: + molar_masses = {'C': 12.0107, 'H': 1.00784} + return molar_masses['C'] * self.carbons + molar_masses['H'] * self.hydrogens + + @property + def carbons(self) -> int: + return self._carbons + + @property + def hydrogens(self) -> int: + return self._hydrogens + + def molecular_formula(self) -> str: # If user wants to see molecular formula + return str(f"C{self.carbons if self.carbons > 1 else ''}H{self.hydrogens}").translate(self.subscripts) + + @staticmethod + def _remove_bonds(string: str) -> str: + return string.translate({ord(i): ' ' for i in '-=~'}) + + @staticmethod + def add_edge(obj: dict, prev_element: str, this_element: str): + obj.setdefault(prev_element, deque([])) + obj.setdefault(this_element, deque([])) + + obj[prev_element].append(this_element) + obj[this_element].append(prev_element) + return obj + + def valency_checker(self) -> bool: + """Checks if valencies of carbon are satisfied and raises error if not satisfied.""" + carbon_index = 0 + val = {'-': 1, '=': 2, '~': 3, '': 0} + for e in self.graph: + valency = val[e.back_bond] + val[e.top_bond] + val[e.bottom_bond] + val[e.front_bond] + e.hydrogens() + carbon_index = self.structure.find('C', carbon_index) + 1 + if valency != 4: + raise ValencyError(f"Check valencies of your compound!\n{self.structure}\n{' ' * (carbon_index - 1)}^") + return True + + def atom_counter(self, element): + if element.upper() == "C": + return self.structure.count('C') + + elif element.upper() == "H": + count = 0 + hydros = {"H": 1, "H2": 1, "H3": 2, "H4": 3} # Each value is less than 1 of parent since 'H' is in it too. + for hydro, value in hydros.items(): + count += self.structure.count(hydro) * value # Multiplied by its value to get actual value of H + return count + raise ValueError(f"Got {element}. Only Carbon ('C') and Hydrogen ('H') is supported in this version!") + + def to_element(self, graph: dict): + _graph = {} + for element_node, connected_nodes in graph.items(): + element = Element(value=element_node, comp=self._carbon_comps[int(element_node[1:]) - 1]) + + position = max(0, int(element_node[1:]) - 2) + element.back_bond = self._bonds_only[position] + + for node, at in zip(range(1, len(connected_nodes)), {'front_bond', 'top_bond', 'bottom_bond'}): + position = int(connected_nodes[node][1:]) - 2 + setattr(element, at, self._bonds_only[position]) + + _graph[element] = connected_nodes + return _graph + + def make_graph(self): + _graph: Dict[str, Deque[str]] = {} + to_repl = {'-': ' ', '=': ' ', '~': ' ', 'C(': 'C (', 'H(': 'H (', 'H)': 'H )', 'H2)': 'H2 )', 'H3)': 'H3 )', + ')(': ') ('} + + splitted = self.structure + + branch_elements = deque([]) + visited = deque(['C1']) + carbon_indexes = [] + index = 0 + + for k, v in to_repl.items(): + splitted = splitted.replace(k, v) + splitted = splitted.split() + previous = splitted[0] + + for ele in splitted: + if 'C' in ele: + index += 1 + carbon_indexes.append(index) + + for index, element in zip(carbon_indexes[1:], splitted[1:]): + if previous != ')' and element == '(': + branch_elements.extend([visited[-1]] * 3) + previous = element + continue + + if previous == ")" and element != '(': + popped1 = branch_elements.pop() + _graph = self.add_edge(_graph, popped1, f"C{index}") + if branch_elements and popped1 == branch_elements[-1]: + branch_elements.pop() + + elif previous == ")" and element == "(": + _graph = self.add_edge(_graph, branch_elements.pop(), f"C{index + 1}") + + elif previous == "(" and f"C{index}" not in _graph: + _graph = self.add_edge(_graph, branch_elements.pop(), f"C{index}") + + elif previous != "(" and element != ')': + _graph = self.add_edge(_graph, visited[-1], f"C{index}") + + visited.append(f"C{index}") + previous = element + + _graph: Dict[Element, Deque[str]] = self.to_element(_graph) # Convert string nodes to Element nodes + return _graph + +# a = CompoundObject(structure="CH3-C(=CH-C~CH)(-CH=CH2)-C~CH") +# a = CompoundObject(structure="CH~C-CH(-C(-CH3)(-CH2-CH3)-CH3)-C(-CH=CH(-CH2-CH3)-CH3)(-CH3)-CH2-CH3") +# +# print(a.carbons) +# for element in a: +# print(element) diff --git a/carbonpy/base/element.py b/carbonpy/base/element.py new file mode 100644 index 0000000..7a59fef --- /dev/null +++ b/carbonpy/base/element.py @@ -0,0 +1,33 @@ +class Element(object): # Node + __slots__ = ('value', 'comp', 'front_bond', 'back_bond', 'top_bond', 'bottom_bond') + + def __init__(self, value: int or str, comp: str) -> None: + # Required- + self.value: int or str = value + self.comp: str = comp + + # Optional- + self.front_bond: str = '' + self.back_bond: str = '' + self.top_bond: str = '' + self.bottom_bond: str = '' + + def __str__(self) -> str: + return self.comp + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.value!r}, {self.comp!r})" + + def __hash__(self) -> hash: + return hash(self.value) + + def __eq__(self, other) -> bool: + if isinstance(other, str): + return self.value == other + return self.value == other.value if isinstance(other, Element) else NotImplemented + + def hydrogens(self) -> int: + last = self.comp[-1] + if last.isdigit(): + return int(last) + return 1 if last == 'H' else 0 diff --git a/carbonpy/constants.py b/carbonpy/constants.py new file mode 100644 index 0000000..83fad40 --- /dev/null +++ b/carbonpy/constants.py @@ -0,0 +1,8 @@ +# prefixes = {1: "meth", 2: "eth", 3: "prop", 4: "but", 5: "pent", 6: "hex", 7: "hept", 8: "oct", 9: "non", 10: "dec", +# 11: "undec", 12: "dodec", 13: "tridec", 14: "tetradec", 15: "pentadec", 16: "hexadec", 17: "heptadec", +# 18: "octadec", 19: "nonadec", 20: "icos"} + +# precedence = {"=": 1, "~": 1} + +multipl_suffixes = {2: "di", 3: "tri", 4: "tetra", 5: "penta", 6: "hexa", 7: "hepta", 8: "octa", 9: "nona"} +symbol = '\u2261' # The triple bond symbol ≡ diff --git a/carbonpy/error.py b/carbonpy/error.py new file mode 100644 index 0000000..2295c1e --- /dev/null +++ b/carbonpy/error.py @@ -0,0 +1,2 @@ +class ValencyError(Exception): + pass diff --git a/carbonpy/examples.py b/carbonpy/examples.py new file mode 100644 index 0000000..0e18104 --- /dev/null +++ b/carbonpy/examples.py @@ -0,0 +1,34 @@ +from namer import Branched, BaseNamer + +# Examples- + +# comp1 = BaseNamer('CH2=C=C=C=CH2') +# comp2 = BaseNamer('CH~C-CH3') +# comp3 = BaseNamer('CH~C-C~C-CH=C=C=CH2') +# comp4 = BaseNamer('CH4') +# comp5 = BaseNamer('CH2=CH-CH=CH-CH=CH2') +# comp6 = BaseNamer('CH2=CH2') +# comp7 = BaseNamer('CH~C-CH=CH2') +# comp_tmp = Branched('CH4') +# comp_tmp2 = Branched('CH3-CH3') +comp8 = Branched('CH3-CH(-CH3)-CH3') +comp9 = Branched('CH3-C(-CH=C=CH2)(-CH3)-CH3') +comp10 = Branched('CH3-C(-CH2-CH2-CH3)(-CH2-CH2-CH3)-CH3') +comp11 = Branched('CH~C-CH(-C(-CH3)(-CH2-CH3)-CH3)-C(-CH=C(-CH2-CH3)-CH3)(-CH3)-CH2-CH3') +# comp12 = Branched('CH3-C(-CH2-CH(-CH3)-CH3)(-CH3)-CH2-CH3') + +# comp10 = Branched('CH3-CH(-CH2-CH3)-CH3') # 2-methylbutane. This shows user can input structure in any order! +# print(f"func :{comp8.branch_splitter()}") +# print() +# a = comp8.determine_longest() +# print() +# b = comp9.determine_longest() +# print() +# c = comp11.determine_longest() +# print() +comp11.valency_checker() + +# comps = [comp1, comp2, comp3, comp4, comp5, comp6, comp7] + +# for comp in comps: +# print(f"{comp}\n{comp.molecular_formula()}\n{comp.analyser()}\n") diff --git a/carbonpy/graphical/__init__.py b/carbonpy/graphical/__init__.py new file mode 100644 index 0000000..bd38086 --- /dev/null +++ b/carbonpy/graphical/__init__.py @@ -0,0 +1,2 @@ +from .playground import Playground +from .interface import Interface diff --git a/carbonpy/graphical/builder.py b/carbonpy/graphical/builder.py new file mode 100644 index 0000000..2beff4e --- /dev/null +++ b/carbonpy/graphical/builder.py @@ -0,0 +1,4 @@ +from gi.repository import Gtk + +builder = Gtk.Builder() +builder.add_from_file("gui.glade") diff --git a/carbonpy/graphical/css_styles/canvas_style.css b/carbonpy/graphical/css_styles/canvas_style.css new file mode 100644 index 0000000..753712c --- /dev/null +++ b/carbonpy/graphical/css_styles/canvas_style.css @@ -0,0 +1,44 @@ +/* Setting carbon canvas and its children's properties- */ +@define-color grey #C6C6C6; +@define-color black #000000; +@define-color dark_grey #A4A4A4; +@define-color off_white #DDDEE7; + + +#carboncanvas { + background-color: @off_white; + font-size: 1em; + border-radius: 1em; +} + +#carboncanvas button { + background: none; + background-image: none; + color: @black; + border-radius: 10em; + padding: 0.2em; + margin: 0px; +} + +#carboncanvas button:hover{ + background: none; + background-color: @grey; + font-size: 1em; +} + +#carboncanvas button:active{ + background-color: @dark_grey; +} + +#carboncanvas button.bond { + border-radius: 10em; + padding: 0.2em; + margin: 0px; +} + +#carboncanvas .bond:hover { + transition-duration: 0s; + background: none; + border-width: 0px; + font-weight: bold; +} diff --git a/carbonpy/graphical/css_styles/styles.css b/carbonpy/graphical/css_styles/styles.css new file mode 100644 index 0000000..7ffb743 --- /dev/null +++ b/carbonpy/graphical/css_styles/styles.css @@ -0,0 +1,2 @@ +@import "canvas_style.css"; +@import "test.css"; \ No newline at end of file diff --git a/carbonpy/graphical/css_styles/test.css b/carbonpy/graphical/css_styles/test.css new file mode 100644 index 0000000..4ea7d84 --- /dev/null +++ b/carbonpy/graphical/css_styles/test.css @@ -0,0 +1,13 @@ +/* General */ +#event_label:hover #test_label{ + background: none; + background-color: red; + color: blue; + font: 10px "Comic Sans"; +} + + +button:hover { + background: none; + background-color: red; +} diff --git a/carbonpy/graphical/forces.py b/carbonpy/graphical/forces.py new file mode 100644 index 0000000..8867a55 --- /dev/null +++ b/carbonpy/graphical/forces.py @@ -0,0 +1,15 @@ +from math import atan2, hypot + + +def dist_diff(x2, y2, x1, y1): + return x2 - x1, y2 - y1 + + +def calc_dist(node2, node1): + adj, opp = dist_diff(node2.x, node2.y, node1.x, node1.y) + return hypot(adj, opp) + + +def calc_angle(node2, node1): + adj, opp = dist_diff(node2.x, node2.y, node1.x, node1.y) + return atan2(opp, adj) diff --git a/carbonpy/graphical/gui.glade b/carbonpy/graphical/gui.glade new file mode 100644 index 0000000..3ebbfc1 --- /dev/null +++ b/carbonpy/graphical/gui.glade @@ -0,0 +1,108 @@ + + + + + + + False + Carbonpy + 1344 + 768 + + + + + + + True + False + + + wow + 64 + 30 + True + True + True + + + 820 + 494 + + + + + event_label + 100 + 80 + True + False + GDK_POINTER_MOTION_MASK | GDK_STRUCTURE_MASK + + + test_label + 53 + 52 + True + False + test + + + + + 713 + 263 + + + + + carboncanvas + 634 + 321 + True + True + GDK_POINTER_MOTION_MASK | GDK_STRUCTURE_MASK + in + + + + True + False + 0 + + + True + False + + + CH4 + 1 + 38 + 32 + True + False + True + none + + + + + + 278 + 142 + + + + + + + + + 16 + 100 + + + + + + diff --git a/carbonpy/graphical/gui.glade~ b/carbonpy/graphical/gui.glade~ new file mode 100644 index 0000000..2546dfd --- /dev/null +++ b/carbonpy/graphical/gui.glade~ @@ -0,0 +1,108 @@ + + + + + + + False + Carbonpy + 1344 + 768 + + + + + + + True + False + + + wow + 64 + 30 + True + True + True + + + 820 + 494 + + + + + event_label + 100 + 80 + True + False + GDK_POINTER_MOTION_MASK | GDK_STRUCTURE_MASK + + + test_label + 53 + 52 + True + False + test + + + + + 713 + 263 + + + + + carboncanvas + 634 + 321 + True + True + GDK_POINTER_MOTION_MASK | GDK_STRUCTURE_MASK + in + + + + True + False + 0 + + + True + False + + + CH4 + C1 + 38 + 32 + True + False + True + none + + + + + + 278 + 142 + + + + + + + + + 16 + 100 + + + + + + diff --git a/carbonpy/graphical/interface.py b/carbonpy/graphical/interface.py new file mode 100644 index 0000000..39977e5 --- /dev/null +++ b/carbonpy/graphical/interface.py @@ -0,0 +1,34 @@ +from gi.repository import Gtk, Gdk + + +class Interface: + + @staticmethod + def on_enter_notify_event(widget, event): + # print('hovering carbons') + widget.get_window().set_cursor(Gdk.Cursor.new_from_name(display=widget.get_display(), name="pointer")) + + @staticmethod + def on_leave_notify_event(widget, event): + # print("HOVEE leave") + widget.get_window().set_cursor(cursor=None) + + @staticmethod + def on_carboncanvas_enter_notify_event(widget, event): + # print('carboncanvas hover') + # Set 'grab' cursor- + cursor = Gdk.Cursor.new_from_name(display=widget.get_display(), name="hand1") # TODO: Revert back to 'grab' + widget.get_window().set_cursor(cursor) + + +# def on_button_clicked(button): +# print("Clicked!!") +# +# +# def on_event_label_button_press_event(widget, event): +# pass # print(widget) +# +# +# def on_event_label_motion_notify_event(widget, event): +# widget.set_state_flags(Gtk.StateFlags.PRELIGHT, clear=True) + diff --git a/carbonpy/graphical/main_ui.py b/carbonpy/graphical/main_ui.py new file mode 100644 index 0000000..332da8f --- /dev/null +++ b/carbonpy/graphical/main_ui.py @@ -0,0 +1,34 @@ +try: + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk, Gdk +except ModuleNotFoundError: + raise ModuleNotFoundError("PyGObject isn't installed on your machine. Run pip install PyGObject") + +from interface import Interface +from playground import Playground +from builder import builder + + +handlers = { + "on_window1_destroy": Gtk.main_quit, + # "on_button_clicked": Interface.on_button_clicked, + "on_enter_notify_event": Interface.on_enter_notify_event, + "on_leave_notify_event": Interface.on_leave_notify_event, + # "on_event_label_button_press_event": on_event_label_button_press_event, + # "on_event_label_motion_notify_event": on_event_label_motion_notify_event, + "on_carbon_clicked": Playground().on_carbon_clicked, + "on_carboncanvas_enter_notify_event": Interface.on_carboncanvas_enter_notify_event, +} + +builder.connect_signals(handlers) + +cssProvider = Gtk.CssProvider() +cssProvider.load_from_path('./css_styles/styles.css') +screen = Gdk.Screen().get_default() +Gtk.StyleContext().add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + +window = builder.get_object("window1") +window.show_all() + +Gtk.main() diff --git a/carbonpy/graphical/playground.py b/carbonpy/graphical/playground.py new file mode 100644 index 0000000..d9f582e --- /dev/null +++ b/carbonpy/graphical/playground.py @@ -0,0 +1,131 @@ +# Class to handle addition of carbons, bonds, etc in carboncanvas. +from gi.repository import Gtk + +from interface import Interface +from builder import builder +from carbonpy.carbonpy.base.GraphElement import GraphElement + + +class Playground: + graph = {} + total_carbons = 1 + + fixed_canvas = builder.get_object("fixedcanvas") + + carbon_buttons: list = fixed_canvas.get_children() + this_node, next_node = None, None + + def on_carbon_clicked(self, button): + # bonds: ‒, =, ≡ + self.this_node = int(button.get_name()) + + x, y = self.fixed_canvas.child_get(button, "x", "y") + print(f"{x=},{y=}") + + molecule = GraphElement(value=self.this_node, comp=button.get_label(), x=x, y=y) + + if not self.graph: + self.graph[molecule] = [] + + self.do_physics() + + # space_available, to_place_x, to_place_y = self.get_adjacent_atoms(button, (x, y), self.fixed_canvas) + # print(space_available, to_place_x, to_place_y) + # + # if not space_available: + # return + + self.total_carbons += 1 + + self.next_node = self.total_carbons + connections = self.graph[self.this_node] + + if not connections: + carbon_label = "CH3" + button.set_label("CH3") + elif len(connections) == 1: + carbon_label = "CH2" + button.set_label(carbon_label) + elif len(connections) == 2: + carbon_label = "CH" + button.set_label(carbon_label) + elif len(connections) == 3: + carbon_label = "C" + button.set_label(carbon_label) + else: + return + + bond_button = self.make_bond_button(None, '‒') + carbon_button = self.make_carbon_button(carbon_label) + + self.set_properties(bond_button, carbon_button) + + # print(self.graph) + self.fixed_canvas.put(bond_button, x + 30, y) + self.fixed_canvas.put(carbon_button, to_place_x, to_place_y) + + # assert len(self.carbon_buttons) == self.total_carbons + + self.carbon_buttons[-1].set_label('CH3') # Terminal carbon should always be CH3 + print() + + def make_bond_button(self, widget, bond_type: str): + bond_button = Gtk.Button(label=bond_type, name="single", relief=Gtk.ReliefStyle.NONE) + + bond_button.connect("enter-notify-event", Interface.on_enter_notify_event) + bond_button.connect("leave-notify-event", Interface.on_leave_notify_event) + + Gtk.StyleContext.add_class(bond_button.get_style_context(), "bond") + + return bond_button + + def make_carbon_button(self, actual_rep: str): + carbon_button = Gtk.Button(label=actual_rep, name=self.next_node, relief=Gtk.ReliefStyle.NONE) + carbon_button.connect("clicked", self.on_carbon_clicked) + carbon_button.connect("enter-notify-event", Interface.on_enter_notify_event) + carbon_button.connect("leave-notify-event", Interface.on_leave_notify_event) + + self.carbon_buttons.append(carbon_button) + + self.add_edge() + return carbon_button + + def add_edge(self): + self.graph.setdefault(self.this_node, []) + self.graph.setdefault(self.next_node, []) + + self.graph[self.this_node].append(self.next_node) + self.graph[self.next_node].append(self.this_node) + + @staticmethod + def set_properties(*widgets): + for widget in widgets: + widget.set_can_focus(False) + widget.show() + + # def get_adjacent_atoms(self, button: Gtk.Button, initial_pos: tuple, canvas: Gtk.Container): + # if button.get_label() == "C": # Can't place any more atoms when octet is satisfied + # return False, None, None + # + # # assert len(initial_pos) == 2 + # + # current_x, current_y = initial_pos + # + # adj_positions = tuple(tuple(canvas.child_get(self.carbon_buttons[adj - 1], "x", "y")) + # for adj in self.graph[self.this_node]) # Obtain coords of each adj atom + # + # print(adj_positions) + # + # if all(adj_x != current_x + 45 for adj_x, adj_y in adj_positions): + # return True, current_x + 45, current_y + # elif all(adj_x != current_x - 45 for adj_x, adj_y in adj_positions): + # return True, current_x - 45, current_y + # elif all(adj_y != current_y + 23 for adj_x, adj_y in adj_positions): + # return True, current_x, current_y + 23 + # elif all(adj_y != current_y - 23 for adj_x, adj_y in adj_positions): + # return True, current_x, current_y - 23 + # + # return True, 323, 142 + + # def do_physics(self): + # force_on_node = \ No newline at end of file diff --git a/carbonpy/namer.py b/carbonpy/namer.py new file mode 100644 index 0000000..79e42c7 --- /dev/null +++ b/carbonpy/namer.py @@ -0,0 +1,172 @@ +from collections import deque +from typing import Union, List + +from base.compound import CompoundObject +from base.element import Element +from constants import multipl_suffixes +from rules.retained import convert +from rules.prefixes import get_prefix + + +class BaseNamer(CompoundObject): + def analyser(self) -> str: + compound_name = "" + many_bonds = "" # Is empty for saturated compounds + + # Checks valencies of atoms in compound- + self.valency_checker() + # Processing and deciding name(s) of the compound- + bond_type = self.suffix_namer() + + if any(suffix in bond_type for suffix in list(multipl_suffixes.values())): + many_bonds += "a-" # This is the 'a' in a compound like butadiene, for euphonic reasons + elif not bond_type == "ane": # If compound has only one unsaturated bond + many_bonds += "-" + compound_name += f"{get_prefix(count=self.carbons, parent=True)}{many_bonds}{bond_type}" + + if compound_name in convert: + compound_name = convert[compound_name] + + return compound_name # returns final name + + def suffix_namer(self) -> str: + lowest_db = lowest_tb = db_suffix = tb_suffix = "" # db,tb- double, triple bond + + lows_pos = self.lowest_position() + if not isinstance(lows_pos, dict): # If compound is saturated + return "ane" # Alkane + + for key, value in lows_pos.items(): + if value == '=': + lowest_db += f"{key}," # Adds position of double bond with ',' for more bonds + elif value == '~': + lowest_tb += f"{key}," # Same as above, except this time for triple bond + + lowest_tb = lowest_tb.strip(',') # Removes ',' + lowest_db = lowest_db.strip(',') + + # If many double/triple bonds present, get their suffix(di, tri, tetra, etc.) + if len(lowest_db) >= 3: + db_suffix = f"-{multipl_suffixes[len(lowest_db.replace(',', ''))]}" # Add that '-' too + else: + db_suffix += "-" # else only '-' + + if len(lowest_tb) >= 3: + tb_suffix = f"-{multipl_suffixes[len(lowest_tb.replace(',', ''))]}" + else: + tb_suffix += "-" + + if '=' in self.processing and '~' in self.processing: # If double and triple bond present + return f"{lowest_db}{db_suffix}en-{lowest_tb}{tb_suffix}yne" + + elif '~' in self.processing: # Only triple bond present + return f"{lowest_tb}{tb_suffix}yne" + + elif '=' in self.processing: # Only double bond present + return f"{lowest_db}{db_suffix}ene" # Return with di,tri,etc + + def bonds_only(self): + self.processing = self.processing.translate({ord(i): None for i in 'CH23'}) # Removes everything except bonds + + def lowest_position(self) -> Union[None, dict]: + """First point of difference rule used""" + lowest_front = {} + lowest_back = {} + # TODO: Maybe number from front and back simultaneously? (Also made me realize this may not work for isomers) + self.bonds_only() + # print(self.processing) + # Adds all occurrences from front + for index, string in enumerate(self.processing): + if string in ('=', '~'): + lowest_front[index + 1] = string # Adds position no. of bond + + # Adds all occurrences from back + for index, string in enumerate(''.join(reversed(self.processing))): + if string in ('=', '~'): + lowest_back[index + 1] = string + + assert (len(lowest_front) == len(lowest_back)) # Make sure they have the same length + for (index, value), (index2, value2) in zip(lowest_front.items(), lowest_back.items()): + # First point of difference- + if index < index2: + return lowest_front + elif index2 < index: + return lowest_back + elif index == index2: # Same index, check for precedence (only = and ~ for now) + # Double bond has more precedence than triple + if value == '=': # Will change into a dict access for func groups priority + return lowest_front + elif value2 == '=': + return lowest_back + + if len(lowest_front) == 0: + return None + else: + return lowest_back # Can also return front(if compound is symmetrical) + + def priority_order(self): + pass + + +class Branched(BaseNamer): + def traverse_node(self, terminal_node: Element): + # Using DFS approach- + stack, visited = deque([terminal_node.value]), deque([terminal_node.value]) + path = terminal_node.value + + while stack: + # noinspection PyTypeChecker + next_nodes = self.graph[stack[-1]] + if len(next_nodes) == 1 and path.count('C') > 1: # When terminal node is reached + yield path.split('-') + + for node in next_nodes: + if node not in visited: + path += f"-{node}" + visited.append(node) + stack.append(node) + break + else: + stack.pop() + path = "-".join(stack) + + def determine_longest(self): + possible_paths: List[List[str]] = [] + longest_paths: List[List[str]] = [] + length: int = 1 + + def calculate_longest(path_list): + nonlocal length, longest_paths + if len(path_list) > length: + length = len(path_list) + longest_paths = [path_list] + elif len(path_list) == length: + longest_paths.append(path_list) + + for terminal_node, value in self.graph.items(): + if len(value) == 1: + for path in self.traverse_node(terminal_node=terminal_node): + possible_paths.append(path) + + list(map(calculate_longest, possible_paths)) + # print(possible_paths) + # print(f"Number of possible paths: {len(possible_paths)}") + # print(longest_paths) + return longest_paths + + # def branch_splitter(self): # split branches and pass each of them to longest() + # + # print(self.processing) + # regex = re.compile('\((.*?)\)') + # chain = re.compile('C[H]*\((.*?)\).+') + # branches: list = regex.findall(self.processing) + # chained = chain.search(self.processing) + # print(chained) + # if branches: + # print(f"matched: {branches}") + # for match in branches: + # self.processing = re.sub(f'\({match}\)', '', self.processing) + # branches.append(self.processing) + # print(self.processing) + # print(branches) + # self.longest(branches) diff --git a/carbonpy/rules/__init__.py b/carbonpy/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/carbonpy/rules/prefixes.py b/carbonpy/rules/prefixes.py new file mode 100644 index 0000000..785bbdd --- /dev/null +++ b/carbonpy/rules/prefixes.py @@ -0,0 +1,47 @@ +common = {5: "pent", 6: "hex", 7: "hept", 8: "oct", 9: "non", 10: "dec", + 11: "undec", 20: "icos", 30: "tricont", 40: "tetracont", 50: "pentacont", 60: "hexacont", + 70: "heptacont", 80: "octacont", 90: "nonacont", 100: "hect", 101: "henhect", 200: "dict", 300: "trict", + 400: "tetract", 500: "pentact", 600: "hexact", 700: "heptact", 800: "octact", 900: "nonact", 1000: "kili", + 1001: "henkili", 2000: "dili", 3000: "trili", 4000: "tetrali", 5000: "pentali", 6000: "hexali", + 7000: "heptali", 8000: "octali", 9000: "nonali"} + +# For parent carbon chain- +prefixes = {1: "meth", 2: "eth", 3: "prop", 4: "but", **common} +# For multiple branches, bonds- +multi_prefixes = {1: "mono", 2: "di", 3: "tri", 4: "tetra", **{k: f"{v}a" for k, v in common.items()}} + + +def to_place_values(num: int): + if num > 99 and num % 100 == 11: + yield 11 + num -= 11 + + value = 1 + while num != 0: + last = num % 10 + yield last * value + num //= 10 + value *= 10 + + +def get_prefix(count: int, parent: bool = False): + try: + return prefixes[count] if parent else multi_prefixes[count] + except KeyError: + pass + + prefix = "" + for num in to_place_values(count): + if num == 0: + continue + elif num == 1: + prefix += "hen" + elif num == 2: + prefix += "do" + elif num == 11: # Special case for 11 where undeca will be used + prefix += multi_prefixes[num] + elif num == 20 and prefix[-1] in "aeiou": # 'i' in 'icosa' is elided if it's after a vowel + prefix += multi_prefixes[num][1:] + else: + prefix += multi_prefixes[num] + return prefix[:-1] if parent else prefix diff --git a/carbonpy/rules/retained.py b/carbonpy/rules/retained.py new file mode 100644 index 0000000..96fa5a6 --- /dev/null +++ b/carbonpy/rules/retained.py @@ -0,0 +1 @@ +convert = {'2-methylpropan-2-yl': '𝘵𝘦𝘳𝘵-butyl', 'eth-1-yne': 'acetylene'} diff --git a/carbonpy/version.py b/carbonpy/version.py new file mode 100644 index 0000000..8f17881 --- /dev/null +++ b/carbonpy/version.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +# Copyright © 2020 Harshil Mehta + +# version.py - File containing current version number. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = '0.52' diff --git a/gui.py b/gui.py deleted file mode 100644 index 390243d..0000000 --- a/gui.py +++ /dev/null @@ -1,6 +0,0 @@ -# The bhaiUI - -import tkinter - -homescreen = tkinter.Tk() -canvas = tkinter.canvas(homescreen, width diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_compound.py b/tests/test_compound.py new file mode 100644 index 0000000..d18b1c2 --- /dev/null +++ b/tests/test_compound.py @@ -0,0 +1,70 @@ +import pytest +from collections import deque +from carbonpy import ValencyError, CompoundObject, Element + +comps = ['CH4', 'CH~CH', 'CH~C-C~C-CH=C=C=CH2', 'CH3-C~C-CH3', 'CH2=CH2', 'CH2=CH-CH=CH-CH=CH2', 'CH~C-CH=CH2'] + +formulas = ['CH₄', 'C₂H₂', 'C₈H₄', 'C₄H₆', 'C₂H₄', 'C₆H₈', 'C₄H₄'] + +carbs_hyds = (1, 4), (2, 2), (8, 4), (4, 6), (2, 4), (6, 8), (4, 4) + + +class TestCompoundObject: + + @pytest.mark.parametrize(argnames='compound,atom', argvalues=zip(comps, carbs_hyds)) + def test_attributes(self, compound, atom): + comp = CompoundObject(compound) + assert comp.carbons == atom[0] + assert comp.hydrogens == atom[1] + assert len(comp._carbon_comps) - 1 == len(comp._bonds_only) + + @pytest.mark.parametrize(argnames='compound,molecular_form', argvalues=zip(comps, formulas)) + def test_molecular_formula(self, compound, molecular_form): + assert CompoundObject(compound).molecular_formula() == molecular_form + + def test_molar_mass(self): + assert round(CompoundObject('CH~CH').molar_mass, 2) == 26.04 + + def test_comparisons(self): + assert CompoundObject('CH~CH') > CompoundObject('CH4') == CompoundObject('CH4') != CompoundObject('CH3-CH3') + + def test_valency_checker(self): + assert CompoundObject('CH3-CH=CH2').valency_checker() + assert CompoundObject('CH3-C(-CH=CH-CH3)(-CH2-C~CH)-CH3').valency_checker() + + with pytest.raises(ValencyError): + CompoundObject('CH3-C=CH2').valency_checker() + CompoundObject('CH~C-CH(-C(-CH3)(-CH2-CH3)-CH3)-C(-CH=CH(-CH2-CH3)-CH3)(-CH3)-CH2-CH3').valency_checker() + + def test_excess_carbons(self): + with pytest.raises(ValueError): + CompoundObject(f"CH3{'-CH2' * 10000}-CH3") + + def test_atom_counter(self): + with pytest.raises(ValueError): + CompoundObject('CH4').atom_counter('N') + + def test_graph(self): + a = CompoundObject('CH~C-CH(-C(-CH3)(-CH=CH2)-CH3)-C(-CH=C(-CH3)-CH3)(-CH3)-CH3') + graph = {'C1': ['C2'], + 'C2': ['C1', 'C3'], + 'C3': ['C2', 'C4', 'C9'], + 'C4': ['C3', 'C5', 'C6', 'C8'], + 'C5': ['C4'], + 'C6': ['C4', 'C7'], + 'C7': ['C6'], + 'C8': ['C4'], + 'C9': ['C3', 'C10', 'C14', 'C15'], + 'C10': ['C9', 'C11'], + 'C11': ['C10', 'C12', 'C13'], + 'C12': ['C11'], + 'C13': ['C11'], + 'C14': ['C9'], + 'C15': ['C9']} + + for element, nodes in graph.items(): + assert a.graph[element] == deque(nodes) + + def test_iteration(self): + for element in CompoundObject('CH3-C(-CH=CH-CH3)(-CH2-C~CH)-CH3'): + assert isinstance(element, Element) diff --git a/tests/test_element.py b/tests/test_element.py new file mode 100644 index 0000000..c933db0 --- /dev/null +++ b/tests/test_element.py @@ -0,0 +1,27 @@ +import pytest + +from carbonpy import Element + + +element_dict = {} +values = (1, 2, 3) + +elements = [Element('C1', 'CH3'), Element('C2', 'CH'), Element('C3', 'C')] + +comp_n_value = (('C1', 'CH3', 3), ('C2', 'CH', 1), ('C3', 'C', 0)) + + +@pytest.fixture(params=elements) +def element(request): + return request.param + + +@pytest.mark.parametrize(argnames='element,attr', argvalues=zip(elements, comp_n_value)) +def test_element_attributes(element, attr): + assert element.value == attr[0] and element.comp == attr[1] and element.hydrogens() == attr[2] + + +@pytest.mark.parametrize(argnames='element,value', argvalues=zip(elements, values)) +def test_dict_access(element, value): + element_dict[element] = value + assert element_dict[element] == element_dict[element.value] == value diff --git a/tests/test_namer.py b/tests/test_namer.py new file mode 100644 index 0000000..b673fab --- /dev/null +++ b/tests/test_namer.py @@ -0,0 +1,14 @@ +import pytest + +from carbonpy import BaseNamer + + +names = ['methane', 'acetylene', 'octa-1,2,3-trien-5,7-diyne', 'but-2-yne', 'eth-1-ene', 'hexa-1,3,5-triene', + 'but-1-en-3-yne'] + +comps = ['CH4', 'CH~CH', 'CH~C-C~C-CH=C=C=CH2', 'CH3-C~C-CH3', 'CH2=CH2', 'CH2=CH-CH=CH-CH=CH2', 'CH~C-CH=CH2'] + + +@pytest.mark.parametrize(argnames='compound,name', argvalues=zip(comps, names)) +def test_analyser(compound, name): + assert BaseNamer(compound).analyser() == name diff --git a/tests/test_rules/test_prefixes.py b/tests/test_rules/test_prefixes.py new file mode 100644 index 0000000..6aa12ec --- /dev/null +++ b/tests/test_rules/test_prefixes.py @@ -0,0 +1,18 @@ +import pytest + +from carbonpy.rules.prefixes import to_place_values, get_prefix + +numbers = [14, 23, 41, 52, 363] +prefixes = ["tetradec", "tricos", "hentetracont", "dopentacont", "trihexacontatrict"] + + +def test_to_place_values(): + assert list(to_place_values(100)) == [0, 0, 100] + assert list(to_place_values(123)) == [3, 20, 100] + assert list(to_place_values(1043)) == [3, 40, 0, 1000] + + +@pytest.mark.parametrize(argnames='number,prefix', argvalues=zip(numbers, prefixes)) +def test_get_prefix(number, prefix): + assert get_prefix(number, parent=True) == prefix + assert get_prefix(number, parent=False) == f"{prefix}a"