Skip to content

API reference

chemsynthcalc

Python package for calculating the masses of substances required for chemical synthesis directly from the reaction string. It includes solutions for all intermediate steps, including chemical formula parsing, molar mass calculation and reaction balancing with different matrix methods.

Example use

Let's say that we need to prepare 3 grams of YBCO by solid-state synthesis from respective carbonates. The reaction string will look something like this (to simplify, let's leave it without oxygen nonstoichiometry):

from chemsynthcalc import ChemicalReaction

reaction_string = "BaCO3 + Y2(CO3)3 + CuCO3 + O2 → YBa2Cu3O7 + CO2"

Now, we can create a chemical reaction object of the ChemicalReaction class, which will be used in the calculation. We need to specify the arguments for our particular case:

>>> reaction = ChemicalReaction(
    reaction = reaction_string, # our reaction string
    target = 0, # index of target compound in the product list
    target_mass = 3, # desired mass of target compound,
    mode = "balance" # mode of coefficients calculations,
)

Now, to perform the automatic calculation, all we need to do is to put:

>>> reaction.print_results(print_rounding_order=4)
# assuming that we use analytical balances with 4 digit presicion

And we get our output in the terminal:

initial reaction: BaCO3+Y2(CO3)3+CuCO3+O2→YBa2Cu3O7+CO2
reaction matrix:
 [[1. 0. 0. 0. 2. 0.]
 [1. 3. 1. 0. 0. 1.]
 [3. 9. 3. 2. 7. 2.]
 [0. 2. 0. 0. 1. 0.]
 [0. 0. 1. 0. 3. 0.]]
mode: balance
formulas: ['BaCO3', 'Y2(CO3)3', 'CuCO3', 'O2', 'YBa2Cu3O7', 'CO2']
coefficients: [8, 2, 12, 1, 4, 26]
normalized coefficients: [2, 0.5, 3, 0.25, 1, 6.5]
algorithm: inverse
is balanced: True
final reaction: 8BaCO3+2Y2(CO3)3+12CuCO3+O2→4YBa2Cu3O7+26CO2
final reaction normalized: 2BaCO3+0.5Y2(CO3)3+3CuCO3+0.25O2→YBa2Cu3O7+6.5CO2
molar masses: [197.335, 357.835676, 123.554, 31.998, 666.190838, 44.009]
target: YBa2Cu3O7
masses: [1.7773, 0.8057, 1.6692, 0.036, 3.0, 1.2882]
BaCO3: M = 197.3350 g/mol, m = 1.7773 g
Y2(CO3)3: M = 357.8357 g/mol, m = 0.8057 g
CuCO3: M = 123.5540 g/mol, m = 1.6692 g
O2: M = 31.9980 g/mol, m = 0.0360 g
YBa2Cu3O7: M = 666.1908 g/mol, m = 3.0000 g
CO2: M = 44.0090 g/mol, m = 1.2882 g

balancer

Balancer

Bases: BalancingAlgorithms

A class for balancing chemical equations automatically by different matrix methods.

Parameters:

Name Type Description Default
matrix NDArray[float64]

Reaction matrix

required
separator_pos int

Position of the reaction separator (usually the separator is "=")

required
round_precision int

Coefficients rounding precision

required
intify bool

Determines whether the coefficients should be integers

True

Attributes:

Name Type Description
coef_limit int

max integer coefficient for _intify_coefficients method

Source code in src/chemsynthcalc/balancer.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class Balancer(BalancingAlgorithms):
    """
    A class for balancing chemical equations automatically by different matrix methods.

    Parameters:
        matrix (npt.NDArray[np.float64]): Reaction matrix
        separator_pos (int): Position of the reaction separator (usually the separator is "=")
        round_precision (int): Coefficients rounding precision
        intify (bool): Determines whether the coefficients should be integers

    Attributes:
        coef_limit (int): max integer coefficient for \
        [_intify_coefficients][chemsynthcalc.balancer.Balancer._intify_coefficients] method
    """

    def __init__(
        self,
        matrix: npt.NDArray[np.float64],
        separator_pos: int,
        round_precision: int,
        intify: bool = True,
    ) -> None:
        super().__init__(matrix, separator_pos)

        if round_precision > 0:
            self.round_precision: int = round_precision
        else:
            raise ValueError("precision <= 0")

        self.intify: bool = intify
        self.coef_limit: int = 1_000_000

    def __str__(self) -> str:
        return f"Balancer object for matrix \n {self.reaction_matrix}"

    def __repr__(self) -> str:
        return f"Balancer({self.reaction_matrix}, {self.separator_pos}, {self.round_precision}, {self.intify})"

    def _intify_coefficients(
        self, coefficients: list[float], limit: int
    ) -> list[float | int] | list[int]:
        """
        Reduce the coefficients to integers by finding the greatest common divider.

        Parameters:
            coefficients (list): List of coefficients to intify
            limit (int): Upper limit (max int coef)

        Returns:
            A list of intified coefficients
        """
        initial_coefficients = coefficients
        frac = [Fraction(x).limit_denominator() for x in coefficients]
        vals = [
            int(
                fr.numerator
                * find_lcm([fr.denominator for fr in frac])
                / fr.denominator
            )
            for fr in frac
        ]
        coefficients = [int(val / find_gcd(vals)) for val in vals]
        if any(x > limit for x in coefficients):
            return initial_coefficients
        return coefficients

    @staticmethod
    def is_reaction_balanced(
        reactant_matrix: npt.NDArray[np.float64],
        product_matrix: npt.NDArray[np.float64],
        coefficients: list[float] | list[int],
        tolerance: float = 1e-8,
    ) -> bool:
        """
        Checks if reaction is balanced by multiplying reactant matrix and product matrix
        by the respective coefficient vector. Method is static to call it outside of balancer
        instance.

        Parameters:
            reactant_matrix (npt.NDArray[np.float64]): Matrix of reactants property generated by [ChemicalReaction][chemsynthcalc.chemical_reaction.ChemicalReaction] class
            product_matrix (npt.NDArray[np.float64]): Matrix of products property generated by [ChemicalReaction][chemsynthcalc.chemical_reaction.ChemicalReaction] class
            coefficients (list[float] | list[int]): Coefficients
            tolerance (float): tolerance limit for the *np.allclose* function

        Returns:
            True if balanced within tolerance

        Examples:
            >>> reaction = ChemicalReaction("NH4ClO4+HNO3+HCl=HClO4+NOCl+N2O+N2O3+H2O+Cl2")
            >>> Balancer.is_reaction_balanced(reaction.reactant_matrix, reaction.product_matrix, [64, 167, 137, 80, 43, 64, 30, 240, 39])
            True
            >>> reaction = ChemicalReaction("H2+O2=H2O")
            >>> Balancer.is_reaction_balanced(reaction.reactant_matrix, reaction.product_matrix, [2,2,2])
            False
        """
        try:
            reactants = np.multiply(
                reactant_matrix.T,
                np.array(coefficients)[: reactant_matrix.shape[1], None],
            )
            products = np.multiply(
                product_matrix.T,
                np.array(coefficients)[reactant_matrix.shape[1] :, None],
            )
            return np.allclose(
                reactants.sum(axis=0), products.sum(axis=0), rtol=tolerance
            )

        except Exception:
            return False

    def _calculate_by_method(self, method: str) -> list[float | int] | list[int]:
        """
        Compute the coefficients list by a specific method.

        Parameters:
            method (str): One of 4 currently implemented methods (inv, gpinv, ppinv, comb)

        Returns:
            A list of coefficients

        Raise:
            ValueError if method is not found. <br />
            [BalancingError][chemsynthcalc.chem_errors.BalancingError] if can't balance reaction by specified method.
        """
        match method:

            case "inv":
                coefficients: list[float] = np.round(
                    self._inv_algorithm(), decimals=self.round_precision
                ).tolist()  # type: ignore

            case "gpinv":
                coefficients: list[float] = np.round(
                    self._gpinv_algorithm(), decimals=self.round_precision + 2
                ).tolist()  # type: ignore

            case "ppinv":
                coefficients: list[float] = np.round(
                    self._ppinv_algorithm(), decimals=self.round_precision + 2
                ).tolist()  # type: ignore

            case "comb":
                res: npt.NDArray[np.int32] | None = self._comb_algorithm()
                if res is not None:
                    return res.tolist()  # type: ignore
                else:
                    raise BalancingError(f"Can't balance reaction by {method} method")

            case _:
                raise ValueError(f"No method {method}")

        if (
            Balancer.is_reaction_balanced(
                self.reactant_matrix, self.product_matrix, coefficients
            )
            and all(x > 0 for x in coefficients)
            and len(coefficients) == self.reaction_matrix.shape[1]
        ):
            if self.intify:
                intified = self._intify_coefficients(coefficients, self.coef_limit)
                if all(x < self.coef_limit for x in intified):
                    return intified
                else:
                    return coefficients
            else:
                return coefficients
        else:
            raise BalancingError(f"Can't balance reaction by {method} method")

    def inv(self) -> list[float | int] | list[int]:
        """
        A high-level function call to compute coefficients by Thorne method.

        Returns:
            A list of coefficients
        """
        return self._calculate_by_method("inv")

    def gpinv(self) -> list[float | int] | list[int]:
        """
        A high-level function call to compute coefficients by
        Risteski general pseudoinverse method.

        Returns:
            A list of coefficients
        """
        return self._calculate_by_method("gpinv")

    def ppinv(self) -> list[float | int] | list[int]:
        """
        A high-level function call to compute coefficients by
        Risteski partial pseudoinverse method.

        Returns:
            A list of coefficients
        """
        return self._calculate_by_method("ppinv")

    def comb(self) -> list[float | int] | list[int]:
        """
        A high-level function call to compute coefficients by
        combinatorial method.

        Returns:
            A list of coefficients
        """
        return self._calculate_by_method("comb")

    def auto(self) -> tuple[list[float | int] | list[int], str]:
        """
        A high-level function call to automatically compute coefficients
        by sequentially calling inv, gpinv, ppinv methods.

        Returns:
            A list of coefficients

        Raise:
            [BalancingError][chemsynthcalc.chem_errors.BalancingError] if can't balance reaction by any method.
        """
        try:
            return (self.inv(), "inverse")
        except Exception:
            pass
        try:
            return (self.gpinv(), "general pseudoinverse")
        except Exception:
            pass
        try:
            return (self.gpinv(), "partial pseudoinverse")
        except Exception:
            raise BalancingError("Can't balance this reaction by any method")

_intify_coefficients(coefficients, limit)

Reduce the coefficients to integers by finding the greatest common divider.

Parameters:

Name Type Description Default
coefficients list

List of coefficients to intify

required
limit int

Upper limit (max int coef)

required

Returns:

Type Description
list[float | int] | list[int]

A list of intified coefficients

Source code in src/chemsynthcalc/balancer.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def _intify_coefficients(
    self, coefficients: list[float], limit: int
) -> list[float | int] | list[int]:
    """
    Reduce the coefficients to integers by finding the greatest common divider.

    Parameters:
        coefficients (list): List of coefficients to intify
        limit (int): Upper limit (max int coef)

    Returns:
        A list of intified coefficients
    """
    initial_coefficients = coefficients
    frac = [Fraction(x).limit_denominator() for x in coefficients]
    vals = [
        int(
            fr.numerator
            * find_lcm([fr.denominator for fr in frac])
            / fr.denominator
        )
        for fr in frac
    ]
    coefficients = [int(val / find_gcd(vals)) for val in vals]
    if any(x > limit for x in coefficients):
        return initial_coefficients
    return coefficients

is_reaction_balanced(reactant_matrix, product_matrix, coefficients, tolerance=1e-08) staticmethod

Checks if reaction is balanced by multiplying reactant matrix and product matrix by the respective coefficient vector. Method is static to call it outside of balancer instance.

Parameters:

Name Type Description Default
reactant_matrix NDArray[float64]

Matrix of reactants property generated by ChemicalReaction class

required
product_matrix NDArray[float64]

Matrix of products property generated by ChemicalReaction class

required
coefficients list[float] | list[int]

Coefficients

required
tolerance float

tolerance limit for the np.allclose function

1e-08

Returns:

Type Description
bool

True if balanced within tolerance

Examples:

>>> reaction = ChemicalReaction("NH4ClO4+HNO3+HCl=HClO4+NOCl+N2O+N2O3+H2O+Cl2")
>>> Balancer.is_reaction_balanced(reaction.reactant_matrix, reaction.product_matrix, [64, 167, 137, 80, 43, 64, 30, 240, 39])
True
>>> reaction = ChemicalReaction("H2+O2=H2O")
>>> Balancer.is_reaction_balanced(reaction.reactant_matrix, reaction.product_matrix, [2,2,2])
False
Source code in src/chemsynthcalc/balancer.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@staticmethod
def is_reaction_balanced(
    reactant_matrix: npt.NDArray[np.float64],
    product_matrix: npt.NDArray[np.float64],
    coefficients: list[float] | list[int],
    tolerance: float = 1e-8,
) -> bool:
    """
    Checks if reaction is balanced by multiplying reactant matrix and product matrix
    by the respective coefficient vector. Method is static to call it outside of balancer
    instance.

    Parameters:
        reactant_matrix (npt.NDArray[np.float64]): Matrix of reactants property generated by [ChemicalReaction][chemsynthcalc.chemical_reaction.ChemicalReaction] class
        product_matrix (npt.NDArray[np.float64]): Matrix of products property generated by [ChemicalReaction][chemsynthcalc.chemical_reaction.ChemicalReaction] class
        coefficients (list[float] | list[int]): Coefficients
        tolerance (float): tolerance limit for the *np.allclose* function

    Returns:
        True if balanced within tolerance

    Examples:
        >>> reaction = ChemicalReaction("NH4ClO4+HNO3+HCl=HClO4+NOCl+N2O+N2O3+H2O+Cl2")
        >>> Balancer.is_reaction_balanced(reaction.reactant_matrix, reaction.product_matrix, [64, 167, 137, 80, 43, 64, 30, 240, 39])
        True
        >>> reaction = ChemicalReaction("H2+O2=H2O")
        >>> Balancer.is_reaction_balanced(reaction.reactant_matrix, reaction.product_matrix, [2,2,2])
        False
    """
    try:
        reactants = np.multiply(
            reactant_matrix.T,
            np.array(coefficients)[: reactant_matrix.shape[1], None],
        )
        products = np.multiply(
            product_matrix.T,
            np.array(coefficients)[reactant_matrix.shape[1] :, None],
        )
        return np.allclose(
            reactants.sum(axis=0), products.sum(axis=0), rtol=tolerance
        )

    except Exception:
        return False

_calculate_by_method(method)

Compute the coefficients list by a specific method.

Parameters:

Name Type Description Default
method str

One of 4 currently implemented methods (inv, gpinv, ppinv, comb)

required

Returns:

Type Description
list[float | int] | list[int]

A list of coefficients

Raise

ValueError if method is not found.
BalancingError if can't balance reaction by specified method.

Source code in src/chemsynthcalc/balancer.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def _calculate_by_method(self, method: str) -> list[float | int] | list[int]:
    """
    Compute the coefficients list by a specific method.

    Parameters:
        method (str): One of 4 currently implemented methods (inv, gpinv, ppinv, comb)

    Returns:
        A list of coefficients

    Raise:
        ValueError if method is not found. <br />
        [BalancingError][chemsynthcalc.chem_errors.BalancingError] if can't balance reaction by specified method.
    """
    match method:

        case "inv":
            coefficients: list[float] = np.round(
                self._inv_algorithm(), decimals=self.round_precision
            ).tolist()  # type: ignore

        case "gpinv":
            coefficients: list[float] = np.round(
                self._gpinv_algorithm(), decimals=self.round_precision + 2
            ).tolist()  # type: ignore

        case "ppinv":
            coefficients: list[float] = np.round(
                self._ppinv_algorithm(), decimals=self.round_precision + 2
            ).tolist()  # type: ignore

        case "comb":
            res: npt.NDArray[np.int32] | None = self._comb_algorithm()
            if res is not None:
                return res.tolist()  # type: ignore
            else:
                raise BalancingError(f"Can't balance reaction by {method} method")

        case _:
            raise ValueError(f"No method {method}")

    if (
        Balancer.is_reaction_balanced(
            self.reactant_matrix, self.product_matrix, coefficients
        )
        and all(x > 0 for x in coefficients)
        and len(coefficients) == self.reaction_matrix.shape[1]
    ):
        if self.intify:
            intified = self._intify_coefficients(coefficients, self.coef_limit)
            if all(x < self.coef_limit for x in intified):
                return intified
            else:
                return coefficients
        else:
            return coefficients
    else:
        raise BalancingError(f"Can't balance reaction by {method} method")

inv()

A high-level function call to compute coefficients by Thorne method.

Returns:

Type Description
list[float | int] | list[int]

A list of coefficients

Source code in src/chemsynthcalc/balancer.py
181
182
183
184
185
186
187
188
def inv(self) -> list[float | int] | list[int]:
    """
    A high-level function call to compute coefficients by Thorne method.

    Returns:
        A list of coefficients
    """
    return self._calculate_by_method("inv")

gpinv()

A high-level function call to compute coefficients by Risteski general pseudoinverse method.

Returns:

Type Description
list[float | int] | list[int]

A list of coefficients

Source code in src/chemsynthcalc/balancer.py
190
191
192
193
194
195
196
197
198
def gpinv(self) -> list[float | int] | list[int]:
    """
    A high-level function call to compute coefficients by
    Risteski general pseudoinverse method.

    Returns:
        A list of coefficients
    """
    return self._calculate_by_method("gpinv")

ppinv()

A high-level function call to compute coefficients by Risteski partial pseudoinverse method.

Returns:

Type Description
list[float | int] | list[int]

A list of coefficients

Source code in src/chemsynthcalc/balancer.py
200
201
202
203
204
205
206
207
208
def ppinv(self) -> list[float | int] | list[int]:
    """
    A high-level function call to compute coefficients by
    Risteski partial pseudoinverse method.

    Returns:
        A list of coefficients
    """
    return self._calculate_by_method("ppinv")

comb()

A high-level function call to compute coefficients by combinatorial method.

Returns:

Type Description
list[float | int] | list[int]

A list of coefficients

Source code in src/chemsynthcalc/balancer.py
210
211
212
213
214
215
216
217
218
def comb(self) -> list[float | int] | list[int]:
    """
    A high-level function call to compute coefficients by
    combinatorial method.

    Returns:
        A list of coefficients
    """
    return self._calculate_by_method("comb")

auto()

A high-level function call to automatically compute coefficients by sequentially calling inv, gpinv, ppinv methods.

Returns:

Type Description
tuple[list[float | int] | list[int], str]

A list of coefficients

Raise

BalancingError if can't balance reaction by any method.

Source code in src/chemsynthcalc/balancer.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def auto(self) -> tuple[list[float | int] | list[int], str]:
    """
    A high-level function call to automatically compute coefficients
    by sequentially calling inv, gpinv, ppinv methods.

    Returns:
        A list of coefficients

    Raise:
        [BalancingError][chemsynthcalc.chem_errors.BalancingError] if can't balance reaction by any method.
    """
    try:
        return (self.inv(), "inverse")
    except Exception:
        pass
    try:
        return (self.gpinv(), "general pseudoinverse")
    except Exception:
        pass
    try:
        return (self.gpinv(), "partial pseudoinverse")
    except Exception:
        raise BalancingError("Can't balance this reaction by any method")

balancing_algos

BalancingAlgorithms

A collection of functions for balancing chemical reactions

Currently implemented: Thorne algorithm (see _inv_algorithm method for details), Risteski general pseudo-inverse algorithm (see _gpinv_algorithm method for details), Risteski partial pseudo-inverse algorithm (see _ppinv_algorithm method for details), and naive combinational search algorithm (see _comb_algorithm method for details).

Parameters:

Name Type Description Default
matrix NDArray[float64]

Reaction matrix

required
separator_pos int

Position of the reaction separator (usually the separator is "=")

required

Attributes:

Name Type Description
reactant_matrix NDArray[float64]

A matrix of the left part of the equation

product_matrix NDArray[float64]

A matrix of the right part of the equation

Note

Why use scipy.linalg.pinv, when numpy.linalg.pinv is doing the same thing and does not require the whole SciPy import?

There are some peculiar reaction cases where (especially for _ppinv_algorithm method) the results for numpy.linalg.pinv differs from system to system (np version, OS, python version etc.). My understanding is that the cause of this behaviour lies in small differences for pinv algorithm in numpy C-libraries and BLAS-libraries, hence the difference. To avoid this, a more consistent method scipy.linalg.pinv was used.

Source code in src/chemsynthcalc/balancing_algos.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
class BalancingAlgorithms:
    """
    A collection of functions for balancing chemical reactions

    Currently implemented: Thorne algorithm (see
    [_inv_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._inv_algorithm] method for details),
    Risteski general pseudo-inverse algorithm (see
    [_gpinv_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._gpinv_algorithm] method for details),
    Risteski partial pseudo-inverse algorithm (see
    [_ppinv_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._ppinv_algorithm] method for details),
    and naive combinational search algorithm (see
    [_comb_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._comb_algorithm] method for details).

    Parameters:
        matrix (npt.NDArray[np.float64]): Reaction matrix
        separator_pos (int): Position of the reaction separator (usually the separator is "=")

    Attributes:
        reactant_matrix (npt.NDArray[np.float64]): A matrix of the left part of the equation
        product_matrix (npt.NDArray[np.float64]): A matrix of the right part of the equation

    Note:
        Why use
        [scipy.linalg.pinv](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.pinv.html),
        when
        [numpy.linalg.pinv](https://numpy.org/doc/stable/reference/generated/numpy.linalg.pinv.html)
        is doing the same thing and does not require the whole SciPy import?

        There are some peculiar reaction cases where
        (especially for [_ppinv_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._ppinv_algorithm] method)
        the results for [numpy.linalg.pinv](https://numpy.org/doc/stable/reference/generated/numpy.linalg.pinv.html)
        differs from system to system (np version, OS, python version etc.). My understanding is that the cause of
        this behaviour lies in small differences for pinv algorithm in numpy C-libraries and BLAS-libraries,
        hence the difference.
        To avoid this, a more consistent method
        [scipy.linalg.pinv](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.pinv.html) was used.
    """

    def __init__(self, matrix: npt.NDArray[np.float64], separator_pos: int) -> None:
        self.separator_pos = separator_pos
        self.reaction_matrix: npt.NDArray[np.float64] = matrix
        self.reactant_matrix: npt.NDArray[np.float64] = self.reaction_matrix[
            :, : self.separator_pos
        ]
        self.product_matrix: npt.NDArray[np.float64] = self.reaction_matrix[
            :, self.separator_pos :
        ]

    def _inv_algorithm(self) -> npt.NDArray[np.float64]:
        """Matrix inverse algorithm for reaction balancing.

        A reaction matrix inverse algorithm proposed by [Thorne](https://arxiv.org/abs/1110.4321).
        The calculation is based on the nullity, or dimensionality, of the matrix.

        The algorithm can be described in steps:

        1) If the number of rows is greater than the number of columns, \
        add zero columns until the matrix becomes square \
        (Note: this is a modification of the original \
        Thorne method described in the article).

        2) If reaction matrix is square (which means that the number \
        of atoms involved is equal to the number of compounds) than \
        we turn matrix in its row-echelon form by singular value \
        decomposition.

        3) Calculation of the nullity of the matrix, which is \
        basically number of compounds minus rank of the matrix.

        4) Create a  matrix augumented by nullity number of rows \
        of flipped identity matrix. If any rows are zeros, \
        replace them with identity matrix rows.

        5) Inverse the augumented matrix.

        6) Exctract and transpose rightmost column.

        7) Normalize this value with the absolute min value of the vector.

        8) Round up float operations errors.

        The absolute values of this vector are coefficients of the
        reaction.

        Note:
            While this method works great for reactions with 0 and 1
            nullity, it generally cannot work with nullities 2 and higher.
            Thorne claims that for higher nullities, a nullity number
            of vectors should be extracted, and each of them contains
            a set of correct coefficients. However, if number of rows in
            the flipped augmentation identity matrix is 2 or more, one can
            easily see that each vector will contain nullity-1 zeroes,
            therefore they cannot be a correct vector of coefficients.

        Returns:
            A 1D NumPy array of calculated coefficients
        """
        reaction_matrix = self.reaction_matrix

        if reaction_matrix.shape[0] > reaction_matrix.shape[1]:
            zeros_added = reaction_matrix.shape[0] - reaction_matrix.shape[1]
            zero_columns = np.zeros(
                (
                    reaction_matrix.shape[0],
                    zeros_added,
                )
            )
            reaction_matrix = np.hstack((reaction_matrix, zero_columns))
        else:
            zeros_added = 0

        if reaction_matrix.shape[0] == reaction_matrix.shape[1]:
            _, _, reaction_matrix = np.linalg.svd(reaction_matrix)

        number_of_cols = reaction_matrix.shape[1]
        rank = np.linalg.matrix_rank(reaction_matrix, tol=1e-100)
        nullity = number_of_cols - rank
        augument = np.flip(np.identity(reaction_matrix.shape[1])[:nullity], axis=1)
        augumented_matrix = np.vstack((reaction_matrix, augument))
        if np.where(~augumented_matrix.any(axis=1))[0].size > 0:
            augumented_matrix = augumented_matrix[
                ~np.all(augumented_matrix == 0, axis=1)
            ]
        inversed_matrix = np.linalg.inv(augumented_matrix)
        vector = inversed_matrix[:, -zeros_added - 1].T
        vector = np.absolute(np.squeeze(np.asarray(vector)))
        vector = vector[vector != 0]
        coefs = np.divide(vector, vector.min())
        return coefs

    def _gpinv_algorithm(self) -> npt.NDArray[np.float64]:
        """Matrix gerenal pseudoinverse algorithm for reaction balancing.

        A reaction matrix pseudoinverse algorithm
        proposed by [Risteski](http://koreascience.or.kr/article/JAKO201314358624990.page).
        There are other articles and methods of chemical
        equation balancing by this author, however, this particular
        algorithm seems to be most convenient for matrix calculations.
        The algorithm can be described in steps:

        1) Stack reactant matrix and negative product matrix.

        2) Calculate MP pseudoinverse of this matrix.

        3) Calculate coefficients by formula:
        x = (I – A+A)a, where x is the coefficients vector,
        I - identity matrix, A+ - MP inverse, A - matrix,
        a - arbitrary vector (in this case, vector of ones).

        Note:
            This method is more general than Thorne's method, although it has some
            peculiarities of its own. First of all, the output of this method is float array,
            so, to generate an int coefs list, it needs to be converted, which is
            not always leads to a good result. Secondly, MP pseudoinverse
            is sensetive to row order in the reaction matrix. The rows should
            be ordered by atoms apperances in the reaction string.

        Returns:
            A 1D NumPy array of calculated coefficients
        """
        matrix = np.hstack((self.reactant_matrix, -self.product_matrix))
        inverse = scipy.linalg.pinv(matrix)
        a = np.ones((matrix.shape[1], 1))
        i = np.identity(matrix.shape[1])
        coefs = (i - inverse @ matrix) @ a
        return coefs.flat[:]

    def _ppinv_algorithm(self) -> npt.NDArray[np.float64]:
        """
        Matrix partial pseudoinverse algorithm for reaction balancing.

        A reaction matrix pseudoinverse algorithm also
        proposed by [Risteski](https://www.koreascience.or.kr/article/JAKO200802727293429.page).
        The method is founded on virtue of the solution of a
        Diophantine matrix equation by using of a Moore-Penrose
        pseudoinverse matrix.

        The algorithm can be described in steps:

        1) Take the Moore-Penrose pseudoinverse of the reactant matrix.

        2) Create a G matrix in the form of (I-AA^-)B, where
        I is the identity matrix, A is the reactant matrix, A^- is
        the MP pseudoinverse of A and B is the product matrix.

        3) Then, the vector y (coefficients of products) is equal to
        (I-G^-G)u.

        4) Vector x (coefficients of reactants) is equal to
        A^-By + (I-A^-A)v, where u and v are columns of ones.

        Note:
            While this algorithm and
            [_gpinv_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._gpinv_algorithm]
            are very similar, there are some differences in output results.
            This method exists mostly for legacy purposes, like balancing
            some reactions according to [Risteski](https://www.koreascience.or.kr/article/JAKO200802727293429.page).

        Returns:
            A 1D NumPy array of calculated coefficients
        """
        MP_inverse = scipy.linalg.pinv(self.reactant_matrix)
        g_matrix = (
            np.identity(self.reaction_matrix.shape[0])
            - self.reactant_matrix @ MP_inverse
        )
        g_matrix = g_matrix @ self.product_matrix
        y_multiply = scipy.linalg.pinv(g_matrix) @ g_matrix
        y_vector = (np.identity(y_multiply.shape[1]) - y_multiply).dot(
            np.ones(y_multiply.shape[1])
        )
        x_multiply = MP_inverse @ self.reactant_matrix
        x_multiply = (
            np.identity(x_multiply.shape[1]) - x_multiply
        ) + MP_inverse @ self.product_matrix @ y_vector.T
        x_vector = x_multiply[0].T
        coefs = np.squeeze(np.asarray(np.hstack((x_vector, y_vector))))
        return coefs

    def _comb_algorithm(
        self, max_number_of_iterations: float = 1e8
    ) -> npt.NDArray[np.int32] | None:
        """
        Matrix combinatorial algorithm for reaction balancing.

        Finds a solution solution of a Diophantine matrix equation
        by simply enumerating of all possible solutions of number_of_iterations
        coefficients. The solution space is created by Cartesian product
        (in this case, *np.meshgrid* function), and therefore it is very
        limited by memory. There must a better, clever and fast solution
        to this!

        Important:
            Only for integer coefficients less than 128. Only for reactions
            with total compound count <=10.
            A GPU-accelerated version of this method can be done by importing
            CuPy and replacing np. with cp.

        Note:
            All possible variations of coefficients vectors are
            combinations = max_coefficients**number_of_compounds,
            therefore this method is most effective for reaction with
            small numbers of compounds.

        Returns:
            A 1D NumPy array of calculated coefficients of None if can't compute
        """
        byte = 127
        number_of_compounds = self.reaction_matrix.shape[1]
        if number_of_compounds > 10:
            raise ValueError("Sorry, this method is only for n of compound <=10")

        number_of_iterations = int(
            max_number_of_iterations ** (1 / number_of_compounds)
        )

        if number_of_iterations > byte:
            number_of_iterations = byte

        trans_reaction_matrix = (self.reaction_matrix).T
        lenght = self.reactant_matrix.shape[1]
        old_reactants = trans_reaction_matrix[:lenght].astype("ushort")
        old_products = trans_reaction_matrix[lenght:].astype("ushort")
        for i in range(2, number_of_iterations + 2):
            cart_array = (np.arange(1, i, dtype="ubyte"),) * number_of_compounds
            permuted = np.array(np.meshgrid(*cart_array), dtype="ubyte").T.reshape(
                -1, number_of_compounds
            )
            filter = np.asarray([i - 1], dtype="ubyte")
            permuted = permuted[np.any(permuted == filter, axis=1)]
            # print("calculating max coef %s of %s" % (i-1, number_of_iterations), end='\r', flush=False)
            reactants_vectors = permuted[:, :lenght]
            products_vectors = permuted[:, lenght:]
            del permuted
            reactants = (old_reactants[None, :, :] * reactants_vectors[:, :, None]).sum(
                axis=1
            )
            products = (old_products[None, :, :] * products_vectors[:, :, None]).sum(
                axis=1
            )
            diff = np.subtract(reactants, products)
            del reactants
            del products
            where = np.where(~diff.any(axis=1))[0]
            if np.any(where):
                if where.shape[0] == 1:
                    idx = where
                else:
                    idx = where[0]
                # print("")
                return np.array(
                    np.concatenate(
                        (
                            reactants_vectors[idx].flatten(),
                            products_vectors[idx].flatten(),
                        )
                    )
                )
            gc.collect()
        # print("")
        return None

_inv_algorithm()

Matrix inverse algorithm for reaction balancing.

A reaction matrix inverse algorithm proposed by Thorne. The calculation is based on the nullity, or dimensionality, of the matrix.

The algorithm can be described in steps:

1) If the number of rows is greater than the number of columns, add zero columns until the matrix becomes square (Note: this is a modification of the original Thorne method described in the article).

2) If reaction matrix is square (which means that the number of atoms involved is equal to the number of compounds) than we turn matrix in its row-echelon form by singular value decomposition.

3) Calculation of the nullity of the matrix, which is basically number of compounds minus rank of the matrix.

4) Create a matrix augumented by nullity number of rows of flipped identity matrix. If any rows are zeros, replace them with identity matrix rows.

5) Inverse the augumented matrix.

6) Exctract and transpose rightmost column.

7) Normalize this value with the absolute min value of the vector.

8) Round up float operations errors.

The absolute values of this vector are coefficients of the reaction.

Note

While this method works great for reactions with 0 and 1 nullity, it generally cannot work with nullities 2 and higher. Thorne claims that for higher nullities, a nullity number of vectors should be extracted, and each of them contains a set of correct coefficients. However, if number of rows in the flipped augmentation identity matrix is 2 or more, one can easily see that each vector will contain nullity-1 zeroes, therefore they cannot be a correct vector of coefficients.

Returns:

Type Description
NDArray[float64]

A 1D NumPy array of calculated coefficients

Source code in src/chemsynthcalc/balancing_algos.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def _inv_algorithm(self) -> npt.NDArray[np.float64]:
    """Matrix inverse algorithm for reaction balancing.

    A reaction matrix inverse algorithm proposed by [Thorne](https://arxiv.org/abs/1110.4321).
    The calculation is based on the nullity, or dimensionality, of the matrix.

    The algorithm can be described in steps:

    1) If the number of rows is greater than the number of columns, \
    add zero columns until the matrix becomes square \
    (Note: this is a modification of the original \
    Thorne method described in the article).

    2) If reaction matrix is square (which means that the number \
    of atoms involved is equal to the number of compounds) than \
    we turn matrix in its row-echelon form by singular value \
    decomposition.

    3) Calculation of the nullity of the matrix, which is \
    basically number of compounds minus rank of the matrix.

    4) Create a  matrix augumented by nullity number of rows \
    of flipped identity matrix. If any rows are zeros, \
    replace them with identity matrix rows.

    5) Inverse the augumented matrix.

    6) Exctract and transpose rightmost column.

    7) Normalize this value with the absolute min value of the vector.

    8) Round up float operations errors.

    The absolute values of this vector are coefficients of the
    reaction.

    Note:
        While this method works great for reactions with 0 and 1
        nullity, it generally cannot work with nullities 2 and higher.
        Thorne claims that for higher nullities, a nullity number
        of vectors should be extracted, and each of them contains
        a set of correct coefficients. However, if number of rows in
        the flipped augmentation identity matrix is 2 or more, one can
        easily see that each vector will contain nullity-1 zeroes,
        therefore they cannot be a correct vector of coefficients.

    Returns:
        A 1D NumPy array of calculated coefficients
    """
    reaction_matrix = self.reaction_matrix

    if reaction_matrix.shape[0] > reaction_matrix.shape[1]:
        zeros_added = reaction_matrix.shape[0] - reaction_matrix.shape[1]
        zero_columns = np.zeros(
            (
                reaction_matrix.shape[0],
                zeros_added,
            )
        )
        reaction_matrix = np.hstack((reaction_matrix, zero_columns))
    else:
        zeros_added = 0

    if reaction_matrix.shape[0] == reaction_matrix.shape[1]:
        _, _, reaction_matrix = np.linalg.svd(reaction_matrix)

    number_of_cols = reaction_matrix.shape[1]
    rank = np.linalg.matrix_rank(reaction_matrix, tol=1e-100)
    nullity = number_of_cols - rank
    augument = np.flip(np.identity(reaction_matrix.shape[1])[:nullity], axis=1)
    augumented_matrix = np.vstack((reaction_matrix, augument))
    if np.where(~augumented_matrix.any(axis=1))[0].size > 0:
        augumented_matrix = augumented_matrix[
            ~np.all(augumented_matrix == 0, axis=1)
        ]
    inversed_matrix = np.linalg.inv(augumented_matrix)
    vector = inversed_matrix[:, -zeros_added - 1].T
    vector = np.absolute(np.squeeze(np.asarray(vector)))
    vector = vector[vector != 0]
    coefs = np.divide(vector, vector.min())
    return coefs

_gpinv_algorithm()

Matrix gerenal pseudoinverse algorithm for reaction balancing.

A reaction matrix pseudoinverse algorithm proposed by Risteski. There are other articles and methods of chemical equation balancing by this author, however, this particular algorithm seems to be most convenient for matrix calculations. The algorithm can be described in steps:

1) Stack reactant matrix and negative product matrix.

2) Calculate MP pseudoinverse of this matrix.

3) Calculate coefficients by formula: x = (I – A+A)a, where x is the coefficients vector, I - identity matrix, A+ - MP inverse, A - matrix, a - arbitrary vector (in this case, vector of ones).

Note

This method is more general than Thorne's method, although it has some peculiarities of its own. First of all, the output of this method is float array, so, to generate an int coefs list, it needs to be converted, which is not always leads to a good result. Secondly, MP pseudoinverse is sensetive to row order in the reaction matrix. The rows should be ordered by atoms apperances in the reaction string.

Returns:

Type Description
NDArray[float64]

A 1D NumPy array of calculated coefficients

Source code in src/chemsynthcalc/balancing_algos.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def _gpinv_algorithm(self) -> npt.NDArray[np.float64]:
    """Matrix gerenal pseudoinverse algorithm for reaction balancing.

    A reaction matrix pseudoinverse algorithm
    proposed by [Risteski](http://koreascience.or.kr/article/JAKO201314358624990.page).
    There are other articles and methods of chemical
    equation balancing by this author, however, this particular
    algorithm seems to be most convenient for matrix calculations.
    The algorithm can be described in steps:

    1) Stack reactant matrix and negative product matrix.

    2) Calculate MP pseudoinverse of this matrix.

    3) Calculate coefficients by formula:
    x = (I – A+A)a, where x is the coefficients vector,
    I - identity matrix, A+ - MP inverse, A - matrix,
    a - arbitrary vector (in this case, vector of ones).

    Note:
        This method is more general than Thorne's method, although it has some
        peculiarities of its own. First of all, the output of this method is float array,
        so, to generate an int coefs list, it needs to be converted, which is
        not always leads to a good result. Secondly, MP pseudoinverse
        is sensetive to row order in the reaction matrix. The rows should
        be ordered by atoms apperances in the reaction string.

    Returns:
        A 1D NumPy array of calculated coefficients
    """
    matrix = np.hstack((self.reactant_matrix, -self.product_matrix))
    inverse = scipy.linalg.pinv(matrix)
    a = np.ones((matrix.shape[1], 1))
    i = np.identity(matrix.shape[1])
    coefs = (i - inverse @ matrix) @ a
    return coefs.flat[:]

_ppinv_algorithm()

Matrix partial pseudoinverse algorithm for reaction balancing.

A reaction matrix pseudoinverse algorithm also proposed by Risteski. The method is founded on virtue of the solution of a Diophantine matrix equation by using of a Moore-Penrose pseudoinverse matrix.

The algorithm can be described in steps:

1) Take the Moore-Penrose pseudoinverse of the reactant matrix.

2) Create a G matrix in the form of (I-AA^-)B, where I is the identity matrix, A is the reactant matrix, A^- is the MP pseudoinverse of A and B is the product matrix.

3) Then, the vector y (coefficients of products) is equal to (I-G^-G)u.

4) Vector x (coefficients of reactants) is equal to A^-By + (I-A^-A)v, where u and v are columns of ones.

Note

While this algorithm and _gpinv_algorithm are very similar, there are some differences in output results. This method exists mostly for legacy purposes, like balancing some reactions according to Risteski.

Returns:

Type Description
NDArray[float64]

A 1D NumPy array of calculated coefficients

Source code in src/chemsynthcalc/balancing_algos.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def _ppinv_algorithm(self) -> npt.NDArray[np.float64]:
    """
    Matrix partial pseudoinverse algorithm for reaction balancing.

    A reaction matrix pseudoinverse algorithm also
    proposed by [Risteski](https://www.koreascience.or.kr/article/JAKO200802727293429.page).
    The method is founded on virtue of the solution of a
    Diophantine matrix equation by using of a Moore-Penrose
    pseudoinverse matrix.

    The algorithm can be described in steps:

    1) Take the Moore-Penrose pseudoinverse of the reactant matrix.

    2) Create a G matrix in the form of (I-AA^-)B, where
    I is the identity matrix, A is the reactant matrix, A^- is
    the MP pseudoinverse of A and B is the product matrix.

    3) Then, the vector y (coefficients of products) is equal to
    (I-G^-G)u.

    4) Vector x (coefficients of reactants) is equal to
    A^-By + (I-A^-A)v, where u and v are columns of ones.

    Note:
        While this algorithm and
        [_gpinv_algorithm][chemsynthcalc.balancing_algos.BalancingAlgorithms._gpinv_algorithm]
        are very similar, there are some differences in output results.
        This method exists mostly for legacy purposes, like balancing
        some reactions according to [Risteski](https://www.koreascience.or.kr/article/JAKO200802727293429.page).

    Returns:
        A 1D NumPy array of calculated coefficients
    """
    MP_inverse = scipy.linalg.pinv(self.reactant_matrix)
    g_matrix = (
        np.identity(self.reaction_matrix.shape[0])
        - self.reactant_matrix @ MP_inverse
    )
    g_matrix = g_matrix @ self.product_matrix
    y_multiply = scipy.linalg.pinv(g_matrix) @ g_matrix
    y_vector = (np.identity(y_multiply.shape[1]) - y_multiply).dot(
        np.ones(y_multiply.shape[1])
    )
    x_multiply = MP_inverse @ self.reactant_matrix
    x_multiply = (
        np.identity(x_multiply.shape[1]) - x_multiply
    ) + MP_inverse @ self.product_matrix @ y_vector.T
    x_vector = x_multiply[0].T
    coefs = np.squeeze(np.asarray(np.hstack((x_vector, y_vector))))
    return coefs

_comb_algorithm(max_number_of_iterations=100000000.0)

Matrix combinatorial algorithm for reaction balancing.

Finds a solution solution of a Diophantine matrix equation by simply enumerating of all possible solutions of number_of_iterations coefficients. The solution space is created by Cartesian product (in this case, np.meshgrid function), and therefore it is very limited by memory. There must a better, clever and fast solution to this!

Important

Only for integer coefficients less than 128. Only for reactions with total compound count <=10. A GPU-accelerated version of this method can be done by importing CuPy and replacing np. with cp.

Note

All possible variations of coefficients vectors are combinations = max_coefficients**number_of_compounds, therefore this method is most effective for reaction with small numbers of compounds.

Returns:

Type Description
NDArray[int32] | None

A 1D NumPy array of calculated coefficients of None if can't compute

Source code in src/chemsynthcalc/balancing_algos.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def _comb_algorithm(
    self, max_number_of_iterations: float = 1e8
) -> npt.NDArray[np.int32] | None:
    """
    Matrix combinatorial algorithm for reaction balancing.

    Finds a solution solution of a Diophantine matrix equation
    by simply enumerating of all possible solutions of number_of_iterations
    coefficients. The solution space is created by Cartesian product
    (in this case, *np.meshgrid* function), and therefore it is very
    limited by memory. There must a better, clever and fast solution
    to this!

    Important:
        Only for integer coefficients less than 128. Only for reactions
        with total compound count <=10.
        A GPU-accelerated version of this method can be done by importing
        CuPy and replacing np. with cp.

    Note:
        All possible variations of coefficients vectors are
        combinations = max_coefficients**number_of_compounds,
        therefore this method is most effective for reaction with
        small numbers of compounds.

    Returns:
        A 1D NumPy array of calculated coefficients of None if can't compute
    """
    byte = 127
    number_of_compounds = self.reaction_matrix.shape[1]
    if number_of_compounds > 10:
        raise ValueError("Sorry, this method is only for n of compound <=10")

    number_of_iterations = int(
        max_number_of_iterations ** (1 / number_of_compounds)
    )

    if number_of_iterations > byte:
        number_of_iterations = byte

    trans_reaction_matrix = (self.reaction_matrix).T
    lenght = self.reactant_matrix.shape[1]
    old_reactants = trans_reaction_matrix[:lenght].astype("ushort")
    old_products = trans_reaction_matrix[lenght:].astype("ushort")
    for i in range(2, number_of_iterations + 2):
        cart_array = (np.arange(1, i, dtype="ubyte"),) * number_of_compounds
        permuted = np.array(np.meshgrid(*cart_array), dtype="ubyte").T.reshape(
            -1, number_of_compounds
        )
        filter = np.asarray([i - 1], dtype="ubyte")
        permuted = permuted[np.any(permuted == filter, axis=1)]
        # print("calculating max coef %s of %s" % (i-1, number_of_iterations), end='\r', flush=False)
        reactants_vectors = permuted[:, :lenght]
        products_vectors = permuted[:, lenght:]
        del permuted
        reactants = (old_reactants[None, :, :] * reactants_vectors[:, :, None]).sum(
            axis=1
        )
        products = (old_products[None, :, :] * products_vectors[:, :, None]).sum(
            axis=1
        )
        diff = np.subtract(reactants, products)
        del reactants
        del products
        where = np.where(~diff.any(axis=1))[0]
        if np.any(where):
            if where.shape[0] == 1:
                idx = where
            else:
                idx = where[0]
            # print("")
            return np.array(
                np.concatenate(
                    (
                        reactants_vectors[idx].flatten(),
                        products_vectors[idx].flatten(),
                    )
                )
            )
        gc.collect()
    # print("")
    return None

chem_errors

Module that contains custom errors for use in ChemSynthCalc

EmptyFormula

Bases: Exception

The formula string is empty

Source code in src/chemsynthcalc/chem_errors.py
 6
 7
 8
 9
10
11
class EmptyFormula(Exception):
    """
    The formula string is empty
    """

    pass

NoSuchAtom

Bases: Exception

Found atom(s) that are not in the periodic table.

Source code in src/chemsynthcalc/chem_errors.py
14
15
16
17
18
19
class NoSuchAtom(Exception):
    """
    Found atom(s) that are not in the periodic table.
    """

    pass

InvalidCharacter

Bases: Exception

Found some characters that do not belong in the chemical formula or reaction.

Source code in src/chemsynthcalc/chem_errors.py
22
23
24
25
26
27
28
class InvalidCharacter(Exception):
    """
    Found some characters that do not belong in the
    chemical formula or reaction.
    """

    pass

MoreThanOneAdduct

Bases: Exception

There is more than one adduct (*).

Source code in src/chemsynthcalc/chem_errors.py
31
32
33
34
35
36
class MoreThanOneAdduct(Exception):
    """
    There is more than one adduct (*).
    """

    pass

BracketsNotPaired

Bases: Exception

Some brackets do not come in pairs.

Source code in src/chemsynthcalc/chem_errors.py
39
40
41
42
43
44
class BracketsNotPaired(Exception):
    """
    Some brackets do not come in pairs.
    """

    pass

EmptyReaction

Bases: Exception

The reaction string is empty

Source code in src/chemsynthcalc/chem_errors.py
47
48
49
50
51
52
class EmptyReaction(Exception):
    """
    The reaction string is empty
    """

    pass

NoSuchMode

Bases: Exception

Invalid calculation mode detected.

Source code in src/chemsynthcalc/chem_errors.py
55
56
57
58
59
60
class NoSuchMode(Exception):
    """
    Invalid calculation mode detected.
    """

    pass

NoSuchAlgorithm

Bases: Exception

Invalid calculation algorithm detected.

Source code in src/chemsynthcalc/chem_errors.py
63
64
65
66
67
68
class NoSuchAlgorithm(Exception):
    """
    Invalid calculation algorithm detected.
    """

    pass

NoSeparator

Bases: Exception

No separator was found in the reaction string.

Source code in src/chemsynthcalc/chem_errors.py
71
72
73
74
75
76
class NoSeparator(Exception):
    """
    No separator was found in the reaction string.
    """

    pass

ReactionNotBalanced

Bases: Exception

This reaction is not balanced.

Source code in src/chemsynthcalc/chem_errors.py
79
80
81
82
83
84
class ReactionNotBalanced(Exception):
    """
    This reaction is not balanced.
    """

    pass

ReactantProductDifference

Bases: Exception

The elements in reaction are not evenly distributed in reactants and products: some of atoms are only in one part of reaction.

Source code in src/chemsynthcalc/chem_errors.py
87
88
89
90
91
92
93
94
95
class ReactantProductDifference(Exception):
    """
    The elements in reaction are not evenly
    distributed in reactants and products:
    some of atoms are only in one part of
    reaction.
    """

    pass

BadCoeffiecients

Bases: Exception

The coefficients are not not compliant (they have no physical meaning).

Source code in src/chemsynthcalc/chem_errors.py
 98
 99
100
101
102
103
104
class BadCoeffiecients(Exception):
    """
    The coefficients are not not compliant
    (they have no physical meaning).
    """

    pass

BalancingError

Bases: Exception

Can't balance reaction by this method

Source code in src/chemsynthcalc/chem_errors.py
107
108
109
110
111
112
class BalancingError(Exception):
    """
    Can't balance reaction by this method
    """

    pass

chem_output

ChemicalOutput

Methods of this class prepare output from ChemicalFormula and ChemicalReaction objects and output it in different ways.

Parameters:

Name Type Description Default
output dict[str, object]

Output dictionary

required
print_precision int

How many decimal places to print out

required
obj str

Type of object ("formula" or "reaction")

required

Attributes:

Name Type Description
rounded_values dict[str, object]

Output dictionary rounded to print_precision

original_stdout TextIO | Any

Default stdout

Raise

ValueError if print_precision <= 0
ValueError if obj is not "ChemicalFormula" or "ChemicalReaction"

Source code in src/chemsynthcalc/chem_output.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class ChemicalOutput:
    """
    Methods of this class prepare output from
    [ChemicalFormula][chemsynthcalc.chemical_formula.ChemicalFormula]
    and
    [ChemicalReaction][chemsynthcalc.chemical_reaction.ChemicalReaction]
    objects and output it in different ways.

    Arguments:
        output (dict[str, object]): Output dictionary
        print_precision (int): How many decimal places to print out
        obj (str): Type of object ("formula" or "reaction")

    Attributes:
        rounded_values (dict[str, object]): Output dictionary rounded to print_precision
        original_stdout (TextIO | Any): Default stdout

    Raise:
        ValueError if print_precision <= 0 <br / >
        ValueError if obj is not "ChemicalFormula" or "ChemicalReaction"
    """

    def __init__(
        self, output: dict[str, object], print_precision: int, obj: str
    ) -> None:
        if print_precision > 0:
            self.print_precision: int = print_precision
        else:
            raise ValueError("precision <= 0")

        if obj in {"ChemicalFormula", "ChemicalReaction"}:
            self.obj = obj
        else:
            raise ValueError(f"No object of a class: {obj}")

        self.output: dict[str, object] = output
        self.rounded_values: dict[str, object] = self._round_values()
        self.original_stdout = sys.stdout

    def _round_values(self) -> dict[str, object]:
        """
        Round values of output dictionary to the print_precision.
        Rounding is different depending on the type of the value.

        Returns:
            Rounded dictionary
        """
        rounded_dict: dict[str, object] = {}
        for name, value in self.output.items():
            if isinstance(value, float):
                rounded_value = round(value, self.print_precision)
            elif isinstance(value, dict):
                rounded_value = round_dict_content(value, self.print_precision)  # type: ignore
            elif name == "masses":
                rounded_value = [round(v, self.print_precision) for v in value]  # type: ignore
            elif name == "reaction matrix":
                rounded_value = np.array2string(value)  # type: ignore
            else:
                rounded_value = value

            rounded_dict.update({name: rounded_value})  # type: ignore

        return rounded_dict

    def _generate_filename(self, file_type: str) -> str:
        """
        Generates a filename for an output file in the form of:
        "CSC_object type_formula or target_nanosec since the Epoch.txt or json"

        Returns:
            String of a filename
        """
        if self.obj == "ChemicalFormula":
            filename: str = (
                f"CSC_{self.obj}_{self.output.get('formula')}_{time.time_ns()}.{file_type}"
            )
        else:
            filename: str = (
                f"CSC_{self.obj}_{self.output.get('target')}_{time.time_ns()}.{file_type}"
            )

        return filename

    def _print_additional_reaction_results(self) -> None:
        """
        Print output masses in a user-friendly human-readable format.
        """
        for i, formula in enumerate(self.output["formulas"]):  # type: ignore
            print(
                "%s: M = %s g/mol, m = %s g"
                % (
                    formula,
                    "%.{0}f".format(self.print_precision)
                    % round(self.output["molar masses"][i], self.print_precision),  # type: ignore
                    "%.{0}f".format(self.print_precision)
                    % round(self.output["masses"][i], self.print_precision),  # type: ignore
                )
            )

    def _print_stream(self) -> None:
        """
        Final print stream that can go to different outputs.
        """
        for name, rounded_value in self.rounded_values.items():
            if name == "reaction matrix":
                print(name + ":\n", rounded_value)
            else:
                print(name + ":", rounded_value)
        if self.obj == "ChemicalReaction":
            self._print_additional_reaction_results()

    def print_results(self) -> None:
        """
        Print a final result of calculations in stdout.
        """
        sys.stdout = self.original_stdout
        self._print_stream()

    def write_to_txt(self, filename: str) -> None:
        """
        Export the final result of the calculations in a txt file.

        Arguments:
            filename (str): filename string (should end with .txt)
        """
        if filename == "default":
            filename = self._generate_filename("txt")

        with open(filename, "w", encoding="utf-8") as file:
            sys.stdout = file
            self._print_stream()

        sys.stdout = self.original_stdout

    def dump_to_json(self) -> str:
        """
        Serialization of output into JSON object.

        Returns:
            A JSON-type object
        """
        return json.dumps(self.rounded_values, ensure_ascii=False)

    def write_to_json_file(self, filename: str) -> None:
        """
        Export a final result of calculations in a JSON file.

        Arguments:
            filename (str): filename string (should end with .json)
        """
        if filename == "default":
            filename = self._generate_filename("json")

        with open(filename, "w", encoding="utf-8") as file:
            json.dump(json.loads(self.dump_to_json()), file, ensure_ascii=False)

_round_values()

Round values of output dictionary to the print_precision. Rounding is different depending on the type of the value.

Returns:

Type Description
dict[str, object]

Rounded dictionary

Source code in src/chemsynthcalc/chem_output.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def _round_values(self) -> dict[str, object]:
    """
    Round values of output dictionary to the print_precision.
    Rounding is different depending on the type of the value.

    Returns:
        Rounded dictionary
    """
    rounded_dict: dict[str, object] = {}
    for name, value in self.output.items():
        if isinstance(value, float):
            rounded_value = round(value, self.print_precision)
        elif isinstance(value, dict):
            rounded_value = round_dict_content(value, self.print_precision)  # type: ignore
        elif name == "masses":
            rounded_value = [round(v, self.print_precision) for v in value]  # type: ignore
        elif name == "reaction matrix":
            rounded_value = np.array2string(value)  # type: ignore
        else:
            rounded_value = value

        rounded_dict.update({name: rounded_value})  # type: ignore

    return rounded_dict

_generate_filename(file_type)

Generates a filename for an output file in the form of: "CSC_object type_formula or target_nanosec since the Epoch.txt or json"

Returns:

Type Description
str

String of a filename

Source code in src/chemsynthcalc/chem_output.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def _generate_filename(self, file_type: str) -> str:
    """
    Generates a filename for an output file in the form of:
    "CSC_object type_formula or target_nanosec since the Epoch.txt or json"

    Returns:
        String of a filename
    """
    if self.obj == "ChemicalFormula":
        filename: str = (
            f"CSC_{self.obj}_{self.output.get('formula')}_{time.time_ns()}.{file_type}"
        )
    else:
        filename: str = (
            f"CSC_{self.obj}_{self.output.get('target')}_{time.time_ns()}.{file_type}"
        )

    return filename

_print_additional_reaction_results()

Print output masses in a user-friendly human-readable format.

Source code in src/chemsynthcalc/chem_output.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def _print_additional_reaction_results(self) -> None:
    """
    Print output masses in a user-friendly human-readable format.
    """
    for i, formula in enumerate(self.output["formulas"]):  # type: ignore
        print(
            "%s: M = %s g/mol, m = %s g"
            % (
                formula,
                "%.{0}f".format(self.print_precision)
                % round(self.output["molar masses"][i], self.print_precision),  # type: ignore
                "%.{0}f".format(self.print_precision)
                % round(self.output["masses"][i], self.print_precision),  # type: ignore
            )
        )

_print_stream()

Final print stream that can go to different outputs.

Source code in src/chemsynthcalc/chem_output.py
109
110
111
112
113
114
115
116
117
118
119
def _print_stream(self) -> None:
    """
    Final print stream that can go to different outputs.
    """
    for name, rounded_value in self.rounded_values.items():
        if name == "reaction matrix":
            print(name + ":\n", rounded_value)
        else:
            print(name + ":", rounded_value)
    if self.obj == "ChemicalReaction":
        self._print_additional_reaction_results()

print_results()

Print a final result of calculations in stdout.

Source code in src/chemsynthcalc/chem_output.py
121
122
123
124
125
126
def print_results(self) -> None:
    """
    Print a final result of calculations in stdout.
    """
    sys.stdout = self.original_stdout
    self._print_stream()

write_to_txt(filename)

Export the final result of the calculations in a txt file.

Parameters:

Name Type Description Default
filename str

filename string (should end with .txt)

required
Source code in src/chemsynthcalc/chem_output.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def write_to_txt(self, filename: str) -> None:
    """
    Export the final result of the calculations in a txt file.

    Arguments:
        filename (str): filename string (should end with .txt)
    """
    if filename == "default":
        filename = self._generate_filename("txt")

    with open(filename, "w", encoding="utf-8") as file:
        sys.stdout = file
        self._print_stream()

    sys.stdout = self.original_stdout

dump_to_json()

Serialization of output into JSON object.

Returns:

Type Description
str

A JSON-type object

Source code in src/chemsynthcalc/chem_output.py
144
145
146
147
148
149
150
151
def dump_to_json(self) -> str:
    """
    Serialization of output into JSON object.

    Returns:
        A JSON-type object
    """
    return json.dumps(self.rounded_values, ensure_ascii=False)

write_to_json_file(filename)

Export a final result of calculations in a JSON file.

Parameters:

Name Type Description Default
filename str

filename string (should end with .json)

required
Source code in src/chemsynthcalc/chem_output.py
153
154
155
156
157
158
159
160
161
162
163
164
def write_to_json_file(self, filename: str) -> None:
    """
    Export a final result of calculations in a JSON file.

    Arguments:
        filename (str): filename string (should end with .json)
    """
    if filename == "default":
        filename = self._generate_filename("json")

    with open(filename, "w", encoding="utf-8") as file:
        json.dump(json.loads(self.dump_to_json()), file, ensure_ascii=False)

chemical_formula

ChemicalFormula

A class for operations on a single chemical formula.

It constructs with a formula string and can calculate parsed formula, molar mass, mass percent, atomic percent, oxide percent from this string using ChemicalFormulaParser and MolarMassCalculation.

Parameters:

Name Type Description Default
formula str

String of chemical formula

''
*custom_oxides tuple[str, ...]

An arbitrary number of non-default oxide formulas

()
precision int

Value of rounding precision (8 by default)

8
Raise

ValueError: if precision <= 0

Examples:

>>> ChemicalFormula("H2O")
H2O
>>> ChemicalFormula("H2O").molar_mass
18.015
>>> ChemicalFormula("H2O").mass_percent
{'H': 11.19067444, 'O': 88.80932556}
Source code in src/chemsynthcalc/chemical_formula.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class ChemicalFormula:
    """A class for operations on a single chemical formula.

    It constructs with a formula string and can calculate
    parsed formula, molar mass, mass percent, atomic percent,
    oxide percent from this string using
    [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser] and
    [MolarMassCalculation][chemsynthcalc.molar_mass.MolarMassCalculation].

    Parameters:
        formula (str): String of chemical formula
        *custom_oxides (tuple[str, ...]): An arbitrary number of non-default oxide formulas
        precision (int): Value of rounding precision (8 by default)

    Raise:
        ValueError: if precision <= 0

    Examples:
        >>> ChemicalFormula("H2O")
        H2O
        >>> ChemicalFormula("H2O").molar_mass
        18.015
        >>> ChemicalFormula("H2O").mass_percent
        {'H': 11.19067444, 'O': 88.80932556}
    """

    def __init__(
        self, formula: str = "", *custom_oxides: str, precision: int = 8
    ) -> None:
        if FormulaValidator(formula).validate_formula():
            self.initial_formula: str = formula.replace(" ", "")

        if precision > 0:
            self.precision: int = precision
        else:
            raise ValueError("precision <= 0")

        self.custom_oxides = custom_oxides

    def __str__(self) -> str:
        return self.formula

    def __repr__(self) -> str:
        return f"ChemicalFormula('{self.formula}', {self.precision})"

    @property
    @lru_cache(maxsize=1)
    def formula(self) -> str:
        """
        A string of chemical formula.
        It is made a property to be relatively immutable.

        Returns:
            The formula string

        Examples:
            >>> ChemicalFormula("K2SO4").formula
            K2SO4
        """
        return self.initial_formula

    @property
    @lru_cache(maxsize=1)
    def parsed_formula(self) -> dict[str, float]:
        """
        Formula parsed into dictionary keeping the initial atom order.

        Returns:
            Parsed dictionary representation of formula string created \
            by [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser]

        Examples:
            >>> ChemicalFormula("K2SO4").parsed_formula
            {'K': 2.0, 'S': 1.0, 'O': 4.0}
        """
        parsed: dict[str, float] = ChemicalFormulaParser(self.formula).parse_formula()
        return round_dict_content(parsed, self.precision, plus=3)

    @property
    @lru_cache(maxsize=1)
    def molar_mass(self) -> float:
        """
        Molar mass of the compound.

        Returns:
            The [molar mass](https://en.wikipedia.org/wiki/Molar_mass) \
            of the formula (in g/mol), calculated from \
            parsed the formula using [MolarMassCalculation][chemsynthcalc.molar_mass.MolarMassCalculation].

        Examples:
            >>> ChemicalFormula("K2SO4").molar_mass
            174.252
        """
        return round(
            MolarMassCalculation(self.parsed_formula).calculate_molar_mass(),
            self.precision,
        )

    @property
    @lru_cache(maxsize=1)
    def mass_percent(self) -> dict[str, float]:
        """
        The percentage of mass of atoms in the formula.

        Returns:
            A mass percent or \
            [relative mass fraction](https://en.wikipedia.org/wiki/Mass_fraction_(chemistry)) \
            of atoms in parsed chemical formula. The values of \
            mass content are in % (with 100% sum), not fraction. 

        Examples:
            >>> ChemicalFormula("K2SO4").mass_percent
            {'K': 44.87523816, 'S': 18.39864105, 'O': 36.72612079}
        """
        output = MolarMassCalculation(self.parsed_formula).calculate_mass_percent()
        return round_dict_content(output, self.precision)

    @property
    @lru_cache(maxsize=1)
    def atomic_percent(self) -> dict[str, float]:
        """
        Atomic percents of atoms in the formula.

        Returns:
            An atomic percent or \
            [relative mole fraction](https://en.wikipedia.org/wiki/Mole_fraction) \
            dictionary of atoms in a parsed chemical formula. \
            The values of mole content are in % (with 100% sum), \
            not fraction.

        Examples:
            >>> ChemicalFormula("K2SO4").atomic_percent
            {'K': 28.57142857, 'S': 14.28571429, 'O': 57.14285714}
        """
        output = MolarMassCalculation(self.parsed_formula).calculate_atomic_percent()
        return round_dict_content(output, self.precision)

    @property
    @lru_cache(maxsize=1)
    def oxide_percent(self) -> dict[str, float]:
        """
        Oxide percents of metals in formula. Custom oxide formulas can be provided
        with object init.

        Returns:
            An oxide percent or \
            [oxide fraction](https://d32ogoqmya1dw8.cloudfront.net/files/introgeo/studio/examples/minex02.pdf) \
            dictionary of atoms in parsed chemical formula. Oxide types are listed \
            in the [chemsynthcalc.periodic_table][] file and can be changed to \
            any oxide formula. The values of oxide content \
            are in % (with 100% sum), not fraction.

        Examples:
            >>> ChemicalFormula("K2SO4").oxide_percent
            {'K2O': 54.05676836, 'SO3': 45.94323164}
            >>> ChemicalFormula("K2FeO4", "FeO3").oxide_percent
            {'K2O': 47.56434404, 'FeO3': 52.43565596}
        """
        output = MolarMassCalculation(self.parsed_formula).calculate_oxide_percent(
            *self.custom_oxides
        )
        return round_dict_content(output, self.precision)

    @property
    @lru_cache(maxsize=1)
    def output_results(self) -> dict[str, object]:
        """
        Dictionary of the calculation result output for class.

        Returns:
            Output dictionary for all properties listed above

        Examples:
            >>> ChemicalFormula("K2SO4").output_results
            {'formula': 'K2SO4', 'parsed formula': {'K': 2.0, 'S': 1.0, 'O': 4.0},
            'molar mass': 174.252, 'mass percent': {'K': 44.87523816, 'S': 18.39864105, 'O': 36.72612079},
            'atomic percent': {'K': 28.57142857, 'S': 14.28571429, 'O': 57.14285714},
            'oxide percent': {'K2O': 54.05676836, 'SO3': 45.94323164}}
        """
        return {
            "formula": self.formula,
            "parsed formula": self.parsed_formula,
            "molar mass": self.molar_mass,
            "mass percent": self.mass_percent,
            "atomic percent": self.atomic_percent,
            "oxide percent": self.oxide_percent,
        }

    def print_results(self, print_precision: int = 4) -> None:
        """
        Print a final result of calculations in stdout.

        Arguments:
            print_precision (int): print precision (4 digits by default)
        """
        ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).print_results()

    def to_txt(self, filename: str = "default", print_precision: int = 4) -> None:
        """
        Export the final result of the calculations in a txt file.

        Arguments:
            filename (str): filename string (should end with .txt)
            print_precision (int): print precision (4 digits by default)
        """
        ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).write_to_txt(filename)

    def to_json(self, print_precision: int = 4) -> str:
        """
        Serialization of output into JSON object.

        Arguments:
            print_precision (int): print precision (4 digits by default)

        Returns:
            A JSON-type object
        """
        return ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).dump_to_json()

    def to_json_file(self, filename: str = "default", print_precision: int = 4) -> None:
        """
        Export a final result of calculations in a JSON file.

        Arguments:
            filename (str): filename string (should end with .json)
            print_precision (int): print precision (4 digits by default)
        """
        ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).write_to_json_file(filename)

formula cached property

A string of chemical formula. It is made a property to be relatively immutable.

Returns:

Type Description
str

The formula string

Examples:

>>> ChemicalFormula("K2SO4").formula
K2SO4

parsed_formula cached property

Formula parsed into dictionary keeping the initial atom order.

Returns:

Type Description
dict[str, float]

Parsed dictionary representation of formula string created by ChemicalFormulaParser

Examples:

>>> ChemicalFormula("K2SO4").parsed_formula
{'K': 2.0, 'S': 1.0, 'O': 4.0}

molar_mass cached property

Molar mass of the compound.

Returns:

Type Description
float

The molar mass of the formula (in g/mol), calculated from parsed the formula using MolarMassCalculation.

Examples:

>>> ChemicalFormula("K2SO4").molar_mass
174.252

mass_percent cached property

The percentage of mass of atoms in the formula.

Returns:

Type Description
dict[str, float]

A mass percent or relative mass fraction of atoms in parsed chemical formula. The values of mass content are in % (with 100% sum), not fraction.

Examples:

>>> ChemicalFormula("K2SO4").mass_percent
{'K': 44.87523816, 'S': 18.39864105, 'O': 36.72612079}

atomic_percent cached property

Atomic percents of atoms in the formula.

Returns:

Type Description
dict[str, float]

An atomic percent or relative mole fraction dictionary of atoms in a parsed chemical formula. The values of mole content are in % (with 100% sum), not fraction.

Examples:

>>> ChemicalFormula("K2SO4").atomic_percent
{'K': 28.57142857, 'S': 14.28571429, 'O': 57.14285714}

oxide_percent cached property

Oxide percents of metals in formula. Custom oxide formulas can be provided with object init.

Returns:

Type Description
dict[str, float]

An oxide percent or oxide fraction dictionary of atoms in parsed chemical formula. Oxide types are listed in the chemsynthcalc.periodic_table file and can be changed to any oxide formula. The values of oxide content are in % (with 100% sum), not fraction.

Examples:

>>> ChemicalFormula("K2SO4").oxide_percent
{'K2O': 54.05676836, 'SO3': 45.94323164}
>>> ChemicalFormula("K2FeO4", "FeO3").oxide_percent
{'K2O': 47.56434404, 'FeO3': 52.43565596}

output_results cached property

Dictionary of the calculation result output for class.

Returns:

Type Description
dict[str, object]

Output dictionary for all properties listed above

Examples:

>>> ChemicalFormula("K2SO4").output_results
{'formula': 'K2SO4', 'parsed formula': {'K': 2.0, 'S': 1.0, 'O': 4.0},
'molar mass': 174.252, 'mass percent': {'K': 44.87523816, 'S': 18.39864105, 'O': 36.72612079},
'atomic percent': {'K': 28.57142857, 'S': 14.28571429, 'O': 57.14285714},
'oxide percent': {'K2O': 54.05676836, 'SO3': 45.94323164}}

print_results(print_precision=4)

Print a final result of calculations in stdout.

Parameters:

Name Type Description Default
print_precision int

print precision (4 digits by default)

4
Source code in src/chemsynthcalc/chemical_formula.py
198
199
200
201
202
203
204
205
206
207
def print_results(self, print_precision: int = 4) -> None:
    """
    Print a final result of calculations in stdout.

    Arguments:
        print_precision (int): print precision (4 digits by default)
    """
    ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).print_results()

to_txt(filename='default', print_precision=4)

Export the final result of the calculations in a txt file.

Parameters:

Name Type Description Default
filename str

filename string (should end with .txt)

'default'
print_precision int

print precision (4 digits by default)

4
Source code in src/chemsynthcalc/chemical_formula.py
209
210
211
212
213
214
215
216
217
218
219
def to_txt(self, filename: str = "default", print_precision: int = 4) -> None:
    """
    Export the final result of the calculations in a txt file.

    Arguments:
        filename (str): filename string (should end with .txt)
        print_precision (int): print precision (4 digits by default)
    """
    ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).write_to_txt(filename)

to_json(print_precision=4)

Serialization of output into JSON object.

Parameters:

Name Type Description Default
print_precision int

print precision (4 digits by default)

4

Returns:

Type Description
str

A JSON-type object

Source code in src/chemsynthcalc/chemical_formula.py
221
222
223
224
225
226
227
228
229
230
231
232
233
def to_json(self, print_precision: int = 4) -> str:
    """
    Serialization of output into JSON object.

    Arguments:
        print_precision (int): print precision (4 digits by default)

    Returns:
        A JSON-type object
    """
    return ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).dump_to_json()

to_json_file(filename='default', print_precision=4)

Export a final result of calculations in a JSON file.

Parameters:

Name Type Description Default
filename str

filename string (should end with .json)

'default'
print_precision int

print precision (4 digits by default)

4
Source code in src/chemsynthcalc/chemical_formula.py
235
236
237
238
239
240
241
242
243
244
245
def to_json_file(self, filename: str = "default", print_precision: int = 4) -> None:
    """
    Export a final result of calculations in a JSON file.

    Arguments:
        filename (str): filename string (should end with .json)
        print_precision (int): print precision (4 digits by default)
    """
    ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).write_to_json_file(filename)

chemical_reaction

ChemicalReaction

A class that represents a chemical reaction and do operations on it.

There are three calculation modes:

  1. The "force" mode is used when a user enters coefficients in the reaction string and wants the masses to be calculated whether the reaction is balanced or not.

  2. "check" mode is the same as force, but with reaction balance checks.

  3. "balance" mode tries to automatically calculate coefficients from the reaction string.

Important

Unlike other properties of this class, the coefficients property can be set directly.

Parameters:

Name Type Description Default
reaction str

A reaction string

''
mode str

Coefficients calculation mode

'balance'
target int

Index of target compound (0 by default, or first compound in the products), can be negative (limited by reactant)

0
target_mass float

Desired mass of target compound (in grams)

1.0
precision int

Value of rounding precision (8 by default)

8
intify bool

Is it required to convert the coefficients to integer values?

True

Attributes:

Name Type Description
algorithm str

Currently used calculation algorithm

Raise

ValueError if precision or target mass <= 0

Source code in src/chemsynthcalc/chemical_reaction.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
class ChemicalReaction:
    """
    A class that represents a chemical reaction and do operations on it.

    There are three calculation modes:

    1. The "force" mode is used when a user enters coefficients
    in the reaction string and wants the masses to be calculated
    whether the reaction is balanced or not.

    2. "check" mode is the same as force, but with reaction
    balance checks.

    3. "balance" mode  tries to automatically calculate
    coefficients from the reaction string.

    Important:
        Unlike other properties of this class, the [coefficients][chemsynthcalc.chemical_reaction.ChemicalReaction.coefficients]
        property can be set directly.

    Arguments:
        reaction (str): A reaction string
        mode (str): Coefficients calculation mode
        target (int): Index of target compound (0 by default, or first compound in the products), can be negative (limited by reactant)
        target_mass (float): Desired mass of target compound (in grams)
        precision (int): Value of rounding precision (8 by default)
        intify (bool): Is it required to convert the coefficients to integer values?

    Attributes:
        algorithm (str): Currently used calculation algorithm

    Raise:
        ValueError if precision or target mass <= 0
    """

    def __init__(
        self,
        reaction: str = "",
        mode: str = "balance",
        target: int = 0,
        target_mass: float = 1.0,
        precision: int = 8,
        intify: bool = True,
    ) -> None:
        if ReactionValidator(reaction).validate_reaction():
            self.initial_reaction = reaction.replace(" ", "")

        if precision > 0:
            self.precision: int = precision
        else:
            raise ValueError("precision <= 0")

        if target_mass > 0:
            self.target_mass: float = target_mass
        else:
            raise ValueError("target mass <= 0")

        self.intify: bool = intify
        self.mode: str = mode
        self.algorithm: str = "user"
        self.initial_target: int = target

    def __repr__(self) -> str:
        return f"ChemicalReaction({self.reaction}, {self.mode}, {self.initial_target}, {self.target_mass}, {self.precision}, {self.intify})"

    def __str__(self) -> str:
        return self.reaction

    @property
    @lru_cache(maxsize=1)
    def reaction(self) -> str:
        """
        A string of chemical reaction.
        It is made a property to be relatively immutable.

        Returns:
            The reaction string

        Examples:
            >>> ChemicalReaction("H2+O2=H2O").reaction
            H2+O2=H2O
        """
        return self.initial_reaction

    @property
    @lru_cache(maxsize=1)
    def decomposed_reaction(self) -> ReactionDecomposer:
        """
        Decomposition of chemical reaction string and extraction of
        reaction separator, reactants, products and initial coefficients.

        Returns:
            A ReactionDecomposer object

        Examples:
            >>> ChemicalReaction("H2+O2=H2O").decomposed_reaction
            separator: =; reactants: ['H2', 'O2']; products: ['H2O']
        """
        return ReactionDecomposer(self.reaction)

    @property
    @lru_cache(maxsize=1)
    def _calculated_target(self) -> int:
        """
        Checks if initial_target is in the reaction's compounds range,
        and calculates the usable target integer.

        Returns:
            Final target

        Raise:
            IndexError if The target integer is not in the range

        Examples:
            >>> ChemicalReaction("H2+O2=H2O")._calculated_target
            2
        """
        high = len(self.decomposed_reaction.products) - 1
        low = -len(self.decomposed_reaction.reactants)
        if self.initial_target <= high and self.initial_target >= low:
            return self.initial_target - low
        else:
            raise IndexError(
                f"The target integer {self.initial_target} should be in range {low} : {high}"
            )

    @property
    @lru_cache(maxsize=1)
    def chemformula_objs(self) -> list[ChemicalFormula]:
        """Decomposition of a list of formulas from the decomposed_reaction.

        Returns:
            Every compound as ChemicalFormula object

        Examples:
            >>> ChemicalReaction("H2+O2=H2O").chemformula_objs
            [ChemicalFormula('H2', 8), ChemicalFormula('O2', 8), ChemicalFormula('H2O', 8)]
        """
        return [
            ChemicalFormula(formula, precision=self.precision)
            for formula in self.decomposed_reaction.compounds
        ]

    @property
    @lru_cache(maxsize=1)
    def parsed_formulas(self) -> list[dict[str, float]]:
        """
        List of formulas parsed by [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser]

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").parsed_formulas
            [{'K': 1.0, 'Mn': 1.0, 'O': 4.0}, {'H': 1.0, 'Cl': 1.0}, {'Mn': 1.0, 'Cl': 2.0}, {'Cl': 2.0}, {'H': 2.0, 'O': 1.0}, {'K': 1.0, 'Cl': 1.0}]
        """
        return [compound.parsed_formula for compound in self.chemformula_objs]

    @property
    @lru_cache(maxsize=1)
    def matrix(self) -> npt.NDArray[np.float64]:
        """Chemical reaction matrix.

        The first implementation of reaction matrix method is probably
        belongs to [Blakley](https://doi.org/10.1021/ed059p728). In general,
        a chemical reaction matrix is composed of the coefficients of each
        atom in each compound, giving a 2D array. The matrix composes
        naturally from previously parsed formulas.

        Returns:
            2D array of each atom amount in each formula

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").matrix
            [[1. 0. 0. 0. 0. 1.]  (K)
            [1. 0. 1. 0. 0. 0.]   (Mn)
            [4. 0. 0. 0. 1. 0.]   (O)
            [0. 1. 0. 0. 2. 0.]   (H)
            [0. 1. 2. 2. 0. 1.]]  (Cl)
        """
        return ChemicalReactionMatrix(self.parsed_formulas).matrix

    @property
    @lru_cache(maxsize=1)
    def balancer(self) -> Balancer:
        """
        A balancer to  automatically balance chemical reaction by different matrix methods.

        Returns:
            A Balancer object

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").balancer
            Balancer object for matrix
            [[1. 0. 0. 0. 0. 1.]
            [1. 0. 1. 0. 0. 0.]
            [4. 0. 0. 0. 1. 0.]
            [0. 1. 0. 0. 2. 0.]
            [0. 1. 2. 2. 0. 1.]]
        """
        return Balancer(
            self.matrix,
            len(self.decomposed_reaction.reactants),
            self.precision,
            intify=self.intify,
        )

    @property
    @lru_cache(maxsize=1)
    def molar_masses(self) -> list[float]:
        """
        List of molar masses (in g/mol)

        Returns:
            List of molar masses of each compound in [chemformula_objs][chemsynthcalc.chemical_reaction.ChemicalReaction.chemformula_objs]"

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").molar_masses
            [158.032043, 36.458, 125.838043, 70.9, 18.015, 74.548]
        """
        return [compound.molar_mass for compound in self.chemformula_objs]

    @cached_property
    def coefficients(self) -> list[float | int] | list[int]:
        """
        Coefficients of the chemical reaction. Can be calculated (balance mode),
        striped off the initial reaction string (force or check modes) or set directly.

        Returns:
            A list of coefficients

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl", mode="balance").coefficients
            [2, 16, 2, 5, 8, 2]
            >>> reaction = ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl", mode="check")
            >>> reaction.coefficients = [2, 16, 2, 5, 8, 2]
            >>> reaction.coefficients
            [2, 16, 2, 5, 8, 2]
            >>> ChemicalReaction("2H2+2O2=H2O", mode="force").coefficients
            [2, 2, 1]
        """
        coefs, self.algorithm = Coefficients(
            self.mode,
            self.parsed_formulas,
            self.matrix,
            self.balancer,
            self.decomposed_reaction,
        ).get_coefficients()
        return coefs

    @property
    @lru_cache(maxsize=1)
    def normalized_coefficients(self) -> list[float | int] | list[int]:
        """
        List of coefficients normalized on target compound.

        Target coefficient = 1.0

        Returns:
            Normalized coefficients

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").normalized_coefficients
            [1, 8, 1, 2.5, 4, 1]
        """
        Coefficients(
            self.mode,
            self.parsed_formulas,
            self.matrix,
            self.balancer,
            self.decomposed_reaction,
        ).coefficients_validation(self.coefficients)

        target_compound = self.coefficients[self._calculated_target]
        normalized_coefficients: list[float | int] | list[int] = [
            coef / target_compound for coef in self.coefficients
        ]
        return [
            int(i) if i.is_integer() else round(i, self.precision)
            for i in normalized_coefficients
        ]

    @property
    def is_balanced(self) -> bool:
        """
        Is the reaction balanced with the current coefficients?

        Returns:
            True if the reaction is balanced

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").is_balanced
            True
        """
        return Balancer.is_reaction_balanced(
            self.balancer.reactant_matrix,
            self.balancer.product_matrix,
            self.coefficients,
        )

    def _generate_final_reaction(self, coefs: list[float | int] | list[int]) -> str:
        """
        Final reaction string with connotated formulas and calculated coefficients.

        Parameters:
            coefs ( list[float | int] | list[int]): list of coefficients

        Returns:
            String of the final reaction

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl")._generate_final_reaction([2, 16, 2, 5, 8, 2])
            2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl
        """
        final_reaction = [
            (
                str(coefs[i]) + str(compound)
                if coefs[i] != 1 or coefs[i] != 1.0
                else str(compound)
            )
            for i, compound in enumerate(self.decomposed_reaction.compounds)
        ]
        final_reaction = (self.decomposed_reaction.reactant_separator).join(
            final_reaction
        )
        final_reaction = final_reaction.replace(
            self.decomposed_reaction.reactants[-1]
            + self.decomposed_reaction.reactant_separator,
            self.decomposed_reaction.reactants[-1] + self.decomposed_reaction.separator,
        )
        return final_reaction

    @property
    @lru_cache(maxsize=1)
    def final_reaction(self) -> str:
        """
        Final representation of the reaction with coefficients.

        Returns:
            A string of the final reaction

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").final_reaction
            2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl
        """
        return self._generate_final_reaction(self.coefficients)

    @property
    @lru_cache(maxsize=1)
    def final_reaction_normalized(self) -> str:
        """
        Final representation of the reaction with normalized coefficients.

        Returns:
            A string of the normalized final reaction

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").final_reaction_normalized
            KMnO4+8HCl=MnCl2+2.5Cl2+4H2O+KCl
        """
        return self._generate_final_reaction(self.normalized_coefficients)

    @property
    @lru_cache(maxsize=1)
    def masses(self) -> list[float]:
        """
        List of masses of compounds (in grams).

        List of masses of the of formulas in reaction
        calculated with coefficients obtained by any of the 3 methods.
        Calculates masses by calculating amount of substance nu (nu=mass/molar mass).
        Coefficients of reaction are normalized to the target. After nu of target compound is
        calculated, it broadcasted to other compounds (with respect to their coefficients).

        Returns:
            A list of masses of compounds

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").masses
            [1.25583678, 2.31777285, 1.0, 1.40855655, 0.57264082, 0.59241226]
        """
        nu = self.target_mass / self.molar_masses[self._calculated_target]
        masses = [
            round(molar * nu * self.normalized_coefficients[i], self.precision)
            for i, molar in enumerate(self.molar_masses)
        ]
        return masses

    @property
    @lru_cache(maxsize=1)
    def output_results(self) -> dict[str, object]:
        """
        Collection of every output of calculated ChemicalReaction properties.

        Returns:
            All outputs collected in one dictionary.

        Examples:
            >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").output_results
            {'initial reaction': 'KMnO4+HCl=MnCl2+Cl2+H2O+KCl',
            'reaction matrix': array([[1., 0., 0., 0., 0., 1.],
            [1., 0., 1., 0., 0., 0.],
            [4., 0., 0., 0., 1., 0.],
            [0., 1., 0., 0., 2., 0.],
            [0., 1., 2., 2., 0., 1.]]),
            'mode': 'balance',
            'formulas': ['KMnO4', 'HCl', 'MnCl2', 'Cl2', 'H2O', 'KCl'],
            'coefficients': [2, 16, 2, 5, 8, 2],
            'normalized coefficients': [1, 8, 1, 2.5, 4, 1],
            'algorithm': 'inverse',
            'is balanced': True,
            'final reaction': '2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl',
            'final reaction normalized': 'KMnO4+8HCl=MnCl2+2.5Cl2+4H2O+KCl',
            'molar masses': [158.032043, 36.458, 125.838043, 70.9, 18.015, 74.548],
            'target': 'MnCl2',
            'masses': [1.25583678, 2.31777285, 1.0, 1.40855655, 0.57264082, 0.59241226]}
        """
        return {
            "initial reaction": self.reaction,
            "reaction matrix": self.matrix,
            "mode": self.mode,
            "formulas": self.decomposed_reaction.compounds,
            "coefficients": self.coefficients,
            "normalized coefficients": self.normalized_coefficients,
            "algorithm": self.algorithm,
            "is balanced": self.is_balanced,
            "final reaction": self.final_reaction,
            "final reaction normalized": self.final_reaction_normalized,
            "molar masses": self.molar_masses,
            "target": self.decomposed_reaction.compounds[self._calculated_target],
            "masses": self.masses,
        }

    def print_results(self, print_precision: int = 4) -> None:
        """
        Print a final result of calculations in stdout.

        Arguments:
            print_precision (int): print precision (4 digits by default)
        """
        ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).print_results()

    def to_txt(self, filename: str = "default", print_precision: int = 4) -> None:
        """
        Export the final result of the calculations in a txt file.

        Arguments:
            filename (str): filename string (should end with .txt)
            print_precision (int): print precision (4 digits by default)
        """
        ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).write_to_txt(filename)

    def to_json(self, print_precision: int = 4) -> str:
        """
        Serialization of output into JSON object.

        Arguments:
            print_precision (int): print precision (4 digits by default)

        Returns:
            A JSON-type object
        """
        return ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).dump_to_json()

    def to_json_file(self, filename: str = "default", print_precision: int = 4) -> None:
        """
        Export a final result of calculations in a JSON file.

        Arguments:
            filename (str): filename string (should end with .json)
            print_precision (int): print precision (4 digits by default)
        """
        ChemicalOutput(
            self.output_results, print_precision, obj=self.__class__.__name__
        ).write_to_json_file(filename)

reaction cached property

A string of chemical reaction. It is made a property to be relatively immutable.

Returns:

Type Description
str

The reaction string

Examples:

>>> ChemicalReaction("H2+O2=H2O").reaction
H2+O2=H2O

decomposed_reaction cached property

Decomposition of chemical reaction string and extraction of reaction separator, reactants, products and initial coefficients.

Returns:

Type Description
ReactionDecomposer

A ReactionDecomposer object

Examples:

>>> ChemicalReaction("H2+O2=H2O").decomposed_reaction
separator: =; reactants: ['H2', 'O2']; products: ['H2O']

_calculated_target cached property

Checks if initial_target is in the reaction's compounds range, and calculates the usable target integer.

Returns:

Type Description
int

Final target

Raise

IndexError if The target integer is not in the range

Examples:

>>> ChemicalReaction("H2+O2=H2O")._calculated_target
2

chemformula_objs cached property

Decomposition of a list of formulas from the decomposed_reaction.

Returns:

Type Description
list[ChemicalFormula]

Every compound as ChemicalFormula object

Examples:

>>> ChemicalReaction("H2+O2=H2O").chemformula_objs
[ChemicalFormula('H2', 8), ChemicalFormula('O2', 8), ChemicalFormula('H2O', 8)]

parsed_formulas cached property

List of formulas parsed by ChemicalFormulaParser

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").parsed_formulas
[{'K': 1.0, 'Mn': 1.0, 'O': 4.0}, {'H': 1.0, 'Cl': 1.0}, {'Mn': 1.0, 'Cl': 2.0}, {'Cl': 2.0}, {'H': 2.0, 'O': 1.0}, {'K': 1.0, 'Cl': 1.0}]

matrix cached property

Chemical reaction matrix.

The first implementation of reaction matrix method is probably belongs to Blakley. In general, a chemical reaction matrix is composed of the coefficients of each atom in each compound, giving a 2D array. The matrix composes naturally from previously parsed formulas.

Returns:

Type Description
NDArray[float64]

2D array of each atom amount in each formula

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").matrix
[[1. 0. 0. 0. 0. 1.]  (K)
[1. 0. 1. 0. 0. 0.]   (Mn)
[4. 0. 0. 0. 1. 0.]   (O)
[0. 1. 0. 0. 2. 0.]   (H)
[0. 1. 2. 2. 0. 1.]]  (Cl)

balancer cached property

A balancer to automatically balance chemical reaction by different matrix methods.

Returns:

Type Description
Balancer

A Balancer object

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").balancer
Balancer object for matrix
[[1. 0. 0. 0. 0. 1.]
[1. 0. 1. 0. 0. 0.]
[4. 0. 0. 0. 1. 0.]
[0. 1. 0. 0. 2. 0.]
[0. 1. 2. 2. 0. 1.]]

molar_masses cached property

List of molar masses (in g/mol)

Returns:

Type Description
list[float]

List of molar masses of each compound in chemformula_objs"

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").molar_masses
[158.032043, 36.458, 125.838043, 70.9, 18.015, 74.548]

coefficients cached property

Coefficients of the chemical reaction. Can be calculated (balance mode), striped off the initial reaction string (force or check modes) or set directly.

Returns:

Type Description
list[float | int] | list[int]

A list of coefficients

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl", mode="balance").coefficients
[2, 16, 2, 5, 8, 2]
>>> reaction = ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl", mode="check")
>>> reaction.coefficients = [2, 16, 2, 5, 8, 2]
>>> reaction.coefficients
[2, 16, 2, 5, 8, 2]
>>> ChemicalReaction("2H2+2O2=H2O", mode="force").coefficients
[2, 2, 1]

normalized_coefficients cached property

List of coefficients normalized on target compound.

Target coefficient = 1.0

Returns:

Type Description
list[float | int] | list[int]

Normalized coefficients

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").normalized_coefficients
[1, 8, 1, 2.5, 4, 1]

is_balanced property

Is the reaction balanced with the current coefficients?

Returns:

Type Description
bool

True if the reaction is balanced

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").is_balanced
True

final_reaction cached property

Final representation of the reaction with coefficients.

Returns:

Type Description
str

A string of the final reaction

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").final_reaction
2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl

final_reaction_normalized cached property

Final representation of the reaction with normalized coefficients.

Returns:

Type Description
str

A string of the normalized final reaction

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").final_reaction_normalized
KMnO4+8HCl=MnCl2+2.5Cl2+4H2O+KCl

masses cached property

List of masses of compounds (in grams).

List of masses of the of formulas in reaction calculated with coefficients obtained by any of the 3 methods. Calculates masses by calculating amount of substance nu (nu=mass/molar mass). Coefficients of reaction are normalized to the target. After nu of target compound is calculated, it broadcasted to other compounds (with respect to their coefficients).

Returns:

Type Description
list[float]

A list of masses of compounds

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").masses
[1.25583678, 2.31777285, 1.0, 1.40855655, 0.57264082, 0.59241226]

output_results cached property

Collection of every output of calculated ChemicalReaction properties.

Returns:

Type Description
dict[str, object]

All outputs collected in one dictionary.

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl").output_results
{'initial reaction': 'KMnO4+HCl=MnCl2+Cl2+H2O+KCl',
'reaction matrix': array([[1., 0., 0., 0., 0., 1.],
[1., 0., 1., 0., 0., 0.],
[4., 0., 0., 0., 1., 0.],
[0., 1., 0., 0., 2., 0.],
[0., 1., 2., 2., 0., 1.]]),
'mode': 'balance',
'formulas': ['KMnO4', 'HCl', 'MnCl2', 'Cl2', 'H2O', 'KCl'],
'coefficients': [2, 16, 2, 5, 8, 2],
'normalized coefficients': [1, 8, 1, 2.5, 4, 1],
'algorithm': 'inverse',
'is balanced': True,
'final reaction': '2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl',
'final reaction normalized': 'KMnO4+8HCl=MnCl2+2.5Cl2+4H2O+KCl',
'molar masses': [158.032043, 36.458, 125.838043, 70.9, 18.015, 74.548],
'target': 'MnCl2',
'masses': [1.25583678, 2.31777285, 1.0, 1.40855655, 0.57264082, 0.59241226]}

_generate_final_reaction(coefs)

Final reaction string with connotated formulas and calculated coefficients.

Parameters:

Name Type Description Default
coefs list[float | int] | list[int]

list of coefficients

required

Returns:

Type Description
str

String of the final reaction

Examples:

>>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl")._generate_final_reaction([2, 16, 2, 5, 8, 2])
2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl
Source code in src/chemsynthcalc/chemical_reaction.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def _generate_final_reaction(self, coefs: list[float | int] | list[int]) -> str:
    """
    Final reaction string with connotated formulas and calculated coefficients.

    Parameters:
        coefs ( list[float | int] | list[int]): list of coefficients

    Returns:
        String of the final reaction

    Examples:
        >>> ChemicalReaction("KMnO4+HCl=MnCl2+Cl2+H2O+KCl")._generate_final_reaction([2, 16, 2, 5, 8, 2])
        2KMnO4+16HCl=2MnCl2+5Cl2+8H2O+2KCl
    """
    final_reaction = [
        (
            str(coefs[i]) + str(compound)
            if coefs[i] != 1 or coefs[i] != 1.0
            else str(compound)
        )
        for i, compound in enumerate(self.decomposed_reaction.compounds)
    ]
    final_reaction = (self.decomposed_reaction.reactant_separator).join(
        final_reaction
    )
    final_reaction = final_reaction.replace(
        self.decomposed_reaction.reactants[-1]
        + self.decomposed_reaction.reactant_separator,
        self.decomposed_reaction.reactants[-1] + self.decomposed_reaction.separator,
    )
    return final_reaction

print_results(print_precision=4)

Print a final result of calculations in stdout.

Parameters:

Name Type Description Default
print_precision int

print precision (4 digits by default)

4
Source code in src/chemsynthcalc/chemical_reaction.py
445
446
447
448
449
450
451
452
453
454
def print_results(self, print_precision: int = 4) -> None:
    """
    Print a final result of calculations in stdout.

    Arguments:
        print_precision (int): print precision (4 digits by default)
    """
    ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).print_results()

to_txt(filename='default', print_precision=4)

Export the final result of the calculations in a txt file.

Parameters:

Name Type Description Default
filename str

filename string (should end with .txt)

'default'
print_precision int

print precision (4 digits by default)

4
Source code in src/chemsynthcalc/chemical_reaction.py
456
457
458
459
460
461
462
463
464
465
466
def to_txt(self, filename: str = "default", print_precision: int = 4) -> None:
    """
    Export the final result of the calculations in a txt file.

    Arguments:
        filename (str): filename string (should end with .txt)
        print_precision (int): print precision (4 digits by default)
    """
    ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).write_to_txt(filename)

to_json(print_precision=4)

Serialization of output into JSON object.

Parameters:

Name Type Description Default
print_precision int

print precision (4 digits by default)

4

Returns:

Type Description
str

A JSON-type object

Source code in src/chemsynthcalc/chemical_reaction.py
468
469
470
471
472
473
474
475
476
477
478
479
480
def to_json(self, print_precision: int = 4) -> str:
    """
    Serialization of output into JSON object.

    Arguments:
        print_precision (int): print precision (4 digits by default)

    Returns:
        A JSON-type object
    """
    return ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).dump_to_json()

to_json_file(filename='default', print_precision=4)

Export a final result of calculations in a JSON file.

Parameters:

Name Type Description Default
filename str

filename string (should end with .json)

'default'
print_precision int

print precision (4 digits by default)

4
Source code in src/chemsynthcalc/chemical_reaction.py
482
483
484
485
486
487
488
489
490
491
492
def to_json_file(self, filename: str = "default", print_precision: int = 4) -> None:
    """
    Export a final result of calculations in a JSON file.

    Arguments:
        filename (str): filename string (should end with .json)
        print_precision (int): print precision (4 digits by default)
    """
    ChemicalOutput(
        self.output_results, print_precision, obj=self.__class__.__name__
    ).write_to_json_file(filename)

coefs

Coefficients

A class to calculate and validate reaction coefficients depending on the calculation "mode".

Parameters:

Name Type Description Default
mode str

Calculation mode ("force", "check", "balance")

required
parsed_formulas list[dict[str, float]]

List of formulas parsed by ChemicalFormulaParser

required
matrix NDArray[float64]

Reaction matrix created by ChemicalReactionMatrix

required
balancer Balancer

A Balancer object

required
decomposed_reaction ReactionDecomposer required

Attributes:

Name Type Description
initial_coefficients list[float]

List of initial coefficients from decomposed reaction

Source code in src/chemsynthcalc/coefs.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class Coefficients:
    """
    A class to calculate and validate reaction coefficients depending on the calculation "mode".

    Arguments:
        mode (str): Calculation mode ("force", "check", "balance")
        parsed_formulas (list[dict[str, float]]): List of formulas parsed by [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser]
        matrix (npt.NDArray[np.float64]): Reaction matrix created by [ChemicalReactionMatrix][chemsynthcalc.reaction_matrix.ChemicalReactionMatrix]
        balancer (Balancer): A [Balancer][chemsynthcalc.balancer.Balancer] object
        decomposed_reaction (ReactionDecomposer): A [ReactionDecomposer][chemsynthcalc.reaction_decomposer.ReactionDecomposer] object

    Attributes:
        initial_coefficients (list[float]): List of initial coefficients from decomposed reaction
    """

    def __init__(
        self,
        mode: str,
        parsed_formulas: list[dict[str, float]],
        matrix: npt.NDArray[np.float64],
        balancer: Balancer,
        decomposed_reaction: ReactionDecomposer,
    ) -> None:
        self.mode = mode
        self.matrix = matrix
        self.balancer = balancer
        self.initial_coefficients = decomposed_reaction.initial_coefficients
        self.decomposed_reaction = decomposed_reaction
        self.parsed_formulas = parsed_formulas

    def _calculate_coefficients(self) -> tuple[list[float | int] | list[int], str]:
        """
        Match a mode string and get coefficients depending on the mode

        Returns:
            Tuple of (coefficients, algorithm)

        Raise:
            [ReactionNotBalanced][chemsynthcalc.chem_errors.ReactionNotBalanced] if reaction is not balanced in the "check" mode <br />
            [NoSuchMode][chemsynthcalc.chem_errors.NoSuchMode] if there is no mode with that name
        """
        match self.mode:

            case "force":
                return (
                    to_integer(self.decomposed_reaction.initial_coefficients),
                    "user",
                )

            case "check":
                if Balancer.is_reaction_balanced(
                    self.balancer.reactant_matrix,
                    self.balancer.product_matrix,
                    self.decomposed_reaction.initial_coefficients,
                ):
                    return (
                        to_integer(self.decomposed_reaction.initial_coefficients),
                        "user",
                    )
                else:
                    raise ReactionNotBalanced("This reaction is not balanced!")

            case "balance":
                coefs, algorithm = self.balancer.auto()
                return (coefs, algorithm)

            case _:
                raise NoSuchMode(f"No mode {self.mode}")

    def coefficients_validation(
        self, coefficients: list[float | int] | list[int]
    ) -> None:
        """
        Validate a list of coefs.

        Arguments:
            coefficients (list[float | int] | list[int]): List of coefs

        Raise:
            [BadCoeffiecients][chemsynthcalc.chem_errors.BadCoeffiecients] if any coef <= 0 or
            lenght of list is not equal to the number of compounds.
        """
        if any(x <= 0 for x in coefficients):
            raise BadCoeffiecients("0 or -x in coefficients")
        elif len(coefficients) != self.matrix.shape[1]:
            raise BadCoeffiecients(
                f"Number of coefficients should be equal to {self.matrix.shape[1]}"
            )

    def _element_count_validation(self) -> None:
        """
        Calculate a [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
        of two sets - left and right parts of the reaction. If this set is not empty, than
        some atoms are only in one part of the reaction (which is impossible).

        Raise:
            [ReactantProductDifference][chemsynthcalc.chem_errors.ReactantProductDifference] if diff set is not empty.
        """
        if self.mode != "force":
            reactants = {
                k: v
                for d in self.parsed_formulas[: len(self.decomposed_reaction.reactants)]
                for k, v in d.items()
            }
            products = {
                k: v
                for d in self.parsed_formulas[len(self.decomposed_reaction.reactants) :]
                for k, v in d.items()
            }
            diff = set(reactants.keys()) ^ (set(products.keys()))
            if diff:
                raise ReactantProductDifference(
                    f"Cannot balance this reaction, because element(s) {diff} are only in one part of the reaction"
                )

    def get_coefficients(self) -> tuple[list[float | int] | list[int], str]:
        """
        Validate atom's diff and coefs list and finally get a proper coefficients list.

        Returns:
            Tuple of (coefficients, algorithm)
        """
        self._element_count_validation()
        coefs = self._calculate_coefficients()
        self.coefficients_validation(coefs[0])
        return coefs

_calculate_coefficients()

Match a mode string and get coefficients depending on the mode

Returns:

Type Description
tuple[list[float | int] | list[int], str]

Tuple of (coefficients, algorithm)

Raise

ReactionNotBalanced if reaction is not balanced in the "check" mode
NoSuchMode if there is no mode with that name

Source code in src/chemsynthcalc/coefs.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def _calculate_coefficients(self) -> tuple[list[float | int] | list[int], str]:
    """
    Match a mode string and get coefficients depending on the mode

    Returns:
        Tuple of (coefficients, algorithm)

    Raise:
        [ReactionNotBalanced][chemsynthcalc.chem_errors.ReactionNotBalanced] if reaction is not balanced in the "check" mode <br />
        [NoSuchMode][chemsynthcalc.chem_errors.NoSuchMode] if there is no mode with that name
    """
    match self.mode:

        case "force":
            return (
                to_integer(self.decomposed_reaction.initial_coefficients),
                "user",
            )

        case "check":
            if Balancer.is_reaction_balanced(
                self.balancer.reactant_matrix,
                self.balancer.product_matrix,
                self.decomposed_reaction.initial_coefficients,
            ):
                return (
                    to_integer(self.decomposed_reaction.initial_coefficients),
                    "user",
                )
            else:
                raise ReactionNotBalanced("This reaction is not balanced!")

        case "balance":
            coefs, algorithm = self.balancer.auto()
            return (coefs, algorithm)

        case _:
            raise NoSuchMode(f"No mode {self.mode}")

coefficients_validation(coefficients)

Validate a list of coefs.

Parameters:

Name Type Description Default
coefficients list[float | int] | list[int]

List of coefs

required
Raise

BadCoeffiecients if any coef <= 0 or lenght of list is not equal to the number of compounds.

Source code in src/chemsynthcalc/coefs.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def coefficients_validation(
    self, coefficients: list[float | int] | list[int]
) -> None:
    """
    Validate a list of coefs.

    Arguments:
        coefficients (list[float | int] | list[int]): List of coefs

    Raise:
        [BadCoeffiecients][chemsynthcalc.chem_errors.BadCoeffiecients] if any coef <= 0 or
        lenght of list is not equal to the number of compounds.
    """
    if any(x <= 0 for x in coefficients):
        raise BadCoeffiecients("0 or -x in coefficients")
    elif len(coefficients) != self.matrix.shape[1]:
        raise BadCoeffiecients(
            f"Number of coefficients should be equal to {self.matrix.shape[1]}"
        )

_element_count_validation()

Calculate a symmetric difference of two sets - left and right parts of the reaction. If this set is not empty, than some atoms are only in one part of the reaction (which is impossible).

Raise

ReactantProductDifference if diff set is not empty.

Source code in src/chemsynthcalc/coefs.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def _element_count_validation(self) -> None:
    """
    Calculate a [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
    of two sets - left and right parts of the reaction. If this set is not empty, than
    some atoms are only in one part of the reaction (which is impossible).

    Raise:
        [ReactantProductDifference][chemsynthcalc.chem_errors.ReactantProductDifference] if diff set is not empty.
    """
    if self.mode != "force":
        reactants = {
            k: v
            for d in self.parsed_formulas[: len(self.decomposed_reaction.reactants)]
            for k, v in d.items()
        }
        products = {
            k: v
            for d in self.parsed_formulas[len(self.decomposed_reaction.reactants) :]
            for k, v in d.items()
        }
        diff = set(reactants.keys()) ^ (set(products.keys()))
        if diff:
            raise ReactantProductDifference(
                f"Cannot balance this reaction, because element(s) {diff} are only in one part of the reaction"
            )

get_coefficients()

Validate atom's diff and coefs list and finally get a proper coefficients list.

Returns:

Type Description
tuple[list[float | int] | list[int], str]

Tuple of (coefficients, algorithm)

Source code in src/chemsynthcalc/coefs.py
130
131
132
133
134
135
136
137
138
139
140
def get_coefficients(self) -> tuple[list[float | int] | list[int], str]:
    """
    Validate atom's diff and coefs list and finally get a proper coefficients list.

    Returns:
        Tuple of (coefficients, algorithm)
    """
    self._element_count_validation()
    coefs = self._calculate_coefficients()
    self.coefficients_validation(coefs[0])
    return coefs

formula

Formula

A base class for ChemicalFormulaParser and FormulaValidator containing regexes and symbols.

Parameters:

Name Type Description Default
formula str

Formula string

required

Attributes:

Name Type Description
atom_regex str

Regular expression for finding atoms in formula

coefficient_regex str

Regular expression for atoms amounts in formula

atom_and_coefficient_regex str

atom_regex+coefficient_regex

opener_brackets str

Opener brackets variations

closer_brackets str

Closer brackets variations

adduct_symbols str

Symbols for adduct notation (water of crystallization most often)

Source code in src/chemsynthcalc/formula.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Formula:
    """
    A base class for
    [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser] and
    [FormulaValidator][chemsynthcalc.formula_validator.FormulaValidator]
    containing regexes and symbols.

    Parameters:
        formula (str): Formula string

    Attributes:
        atom_regex (str): Regular expression for finding atoms in formula
        coefficient_regex (str): Regular expression for atoms amounts in formula
        atom_and_coefficient_regex (str): atom_regex+coefficient_regex
        opener_brackets (str): Opener brackets variations
        closer_brackets (str): Closer brackets variations
        adduct_symbols (str): Symbols for adduct notation (water of crystallization most often)
    """

    def __init__(self, formula: str) -> None:
        self.atom_regex: str = r"([A-Z][a-z]*)"
        self.coefficient_regex: str = r"((\d+(\.\d+)?)*)"
        self.allowed_symbols: str = r"[^A-Za-z0-9.({[)}\]*·•]"
        self.atom_and_coefficient_regex: str = self.atom_regex + self.coefficient_regex
        self.opener_brackets: str = "({["
        self.closer_brackets: str = ")}]"
        self.adduct_symbols: str = "*·•"

        self.formula: str = formula.replace(" ", "")

formula_parser

ChemicalFormulaParser

Bases: Formula

Parser of chemical formulas.

Methods of this class take string of compound's chemical formula and turn it into a dict of atoms as keys and their coefficients as values.

Source code in src/chemsynthcalc/formula_parser.py
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class ChemicalFormulaParser(Formula):
    """
    Parser of chemical formulas.

    Methods of this class take string of compound's chemical formula
    and turn it into a dict of atoms as keys and their coefficients as values.
    """

    def _dictify(self, tuples: list[tuple[str, ...]]) -> dict[str, float]:
        """
        Transform list of tuples to a dict of atoms.

        Parameters:
            tuples (list[tuple[str, ...]]): List of tuples of atoms

        Returns:
            Dictionary of atoms and they quantities
        """

        result: dict[str, float] = dict()
        for atom, n, _, _ in tuples:
            try:
                result[atom] += float(n or 1)
            except KeyError:
                result[atom] = float(n or 1)
        return result

    def _fuse(
        self, mol1: dict[str, float], mol2: dict[str, float], weight: float = 1.0
    ) -> dict[str, float]:
        """Fuse together 2 dicts representing molecules.

        Parameters:
            mol1 (dict[str, float]): Dict of atoms 1
            mol2 (dict[str, float]): Dict of atoms 2
            weight (float): Weight

        Returns:
            A new fused dict
        """

        fused_set: set[str] = set(mol1) | set(mol2)
        fused_dict: dict[str, float] = {
            atom: (mol1.get(atom, 0) + mol2.get(atom, 0)) * weight for atom in fused_set
        }

        return fused_dict

    def _parse(self, formula: str) -> tuple[dict[str, float], int]:
        """
        Parse the formula string

        Recurse on opening brackets to parse the subpart and
        return on closing ones because it is the end of said subpart.
        Formula is the argument of this method due to the complications
        of self. Constructions in recursive functions.

        Parameters:
            formula (str): Formula string

        Returns:
            A tuple of the molecule dict and length of parsed part
        """
        token_list: list[str] = []
        mol: dict[str, float] = {}
        i: int = 0

        while i < len(formula):
            token: str = formula[i]

            if token in self.adduct_symbols:
                coefficient_match: re.Match[str] | None = re.match(
                    self.coefficient_regex, formula[i + 1 :]
                )
                if coefficient_match and coefficient_match.group(0) != "":
                    weight: float = float(coefficient_match.group(0))
                    i += len(coefficient_match.group(0))
                else:
                    weight = 1.0
                recursive_dive: tuple[dict[str, float], int] = self._parse(
                    f"({formula[i + 1 :]}){weight}"
                )
                submol = recursive_dive[0]
                lenght: int = recursive_dive[1]
                mol = self._fuse(mol, submol)
                i += lenght + 1

            elif token in self.closer_brackets:
                coefficient_match: re.Match[str] | None = re.match(
                    self.coefficient_regex, formula[i + 1 :]
                )
                if coefficient_match and coefficient_match.group(0) != "":
                    weight: float = float(coefficient_match.group(0))
                    i += len(coefficient_match.group(0))
                else:
                    weight = 1.0
                submol: dict[str, float] = self._dictify(
                    re.findall(self.atom_and_coefficient_regex, "".join(token_list))
                )
                return self._fuse(mol, submol, weight), i

            elif token in self.opener_brackets:
                recursive_dive: tuple[dict[str, float], int] = self._parse(
                    formula[i + 1 :]
                )
                submol = recursive_dive[0]
                lenght: int = recursive_dive[1]
                mol = self._fuse(mol, submol)
                i += lenght + 1

            else:
                token_list.append(token)

            i += 1

        extract_from_tokens: list[tuple[str, ...]] = re.findall(
            self.atom_and_coefficient_regex, "".join(token_list)
        )
        fused_dict: dict[str, float] = self._fuse(
            mol, self._dictify(extract_from_tokens)
        )
        return fused_dict, i

    def _order_output_dict(self, parsed: dict[str, float]) -> dict[str, float]:
        """
        Arranges the unparsed formula in the order in which the chemical
        elements appear in it.

        Parameters:
            parsed (dict[str, float]): A formula parsed by [_parse][chemsynthcalc.formula_parser.ChemicalFormulaParser._parse]

        Returns:
            An ordered dictionary
        """
        atoms_list: list[str] = re.findall(self.atom_regex, self.formula)
        weights: list[float] = []
        for atom in atoms_list:
            weights.append(parsed[atom])
        return dict(zip(atoms_list, weights))

    def parse_formula(self) -> dict[str, float]:
        """
        Parsing and ordering of formula

        Returns:
            Parsed formula
        """
        parsed = self._parse(self.formula)[0]
        return self._order_output_dict(parsed)

_dictify(tuples)

Transform list of tuples to a dict of atoms.

Parameters:

Name Type Description Default
tuples list[tuple[str, ...]]

List of tuples of atoms

required

Returns:

Type Description
dict[str, float]

Dictionary of atoms and they quantities

Source code in src/chemsynthcalc/formula_parser.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def _dictify(self, tuples: list[tuple[str, ...]]) -> dict[str, float]:
    """
    Transform list of tuples to a dict of atoms.

    Parameters:
        tuples (list[tuple[str, ...]]): List of tuples of atoms

    Returns:
        Dictionary of atoms and they quantities
    """

    result: dict[str, float] = dict()
    for atom, n, _, _ in tuples:
        try:
            result[atom] += float(n or 1)
        except KeyError:
            result[atom] = float(n or 1)
    return result

_fuse(mol1, mol2, weight=1.0)

Fuse together 2 dicts representing molecules.

Parameters:

Name Type Description Default
mol1 dict[str, float]

Dict of atoms 1

required
mol2 dict[str, float]

Dict of atoms 2

required
weight float

Weight

1.0

Returns:

Type Description
dict[str, float]

A new fused dict

Source code in src/chemsynthcalc/formula_parser.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def _fuse(
    self, mol1: dict[str, float], mol2: dict[str, float], weight: float = 1.0
) -> dict[str, float]:
    """Fuse together 2 dicts representing molecules.

    Parameters:
        mol1 (dict[str, float]): Dict of atoms 1
        mol2 (dict[str, float]): Dict of atoms 2
        weight (float): Weight

    Returns:
        A new fused dict
    """

    fused_set: set[str] = set(mol1) | set(mol2)
    fused_dict: dict[str, float] = {
        atom: (mol1.get(atom, 0) + mol2.get(atom, 0)) * weight for atom in fused_set
    }

    return fused_dict

_parse(formula)

Parse the formula string

Recurse on opening brackets to parse the subpart and return on closing ones because it is the end of said subpart. Formula is the argument of this method due to the complications of self. Constructions in recursive functions.

Parameters:

Name Type Description Default
formula str

Formula string

required

Returns:

Type Description
tuple[dict[str, float], int]

A tuple of the molecule dict and length of parsed part

Source code in src/chemsynthcalc/formula_parser.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def _parse(self, formula: str) -> tuple[dict[str, float], int]:
    """
    Parse the formula string

    Recurse on opening brackets to parse the subpart and
    return on closing ones because it is the end of said subpart.
    Formula is the argument of this method due to the complications
    of self. Constructions in recursive functions.

    Parameters:
        formula (str): Formula string

    Returns:
        A tuple of the molecule dict and length of parsed part
    """
    token_list: list[str] = []
    mol: dict[str, float] = {}
    i: int = 0

    while i < len(formula):
        token: str = formula[i]

        if token in self.adduct_symbols:
            coefficient_match: re.Match[str] | None = re.match(
                self.coefficient_regex, formula[i + 1 :]
            )
            if coefficient_match and coefficient_match.group(0) != "":
                weight: float = float(coefficient_match.group(0))
                i += len(coefficient_match.group(0))
            else:
                weight = 1.0
            recursive_dive: tuple[dict[str, float], int] = self._parse(
                f"({formula[i + 1 :]}){weight}"
            )
            submol = recursive_dive[0]
            lenght: int = recursive_dive[1]
            mol = self._fuse(mol, submol)
            i += lenght + 1

        elif token in self.closer_brackets:
            coefficient_match: re.Match[str] | None = re.match(
                self.coefficient_regex, formula[i + 1 :]
            )
            if coefficient_match and coefficient_match.group(0) != "":
                weight: float = float(coefficient_match.group(0))
                i += len(coefficient_match.group(0))
            else:
                weight = 1.0
            submol: dict[str, float] = self._dictify(
                re.findall(self.atom_and_coefficient_regex, "".join(token_list))
            )
            return self._fuse(mol, submol, weight), i

        elif token in self.opener_brackets:
            recursive_dive: tuple[dict[str, float], int] = self._parse(
                formula[i + 1 :]
            )
            submol = recursive_dive[0]
            lenght: int = recursive_dive[1]
            mol = self._fuse(mol, submol)
            i += lenght + 1

        else:
            token_list.append(token)

        i += 1

    extract_from_tokens: list[tuple[str, ...]] = re.findall(
        self.atom_and_coefficient_regex, "".join(token_list)
    )
    fused_dict: dict[str, float] = self._fuse(
        mol, self._dictify(extract_from_tokens)
    )
    return fused_dict, i

_order_output_dict(parsed)

Arranges the unparsed formula in the order in which the chemical elements appear in it.

Parameters:

Name Type Description Default
parsed dict[str, float]

A formula parsed by _parse

required

Returns:

Type Description
dict[str, float]

An ordered dictionary

Source code in src/chemsynthcalc/formula_parser.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def _order_output_dict(self, parsed: dict[str, float]) -> dict[str, float]:
    """
    Arranges the unparsed formula in the order in which the chemical
    elements appear in it.

    Parameters:
        parsed (dict[str, float]): A formula parsed by [_parse][chemsynthcalc.formula_parser.ChemicalFormulaParser._parse]

    Returns:
        An ordered dictionary
    """
    atoms_list: list[str] = re.findall(self.atom_regex, self.formula)
    weights: list[float] = []
    for atom in atoms_list:
        weights.append(parsed[atom])
    return dict(zip(atoms_list, weights))

parse_formula()

Parsing and ordering of formula

Returns:

Type Description
dict[str, float]

Parsed formula

Source code in src/chemsynthcalc/formula_parser.py
146
147
148
149
150
151
152
153
154
def parse_formula(self) -> dict[str, float]:
    """
    Parsing and ordering of formula

    Returns:
        Parsed formula
    """
    parsed = self._parse(self.formula)[0]
    return self._order_output_dict(parsed)

formula_validator

FormulaValidator

Bases: Formula

Methods of this class validate the initial input formula.

Source code in src/chemsynthcalc/formula_validator.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class FormulaValidator(Formula):
    """
    Methods of this class validate the initial input formula.
    """

    def _check_empty_formula(self) -> bool:
        """
        Checks if formula is an empty string.
        """
        return self.formula == ""

    def _invalid_charachers(self) -> list[str]:
        """
        Checks if formula contains invalid characters.

        Returns:
            List of invalid characters
        """
        return re.compile(self.allowed_symbols).findall(self.formula)

    def _invalid_atoms(self) -> list[str]:
        """
        Checks whether the formula contains atoms that
        are not in the periodic system.

        Returns:
            List of invalid atoms
        """
        atoms_list: list[str] = re.findall(self.atom_regex, self.formula)
        invalid: list[str] = []
        new_string: str = self.formula
        # a trick to get Cl first of C in cases like CCl4
        atoms_list = sorted(list(set(atoms_list)), key=len, reverse=True)
        for atom in atoms_list:
            if atom not in PeriodicTable().atoms:
                invalid.append(atom)
            new_string = new_string.replace(atom, "")
        found_leftovers: list[str] = re.findall(r"[a-z]", new_string)
        invalid.extend(found_leftovers)
        return invalid

    def _bracket_balance(self) -> bool:
        """
        Checks whether all of the brackets come in pairs.
        """
        c: Counter[str] = Counter(self.formula)
        for i in range(len(self.opener_brackets)):
            if c[self.opener_brackets[i]] != c[self.closer_brackets[i]]:
                return False
        return True

    def _num_of_adducts(self) -> int:
        """
        Counts a number of adduct symbols
        (listed in [Formula base class][chemsynthcalc.formula.Formula]).

        Returns:
            A number of adduct symbols
        """
        c: Counter[str] = Counter(self.formula)
        i: int = 0
        for adduct in self.adduct_symbols:
            i += c[adduct]
        return i

    def validate_formula(self) -> bool:
        """
        Validation of the formula string.
        Calls the private methods of this class in order.

        Raise:
            [EmptyFormula][chemsynthcalc.chem_errors.EmptyFormula] if the formula is an empty string. <br />
            [InvalidCharacter][chemsynthcalc.chem_errors.InvalidCharacter] if there is an invalid character(s) in the string. <br />
            [NoSuchAtom][chemsynthcalc.chem_errors.NoSuchAtom] if there is an invalid atom(s) in the string. <br />
            [BracketsNotPaired][chemsynthcalc.chem_errors.BracketsNotPaired] if the brackets are not in pairs. <br />
            [MoreThanOneAdduct][chemsynthcalc.chem_errors.MoreThanOneAdduct] if there are more than 1 adduct symbols in the string. <br />

        Returns:
            True if all the checks are OK
        """
        if self._check_empty_formula():
            raise EmptyFormula
        elif self._invalid_charachers():
            raise InvalidCharacter(
                f"Invalid character(s) {self._invalid_charachers()} in the formula {self.formula}"
            )
        elif self._invalid_atoms():
            raise NoSuchAtom(
                f"The formula {self.formula} contains atom {self._invalid_atoms()} which is not in the periodic table"
            )
        elif not self._bracket_balance():
            raise BracketsNotPaired(
                f"The brackets {self.opener_brackets} {self.closer_brackets} are not balanced the formula {self.formula}!"
            )
        elif self._num_of_adducts() > 1:
            raise MoreThanOneAdduct(
                f"More than one adduct {self.adduct_symbols} in the formula {self.formula}"
            )
        return True

_check_empty_formula()

Checks if formula is an empty string.

Source code in src/chemsynthcalc/formula_validator.py
20
21
22
23
24
def _check_empty_formula(self) -> bool:
    """
    Checks if formula is an empty string.
    """
    return self.formula == ""

_invalid_charachers()

Checks if formula contains invalid characters.

Returns:

Type Description
list[str]

List of invalid characters

Source code in src/chemsynthcalc/formula_validator.py
26
27
28
29
30
31
32
33
def _invalid_charachers(self) -> list[str]:
    """
    Checks if formula contains invalid characters.

    Returns:
        List of invalid characters
    """
    return re.compile(self.allowed_symbols).findall(self.formula)

_invalid_atoms()

Checks whether the formula contains atoms that are not in the periodic system.

Returns:

Type Description
list[str]

List of invalid atoms

Source code in src/chemsynthcalc/formula_validator.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def _invalid_atoms(self) -> list[str]:
    """
    Checks whether the formula contains atoms that
    are not in the periodic system.

    Returns:
        List of invalid atoms
    """
    atoms_list: list[str] = re.findall(self.atom_regex, self.formula)
    invalid: list[str] = []
    new_string: str = self.formula
    # a trick to get Cl first of C in cases like CCl4
    atoms_list = sorted(list(set(atoms_list)), key=len, reverse=True)
    for atom in atoms_list:
        if atom not in PeriodicTable().atoms:
            invalid.append(atom)
        new_string = new_string.replace(atom, "")
    found_leftovers: list[str] = re.findall(r"[a-z]", new_string)
    invalid.extend(found_leftovers)
    return invalid

_bracket_balance()

Checks whether all of the brackets come in pairs.

Source code in src/chemsynthcalc/formula_validator.py
56
57
58
59
60
61
62
63
64
def _bracket_balance(self) -> bool:
    """
    Checks whether all of the brackets come in pairs.
    """
    c: Counter[str] = Counter(self.formula)
    for i in range(len(self.opener_brackets)):
        if c[self.opener_brackets[i]] != c[self.closer_brackets[i]]:
            return False
    return True

_num_of_adducts()

Counts a number of adduct symbols (listed in Formula base class).

Returns:

Type Description
int

A number of adduct symbols

Source code in src/chemsynthcalc/formula_validator.py
66
67
68
69
70
71
72
73
74
75
76
77
78
def _num_of_adducts(self) -> int:
    """
    Counts a number of adduct symbols
    (listed in [Formula base class][chemsynthcalc.formula.Formula]).

    Returns:
        A number of adduct symbols
    """
    c: Counter[str] = Counter(self.formula)
    i: int = 0
    for adduct in self.adduct_symbols:
        i += c[adduct]
    return i

validate_formula()

Validation of the formula string. Calls the private methods of this class in order.

Raise

EmptyFormula if the formula is an empty string.
InvalidCharacter if there is an invalid character(s) in the string.
NoSuchAtom if there is an invalid atom(s) in the string.
BracketsNotPaired if the brackets are not in pairs.
MoreThanOneAdduct if there are more than 1 adduct symbols in the string.

Returns:

Type Description
bool

True if all the checks are OK

Source code in src/chemsynthcalc/formula_validator.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def validate_formula(self) -> bool:
    """
    Validation of the formula string.
    Calls the private methods of this class in order.

    Raise:
        [EmptyFormula][chemsynthcalc.chem_errors.EmptyFormula] if the formula is an empty string. <br />
        [InvalidCharacter][chemsynthcalc.chem_errors.InvalidCharacter] if there is an invalid character(s) in the string. <br />
        [NoSuchAtom][chemsynthcalc.chem_errors.NoSuchAtom] if there is an invalid atom(s) in the string. <br />
        [BracketsNotPaired][chemsynthcalc.chem_errors.BracketsNotPaired] if the brackets are not in pairs. <br />
        [MoreThanOneAdduct][chemsynthcalc.chem_errors.MoreThanOneAdduct] if there are more than 1 adduct symbols in the string. <br />

    Returns:
        True if all the checks are OK
    """
    if self._check_empty_formula():
        raise EmptyFormula
    elif self._invalid_charachers():
        raise InvalidCharacter(
            f"Invalid character(s) {self._invalid_charachers()} in the formula {self.formula}"
        )
    elif self._invalid_atoms():
        raise NoSuchAtom(
            f"The formula {self.formula} contains atom {self._invalid_atoms()} which is not in the periodic table"
        )
    elif not self._bracket_balance():
        raise BracketsNotPaired(
            f"The brackets {self.opener_brackets} {self.closer_brackets} are not balanced the formula {self.formula}!"
        )
    elif self._num_of_adducts() > 1:
        raise MoreThanOneAdduct(
            f"More than one adduct {self.adduct_symbols} in the formula {self.formula}"
        )
    return True

molar_mass

Oxide

Bases: NamedTuple

A named tuple to represent oxide properties: a first atom (usually metal), full oxide compound string and mass percent of the first atom in the initial formula.

Source code in src/chemsynthcalc/molar_mass.py
 7
 8
 9
10
11
12
13
14
15
16
class Oxide(NamedTuple):
    """
    A named tuple to represent oxide properties: a first atom
    (usually metal), full oxide compound string and mass percent
    of the first atom in the initial formula.
    """

    atom: str
    label: str
    mass_percent: float

MolarMassCalculation

Class for the calculation of molar masses and percentages of a compound.

Compound should be parsed by ChemicalFormulaParser first.

Parameters:

Name Type Description Default
parsed_formula dict

Formula parsed by ChemicalFormulaParser

required

Attributes:

Name Type Description
p_table dict[str, Atom]

Periodic table of elements

Source code in src/chemsynthcalc/molar_mass.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class MolarMassCalculation:
    """
    Class for the calculation of molar masses and percentages of a compound.

    Compound should be parsed by
    [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser] first.

    Parameters:
        parsed_formula (dict): Formula parsed by [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser]

    Attributes:
        p_table (dict[str, Atom]): Periodic table of elements
    """

    def __init__(self, parsed_formula: dict[str, float]) -> None:
        self.parsed_formula: dict[str, float] = parsed_formula
        self.p_table = PeriodicTable().p_table

    def _calculate_atomic_masses(self) -> list[float]:
        """
        Calculation of the molar masses of
        all atoms in a parsed formula.

        Returns:
            List of atomic masses multiplied by the number of corresponding atoms
        """
        masses: list[float] = []
        for atom, weight in self.parsed_formula.items():
            atom_mass: float = self.p_table[atom].atomic_weight
            masses.append(atom_mass * weight)
        return masses

    def calculate_molar_mass(self) -> float:
        """
        Calculation of the molar mass of compound from
        the atomic masses of atoms in a parsed formula.

        Returns:
            Molar mass (in g/mol)

        Examples:
            >>> MolarMassCalculation({'H':2, 'O':1}).calculate_molar_mass()
            18.015
            >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_molar_mass()
            46.069
        """
        return sum(self._calculate_atomic_masses())

    def calculate_mass_percent(self) -> dict[str, float]:
        """
        Calculation of mass percents of atoms in parsed formula.

        Returns:
            Mass percentages of atoms in the formula

        Examples:
            >>> MolarMassCalculation({'H':2, 'O':1}).calculate_mass_percent()
            {'H': 11.19067443796836, 'O': 88.80932556203163}
            >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_mass_percent()
            {'C': 52.14352384466777, 'H': 13.12813388612733, 'O': 34.72834226920489}
        """
        atomic_masses: list[float] = self._calculate_atomic_masses()
        molar_mass: float = self.calculate_molar_mass()
        percents: list[float] = [atomic / molar_mass * 100 for atomic in atomic_masses]
        return dict(zip(self.parsed_formula.keys(), percents))

    def calculate_atomic_percent(self) -> dict[str, float]:
        """
        Calculation of atomic percents of atoms in the parsed formula.

        Returns:
            Atomic percentages of atoms in the formula

        Examples:
            >>> MolarMassCalculation({'H':2, 'O':1}).calculate_atomic_percent()
            {'H': 66.66666666666666, 'O': 33.33333333333333
            >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_atomic_percent()
            {'C': 22.22222222222222, 'H': 66.66666666666666, 'O': 11.11111111111111}
        """
        values: list[float] = list(self.parsed_formula.values())
        atomic: list[float] = [value / sum(values) * 100 for value in values]
        return dict(zip(self.parsed_formula.keys(), atomic))

    def _custom_oxides_input(self, *args: str) -> list[Oxide]:
        """
        Checks if passed non-default oxide formulas can be applied to any
        atom in parsed formula. If so, replaces it with said non-default oxide.
        If not, the default oxide is chosen.

        Parameters:
            *args (tuple[str, ...]): An arbitrary number of non-default oxide formulas

        Returns:
            A list of Oxide objects

        Raise:
            ValueError if compound is not binary or second element is not oxygen
        """
        first_atoms: list[str] = []
        for c_oxide in args:
            parsed_oxide = list(ChemicalFormulaParser(c_oxide).parse_formula().keys())

            if len(parsed_oxide) > 2:
                raise ValueError("Only binary compounds can be considered as input")

            elif parsed_oxide[1] != "O":
                raise ValueError("Only oxides can be considered as input")

            first_atoms.append(parsed_oxide[0])

        custom_oxides = dict(zip(first_atoms, args))
        mass_percents: list[float] = list(self.calculate_mass_percent().values())
        oxides: list[Oxide] = []
        for i, atom in enumerate(self.parsed_formula.keys()):
            if atom != "O":
                if atom in custom_oxides.keys():
                    label = custom_oxides[atom]
                else:
                    label = self.p_table[atom].default_oxide
                oxides.append(Oxide(atom, label, mass_percents[i]))

        return oxides

    def calculate_oxide_percent(self, *args: str) -> dict[str, float]:
        """
        Calculation of oxide percents in parsed formula.

        Calculation of oxide percents in parsed formula from the types of oxide
        declared in the periodic table file. This type of data
        is mostly used in XRF spectrometry and mineralogy. The oxide
        percents are calculated by finding the [convertion factor between element
        and its respective oxide](https://www.geol.umd.edu/~piccoli/probe/molweight.html)
        and normalizing the total sum to 100%. One can change the oxide type
        for certain elements in the [periodic_table.py][chemsynthcalc.periodic_table] file. Theoretically, this function
        should work for other types of binary compound (sulfides, fluorides etc.)
        or even salts, however, modification of this function is required
        (for instance, in case of binary compound, removing X atom
        from the list of future compounds should have X as an argument of this function).

        Parameters:
            *args (tuple[str, ...]): An arbitrary number of non-default oxide formulas

        Returns:
            Percentages of oxides in the formula

        Examples:
            >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_oxide_percent()
            {'CO2': 61.9570190690046, 'H2O': 38.04298093099541}
            >>> MolarMassCalculation({'Ba':1, 'Ti':1, 'O':3}).calculate_oxide_percent()
            {'BaO': 65.7516917244869, 'TiO2': 34.24830827551309}
        """
        oxides: list[Oxide] = self._custom_oxides_input(*args)

        oxide_percents: list[float] = []

        for oxide in oxides:
            parsed_oxide: dict[str, float] = ChemicalFormulaParser(
                oxide.label
            ).parse_formula()
            oxide_mass: float = MolarMassCalculation(
                parsed_oxide
            ).calculate_molar_mass()
            atomic_oxide_coef: float = parsed_oxide[oxide.atom]
            atomic_mass: float = self.p_table[oxide.atom].atomic_weight
            conversion_factor: float = oxide_mass / atomic_mass / atomic_oxide_coef
            oxide_percents.append(oxide.mass_percent * conversion_factor)

        normalized_oxide_percents: list[float] = [
            x / sum(oxide_percents) * 100 for x in oxide_percents
        ]
        oxide_labels: list[str] = [oxide.label for oxide in oxides]

        return dict(zip(oxide_labels, normalized_oxide_percents))

_calculate_atomic_masses()

Calculation of the molar masses of all atoms in a parsed formula.

Returns:

Type Description
list[float]

List of atomic masses multiplied by the number of corresponding atoms

Source code in src/chemsynthcalc/molar_mass.py
37
38
39
40
41
42
43
44
45
46
47
48
49
def _calculate_atomic_masses(self) -> list[float]:
    """
    Calculation of the molar masses of
    all atoms in a parsed formula.

    Returns:
        List of atomic masses multiplied by the number of corresponding atoms
    """
    masses: list[float] = []
    for atom, weight in self.parsed_formula.items():
        atom_mass: float = self.p_table[atom].atomic_weight
        masses.append(atom_mass * weight)
    return masses

calculate_molar_mass()

Calculation of the molar mass of compound from the atomic masses of atoms in a parsed formula.

Returns:

Type Description
float

Molar mass (in g/mol)

Examples:

>>> MolarMassCalculation({'H':2, 'O':1}).calculate_molar_mass()
18.015
>>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_molar_mass()
46.069
Source code in src/chemsynthcalc/molar_mass.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def calculate_molar_mass(self) -> float:
    """
    Calculation of the molar mass of compound from
    the atomic masses of atoms in a parsed formula.

    Returns:
        Molar mass (in g/mol)

    Examples:
        >>> MolarMassCalculation({'H':2, 'O':1}).calculate_molar_mass()
        18.015
        >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_molar_mass()
        46.069
    """
    return sum(self._calculate_atomic_masses())

calculate_mass_percent()

Calculation of mass percents of atoms in parsed formula.

Returns:

Type Description
dict[str, float]

Mass percentages of atoms in the formula

Examples:

>>> MolarMassCalculation({'H':2, 'O':1}).calculate_mass_percent()
{'H': 11.19067443796836, 'O': 88.80932556203163}
>>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_mass_percent()
{'C': 52.14352384466777, 'H': 13.12813388612733, 'O': 34.72834226920489}
Source code in src/chemsynthcalc/molar_mass.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def calculate_mass_percent(self) -> dict[str, float]:
    """
    Calculation of mass percents of atoms in parsed formula.

    Returns:
        Mass percentages of atoms in the formula

    Examples:
        >>> MolarMassCalculation({'H':2, 'O':1}).calculate_mass_percent()
        {'H': 11.19067443796836, 'O': 88.80932556203163}
        >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_mass_percent()
        {'C': 52.14352384466777, 'H': 13.12813388612733, 'O': 34.72834226920489}
    """
    atomic_masses: list[float] = self._calculate_atomic_masses()
    molar_mass: float = self.calculate_molar_mass()
    percents: list[float] = [atomic / molar_mass * 100 for atomic in atomic_masses]
    return dict(zip(self.parsed_formula.keys(), percents))

calculate_atomic_percent()

Calculation of atomic percents of atoms in the parsed formula.

Returns:

Type Description
dict[str, float]

Atomic percentages of atoms in the formula

Examples:

>>> MolarMassCalculation({'H':2, 'O':1}).calculate_atomic_percent()
{'H': 66.66666666666666, 'O': 33.33333333333333
>>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_atomic_percent()
{'C': 22.22222222222222, 'H': 66.66666666666666, 'O': 11.11111111111111}
Source code in src/chemsynthcalc/molar_mass.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def calculate_atomic_percent(self) -> dict[str, float]:
    """
    Calculation of atomic percents of atoms in the parsed formula.

    Returns:
        Atomic percentages of atoms in the formula

    Examples:
        >>> MolarMassCalculation({'H':2, 'O':1}).calculate_atomic_percent()
        {'H': 66.66666666666666, 'O': 33.33333333333333
        >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_atomic_percent()
        {'C': 22.22222222222222, 'H': 66.66666666666666, 'O': 11.11111111111111}
    """
    values: list[float] = list(self.parsed_formula.values())
    atomic: list[float] = [value / sum(values) * 100 for value in values]
    return dict(zip(self.parsed_formula.keys(), atomic))

_custom_oxides_input(*args)

Checks if passed non-default oxide formulas can be applied to any atom in parsed formula. If so, replaces it with said non-default oxide. If not, the default oxide is chosen.

Parameters:

Name Type Description Default
*args tuple[str, ...]

An arbitrary number of non-default oxide formulas

()

Returns:

Type Description
list[Oxide]

A list of Oxide objects

Raise

ValueError if compound is not binary or second element is not oxygen

Source code in src/chemsynthcalc/molar_mass.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def _custom_oxides_input(self, *args: str) -> list[Oxide]:
    """
    Checks if passed non-default oxide formulas can be applied to any
    atom in parsed formula. If so, replaces it with said non-default oxide.
    If not, the default oxide is chosen.

    Parameters:
        *args (tuple[str, ...]): An arbitrary number of non-default oxide formulas

    Returns:
        A list of Oxide objects

    Raise:
        ValueError if compound is not binary or second element is not oxygen
    """
    first_atoms: list[str] = []
    for c_oxide in args:
        parsed_oxide = list(ChemicalFormulaParser(c_oxide).parse_formula().keys())

        if len(parsed_oxide) > 2:
            raise ValueError("Only binary compounds can be considered as input")

        elif parsed_oxide[1] != "O":
            raise ValueError("Only oxides can be considered as input")

        first_atoms.append(parsed_oxide[0])

    custom_oxides = dict(zip(first_atoms, args))
    mass_percents: list[float] = list(self.calculate_mass_percent().values())
    oxides: list[Oxide] = []
    for i, atom in enumerate(self.parsed_formula.keys()):
        if atom != "O":
            if atom in custom_oxides.keys():
                label = custom_oxides[atom]
            else:
                label = self.p_table[atom].default_oxide
            oxides.append(Oxide(atom, label, mass_percents[i]))

    return oxides

calculate_oxide_percent(*args)

Calculation of oxide percents in parsed formula.

Calculation of oxide percents in parsed formula from the types of oxide declared in the periodic table file. This type of data is mostly used in XRF spectrometry and mineralogy. The oxide percents are calculated by finding the convertion factor between element and its respective oxide and normalizing the total sum to 100%. One can change the oxide type for certain elements in the periodic_table.py file. Theoretically, this function should work for other types of binary compound (sulfides, fluorides etc.) or even salts, however, modification of this function is required (for instance, in case of binary compound, removing X atom from the list of future compounds should have X as an argument of this function).

Parameters:

Name Type Description Default
*args tuple[str, ...]

An arbitrary number of non-default oxide formulas

()

Returns:

Type Description
dict[str, float]

Percentages of oxides in the formula

Examples:

>>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_oxide_percent()
{'CO2': 61.9570190690046, 'H2O': 38.04298093099541}
>>> MolarMassCalculation({'Ba':1, 'Ti':1, 'O':3}).calculate_oxide_percent()
{'BaO': 65.7516917244869, 'TiO2': 34.24830827551309}
Source code in src/chemsynthcalc/molar_mass.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def calculate_oxide_percent(self, *args: str) -> dict[str, float]:
    """
    Calculation of oxide percents in parsed formula.

    Calculation of oxide percents in parsed formula from the types of oxide
    declared in the periodic table file. This type of data
    is mostly used in XRF spectrometry and mineralogy. The oxide
    percents are calculated by finding the [convertion factor between element
    and its respective oxide](https://www.geol.umd.edu/~piccoli/probe/molweight.html)
    and normalizing the total sum to 100%. One can change the oxide type
    for certain elements in the [periodic_table.py][chemsynthcalc.periodic_table] file. Theoretically, this function
    should work for other types of binary compound (sulfides, fluorides etc.)
    or even salts, however, modification of this function is required
    (for instance, in case of binary compound, removing X atom
    from the list of future compounds should have X as an argument of this function).

    Parameters:
        *args (tuple[str, ...]): An arbitrary number of non-default oxide formulas

    Returns:
        Percentages of oxides in the formula

    Examples:
        >>> MolarMassCalculation({'C':2, 'H':6, 'O':1}).calculate_oxide_percent()
        {'CO2': 61.9570190690046, 'H2O': 38.04298093099541}
        >>> MolarMassCalculation({'Ba':1, 'Ti':1, 'O':3}).calculate_oxide_percent()
        {'BaO': 65.7516917244869, 'TiO2': 34.24830827551309}
    """
    oxides: list[Oxide] = self._custom_oxides_input(*args)

    oxide_percents: list[float] = []

    for oxide in oxides:
        parsed_oxide: dict[str, float] = ChemicalFormulaParser(
            oxide.label
        ).parse_formula()
        oxide_mass: float = MolarMassCalculation(
            parsed_oxide
        ).calculate_molar_mass()
        atomic_oxide_coef: float = parsed_oxide[oxide.atom]
        atomic_mass: float = self.p_table[oxide.atom].atomic_weight
        conversion_factor: float = oxide_mass / atomic_mass / atomic_oxide_coef
        oxide_percents.append(oxide.mass_percent * conversion_factor)

    normalized_oxide_percents: list[float] = [
        x / sum(oxide_percents) * 100 for x in oxide_percents
    ]
    oxide_labels: list[str] = [oxide.label for oxide in oxides]

    return dict(zip(oxide_labels, normalized_oxide_percents))

periodic_table

Atom

Bases: NamedTuple

Named tuple for representing atomic properties: atomic weight and type of oxide that will be used by default.

Source code in src/chemsynthcalc/periodic_table.py
 4
 5
 6
 7
 8
 9
10
11
class Atom(NamedTuple):
    """
    Named tuple for representing atomic properties:
    atomic weight and type of oxide that will be used by default.
    """

    atomic_weight: float
    default_oxide: str

PeriodicTable

Periodic table of elements in the form of "Atom symbol": Atom NamedTuple. The standard atomic weights are taken from IUPAC.

Source code in src/chemsynthcalc/periodic_table.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class PeriodicTable:
    """
    Periodic table of elements in the form of "Atom symbol": Atom NamedTuple.
    The standard atomic weights are taken from [IUPAC](https://iupac.qmul.ac.uk/AtWt/).
    """

    def __init__(self) -> None:
        self.p_table: dict[str, Atom] = {
            "H": Atom(1.008, "H2O"),
            "He": Atom(4.002602, "He"),
            "Li": Atom(6.94, "Li2O"),
            "Be": Atom(9.0121831, "BeO"),
            "B": Atom(10.81, "B2O3"),
            "C": Atom(12.011, "CO2"),
            "N": Atom(14.007, "NO2"),
            "O": Atom(15.999, "O"),
            "F": Atom(18.998403162, "F2O"),
            "Ne": Atom(20.1797, "Ne"),
            "Na": Atom(22.98976928, "Na2O"),
            "Mg": Atom(24.305, "MgO"),
            "Al": Atom(26.9815384, "Al2O3"),
            "Si": Atom(28.085, "SiO2"),
            "P": Atom(30.973761998, "P2O3"),
            "S": Atom(32.06, "SO3"),
            "Cl": Atom(35.45, "ClO2"),
            "Ar": Atom(39.95, "Ar"),
            "K": Atom(39.098, "K2O"),
            "Ca": Atom(40.078, "CaO"),
            "Sc": Atom(44.955907, "Sc2O3"),
            "Ti": Atom(47.867, "TiO2"),
            "V": Atom(50.9415, "V2O5"),
            "Cr": Atom(51.9961, "Cr2O3"),
            "Mn": Atom(54.938043, "MnO2"),
            "Fe": Atom(55.845, "Fe2O3"),
            "Co": Atom(58.933194, "Co2O3"),
            "Ni": Atom(58.6934, "NiO"),
            "Cu": Atom(63.546, "Cu2O"),
            "Zn": Atom(65.38, "ZnO"),
            "Ga": Atom(69.723, "Ga2O3"),
            "Ge": Atom(72.63, "GeO2"),
            "As": Atom(74.921595, "As2O3"),
            "Se": Atom(78.971, "Se3O4"),
            "Br": Atom(79.904, "BrO2"),
            "Kr": Atom(83.798, "Kr"),
            "Rb": Atom(85.4678, "Rb2O"),
            "Sr": Atom(87.62, "SrO"),
            "Y": Atom(88.905838, "Y2O3"),
            "Zr": Atom(91.222, "ZrO2"),
            "Nb": Atom(92.90637, "Nb2O5"),
            "Mo": Atom(95.95, "MoO3"),
            "Tc": Atom(97, "TcO2"),
            "Ru": Atom(101.07, "RuO2"),
            "Rh": Atom(102.90549, "Rh2O3"),
            "Pd": Atom(106.42, "PdO"),
            "Ag": Atom(107.8682, "Ag2O"),
            "Cd": Atom(112.414, "CdO"),
            "In": Atom(114.818, "In2O3"),
            "Sn": Atom(118.71, "SnO2"),
            "Sb": Atom(121.76, "Sb2O3"),
            "Te": Atom(127.6, "TeO3"),
            "I": Atom(126.90447, "I2O5"),
            "Xe": Atom(131.29, "Xe"),
            "Cs": Atom(132.90545196, "Cs2O"),
            "Ba": Atom(137.327, "BaO"),
            "La": Atom(138.90547, "La2O3"),
            "Ce": Atom(140.116, "CeO2"),
            "Pr": Atom(140.90766, "Pr2O3"),
            "Nd": Atom(144.242, "Nd2O3"),
            "Pm": Atom(145, "Pm2O3"),
            "Sm": Atom(150.36, "Sm2O3"),
            "Eu": Atom(151.964, "Eu2O3"),
            "Gd": Atom(157.249, "Gd2O3"),
            "Tb": Atom(158.925354, "Tb2O3"),
            "Dy": Atom(162.5, "Dy2O3"),
            "Ho": Atom(164.930329, "Ho2O3"),
            "Er": Atom(167.259, "Er2O3"),
            "Tm": Atom(168.934219, "Tm2O3"),
            "Yb": Atom(173.045, "Yb2O3"),
            "Lu": Atom(174.96669, "Lu2O3"),
            "Hf": Atom(178.486, "HfO2"),
            "Ta": Atom(180.94788, "Ta2O5"),
            "W": Atom(183.84, "WO3"),
            "Re": Atom(186.207, "Re2O7"),
            "Os": Atom(190.23, "OsO3"),
            "Ir": Atom(192.217, "Ir2O3"),
            "Pt": Atom(195.084, "PtO"),
            "Au": Atom(196.966570, "Au2O3"),
            "Hg": Atom(200.592, "HgO2"),
            "Tl": Atom(204.38, "Tl2O"),
            "Pb": Atom(207.2, "PbO2"),
            "Bi": Atom(208.98040, "Bi2O3"),
            "Po": Atom(209, "PoO2"),
            "At": Atom(210, "At2O"),
            "Rn": Atom(222, "Rn"),
            "Fr": Atom(223, "Fr2O"),
            "Ra": Atom(226, "RaO"),
            "Ac": Atom(227, "Ac2O3"),
            "Th": Atom(232.0377, "ThO2"),
            "Pa": Atom(231.03588, "Pa2O5"),
            "U": Atom(238.02891, "UO2"),
            "Np": Atom(237, "NpO2"),
            "Pu": Atom(244, "PuO2"),
            "Am": Atom(243, "AmO2"),
            "Cm": Atom(247, "Cm2O3"),
            "Bk": Atom(247, "BkO2"),
            "Cf": Atom(251, "Cf2O3"),
            "Es": Atom(252, "Es2O3"),
            "Fm": Atom(257, "Fm2O3"),
            "Md": Atom(258, "Md2O3"),
            "No": Atom(259, "No2O3"),
            "Lr": Atom(262, "Lr2O3"),
            "Rf": Atom(267, "RfO2"),
            "Db": Atom(270, "Db2O5"),
            "Sg": Atom(269, "SgO4"),
            "Bh": Atom(270, "Bh2O7"),
            "Hs": Atom(270, "HsO3"),
            "Mt": Atom(278, "Mt2O3"),
            "Ds": Atom(281, "DsO2"),
            "Rg": Atom(281, "RgO"),
            "Cn": Atom(285, "Cn2O3"),
            "Nh": Atom(286, "NhO2"),
            "Fl": Atom(289, "FlO2"),
            "Mc": Atom(289, "Mc2O5"),
            "Lv": Atom(293, "LvO3"),
            "Ts": Atom(293, "Ts2O7"),
            "Og": Atom(294, "Og"),
        }
        self.atoms: set[str] = set(self.p_table.keys())

reaction

Reaction

A base class for ReactionDecomposer and ReactionValidator containing regexes and symbols.

Parameters:

Name Type Description Default
reaction str

Reaction string

required

Attributes:

Name Type Description
allowed_symbols str

Regex of all symbols allowed in a reaction string

possible_reaction_separators list[str]

List of all allowed reaction separators (left and right part separator)

reactant_separator str

Only one possible reactant separator ("+")

Source code in src/chemsynthcalc/reaction.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Reaction:
    """
    A base class for
    [ReactionDecomposer][chemsynthcalc.reaction_decomposer.ReactionDecomposer] and
    [ReactionValidator][chemsynthcalc.reaction_validator.ReactionValidator]
    containing regexes and symbols.

    Parameters:
        reaction (str): Reaction string

    Attributes:
        allowed_symbols (str): Regex of all symbols allowed in a reaction string
        possible_reaction_separators (list[str]): List of all allowed reaction separators (left and right part separator)
        reactant_separator (str): Only one possible reactant separator ("+")
    """

    def __init__(self, reaction: str) -> None:
        self.allowed_symbols: str = r"[^a-zA-Z0-9.({[)}\]*·•=<\->→⇄+ ]"
        self.possible_reaction_separators: list[str] = [
            "==",
            "=",
            "<->",
            "->",
            "<>",
            ">",
            "→",
            "⇄",
        ]
        self.reactant_separator: str = "+"

        self.reaction = reaction

    def extract_separator(self) -> str:
        """
        Extract one of possible reaction separator from
        the reaction string.

        Returns:
            Separator string if separator is found, empty string if not
        """
        for separator in self.possible_reaction_separators:
            if self.reaction.find(separator) != -1:
                if (
                    self.reaction.split(separator)[1] != ""
                    and self.reaction.split(separator)[0] != ""
                ):
                    return separator
        return ""

extract_separator()

Extract one of possible reaction separator from the reaction string.

Returns:

Type Description
str

Separator string if separator is found, empty string if not

Source code in src/chemsynthcalc/reaction.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def extract_separator(self) -> str:
    """
    Extract one of possible reaction separator from
    the reaction string.

    Returns:
        Separator string if separator is found, empty string if not
    """
    for separator in self.possible_reaction_separators:
        if self.reaction.find(separator) != -1:
            if (
                self.reaction.split(separator)[1] != ""
                and self.reaction.split(separator)[0] != ""
            ):
                return separator
    return ""

reaction_decomposer

ReactionDecomposer

Bases: Reaction

Decomposition of chemical reaction string and extraction of reaction separator, reactants, products and initial coefficients.

Parameters:

Name Type Description Default
reaction str

A reaction string

required

Attributes:

Name Type Description
separator str

A reactants - products separator (usually "+")

initial_coefficients list[float]

A list of coefficients striped from the formulas

reactants list[str]

A list of compound from letf part of the reaction equation

products list[str]

A list of compound from right part of the reaction equation

compounds list[str]

reactants + products

Source code in src/chemsynthcalc/reaction_decomposer.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class ReactionDecomposer(Reaction):
    """
    Decomposition of chemical reaction string and extraction of
    reaction separator, reactants, products and initial coefficients.

    Arguments:
        reaction (str): A reaction string

    Attributes:
        separator (str): A reactants - products separator (usually "+")
        initial_coefficients (list[float]): A list of coefficients striped from the formulas
        reactants (list[str]): A list of compound from letf part of the reaction equation
        products (list[str]): A list of compound from right part of the reaction equation
        compounds (list[str]): reactants + products
    """

    def __init__(self, reaction: str) -> None:
        super().__init__(reaction)

        self.separator: str = self.extract_separator()

        self._initial_reactants: list[str] = self.reaction.split(self.separator)[
            0
        ].split(self.reactant_separator)
        self._initial_products: list[str] = self.reaction.split(self.separator)[
            1
        ].split(self.reactant_separator)
        self._splitted_compounds: list[tuple[float, str]] = [
            self.split_coefficient_from_formula(formula)
            for formula in self._initial_reactants + self._initial_products
        ]

        self.initial_coefficients: list[float] = [
            atom[0] for atom in self._splitted_compounds
        ]
        self.compounds: list[str] = [atom[1] for atom in self._splitted_compounds]
        self.reactants: list[str] = self.compounds[: len(self._initial_reactants)]
        self.products: list[str] = self.compounds[len(self._initial_reactants) :]

    def __str__(self) -> str:
        return f"separator: {self.separator}; reactants: {self.reactants}; products: {self.products}"

    def __repr__(self) -> str:
        return f"ReactionDecomposer({self.reaction})"

    def split_coefficient_from_formula(self, formula: str) -> tuple[float, str]:
        """
        Split the coefficient (int or float) from string containing formula and coef.

        Parameters:
            formula (str): Formula string

        Returns:
            A tuple of (coefficient, formula)
        """
        if not formula[0].isdigit():
            return 1.0, formula
        else:
            coef: list[str] = []
            i: int = 0
            for i, symbol in enumerate(formula):
                if symbol.isdigit() or symbol == ".":
                    coef.append(symbol)
                else:
                    break
            return float("".join(coef)), formula[i:]

split_coefficient_from_formula(formula)

Split the coefficient (int or float) from string containing formula and coef.

Parameters:

Name Type Description Default
formula str

Formula string

required

Returns:

Type Description
tuple[float, str]

A tuple of (coefficient, formula)

Source code in src/chemsynthcalc/reaction_decomposer.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def split_coefficient_from_formula(self, formula: str) -> tuple[float, str]:
    """
    Split the coefficient (int or float) from string containing formula and coef.

    Parameters:
        formula (str): Formula string

    Returns:
        A tuple of (coefficient, formula)
    """
    if not formula[0].isdigit():
        return 1.0, formula
    else:
        coef: list[str] = []
        i: int = 0
        for i, symbol in enumerate(formula):
            if symbol.isdigit() or symbol == ".":
                coef.append(symbol)
            else:
                break
        return float("".join(coef)), formula[i:]

reaction_matrix

ChemicalReactionMatrix

A class to create a dense float matrix from the parsed formulas.

Parameters:

Name Type Description Default
parsed_formulas list[dict[str, float]]

A list of formulas parsed by ChemicalFormulaParser

required
Source code in src/chemsynthcalc/reaction_matrix.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class ChemicalReactionMatrix:
    """
    A class to create a dense float matrix from the parsed formulas.

    Arguments:
        parsed_formulas (list[dict[str, float]]): A list of formulas parsed by [ChemicalFormulaParser][chemsynthcalc.formula_parser.ChemicalFormulaParser]
    """

    def __init__(self, parsed_formulas: list[dict[str, float]]) -> None:
        self._parsed_formulas = parsed_formulas
        self._merged_dict: dict[str, float] = {
            k: v for d in self._parsed_formulas for k, v in d.items()
        }
        self._elements: list[str] = list(self._merged_dict.keys())
        self.matrix: npt.NDArray[np.float64] = self.create_reaction_matrix()

    def create_reaction_matrix(self) -> npt.NDArray[np.float64]:
        """
        Creates a 2D NumPy array from nested Python lists.
        The content of lists are exctracted from parsed dicts.

        Returns:
            A 2D NumPy array of the reaction matrix
        """
        matrix: list[list[float]] = []
        for element in self._elements:
            row: list[float] = []
            for compound in self._parsed_formulas:
                if element in compound.keys():
                    row.append(compound[element])
                else:
                    row.append(0.0)
            matrix.append(row)
        return np.array(matrix)

create_reaction_matrix()

Creates a 2D NumPy array from nested Python lists. The content of lists are exctracted from parsed dicts.

Returns:

Type Description
NDArray[float64]

A 2D NumPy array of the reaction matrix

Source code in src/chemsynthcalc/reaction_matrix.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def create_reaction_matrix(self) -> npt.NDArray[np.float64]:
    """
    Creates a 2D NumPy array from nested Python lists.
    The content of lists are exctracted from parsed dicts.

    Returns:
        A 2D NumPy array of the reaction matrix
    """
    matrix: list[list[float]] = []
    for element in self._elements:
        row: list[float] = []
        for compound in self._parsed_formulas:
            if element in compound.keys():
                row.append(compound[element])
            else:
                row.append(0.0)
        matrix.append(row)
    return np.array(matrix)

reaction_validator

ReactionValidator

Bases: Reaction

Methods of this class validate the initial input reaction.

Source code in src/chemsynthcalc/reaction_validator.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ReactionValidator(Reaction):
    """
    Methods of this class validate the initial input reaction.
    """

    def _check_empty_reaction(self) -> bool:
        """
        Checks if reaction is an empty string.
        """
        return self.reaction == ""

    def _invalid_charachers(self) -> list[str]:
        """
        Checks if reaction string contains invalid characters.

        Returns:
            List of invalid characters
        """
        return re.compile(self.allowed_symbols).findall(self.reaction)

    def _no_reaction_separator(self) -> bool:
        """
        Checks if reaction string contains a separator.
        """
        return self.extract_separator() == ""

    def _no_reactant_separator(self) -> bool:
        """
        Checks if reaction string contains a "+".
        """
        return self.reaction.find(self.reactant_separator) == -1

    def validate_reaction(self) -> bool:
        """
        Validation of the reaction string.
        Calls the private methods of this class in order.

        Raise:
            [EmptyReaction][chemsynthcalc.chem_errors.EmptyReaction] if reaction string is empty. <br />
            [InvalidCharacter][chemsynthcalc.chem_errors.InvalidCharacter] if some characters are invalid. <br />
            [NoSeparator][chemsynthcalc.chem_errors.NoSeparator] if any of separators (reaction or reactant) are missing.

        Returns:
            True if all the checks are OK
        """

        if self._check_empty_reaction():
            raise EmptyReaction
        elif self._invalid_charachers():
            raise InvalidCharacter(
                f"Invalid character(s) {self._invalid_charachers()} in reaction"
            )
        elif self._no_reaction_separator():
            raise NoSeparator(
                f"No separator between reactants and products: {self.possible_reaction_separators}"
            )
        elif self._no_reactant_separator():
            raise NoSeparator(
                f"No separators between compounds: {self.reactant_separator}"
            )

        return True

_check_empty_reaction()

Checks if reaction is an empty string.

Source code in src/chemsynthcalc/reaction_validator.py
12
13
14
15
16
def _check_empty_reaction(self) -> bool:
    """
    Checks if reaction is an empty string.
    """
    return self.reaction == ""

_invalid_charachers()

Checks if reaction string contains invalid characters.

Returns:

Type Description
list[str]

List of invalid characters

Source code in src/chemsynthcalc/reaction_validator.py
18
19
20
21
22
23
24
25
def _invalid_charachers(self) -> list[str]:
    """
    Checks if reaction string contains invalid characters.

    Returns:
        List of invalid characters
    """
    return re.compile(self.allowed_symbols).findall(self.reaction)

_no_reaction_separator()

Checks if reaction string contains a separator.

Source code in src/chemsynthcalc/reaction_validator.py
27
28
29
30
31
def _no_reaction_separator(self) -> bool:
    """
    Checks if reaction string contains a separator.
    """
    return self.extract_separator() == ""

_no_reactant_separator()

Checks if reaction string contains a "+".

Source code in src/chemsynthcalc/reaction_validator.py
33
34
35
36
37
def _no_reactant_separator(self) -> bool:
    """
    Checks if reaction string contains a "+".
    """
    return self.reaction.find(self.reactant_separator) == -1

validate_reaction()

Validation of the reaction string. Calls the private methods of this class in order.

Raise

EmptyReaction if reaction string is empty.
InvalidCharacter if some characters are invalid.
NoSeparator if any of separators (reaction or reactant) are missing.

Returns:

Type Description
bool

True if all the checks are OK

Source code in src/chemsynthcalc/reaction_validator.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def validate_reaction(self) -> bool:
    """
    Validation of the reaction string.
    Calls the private methods of this class in order.

    Raise:
        [EmptyReaction][chemsynthcalc.chem_errors.EmptyReaction] if reaction string is empty. <br />
        [InvalidCharacter][chemsynthcalc.chem_errors.InvalidCharacter] if some characters are invalid. <br />
        [NoSeparator][chemsynthcalc.chem_errors.NoSeparator] if any of separators (reaction or reactant) are missing.

    Returns:
        True if all the checks are OK
    """

    if self._check_empty_reaction():
        raise EmptyReaction
    elif self._invalid_charachers():
        raise InvalidCharacter(
            f"Invalid character(s) {self._invalid_charachers()} in reaction"
        )
    elif self._no_reaction_separator():
        raise NoSeparator(
            f"No separator between reactants and products: {self.possible_reaction_separators}"
        )
    elif self._no_reactant_separator():
        raise NoSeparator(
            f"No separators between compounds: {self.reactant_separator}"
        )

    return True

utils

A module with some useful utilities functions.

round_dict_content(input, precision, plus=0)

Round all values of a dictionary to arbitrary precision (with optional surplus value).

Parameters:

Name Type Description Default
input dict[str, float]

An input dict

required
precision int

Precision

required
plus int

An optional surplus

0

Returns:

Type Description
dict[str, float]

Rounded dictionary

Source code in src/chemsynthcalc/utils.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def round_dict_content(
    input: dict[str, float], precision: int, plus: int = 0
) -> dict[str, float]:
    """
    Round all values of a dictionary to arbitrary precision
    (with optional surplus value).

    Parameters:
        input (dict[str, float]): An input dict
        precision (int): Precision
        plus (int): An optional surplus

    Returns:
        Rounded dictionary
    """
    return {k: round(v, precision + plus) for k, v in input.items()}

to_integer(coefficients)

Cast a float to int if this float is some x.0 (integer), otherwise keep a float.

Parameters:

Name Type Description Default
coefficients list[float | int]

Mixed list of floats and ints

required

Returns:

Type Description
list[float | int]

Mixed list of floats and ints

Source code in src/chemsynthcalc/utils.py
27
28
29
30
31
32
33
34
35
36
37
38
def to_integer(coefficients: list[float | int]) -> list[float | int]:
    """
    Cast a float to int if this float is some x.0 (integer), otherwise
    keep a float.

    Parameters:
        coefficients (list[float | int]): Mixed list of floats and ints

    Returns:
        Mixed list of floats and ints
    """
    return [int(i) if i.is_integer() else i for i in coefficients]

find_lcm(int_list)

Find Least Common Multiplyer of list of integers

Parameters:

Name Type Description Default
int_list list[int]

A list of integers

required

Returns:

Type Description
int

The LCM

Source code in src/chemsynthcalc/utils.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def find_lcm(int_list: list[int]) -> int:
    """
    Find Least Common Multiplyer of list of integers

    Parameters:
        int_list (list[int]): A list of integers

    Returns:
        The LCM
    """
    lcm = 1
    for i in int_list:
        lcm = lcm * i // gcd(lcm, i)
    return lcm

find_gcd(int_list)

Find Greatest Common Divisor of list of integers

Parameters:

Name Type Description Default
int_list list[int]

A list of integers

required

Returns:

Type Description
int

The GCD

Source code in src/chemsynthcalc/utils.py
57
58
59
60
61
62
63
64
65
66
67
68
def find_gcd(int_list: list[int]) -> int:
    """
    Find Greatest Common Divisor of list of integers

    Parameters:
        int_list (list[int]): A list of integers

    Returns:
        The GCD
    """
    x = reduce(gcd, int_list)
    return x