Name | mixsol JSON |
Version |
1.0.1
JSON |
| download |
home_page | None |
Summary | Planning tool for combinatorial solution mixing. Reach target solutions from mixes of starting solutions, constrained by minimum pipetting volumes. Also aids in computing amounts of powdered reagents required to form solutions with target solutes + molarities. |
upload_time | 2025-07-17 21:26:28 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.11 |
license | GPLv3 |
keywords |
chemistry
combinatoric
dilution
mixing
molarity
planning
solution
|
VCS |
 |
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
[](https://mybinder.org/v2/gh/rekumar/mixsol/HEAD)
[](https://badge.fury.io/py/mixsol)

`pip install mixsol`
Pipetting planner for efficient combinatorial mixing of solutions. Often we want to interpolate a few stock solutions into many target mixtures. If some of these mixtures require only a tiny amount of a stock solution, the minimum volume for our pipette may limit our ability to make this solution without a serial dilution. Mixsol searches for mixing sequences that use only other target solutions as stepping stones to reach these difficult mixtures, minimizing waste.
Mixsol also has the ability to calculate the masses of solid reagents needed to make a target solution. Finally, measured amounts of solid reagents can be input to calculate the actual solution we have made.
Happy mixing!

# Examples
## Solution Mixing
Solutions are defined with the `Solution` class. Solutes and solvents are both defined by either dicts or their formula, which follows the `(name1)(amount1)_(name2)(amount2)_..._(name)(amount)` format. The names do not have to correspond to elements, so you can use placeholders for units that will be mixed. Parentheses can be used to simplify formulae as well: `A2_B2_C` == `(A_B)2_C`. An `alias` can be provided for the solution to simplify later analysis.
```
import mixsol as mx
stock_solutions = [
mx.Solution(
solutes='FA_Pb_I3',
solvents='DMF9_DMSO1',
molarity=1,
alias='FAPI'
),
mx.Solution(
solutes={
"MA": 1,
"Pb": 1,
"I": 3,
},
solvents={
"DMF": 9,
"DMSO": 1,
},
molarity=1,
alias='MAPI'
),
]
```
This process goes for both stock and target solutions.
You can manually generate your target solutions and place them in a list like so:
```
targets = []
for a in np.linspace(0, 0.8, 5):
targets.append(mx.Solution(
solutes={
"FA": a,
"MA": 1-a,
"Pb": 1,
"I": 3,
},
solvents="DMF9_DMSO1",
molarity=1,
alias=f'FA_{a:.3f}'
))
```
Or, if you want to mix in equal steps between two (or more!) endpoint solutions, you can use the `interpolate` function to generate a mesh of `Solution` obects. The following code block is nearly equivalent to the one above (it will interpolate all the way from 0-1 instead of 0-0.8, and it won't generate `alias` values).
```
target_mesh = mx.interpolate(
solutions=stock_solutions, #this should be a list of 2 or more Solution's
steps=5 #number of divisions. In this example, steps=5 will mix the input Solution's in 20% increments
)
```
Stock and target solutions go into a `Mixer` object
```
sm = Mixer(
stock_solutions = stock_solutions,
targets = {
t:60 #Solution:volume dictionary
for t in targets
})
```
which is then solved with constraints
```
sm.solve(
min_volume=20, #minimum volume for a single liquid transfer
max_inputs = 3 #maximum number of solutions (stock or other target) that can be mixed to form one target
)
```
The results can be displayed in two ways:
- plain text output of liquid transfers, in order. use of the `alias` term really simplifies this output
```
sm.print()
```
```
===== Stock Prep =====
120.00 of FAPI
180.00 of MAPI
====== Mixing =====
Distribute FAPI:
54.00 to FA_0.600
36.00 to FA_0.400
30.00 to FA_0.800
Distribute MAPI:
60.00 to FA_0.000
36.00 to FA_0.600
54.00 to FA_0.400
30.00 to FA_0.200
Distribute FA_0.600:
30.00 to FA_0.800
Distribute FA_0.400:
30.00 to FA_0.200
```
- a graph of solution transfers. This is harder to use in practice, but can give an overview of the mixing path.
```
fig, ax = plt.subplots(figsize=(6,6))
sm.plot(ax=ax)
```

Note that the units of volume here are arbitrary. Using SI units for small volumes might cause numerical issues when solving a mixture strategy (eg you should use 10 microliters instead of 1e-5 liters).
## Solution Preparation
Mixsol aids in determining the mass of solid reagents needed to form target solutions. We can also check the actual solution formed from recorded reagent masses. Here, the units *do* matter, and you should stick to SI units (mass in grams, volume in liters).
We define solid reagents with the `Powder` class. This requires at least a chemical formula delimited by underscores, similar to the `Solution` definition earlier. If this formula is a proper chemical formula of elements, the molar mass is calculated automatically. If not, you can pass the molar mass directly. The `calculate_molar_mass` function can be used for convenience. `alias` does the same thing it did for `Solution`.
```
from mixsol import Powder, calculate_molar_mass, Weigher
powders = [
Powder('Cs_I'),
Powder('Pb_I2'),
Powder('Pb_Br2'),
Powder('Pb_Cl2'),
Powder(
formula='MA_I',
molar_mass=calculate_molar_mass('C_H6_N_I'),
alias='MAI',
),
Powder(
formula='FA_I',
molar_mass = calculate_molar_mass('C_H5_N2_I'),
alias='FAI',
)
]
```
The list of available `Powder`s is fed into a `Weigher` object
```
weigher = Weigher(
powders=powders
)
```
which can then be used to determine powder amounts for a given volume of a target `Solution`
```
target=Solution(
solutes='Cs0.05_FA0.8_MA0.15_Pb_I2.4_Br0.45_Cl0.15',
solvents='DMF9_DMSO1',
molarity=1
)
answer = weigher.get_weights(
target,
volume=1e-3, #in L
)
print(answer) #masses of each powder, in grams
```
```
{'Cs_I': 0.012990496098, 'Pb_I2': 0.322706258, 'Pb_Br2': 0.082576575, 'Pb_Cl2': 0.020857935, 'MAI': 0.02384543385, 'FAI': 0.1375746568}
```
Finally, we can also generate a `Solution` object by inputting a `{powder:mass}` dictionary into `Weigher`. We will just use the answer from before, but this can be manually input.
```
result = weigher.weights_to_solution(
weights=answer,
volume=1e-3,
solvents='DMF9_DMSO1',
)
print(result)
```
```
2.4M Cs0.0208_I_MA0.0625_FA0.333_Br0.188_Cl0.0625_Pb0.417 in DMF9_DMSO1
```
The molarity of the output will by default be determined by the largest component amount. This can be a bit silly. Passing a component or a numeric value to `molarity` can be used to manually set the molarity. Note that this does not affect the solution itself, just the relative values of the formula units and the overall molarity.
```
result2 = weigher.weights_to_solution(
weights=answer,
volume=1e-3, #in L
solvents='DMF9_DMSO1',
norm='Pb', #normalize the formula+molarity such that Pb=1
)
print(result2) #result is a Solution object
```
```
1.0M Cs0.05_I2.4_Pb_MA0.15_Br0.45_Cl0.15_FA0.8 in DMF9_DMSO1
```
`Solution` objects can be compared - even if their molarity/formulae are apparently different, they will show as equal if the effective molarity of each component is within 0.01% between the solutions.
```
result == result2
```
```
True
```
Read the full documentation [here](https://mixsol.readthedocs.io/en/latest/).
Raw data
{
"_id": null,
"home_page": null,
"name": "mixsol",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.11",
"maintainer_email": null,
"keywords": "Chemistry, Combinatoric, Dilution, Mixing, Molarity, Planning, Solution",
"author": null,
"author_email": "Rishi E Kumar <rishi42@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/1f/77/e3a99920ba3c37433f550ab7f028d56b57ff1717c4e3807300ac8ba25348/mixsol-1.0.1.tar.gz",
"platform": null,
"description": "[](https://mybinder.org/v2/gh/rekumar/mixsol/HEAD)\n[](https://badge.fury.io/py/mixsol)\n\n\n\n`pip install mixsol`\n\nPipetting planner for efficient combinatorial mixing of solutions. Often we want to interpolate a few stock solutions into many target mixtures. If some of these mixtures require only a tiny amount of a stock solution, the minimum volume for our pipette may limit our ability to make this solution without a serial dilution. Mixsol searches for mixing sequences that use only other target solutions as stepping stones to reach these difficult mixtures, minimizing waste.\n\nMixsol also has the ability to calculate the masses of solid reagents needed to make a target solution. Finally, measured amounts of solid reagents can be input to calculate the actual solution we have made.\n\nHappy mixing!\n\n\n\n\n# Examples\n\n## Solution Mixing\nSolutions are defined with the `Solution` class. Solutes and solvents are both defined by either dicts or their formula, which follows the `(name1)(amount1)_(name2)(amount2)_..._(name)(amount)` format. The names do not have to correspond to elements, so you can use placeholders for units that will be mixed. Parentheses can be used to simplify formulae as well: `A2_B2_C` == `(A_B)2_C`. An `alias` can be provided for the solution to simplify later analysis.\n\n```\nimport mixsol as mx\n\nstock_solutions = [\n mx.Solution(\n solutes='FA_Pb_I3',\n solvents='DMF9_DMSO1',\n molarity=1,\n alias='FAPI'\n ),\n mx.Solution(\n solutes={\n \"MA\": 1,\n \"Pb\": 1,\n \"I\": 3,\n },\n solvents={\n \"DMF\": 9,\n \"DMSO\": 1,\n },\n molarity=1,\n alias='MAPI'\n ),\n]\n```\n\nThis process goes for both stock and target solutions. \n\nYou can manually generate your target solutions and place them in a list like so:\n\n```\ntargets = []\nfor a in np.linspace(0, 0.8, 5):\n targets.append(mx.Solution(\n solutes={\n \"FA\": a,\n \"MA\": 1-a,\n \"Pb\": 1,\n \"I\": 3,\n },\n solvents=\"DMF9_DMSO1\",\n molarity=1,\n alias=f'FA_{a:.3f}'\n ))\n```\n\nOr, if you want to mix in equal steps between two (or more!) endpoint solutions, you can use the `interpolate` function to generate a mesh of `Solution` obects. The following code block is nearly equivalent to the one above (it will interpolate all the way from 0-1 instead of 0-0.8, and it won't generate `alias` values).\n\n```\ntarget_mesh = mx.interpolate(\n\tsolutions=stock_solutions, #this should be a list of 2 or more Solution's\n\tsteps=5 #number of divisions. In this example, steps=5 will mix the input Solution's in 20% increments\n\t)\n```\n\n\nStock and target solutions go into a `Mixer` object\n\n```\nsm = Mixer(\n stock_solutions = stock_solutions,\n targets = {\n t:60 #Solution:volume dictionary\n for t in targets\n })\n```\nwhich is then solved with constraints\n```\nsm.solve(\n min_volume=20, #minimum volume for a single liquid transfer\n max_inputs = 3 #maximum number of solutions (stock or other target) that can be mixed to form one target\n )\n```\n\nThe results can be displayed in two ways:\n- plain text output of liquid transfers, in order. use of the `alias` term really simplifies this output\n```\nsm.print()\n```\n```\n===== Stock Prep =====\n120.00 of FAPI\n180.00 of MAPI\n====== Mixing =====\nDistribute FAPI:\n\t54.00 to FA_0.600\n\t36.00 to FA_0.400\n\t30.00 to FA_0.800\nDistribute MAPI:\n\t60.00 to FA_0.000\n\t36.00 to FA_0.600\n\t54.00 to FA_0.400\n\t30.00 to FA_0.200\nDistribute FA_0.600:\n\t30.00 to FA_0.800\nDistribute FA_0.400:\n\t30.00 to FA_0.200\n```\n\n- a graph of solution transfers. This is harder to use in practice, but can give an overview of the mixing path.\n```\nfig, ax = plt.subplots(figsize=(6,6))\nsm.plot(ax=ax)\n```\n\n\nNote that the units of volume here are arbitrary. Using SI units for small volumes might cause numerical issues when solving a mixture strategy (eg you should use 10 microliters instead of 1e-5 liters). \n\n## Solution Preparation\nMixsol aids in determining the mass of solid reagents needed to form target solutions. We can also check the actual solution formed from recorded reagent masses. Here, the units *do* matter, and you should stick to SI units (mass in grams, volume in liters).\n\nWe define solid reagents with the `Powder` class. This requires at least a chemical formula delimited by underscores, similar to the `Solution` definition earlier. If this formula is a proper chemical formula of elements, the molar mass is calculated automatically. If not, you can pass the molar mass directly. The `calculate_molar_mass` function can be used for convenience. `alias` does the same thing it did for `Solution`.\n\n```\nfrom mixsol import Powder, calculate_molar_mass, Weigher\n\npowders = [\n Powder('Cs_I'),\n Powder('Pb_I2'),\n Powder('Pb_Br2'),\n Powder('Pb_Cl2'),\n Powder(\n formula='MA_I',\n molar_mass=calculate_molar_mass('C_H6_N_I'),\n alias='MAI',\n ),\n Powder(\n formula='FA_I',\n molar_mass = calculate_molar_mass('C_H5_N2_I'),\n alias='FAI',\n )\n]\n```\n\nThe list of available `Powder`s is fed into a `Weigher` object\n\n```\nweigher = Weigher(\n powders=powders\n)\n```\nwhich can then be used to determine powder amounts for a given volume of a target `Solution`\n\n```\ntarget=Solution(\n solutes='Cs0.05_FA0.8_MA0.15_Pb_I2.4_Br0.45_Cl0.15',\n solvents='DMF9_DMSO1',\n molarity=1\n)\n\nanswer = weigher.get_weights(\n target,\n volume=1e-3, #in L\n)\nprint(answer) #masses of each powder, in grams\n```\n```\n{'Cs_I': 0.012990496098, 'Pb_I2': 0.322706258, 'Pb_Br2': 0.082576575, 'Pb_Cl2': 0.020857935, 'MAI': 0.02384543385, 'FAI': 0.1375746568}\n```\n\nFinally, we can also generate a `Solution` object by inputting a `{powder:mass}` dictionary into `Weigher`. We will just use the answer from before, but this can be manually input. \n```\nresult = weigher.weights_to_solution(\n weights=answer,\n volume=1e-3,\n solvents='DMF9_DMSO1',\n)\nprint(result)\n```\n```\n2.4M Cs0.0208_I_MA0.0625_FA0.333_Br0.188_Cl0.0625_Pb0.417 in DMF9_DMSO1\n```\nThe molarity of the output will by default be determined by the largest component amount. This can be a bit silly. Passing a component or a numeric value to `molarity` can be used to manually set the molarity. Note that this does not affect the solution itself, just the relative values of the formula units and the overall molarity.\n\n```\nresult2 = weigher.weights_to_solution(\n weights=answer,\n volume=1e-3, #in L\n solvents='DMF9_DMSO1',\n norm='Pb', #normalize the formula+molarity such that Pb=1\n)\nprint(result2) #result is a Solution object\n```\n```\n1.0M Cs0.05_I2.4_Pb_MA0.15_Br0.45_Cl0.15_FA0.8 in DMF9_DMSO1\n```\n\n`Solution` objects can be compared - even if their molarity/formulae are apparently different, they will show as equal if the effective molarity of each component is within 0.01% between the solutions.\n\n```\nresult == result2\n```\n```\nTrue\n```\n\nRead the full documentation [here](https://mixsol.readthedocs.io/en/latest/).\n",
"bugtrack_url": null,
"license": "GPLv3",
"summary": "Planning tool for combinatorial solution mixing. Reach target solutions from mixes of starting solutions, constrained by minimum pipetting volumes. Also aids in computing amounts of powdered reagents required to form solutions with target solutes + molarities.",
"version": "1.0.1",
"project_urls": {
"Download": "https://github.com/rekumar/mixsol/archive/refs/tags/v0.6.tar.gz",
"Homepage": "https://github.com/rekumar/mixsol"
},
"split_keywords": [
"chemistry",
" combinatoric",
" dilution",
" mixing",
" molarity",
" planning",
" solution"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "07c5c7b1105d2d5f982d0f04510b8ce753b95e654306f4612c378afbb9e720e5",
"md5": "8bfde36d28c729eddc0b7aea760c9f33",
"sha256": "7e24050aff518e2b5fda68985481d354327995a0e6dfafdc42f676402a9f7e79"
},
"downloads": -1,
"filename": "mixsol-1.0.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "8bfde36d28c729eddc0b7aea760c9f33",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.11",
"size": 29875,
"upload_time": "2025-07-17T21:26:27",
"upload_time_iso_8601": "2025-07-17T21:26:27.027489Z",
"url": "https://files.pythonhosted.org/packages/07/c5/c7b1105d2d5f982d0f04510b8ce753b95e654306f4612c378afbb9e720e5/mixsol-1.0.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "1f77e3a99920ba3c37433f550ab7f028d56b57ff1717c4e3807300ac8ba25348",
"md5": "fac3bac8745f15c63c80b85eb642e7b2",
"sha256": "3a1c660013f2bfaf8250dca144e8d959a7ba667b57877abeb270843d83c609d0"
},
"downloads": -1,
"filename": "mixsol-1.0.1.tar.gz",
"has_sig": false,
"md5_digest": "fac3bac8745f15c63c80b85eb642e7b2",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.11",
"size": 28840,
"upload_time": "2025-07-17T21:26:28",
"upload_time_iso_8601": "2025-07-17T21:26:28.563793Z",
"url": "https://files.pythonhosted.org/packages/1f/77/e3a99920ba3c37433f550ab7f028d56b57ff1717c4e3807300ac8ba25348/mixsol-1.0.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-17 21:26:28",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "rekumar",
"github_project": "mixsol",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "mixsol"
}