atomecon 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atomecon-0.1.0/PKG-INFO +196 -0
- atomecon-0.1.0/README.md +179 -0
- atomecon-0.1.0/pyproject.toml +29 -0
- atomecon-0.1.0/setup.cfg +4 -0
- atomecon-0.1.0/src/atomecon/__init__.py +24 -0
- atomecon-0.1.0/src/atomecon/balance.py +163 -0
- atomecon-0.1.0/src/atomecon/convenience.py +27 -0
- atomecon-0.1.0/src/atomecon/elements.py +34 -0
- atomecon-0.1.0/src/atomecon/formula.py +104 -0
- atomecon-0.1.0/src/atomecon/reaction.py +349 -0
- atomecon-0.1.0/src/atomecon/reaction_log.py +121 -0
- atomecon-0.1.0/src/atomecon/valence.py +117 -0
- atomecon-0.1.0/src/atomecon.egg-info/PKG-INFO +196 -0
- atomecon-0.1.0/src/atomecon.egg-info/SOURCES.txt +19 -0
- atomecon-0.1.0/src/atomecon.egg-info/dependency_links.txt +1 -0
- atomecon-0.1.0/src/atomecon.egg-info/top_level.txt +1 -0
- atomecon-0.1.0/tests/test_balance.py +62 -0
- atomecon-0.1.0/tests/test_formula.py +61 -0
- atomecon-0.1.0/tests/test_reaction.py +172 -0
- atomecon-0.1.0/tests/test_reaction_log.py +79 -0
- atomecon-0.1.0/tests/test_valence.py +43 -0
atomecon-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atomecon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight green chemistry metrics (atom economy, E-factor, yield) from plain chemical formulas - no RDKit, no SMILES.
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/atomecon
|
|
8
|
+
Project-URL: Issues, https://github.com/YOUR_USERNAME/atomecon/issues
|
|
9
|
+
Keywords: chemistry,green-chemistry,atom-economy,stoichiometry,e-factor
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# atomecon
|
|
19
|
+
|
|
20
|
+
🧪 **[Try it live in your browser](https://atomecon.streamlit.app/)** - no install needed.
|
|
21
|
+
|
|
22
|
+
Lightweight green chemistry metrics - atom economy, theoretical yield, percent yield, E-factor, and automatic equation balancing - computed from **plain chemical formulas**. No RDKit, no SMILES, no heavy dependencies.
|
|
23
|
+
|
|
24
|
+
## Who this is for
|
|
25
|
+
|
|
26
|
+
- Chemistry students taking green chemistry coursework, tired of redoing molar-mass arithmetic by hand for every reaction
|
|
27
|
+
- Anyone comparing multiple synthesis routes for the same product, who wants to score them programmatically instead of recalculating each one manually
|
|
28
|
+
- Developers who want atom economy and equation balancing without installing RDKit or learning SMILES notation
|
|
29
|
+
|
|
30
|
+
## Why this exists
|
|
31
|
+
|
|
32
|
+
Existing chemistry packages either don't cover green chemistry metrics at all (they focus on general stoichiometry), or require RDKit and SMILES notation to calculate even a single metric like atom economy. `atomecon` bundles the standard metrics together, balances equations automatically, takes formulas the way you'd write them in a chemistry class (`"C2H5OH"`, not `"CCO"`), and has zero dependencies.
|
|
33
|
+
|
|
34
|
+
## Try it without installing anything
|
|
35
|
+
|
|
36
|
+
**[atomecon.streamlit.app](https://atomecon.streamlit.app/)** - type in a reaction's formulas, get it balanced and analyzed instantly in your browser.
|
|
37
|
+
|
|
38
|
+
## Install (to use in your own Python code)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install directly from this repo:
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/JvenDeepak0203/atomecon.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Core concepts
|
|
50
|
+
|
|
51
|
+
- **Atom economy** is *theoretical* - it only depends on the balanced equation and molar masses. It's always computable, with no lab data.
|
|
52
|
+
- **Theoretical yield**, **percent yield**, and **E-factor** are *experimental* - they depend on the real masses of reactants you used and the real mass of product you isolated. These vary run to run.
|
|
53
|
+
|
|
54
|
+
`atomecon` keeps this distinction explicit: `atom_economy()` takes no arguments beyond the reaction itself, while `e_factor()` and `percent_yield()` require you to supply real measured masses.
|
|
55
|
+
|
|
56
|
+
## Quick start - the one-line version
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from atomecon import analyze
|
|
60
|
+
|
|
61
|
+
analyze(["CH4", "O2"], ["CO2", "H2O"], desired_product="CO2")
|
|
62
|
+
```
|
|
63
|
+
Balances the equation automatically and prints a full report - no coefficients, no separate method calls needed.
|
|
64
|
+
|
|
65
|
+
## Full walkthrough
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from atomecon import Reaction
|
|
69
|
+
|
|
70
|
+
# Aspirin synthesis: salicylic acid + acetic anhydride -> aspirin + acetic acid
|
|
71
|
+
rxn = Reaction(
|
|
72
|
+
reactants={"C7H6O3": 1, "C4H6O3": 1},
|
|
73
|
+
products={"C9H8O4": 1, "C2H4O2": 1},
|
|
74
|
+
desired_product="C9H8O4",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(rxn.atom_economy())
|
|
78
|
+
# 75.0 (theoretical - no lab data needed)
|
|
79
|
+
|
|
80
|
+
masses = {"C7H6O3": 5.0, "C4H6O3": 5.0}
|
|
81
|
+
print(rxn.theoretical_yield_g(masses))
|
|
82
|
+
print(rxn.percent_yield(masses, actual_yield_g=4.2))
|
|
83
|
+
print(rxn.e_factor(masses, actual_yield_g=4.2))
|
|
84
|
+
print(rxn.green_grade(masses, actual_yield_g=4.2))
|
|
85
|
+
|
|
86
|
+
print(rxn.summary_table(reactant_masses_g=masses, actual_yield_g=4.2))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Automatic equation balancing
|
|
90
|
+
|
|
91
|
+
Don't want to work out coefficients yourself? Just give formulas:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from atomecon import Reaction
|
|
95
|
+
|
|
96
|
+
rxn = Reaction.auto(["N2", "H2"], ["NH3"], desired_product="NH3")
|
|
97
|
+
print(rxn)
|
|
98
|
+
# <Reaction N2 + 3H2 -> 2NH3>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This uses real linear algebra (Gaussian elimination over exact fractions) to solve for the smallest whole-number coefficients - the same process you'd do by hand, automated.
|
|
102
|
+
|
|
103
|
+
**Known limitation:** a small number of equations have more than one valid balancing ratio (a genuine mathematical ambiguity, not a bug) and will raise a clear error asking you to specify coefficients manually instead of guessing.
|
|
104
|
+
|
|
105
|
+
## Learning mode: see the calculation, not just the answer
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
print(rxn.explain_atom_economy())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Green grade: one number to compare reactions at a glance
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
print(rxn.green_grade(masses, actual_yield_g=4.2))
|
|
115
|
+
# B (Atom economy: 75% | E-factor: 1.4)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A simple, transparent scoring rule (not a scientific standard) - combines atom economy and E-factor into a single A-F grade.
|
|
119
|
+
|
|
120
|
+
## Comparing multiple reactions
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from atomecon import Reaction, ReactionLog
|
|
124
|
+
|
|
125
|
+
log = ReactionLog()
|
|
126
|
+
log.add("Aspirin route", rxn, reactant_masses_g=masses, actual_yield_g=4.2)
|
|
127
|
+
|
|
128
|
+
combustion = Reaction.auto(["CH4", "O2"], ["CO2", "H2O"], desired_product="CO2")
|
|
129
|
+
log.add("Methane combustion", combustion) # lab data is optional per entry
|
|
130
|
+
|
|
131
|
+
print(log.comparison_table())
|
|
132
|
+
```
|
|
133
|
+
Builds a table sorted by atom economy (greenest first). `ReactionLog` is in-memory only - it resets each time your program runs.
|
|
134
|
+
|
|
135
|
+
## Formula plausibility checking
|
|
136
|
+
|
|
137
|
+
Before balancing, `atomecon` can flag formulas that are chemically impossible using valence-parity math (every bond connects exactly 2 atoms, so a molecule's total valence must be even):
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from atomecon import is_formula_plausible
|
|
141
|
+
|
|
142
|
+
is_formula_plausible("C8H18") # True (real octane)
|
|
143
|
+
is_formula_plausible("C8H23") # False (impossible - odd total valence)
|
|
144
|
+
is_formula_plausible("Fe2O3") # True (iron has variable valence - not checked, benefit of the doubt)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Important limitation:** this can only rule out formulas as impossible - it cannot prove a formula is real, and it deliberately skips elements with variable real-world valence (iron, sulfur, phosphorus, nitrogen, most transition metals) rather than risk a wrong answer. This is a long way from full molecular validity checking (which is what RDKit does using real molecular structure) - it's one useful mathematical shortcut, not a replacement for it.
|
|
148
|
+
|
|
149
|
+
## Formula syntax
|
|
150
|
+
|
|
151
|
+
Supports condensed formulas and nested parentheses:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from atomecon import parse_formula, molar_mass
|
|
155
|
+
|
|
156
|
+
parse_formula("Ca(OH)2") # {"Ca": 1, "O": 2, "H": 2}
|
|
157
|
+
parse_formula("Fe3(Fe(CN)6)2") # {"Fe": 5, "C": 12, "N": 12}
|
|
158
|
+
molar_mass("C6H12O6") # 180.156
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Reaction balance checking
|
|
162
|
+
|
|
163
|
+
`Reaction` verifies the equation is atom-balanced on construction and raises a clear error if it isn't (atom economy is not meaningful for an unbalanced equation). Pass `allow_unbalanced=True` to override.
|
|
164
|
+
|
|
165
|
+
## What this library does NOT do (known scope limits)
|
|
166
|
+
|
|
167
|
+
Being upfront about the boundaries:
|
|
168
|
+
- **Does not verify a formula represents a real molecule** beyond the basic valence-parity check above - no bonding/structure model like RDKit
|
|
169
|
+
- **Does not verify a reaction is chemically real** - if you give it atom-balanced but chemically implausible reactants/products (e.g. a reaction that wouldn't actually occur), it will still calculate metrics for it. Verifying real reaction mechanisms is a much larger problem (closer to quantum chemistry / reaction databases) that's out of scope here.
|
|
170
|
+
- **No charge/ionic support** - formulas are tracked by atoms only, not electric charge, so redox half-reactions and charged species aren't supported
|
|
171
|
+
|
|
172
|
+
## Try the demo script
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python demo.py
|
|
176
|
+
```
|
|
177
|
+
Runs through every feature of the library end to end.
|
|
178
|
+
|
|
179
|
+
## Web app
|
|
180
|
+
|
|
181
|
+
The `app.py` file is a Streamlit interface to the library - see it live at [atomecon.streamlit.app](https://atomecon.streamlit.app/), or run it yourself:
|
|
182
|
+
```bash
|
|
183
|
+
pip install streamlit
|
|
184
|
+
streamlit run app.py
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Running tests
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install pytest
|
|
191
|
+
pytest
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
atomecon-0.1.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# atomecon
|
|
2
|
+
|
|
3
|
+
🧪 **[Try it live in your browser](https://atomecon.streamlit.app/)** - no install needed.
|
|
4
|
+
|
|
5
|
+
Lightweight green chemistry metrics - atom economy, theoretical yield, percent yield, E-factor, and automatic equation balancing - computed from **plain chemical formulas**. No RDKit, no SMILES, no heavy dependencies.
|
|
6
|
+
|
|
7
|
+
## Who this is for
|
|
8
|
+
|
|
9
|
+
- Chemistry students taking green chemistry coursework, tired of redoing molar-mass arithmetic by hand for every reaction
|
|
10
|
+
- Anyone comparing multiple synthesis routes for the same product, who wants to score them programmatically instead of recalculating each one manually
|
|
11
|
+
- Developers who want atom economy and equation balancing without installing RDKit or learning SMILES notation
|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
Existing chemistry packages either don't cover green chemistry metrics at all (they focus on general stoichiometry), or require RDKit and SMILES notation to calculate even a single metric like atom economy. `atomecon` bundles the standard metrics together, balances equations automatically, takes formulas the way you'd write them in a chemistry class (`"C2H5OH"`, not `"CCO"`), and has zero dependencies.
|
|
16
|
+
|
|
17
|
+
## Try it without installing anything
|
|
18
|
+
|
|
19
|
+
**[atomecon.streamlit.app](https://atomecon.streamlit.app/)** - type in a reaction's formulas, get it balanced and analyzed instantly in your browser.
|
|
20
|
+
|
|
21
|
+
## Install (to use in your own Python code)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install directly from this repo:
|
|
28
|
+
```bash
|
|
29
|
+
pip install git+https://github.com/JvenDeepak0203/atomecon.git
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Core concepts
|
|
33
|
+
|
|
34
|
+
- **Atom economy** is *theoretical* - it only depends on the balanced equation and molar masses. It's always computable, with no lab data.
|
|
35
|
+
- **Theoretical yield**, **percent yield**, and **E-factor** are *experimental* - they depend on the real masses of reactants you used and the real mass of product you isolated. These vary run to run.
|
|
36
|
+
|
|
37
|
+
`atomecon` keeps this distinction explicit: `atom_economy()` takes no arguments beyond the reaction itself, while `e_factor()` and `percent_yield()` require you to supply real measured masses.
|
|
38
|
+
|
|
39
|
+
## Quick start - the one-line version
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from atomecon import analyze
|
|
43
|
+
|
|
44
|
+
analyze(["CH4", "O2"], ["CO2", "H2O"], desired_product="CO2")
|
|
45
|
+
```
|
|
46
|
+
Balances the equation automatically and prints a full report - no coefficients, no separate method calls needed.
|
|
47
|
+
|
|
48
|
+
## Full walkthrough
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from atomecon import Reaction
|
|
52
|
+
|
|
53
|
+
# Aspirin synthesis: salicylic acid + acetic anhydride -> aspirin + acetic acid
|
|
54
|
+
rxn = Reaction(
|
|
55
|
+
reactants={"C7H6O3": 1, "C4H6O3": 1},
|
|
56
|
+
products={"C9H8O4": 1, "C2H4O2": 1},
|
|
57
|
+
desired_product="C9H8O4",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
print(rxn.atom_economy())
|
|
61
|
+
# 75.0 (theoretical - no lab data needed)
|
|
62
|
+
|
|
63
|
+
masses = {"C7H6O3": 5.0, "C4H6O3": 5.0}
|
|
64
|
+
print(rxn.theoretical_yield_g(masses))
|
|
65
|
+
print(rxn.percent_yield(masses, actual_yield_g=4.2))
|
|
66
|
+
print(rxn.e_factor(masses, actual_yield_g=4.2))
|
|
67
|
+
print(rxn.green_grade(masses, actual_yield_g=4.2))
|
|
68
|
+
|
|
69
|
+
print(rxn.summary_table(reactant_masses_g=masses, actual_yield_g=4.2))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Automatic equation balancing
|
|
73
|
+
|
|
74
|
+
Don't want to work out coefficients yourself? Just give formulas:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from atomecon import Reaction
|
|
78
|
+
|
|
79
|
+
rxn = Reaction.auto(["N2", "H2"], ["NH3"], desired_product="NH3")
|
|
80
|
+
print(rxn)
|
|
81
|
+
# <Reaction N2 + 3H2 -> 2NH3>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This uses real linear algebra (Gaussian elimination over exact fractions) to solve for the smallest whole-number coefficients - the same process you'd do by hand, automated.
|
|
85
|
+
|
|
86
|
+
**Known limitation:** a small number of equations have more than one valid balancing ratio (a genuine mathematical ambiguity, not a bug) and will raise a clear error asking you to specify coefficients manually instead of guessing.
|
|
87
|
+
|
|
88
|
+
## Learning mode: see the calculation, not just the answer
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
print(rxn.explain_atom_economy())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Green grade: one number to compare reactions at a glance
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
print(rxn.green_grade(masses, actual_yield_g=4.2))
|
|
98
|
+
# B (Atom economy: 75% | E-factor: 1.4)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A simple, transparent scoring rule (not a scientific standard) - combines atom economy and E-factor into a single A-F grade.
|
|
102
|
+
|
|
103
|
+
## Comparing multiple reactions
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from atomecon import Reaction, ReactionLog
|
|
107
|
+
|
|
108
|
+
log = ReactionLog()
|
|
109
|
+
log.add("Aspirin route", rxn, reactant_masses_g=masses, actual_yield_g=4.2)
|
|
110
|
+
|
|
111
|
+
combustion = Reaction.auto(["CH4", "O2"], ["CO2", "H2O"], desired_product="CO2")
|
|
112
|
+
log.add("Methane combustion", combustion) # lab data is optional per entry
|
|
113
|
+
|
|
114
|
+
print(log.comparison_table())
|
|
115
|
+
```
|
|
116
|
+
Builds a table sorted by atom economy (greenest first). `ReactionLog` is in-memory only - it resets each time your program runs.
|
|
117
|
+
|
|
118
|
+
## Formula plausibility checking
|
|
119
|
+
|
|
120
|
+
Before balancing, `atomecon` can flag formulas that are chemically impossible using valence-parity math (every bond connects exactly 2 atoms, so a molecule's total valence must be even):
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from atomecon import is_formula_plausible
|
|
124
|
+
|
|
125
|
+
is_formula_plausible("C8H18") # True (real octane)
|
|
126
|
+
is_formula_plausible("C8H23") # False (impossible - odd total valence)
|
|
127
|
+
is_formula_plausible("Fe2O3") # True (iron has variable valence - not checked, benefit of the doubt)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Important limitation:** this can only rule out formulas as impossible - it cannot prove a formula is real, and it deliberately skips elements with variable real-world valence (iron, sulfur, phosphorus, nitrogen, most transition metals) rather than risk a wrong answer. This is a long way from full molecular validity checking (which is what RDKit does using real molecular structure) - it's one useful mathematical shortcut, not a replacement for it.
|
|
131
|
+
|
|
132
|
+
## Formula syntax
|
|
133
|
+
|
|
134
|
+
Supports condensed formulas and nested parentheses:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from atomecon import parse_formula, molar_mass
|
|
138
|
+
|
|
139
|
+
parse_formula("Ca(OH)2") # {"Ca": 1, "O": 2, "H": 2}
|
|
140
|
+
parse_formula("Fe3(Fe(CN)6)2") # {"Fe": 5, "C": 12, "N": 12}
|
|
141
|
+
molar_mass("C6H12O6") # 180.156
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Reaction balance checking
|
|
145
|
+
|
|
146
|
+
`Reaction` verifies the equation is atom-balanced on construction and raises a clear error if it isn't (atom economy is not meaningful for an unbalanced equation). Pass `allow_unbalanced=True` to override.
|
|
147
|
+
|
|
148
|
+
## What this library does NOT do (known scope limits)
|
|
149
|
+
|
|
150
|
+
Being upfront about the boundaries:
|
|
151
|
+
- **Does not verify a formula represents a real molecule** beyond the basic valence-parity check above - no bonding/structure model like RDKit
|
|
152
|
+
- **Does not verify a reaction is chemically real** - if you give it atom-balanced but chemically implausible reactants/products (e.g. a reaction that wouldn't actually occur), it will still calculate metrics for it. Verifying real reaction mechanisms is a much larger problem (closer to quantum chemistry / reaction databases) that's out of scope here.
|
|
153
|
+
- **No charge/ionic support** - formulas are tracked by atoms only, not electric charge, so redox half-reactions and charged species aren't supported
|
|
154
|
+
|
|
155
|
+
## Try the demo script
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python demo.py
|
|
159
|
+
```
|
|
160
|
+
Runs through every feature of the library end to end.
|
|
161
|
+
|
|
162
|
+
## Web app
|
|
163
|
+
|
|
164
|
+
The `app.py` file is a Streamlit interface to the library - see it live at [atomecon.streamlit.app](https://atomecon.streamlit.app/), or run it yourself:
|
|
165
|
+
```bash
|
|
166
|
+
pip install streamlit
|
|
167
|
+
streamlit run app.py
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Running tests
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pip install pytest
|
|
174
|
+
pytest
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "atomecon"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight green chemistry metrics (atom economy, E-factor, yield) from plain chemical formulas - no RDKit, no SMILES."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Your Name", email = "you@example.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["chemistry", "green-chemistry", "atom-economy", "stoichiometry", "e-factor"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Intended Audience :: Education",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/YOUR_USERNAME/atomecon"
|
|
26
|
+
Issues = "https://github.com/YOUR_USERNAME/atomecon/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
atomecon-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
atomecon - lightweight green chemistry metrics from plain chemical formulas.
|
|
3
|
+
|
|
4
|
+
No RDKit, no SMILES - just formulas and molar masses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .formula import parse_formula, molar_mass
|
|
8
|
+
from .reaction import Reaction
|
|
9
|
+
from .reaction_log import ReactionLog
|
|
10
|
+
from .balance import balance_equation
|
|
11
|
+
from .convenience import analyze
|
|
12
|
+
from .valence import is_formula_plausible, explain_formula_plausibility
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"parse_formula",
|
|
16
|
+
"molar_mass",
|
|
17
|
+
"Reaction",
|
|
18
|
+
"ReactionLog",
|
|
19
|
+
"balance_equation",
|
|
20
|
+
"analyze",
|
|
21
|
+
"is_formula_plausible",
|
|
22
|
+
"explain_formula_plausibility",
|
|
23
|
+
]
|
|
24
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automatic chemical equation balancing.
|
|
3
|
+
|
|
4
|
+
Given just the reactant and product formulas (no coefficients), this
|
|
5
|
+
works out the smallest whole-number coefficients that balance the
|
|
6
|
+
equation - the same thing you'd do by hand in intro chemistry, done
|
|
7
|
+
here with linear algebra instead of trial and error.
|
|
8
|
+
|
|
9
|
+
HOW IT WORKS (high level):
|
|
10
|
+
Every element must appear in equal total amounts on both sides. That
|
|
11
|
+
gives one equation per element, with the coefficients as the unknowns.
|
|
12
|
+
Solving that system of equations (its "null space") gives the ratio of
|
|
13
|
+
coefficients; scaling that ratio up to whole numbers gives the answer.
|
|
14
|
+
|
|
15
|
+
This file is more mathematically advanced than the rest of the library
|
|
16
|
+
(it uses exact fractions and row reduction). If you're reading through
|
|
17
|
+
line by line, it's fine to treat this as "trusted machinery" - the way
|
|
18
|
+
you'd use a library function without re-deriving how it works - rather
|
|
19
|
+
than trace every line the way we did with formula.py.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from fractions import Fraction
|
|
24
|
+
from typing import Dict, List, Tuple
|
|
25
|
+
|
|
26
|
+
from .formula import parse_formula
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def balance_equation(
|
|
30
|
+
reactant_formulas: List[str], product_formulas: List[str]
|
|
31
|
+
) -> Tuple[Dict[str, int], Dict[str, int]]:
|
|
32
|
+
species = reactant_formulas + product_formulas
|
|
33
|
+
num_species = len(species)
|
|
34
|
+
|
|
35
|
+
element_counts_per_species = []
|
|
36
|
+
all_elements = []
|
|
37
|
+
for formula in species:
|
|
38
|
+
counts = parse_formula(formula)
|
|
39
|
+
element_counts_per_species.append(counts)
|
|
40
|
+
for element in counts:
|
|
41
|
+
if element not in all_elements:
|
|
42
|
+
all_elements.append(element)
|
|
43
|
+
|
|
44
|
+
num_reactants = len(reactant_formulas)
|
|
45
|
+
|
|
46
|
+
matrix = []
|
|
47
|
+
for element in all_elements:
|
|
48
|
+
row = []
|
|
49
|
+
for i, counts in enumerate(element_counts_per_species):
|
|
50
|
+
count = counts.get(element, 0)
|
|
51
|
+
if i >= num_reactants:
|
|
52
|
+
count = -count
|
|
53
|
+
row.append(Fraction(count))
|
|
54
|
+
matrix.append(row)
|
|
55
|
+
|
|
56
|
+
coefficients = _solve_null_space(matrix, num_species)
|
|
57
|
+
|
|
58
|
+
reactant_coeffs = {}
|
|
59
|
+
for i in range(num_reactants):
|
|
60
|
+
reactant_coeffs[reactant_formulas[i]] = coefficients[i]
|
|
61
|
+
|
|
62
|
+
product_coeffs = {}
|
|
63
|
+
for i in range(num_reactants, num_species):
|
|
64
|
+
product_index = i - num_reactants
|
|
65
|
+
product_coeffs[product_formulas[product_index]] = coefficients[i]
|
|
66
|
+
|
|
67
|
+
return reactant_coeffs, product_coeffs
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _solve_null_space(matrix, num_species):
|
|
71
|
+
num_rows = len(matrix)
|
|
72
|
+
|
|
73
|
+
working = []
|
|
74
|
+
for row in matrix:
|
|
75
|
+
working.append(list(row))
|
|
76
|
+
|
|
77
|
+
pivot_columns = []
|
|
78
|
+
current_row = 0
|
|
79
|
+
|
|
80
|
+
for col in range(num_species):
|
|
81
|
+
pivot_row = None
|
|
82
|
+
for r in range(current_row, num_rows):
|
|
83
|
+
if working[r][col] != 0:
|
|
84
|
+
pivot_row = r
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
if pivot_row is None:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
working[current_row], working[pivot_row] = working[pivot_row], working[current_row]
|
|
91
|
+
|
|
92
|
+
pivot_value = working[current_row][col]
|
|
93
|
+
for c in range(num_species):
|
|
94
|
+
working[current_row][c] = working[current_row][c] / pivot_value
|
|
95
|
+
|
|
96
|
+
for r in range(num_rows):
|
|
97
|
+
if r != current_row and working[r][col] != 0:
|
|
98
|
+
factor = working[r][col]
|
|
99
|
+
for c in range(num_species):
|
|
100
|
+
working[r][c] = working[r][c] - factor * working[current_row][c]
|
|
101
|
+
|
|
102
|
+
pivot_columns.append(col)
|
|
103
|
+
current_row += 1
|
|
104
|
+
if current_row == num_rows:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
free_columns = []
|
|
108
|
+
for col in range(num_species):
|
|
109
|
+
if col not in pivot_columns:
|
|
110
|
+
free_columns.append(col)
|
|
111
|
+
|
|
112
|
+
if len(free_columns) != 1:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"Could not automatically balance this equation (expected exactly "
|
|
115
|
+
"one degree of freedom, found "
|
|
116
|
+
f"{len(free_columns)}). Try specifying coefficients manually "
|
|
117
|
+
"with Reaction(reactants={...}, products={...})."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
free_col = free_columns[0]
|
|
121
|
+
|
|
122
|
+
values = [Fraction(0)] * num_species
|
|
123
|
+
values[free_col] = Fraction(1)
|
|
124
|
+
|
|
125
|
+
for i, col in enumerate(pivot_columns):
|
|
126
|
+
values[col] = -working[i][free_col]
|
|
127
|
+
|
|
128
|
+
lcm_of_denominators = 1
|
|
129
|
+
for v in values:
|
|
130
|
+
lcm_of_denominators = _lcm(lcm_of_denominators, v.denominator)
|
|
131
|
+
|
|
132
|
+
whole_numbers = []
|
|
133
|
+
for v in values:
|
|
134
|
+
whole_numbers.append(int(v * lcm_of_denominators))
|
|
135
|
+
|
|
136
|
+
all_non_positive = True
|
|
137
|
+
for w in whole_numbers:
|
|
138
|
+
if w > 0:
|
|
139
|
+
all_non_positive = False
|
|
140
|
+
if all_non_positive:
|
|
141
|
+
whole_numbers = [-w for w in whole_numbers]
|
|
142
|
+
|
|
143
|
+
for w in whole_numbers:
|
|
144
|
+
if w <= 0:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"Could not automatically balance this equation into all "
|
|
147
|
+
"positive coefficients. Try specifying coefficients "
|
|
148
|
+
"manually with Reaction(reactants={...}, products={...})."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
overall_gcd = whole_numbers[0]
|
|
152
|
+
for w in whole_numbers[1:]:
|
|
153
|
+
overall_gcd = math.gcd(overall_gcd, w)
|
|
154
|
+
|
|
155
|
+
simplified = []
|
|
156
|
+
for w in whole_numbers:
|
|
157
|
+
simplified.append(w // overall_gcd)
|
|
158
|
+
|
|
159
|
+
return simplified
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _lcm(a, b):
|
|
163
|
+
return a * b // math.gcd(a, b)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
One-call convenience function for the common case: you just have
|
|
3
|
+
formulas (no coefficients) and, optionally, real lab masses, and you
|
|
4
|
+
want the full picture without creating objects and calling several
|
|
5
|
+
methods yourself.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from .reaction import Reaction
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def analyze(
|
|
14
|
+
reactants: List[str],
|
|
15
|
+
products: List[str],
|
|
16
|
+
desired_product: str,
|
|
17
|
+
reactant_masses_g: Optional[Dict[str, float]] = None,
|
|
18
|
+
actual_yield_g: Optional[float] = None,
|
|
19
|
+
) -> Reaction:
|
|
20
|
+
"""
|
|
21
|
+
Auto-balance the equation, build the Reaction, print a full summary
|
|
22
|
+
table, and return the Reaction object in case you want to do more
|
|
23
|
+
with it afterward.
|
|
24
|
+
"""
|
|
25
|
+
rxn = Reaction.auto(reactants, products, desired_product)
|
|
26
|
+
print(rxn.summary_table(reactant_masses_g, actual_yield_g))
|
|
27
|
+
return rxn
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standard atomic weights for elements 1-118 (approximate, IUPAC-style averages).
|
|
3
|
+
|
|
4
|
+
These values are sufficient for typical stoichiometry and green-chemistry
|
|
5
|
+
calculations in an educational context. They are not intended for
|
|
6
|
+
high-precision analytical work.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
ATOMIC_MASSES = {
|
|
10
|
+
"H": 1.008, "He": 4.0026, "Li": 6.94, "Be": 9.0122, "B": 10.81,
|
|
11
|
+
"C": 12.011, "N": 14.007, "O": 15.999, "F": 18.998, "Ne": 20.180,
|
|
12
|
+
"Na": 22.990, "Mg": 24.305, "Al": 26.982, "Si": 28.085, "P": 30.974,
|
|
13
|
+
"S": 32.06, "Cl": 35.45, "Ar": 39.948, "K": 39.098, "Ca": 40.078,
|
|
14
|
+
"Sc": 44.956, "Ti": 47.867, "V": 50.942, "Cr": 51.996, "Mn": 54.938,
|
|
15
|
+
"Fe": 55.845, "Co": 58.933, "Ni": 58.693, "Cu": 63.546, "Zn": 65.38,
|
|
16
|
+
"Ga": 69.723, "Ge": 72.630, "As": 74.922, "Se": 78.971, "Br": 79.904,
|
|
17
|
+
"Kr": 83.798, "Rb": 85.468, "Sr": 87.62, "Y": 88.906, "Zr": 91.224,
|
|
18
|
+
"Nb": 92.906, "Mo": 95.95, "Tc": 98.0, "Ru": 101.07, "Rh": 102.91,
|
|
19
|
+
"Pd": 106.42, "Ag": 107.87, "Cd": 112.41, "In": 114.82, "Sn": 118.71,
|
|
20
|
+
"Sb": 121.76, "Te": 127.60, "I": 126.90, "Xe": 131.29, "Cs": 132.91,
|
|
21
|
+
"Ba": 137.33, "La": 138.91, "Ce": 140.12, "Pr": 140.91, "Nd": 144.24,
|
|
22
|
+
"Pm": 145.0, "Sm": 150.36, "Eu": 151.96, "Gd": 157.25, "Tb": 158.93,
|
|
23
|
+
"Dy": 162.50, "Ho": 164.93, "Er": 167.26, "Tm": 168.93, "Yb": 173.05,
|
|
24
|
+
"Lu": 174.97, "Hf": 178.49, "Ta": 180.95, "W": 183.84, "Re": 186.21,
|
|
25
|
+
"Os": 190.23, "Ir": 192.22, "Pt": 195.08, "Au": 196.97, "Hg": 200.59,
|
|
26
|
+
"Tl": 204.38, "Pb": 207.2, "Bi": 208.98, "Po": 209.0, "At": 210.0,
|
|
27
|
+
"Rn": 222.0, "Fr": 223.0, "Ra": 226.0, "Ac": 227.0, "Th": 232.04,
|
|
28
|
+
"Pa": 231.04, "U": 238.03, "Np": 237.0, "Pu": 244.0, "Am": 243.0,
|
|
29
|
+
"Cm": 247.0, "Bk": 247.0, "Cf": 251.0, "Es": 252.0, "Fm": 257.0,
|
|
30
|
+
"Md": 258.0, "No": 259.0, "Lr": 262.0, "Rf": 267.0, "Db": 270.0,
|
|
31
|
+
"Sg": 271.0, "Bh": 270.0, "Hs": 277.0, "Mt": 278.0, "Ds": 281.0,
|
|
32
|
+
"Rg": 282.0, "Cn": 285.0, "Nh": 286.0, "Fl": 289.0, "Mc": 290.0,
|
|
33
|
+
"Lv": 293.0, "Ts": 294.0, "Og": 294.0,
|
|
34
|
+
}
|