Spaces:
Running
Running
Commit ·
843a502
0
Parent(s):
first commit
Browse files- .gitattributes +3 -0
- LICENSE +21 -0
- README.md +2 -0
- build/lib/pycek_public/__init__.py +10 -0
- build/lib/pycek_public/bomb_calorimetry.py +113 -0
- build/lib/pycek_public/cek_labs.py +388 -0
- build/lib/pycek_public/crystal_violet.py +75 -0
- build/lib/pycek_public/generate_random_filenames.py +166 -0
- build/lib/pycek_public/logger.py +112 -0
- build/lib/pycek_public/statistics_lab.py +125 -0
- build/lib/pycek_public/surface_adsorption.py +89 -0
- deployment/Dockerfile +22 -0
- deployment/README.md +9 -0
- deployment/build.sh +3 -0
- deployment/requirements.txt +3 -0
- marimo/__pycache__/app.cpython-311.pyc +0 -0
- marimo/__pycache__/bomb_calorimetry.cpython-311.pyc +0 -0
- marimo/__pycache__/index.cpython-311.pyc +0 -0
- marimo/app.py +26 -0
- marimo/bomb_calorimetry.py +140 -0
- marimo/crystal_violet.py +167 -0
- marimo/index.py +23 -0
- marimo/statistics_lab.py +140 -0
- marimo/surface_adsorption.py +137 -0
- pyproject.toml +31 -0
- src/pycek_public.egg-info/PKG-INFO +42 -0
- src/pycek_public.egg-info/SOURCES.txt +17 -0
- src/pycek_public.egg-info/dependency_links.txt +1 -0
- src/pycek_public.egg-info/requires.txt +10 -0
- src/pycek_public.egg-info/top_level.txt +1 -0
- src/pycek_public/__init__.py +12 -0
- src/pycek_public/bomb_calorimetry.py +113 -0
- src/pycek_public/cek_labs.py +388 -0
- src/pycek_public/crystal_violet.py +75 -0
- src/pycek_public/generate_random_filenames.py +166 -0
- src/pycek_public/logger.py +112 -0
- src/pycek_public/plotting.py +49 -0
- src/pycek_public/statistics_lab.py +121 -0
- src/pycek_public/surface_adsorption.py +83 -0
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
marimo/docs/*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
marimo/docs/*.docx filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Paolo Raiteri
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pycek
|
| 2 |
+
python package for CHEM2000
|
build/lib/pycek_public/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .cek_labs import *
|
| 2 |
+
|
| 3 |
+
from .generate_random_filenames import *
|
| 4 |
+
from .logger import *
|
| 5 |
+
|
| 6 |
+
from .statistics_lab import *
|
| 7 |
+
from .bomb_calorimetry import *
|
| 8 |
+
from .crystal_violet import *
|
| 9 |
+
from .surface_adsorption import *
|
| 10 |
+
|
build/lib/pycek_public/bomb_calorimetry.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pprint as pp
|
| 4 |
+
|
| 5 |
+
class bomb_calorimetry(cek.cek_labs):
|
| 6 |
+
def setup_lab(self):
|
| 7 |
+
"""
|
| 8 |
+
Define base information for the lab
|
| 9 |
+
"""
|
| 10 |
+
self.add_metadata(
|
| 11 |
+
laboratory = 'Bomb Calorimetry',
|
| 12 |
+
columns = ["Time (s)","Temperature (K)"]
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
self.available_samples = ['benzoic', 'sucrose', 'naphthalene']
|
| 16 |
+
|
| 17 |
+
self.ignition_time = 20
|
| 18 |
+
self.relaxation_time = 3
|
| 19 |
+
self.number_of_values = 100
|
| 20 |
+
self.noise_level = 0.1
|
| 21 |
+
|
| 22 |
+
self.slope_before = np.random.uniform(0., self.noise_level) / 3
|
| 23 |
+
self.slope_after = np.random.uniform(0., self.noise_level) / 3
|
| 24 |
+
|
| 25 |
+
self.RT = self.R * self.temperature
|
| 26 |
+
|
| 27 |
+
# calorimeter constant (J/K)
|
| 28 |
+
self.calorimeter_constant = {'value':10135,'std_error':0.0}
|
| 29 |
+
self.sample_parameters["co2"] = {
|
| 30 |
+
"mM" : 44.01,
|
| 31 |
+
"dH" : -393.51e3, # co2 enthapy of formation (J/mol/K)
|
| 32 |
+
}
|
| 33 |
+
self.sample_parameters["h2o"] = {
|
| 34 |
+
"mM" : 18.015,
|
| 35 |
+
"dH" : -285.83e3,# h2o enthapy of formation (J/mol/K)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
self.sample_parameters["benzoic"] = {
|
| 39 |
+
"mM" : 122.123,
|
| 40 |
+
"n1" : 7,
|
| 41 |
+
"n2" : 3,
|
| 42 |
+
"dn" : 7-15/2,
|
| 43 |
+
"dHf" : {'value':-384.8e3,'std_error':0.5e3},
|
| 44 |
+
"dHc" : {'value':-3227.26e3,'std_error':0.2e3},
|
| 45 |
+
}
|
| 46 |
+
self.sample_parameters["sucrose"] = {
|
| 47 |
+
"mM" : 342.3,
|
| 48 |
+
"n1" : 12,
|
| 49 |
+
"n2" : 11,
|
| 50 |
+
"dn" : 0,
|
| 51 |
+
"dHf" : {'value':-2221.2e3,'std_error':0.2e3},
|
| 52 |
+
"dHc" : {'value':-5643.4e3,'std_error':1.8e3},
|
| 53 |
+
}
|
| 54 |
+
self.sample_parameters["naphthalene"] = {
|
| 55 |
+
"mM" : 128.17,
|
| 56 |
+
"n1" : 10,
|
| 57 |
+
"n2" : 4,
|
| 58 |
+
"dn" : 10 - 12,
|
| 59 |
+
"dHf" : {'value':77e3,'std_error':10.0e3},
|
| 60 |
+
"dHc" : {'value':-5160e3,'std_error':20.0e3},
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
def create_data(self):
|
| 64 |
+
"""
|
| 65 |
+
Generate the data
|
| 66 |
+
"""
|
| 67 |
+
if self.sample is None:
|
| 68 |
+
raise Exception("Sample not defined")
|
| 69 |
+
|
| 70 |
+
prm = self.sample_parameters[ self.sample ]
|
| 71 |
+
|
| 72 |
+
self.set_parameters(
|
| 73 |
+
sample = self.sample,
|
| 74 |
+
number_of_values = self.number_of_values,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
self.mass = np.random.normal(1000, 100)
|
| 78 |
+
self.add_metadata(**{
|
| 79 |
+
'Tablet mass (mg)': self.mass,
|
| 80 |
+
"Ignition time (s)" : self.ignition_time,
|
| 81 |
+
"Sample" : self.sample,
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
moles = self.mass / 1000 / prm["mM"]
|
| 85 |
+
|
| 86 |
+
# combustion enthalpy
|
| 87 |
+
# nH{co2} + mH{h2o} - H = DcH
|
| 88 |
+
DcH = prm["n1"] * self.sample_parameters["co2"]["dH"] + \
|
| 89 |
+
prm["n2"] * self.sample_parameters["h2o"]["dH"] - prm["dHf"]["value"]
|
| 90 |
+
|
| 91 |
+
dH = DcH * moles
|
| 92 |
+
dnrt = moles * self.RT * prm["dn"]
|
| 93 |
+
dU = dH - dnrt
|
| 94 |
+
|
| 95 |
+
deltaT = -dU / self.calorimeter_constant['value']
|
| 96 |
+
|
| 97 |
+
x = np.linspace(0, self.number_of_values, self.number_of_values)
|
| 98 |
+
y = np.random.normal(0, self.noise_level, self.number_of_values)
|
| 99 |
+
|
| 100 |
+
dd = 0.
|
| 101 |
+
T = self.temperature
|
| 102 |
+
for i in range(self.number_of_values):
|
| 103 |
+
if i < self.ignition_time:
|
| 104 |
+
T += self.slope_before
|
| 105 |
+
else:
|
| 106 |
+
T += self.slope_after
|
| 107 |
+
dd = deltaT * (1 - np.exp( - (i - self.ignition_time) / self.relaxation_time) )
|
| 108 |
+
y[i] += T + dd
|
| 109 |
+
|
| 110 |
+
self.data = np.column_stack((x,y))
|
| 111 |
+
|
| 112 |
+
return
|
| 113 |
+
|
build/lib/pycek_public/cek_labs.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
|
| 5 |
+
from abc import ABC, abstractmethod
|
| 6 |
+
class cek_labs(ABC):
|
| 7 |
+
def __init__(self, **kwargs):
|
| 8 |
+
self.token = None
|
| 9 |
+
self.student_ID = 123456789
|
| 10 |
+
|
| 11 |
+
self.noise_level = 1
|
| 12 |
+
self.precision = 1
|
| 13 |
+
|
| 14 |
+
self.available_samples = []
|
| 15 |
+
self.sample_parameters = {}
|
| 16 |
+
self.sample = None
|
| 17 |
+
|
| 18 |
+
self.R = 8.314
|
| 19 |
+
self.NA = 6.022e23
|
| 20 |
+
self.temperature = 298
|
| 21 |
+
|
| 22 |
+
self.number_of_values = 100
|
| 23 |
+
self.output_file = None
|
| 24 |
+
self.filename_gen = cek.TempFilenameGenerator()
|
| 25 |
+
|
| 26 |
+
self.metadata = OrderedDict({
|
| 27 |
+
'student_ID' : self.student_ID,
|
| 28 |
+
'number_of_values' : self.number_of_values,
|
| 29 |
+
'output_file' : self.output_file,
|
| 30 |
+
})
|
| 31 |
+
|
| 32 |
+
self.logger_level = "ERROR"
|
| 33 |
+
|
| 34 |
+
# Define some lab specific parameters
|
| 35 |
+
# Can overwrite the defaults
|
| 36 |
+
for k,w in kwargs.items():
|
| 37 |
+
setattr(self, k, w)
|
| 38 |
+
np.random.seed(self.student_ID)
|
| 39 |
+
|
| 40 |
+
self.logger = cek.setup_logger(level=self.logger_level)
|
| 41 |
+
# self.logger.debug("This is a debug message")
|
| 42 |
+
# self.logger.verbose("This is a verbose message") # New verbose level
|
| 43 |
+
# self.logger.info("This is an info message")
|
| 44 |
+
# self.logger.result("This is an result message")
|
| 45 |
+
# self.logger.warning("This is a warning message")
|
| 46 |
+
# self.logger.error("This is an error message")
|
| 47 |
+
# self.logger.critical("This is a critical message")
|
| 48 |
+
# quit()
|
| 49 |
+
|
| 50 |
+
# Lab specific parameters
|
| 51 |
+
self.setup_lab()
|
| 52 |
+
|
| 53 |
+
def __str__(self):
|
| 54 |
+
return f'CHEM2000 Lab: {self.__class__.__name__}'
|
| 55 |
+
|
| 56 |
+
def set_student_ID(self,student_ID):
|
| 57 |
+
if isinstance(student_ID,int):
|
| 58 |
+
self.student_ID = student_ID
|
| 59 |
+
elif isinstance(student_ID,str):
|
| 60 |
+
if student_ID.isdigit():
|
| 61 |
+
self.student_ID = int(student_ID)
|
| 62 |
+
else:
|
| 63 |
+
raise ValueError("student_ID must be an integer")
|
| 64 |
+
else:
|
| 65 |
+
raise ValueError("student_ID must be an integer")
|
| 66 |
+
np.random.seed(self.student_ID)
|
| 67 |
+
self.update_metadata_from_attr()
|
| 68 |
+
self.logger.debug(f"Initial seed = {np.random.get_state()[1][0]}")
|
| 69 |
+
|
| 70 |
+
def set_token(self, token):
|
| 71 |
+
self.token = token
|
| 72 |
+
#print(f"Check: {self._check_token()}")
|
| 73 |
+
|
| 74 |
+
def _check_token(self):
|
| 75 |
+
if self.token != 23745419:
|
| 76 |
+
return True
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
def add_metadata(self, **kwargs):
|
| 80 |
+
for key, value in kwargs.items():
|
| 81 |
+
self.metadata[key] = value
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
def update_metadata_from_attr(self):
|
| 85 |
+
for k in self.metadata:
|
| 86 |
+
try:
|
| 87 |
+
self.metadata[k] = getattr(self, k)
|
| 88 |
+
except:
|
| 89 |
+
pass
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
def set_parameters(self, **kwargs):
|
| 93 |
+
"""
|
| 94 |
+
Set parameters for the lab
|
| 95 |
+
"""
|
| 96 |
+
for k,w in kwargs.items():
|
| 97 |
+
if k == "student_ID":
|
| 98 |
+
self.set_student_ID(w)
|
| 99 |
+
else:
|
| 100 |
+
setattr(self, k, w)
|
| 101 |
+
self.update_metadata_from_attr()
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
def write_metadata(self,f=None):
|
| 105 |
+
"""
|
| 106 |
+
Write metadata to the data file
|
| 107 |
+
"""
|
| 108 |
+
if f is None:
|
| 109 |
+
def dump(s):
|
| 110 |
+
self.logger.info(s)
|
| 111 |
+
else:
|
| 112 |
+
def dump(s):
|
| 113 |
+
with open(f, 'a') as file:
|
| 114 |
+
file.write(f"# {s}\n")
|
| 115 |
+
|
| 116 |
+
for key, value in self.metadata.items():
|
| 117 |
+
string = f"{key}"
|
| 118 |
+
string = string.replace("_"," ")
|
| 119 |
+
string = string[0].upper() + string[1:] + f" = {value}"
|
| 120 |
+
dump(string)
|
| 121 |
+
|
| 122 |
+
def read_metadata(self,f):
|
| 123 |
+
"""
|
| 124 |
+
Read metadata from the data file
|
| 125 |
+
|
| 126 |
+
Return: metadata (dict)
|
| 127 |
+
"""
|
| 128 |
+
metadata = OrderedDict({})
|
| 129 |
+
|
| 130 |
+
hash_lines = []
|
| 131 |
+
with open(f, 'r') as file:
|
| 132 |
+
for line in file:
|
| 133 |
+
if line.strip().startswith('#'):
|
| 134 |
+
hash_lines.append(line.replace('#','').strip())
|
| 135 |
+
|
| 136 |
+
for l in hash_lines:
|
| 137 |
+
if ":" in l:
|
| 138 |
+
key, value = l.split(':')
|
| 139 |
+
elif "=" in l:
|
| 140 |
+
key, value = l.split('=')
|
| 141 |
+
else:
|
| 142 |
+
raise Exception("Unknown separator")
|
| 143 |
+
key = key.replace("#","").strip()
|
| 144 |
+
metadata[key] = value.strip()
|
| 145 |
+
|
| 146 |
+
return metadata
|
| 147 |
+
|
| 148 |
+
def write_data_to_file(self, **kwargs):
|
| 149 |
+
"""
|
| 150 |
+
"""
|
| 151 |
+
if self.output_file is None:
|
| 152 |
+
filename = self.filename_gen.random
|
| 153 |
+
else:
|
| 154 |
+
filename = self.output_file
|
| 155 |
+
self.add_metadata(output_file=filename)
|
| 156 |
+
|
| 157 |
+
with open(filename, 'w') as f:
|
| 158 |
+
# Write the column names
|
| 159 |
+
cols = None
|
| 160 |
+
if "columns" in kwargs:
|
| 161 |
+
cols = kwargs["columns"]
|
| 162 |
+
elif "columns" in self.metadata:
|
| 163 |
+
cols = self.metadata["columns" ]
|
| 164 |
+
if cols is not None:
|
| 165 |
+
f.write(",".join(cols) + "\n")
|
| 166 |
+
|
| 167 |
+
# Convert NumPy array to list if needed
|
| 168 |
+
# if isinstance(self.data, np.ndarray):
|
| 169 |
+
# self.data = self.data.tolist()
|
| 170 |
+
|
| 171 |
+
# Write data
|
| 172 |
+
for row in self.data:
|
| 173 |
+
# Handle multiple columns
|
| 174 |
+
if isinstance(row, (list, tuple, np.ndarray)):
|
| 175 |
+
line = ",".join(map(str, row))
|
| 176 |
+
# Handle single-column case
|
| 177 |
+
else:
|
| 178 |
+
line = str(row)
|
| 179 |
+
f.write(line + "\n")
|
| 180 |
+
|
| 181 |
+
# Write the kwargs as metadata
|
| 182 |
+
self.write_metadata(filename)
|
| 183 |
+
|
| 184 |
+
return filename
|
| 185 |
+
|
| 186 |
+
def read_data_file(self,filename=None):
|
| 187 |
+
if filename is None:
|
| 188 |
+
raise ValueError("Filename is missing")
|
| 189 |
+
|
| 190 |
+
# Read file and separate comments from data
|
| 191 |
+
comments = []
|
| 192 |
+
data_lines = []
|
| 193 |
+
|
| 194 |
+
with open(filename, "r") as f:
|
| 195 |
+
for line in f:
|
| 196 |
+
if line.startswith("#"):
|
| 197 |
+
comments.append(line.strip()) # Store comment lines
|
| 198 |
+
else:
|
| 199 |
+
data_lines.append(line.strip()) # Store data lines
|
| 200 |
+
|
| 201 |
+
# Extract header and data
|
| 202 |
+
header = data_lines[0] # First non-comment line is the header
|
| 203 |
+
data_lines = "\n".join(data_lines[0:]) # Join remaining lines as CSV data
|
| 204 |
+
|
| 205 |
+
# Convert CSV data to NumPy array
|
| 206 |
+
from io import StringIO
|
| 207 |
+
from numpy.lib.recfunctions import structured_to_unstructured
|
| 208 |
+
|
| 209 |
+
data = np.genfromtxt(
|
| 210 |
+
StringIO(data_lines), delimiter=',',
|
| 211 |
+
comments='#', names=True,
|
| 212 |
+
skip_header=0, dtype=None)
|
| 213 |
+
|
| 214 |
+
data_array = structured_to_unstructured(data)
|
| 215 |
+
|
| 216 |
+
metadata = None
|
| 217 |
+
if len(comments) > 0:
|
| 218 |
+
metadata = OrderedDict({})
|
| 219 |
+
for l in comments:
|
| 220 |
+
if ":" in l:
|
| 221 |
+
key, value = l.split(':')
|
| 222 |
+
elif "=" in l:
|
| 223 |
+
key, value = l.split('=')
|
| 224 |
+
else:
|
| 225 |
+
raise Exception("Unknown separator")
|
| 226 |
+
key = key.replace("#","").strip()
|
| 227 |
+
metadata[key.strip()] = value.strip()
|
| 228 |
+
|
| 229 |
+
# Output results
|
| 230 |
+
# print("Comments:")
|
| 231 |
+
# print("\n".join(comments))
|
| 232 |
+
# print("\nExtracted Data:")
|
| 233 |
+
# print(data_array)
|
| 234 |
+
return data_array, header, metadata
|
| 235 |
+
|
| 236 |
+
def process_file(self, filename=None):
|
| 237 |
+
self.read_data(filename)
|
| 238 |
+
result = self.process_data()
|
| 239 |
+
return result
|
| 240 |
+
|
| 241 |
+
def _valid_ID(self,ID):
|
| 242 |
+
if ID in ["23745411"]:
|
| 243 |
+
return True
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
def _round_values(self, values, precision=None):
|
| 247 |
+
if precision is None:
|
| 248 |
+
precision = self.precision
|
| 249 |
+
rounded_values = [round(v, precision) for v in values]
|
| 250 |
+
values = np.array(rounded_values, dtype=float)
|
| 251 |
+
return values
|
| 252 |
+
|
| 253 |
+
def _generate_uniform_random(self, lower, upper, n):
|
| 254 |
+
return self._round_values(np.random.uniform(lower, upper, n))
|
| 255 |
+
|
| 256 |
+
def _generate_normal_random(self,n,prm):
|
| 257 |
+
list_of_1d_arrays = []
|
| 258 |
+
for p in prm:
|
| 259 |
+
values = np.random.normal(p[0], p[1], size=n)
|
| 260 |
+
list_of_1d_arrays.append(self._round_values(values))
|
| 261 |
+
|
| 262 |
+
if len(prm) == 1:
|
| 263 |
+
return np.array(self._round_values(values))
|
| 264 |
+
else:
|
| 265 |
+
return np.column_stack( [*list_of_1d_arrays] )
|
| 266 |
+
|
| 267 |
+
def _generate_noise(self,n,noise_level=None,ntype="normal"):
|
| 268 |
+
if noise_level == None:
|
| 269 |
+
raise ValueError("Missing noise level")
|
| 270 |
+
if noise_level <= 0:
|
| 271 |
+
return np.zeros(n)
|
| 272 |
+
if ntype == "normal":
|
| 273 |
+
return np.random.normal(0, noise_level, size=n)
|
| 274 |
+
|
| 275 |
+
def _generate_data_from_function(self, func, params, nvalues, xrange):
|
| 276 |
+
x = np.sort(self._generate_uniform_random(nvalues,*xrange))
|
| 277 |
+
y = func(x, *params) + self._generate_noise(nvalues)
|
| 278 |
+
y = self._round_values(y)
|
| 279 |
+
return np.column_stack((x,y))
|
| 280 |
+
|
| 281 |
+
import numpy as np
|
| 282 |
+
from typing import Callable, Dict, Optional, Union, Tuple
|
| 283 |
+
|
| 284 |
+
def generate_data_from_function(
|
| 285 |
+
self,
|
| 286 |
+
function: Callable,
|
| 287 |
+
params: Dict,
|
| 288 |
+
nvalues: int,
|
| 289 |
+
xrange: Optional[Tuple[float, float]] = None,
|
| 290 |
+
xspacing: str = 'random',
|
| 291 |
+
noise_level: Optional[float] = None,
|
| 292 |
+
background: Optional[float] = None,
|
| 293 |
+
weights: Optional[bool] = None,
|
| 294 |
+
positive: bool = False
|
| 295 |
+
) -> np.ndarray:
|
| 296 |
+
"""
|
| 297 |
+
Generate synthetic data points from a given function with optional noise and background.
|
| 298 |
+
|
| 299 |
+
Parameters
|
| 300 |
+
----------
|
| 301 |
+
function : callable
|
| 302 |
+
The model function to generate data from. Should accept x values and **kwargs.
|
| 303 |
+
params : dict
|
| 304 |
+
Parameters to pass to the function as keyword arguments.
|
| 305 |
+
nvalues : int
|
| 306 |
+
Number of data points to generate.
|
| 307 |
+
xrange : tuple of float, optional
|
| 308 |
+
Range of x values (min, max). Required if generating data points.
|
| 309 |
+
xspacing : str, default='random'
|
| 310 |
+
Method to space x values. Options:
|
| 311 |
+
- 'linear': Evenly spaced points
|
| 312 |
+
- 'random': Uniformly distributed random points
|
| 313 |
+
noise_level : float, optional
|
| 314 |
+
Standard deviation of Gaussian noise to add to y values.
|
| 315 |
+
background : float, optional
|
| 316 |
+
Constant background level to add to all y values.
|
| 317 |
+
weights : bool, optional
|
| 318 |
+
If True, include weights in output (NOT IMPLEMENTED).
|
| 319 |
+
positive : bool, default=False
|
| 320 |
+
If True, take absolute value of final y values.
|
| 321 |
+
|
| 322 |
+
Returns
|
| 323 |
+
-------
|
| 324 |
+
np.ndarray
|
| 325 |
+
2D array with shape (nvalues, 2) containing (x, y) pairs.
|
| 326 |
+
|
| 327 |
+
Raises
|
| 328 |
+
------
|
| 329 |
+
ValueError
|
| 330 |
+
If xrange is None or invalid xspacing type is provided.
|
| 331 |
+
"""
|
| 332 |
+
# Validate inputs
|
| 333 |
+
if xrange is None:
|
| 334 |
+
raise ValueError("xrange must be provided as (min, max) tuple")
|
| 335 |
+
|
| 336 |
+
if not isinstance(nvalues, int) or nvalues <= 0:
|
| 337 |
+
raise ValueError("nvalues must be a positive integer")
|
| 338 |
+
|
| 339 |
+
# Generate x values
|
| 340 |
+
if xspacing == "linear":
|
| 341 |
+
x = np.linspace(*xrange, nvalues)
|
| 342 |
+
elif xspacing == "random":
|
| 343 |
+
x = np.sort(self._generate_uniform_random(*xrange, nvalues))
|
| 344 |
+
else:
|
| 345 |
+
raise ValueError(f"xspacing must be 'linear' or 'random', got '{xspacing}'")
|
| 346 |
+
|
| 347 |
+
# Generate base y values from function
|
| 348 |
+
y = function(x, **params)
|
| 349 |
+
|
| 350 |
+
# Add optional modifications
|
| 351 |
+
if background is not None:
|
| 352 |
+
y += background
|
| 353 |
+
|
| 354 |
+
if noise_level is not None:
|
| 355 |
+
y += self._generate_noise(nvalues,noise_level)
|
| 356 |
+
|
| 357 |
+
if positive:
|
| 358 |
+
y = np.abs(y)
|
| 359 |
+
|
| 360 |
+
# Note: weights parameter is currently unused
|
| 361 |
+
if weights is not None:
|
| 362 |
+
# TODO: Implement weights handling
|
| 363 |
+
pass
|
| 364 |
+
|
| 365 |
+
y = self._round_values(y)
|
| 366 |
+
|
| 367 |
+
return np.column_stack((x, y))
|
| 368 |
+
|
| 369 |
+
def create_data_file(self):
|
| 370 |
+
data = self.create_data()
|
| 371 |
+
self.write_data_to_file(
|
| 372 |
+
self.metadata['output_file'],
|
| 373 |
+
data, **self.metadata )
|
| 374 |
+
return self.metadata['output_file']
|
| 375 |
+
|
| 376 |
+
def get_data(self):
|
| 377 |
+
return self.data
|
| 378 |
+
def get_metadata(self):
|
| 379 |
+
return self.metadata
|
| 380 |
+
|
| 381 |
+
@abstractmethod
|
| 382 |
+
def setup_lab(self,**kwargs):
|
| 383 |
+
pass
|
| 384 |
+
|
| 385 |
+
@abstractmethod
|
| 386 |
+
def create_data(self):
|
| 387 |
+
pass
|
| 388 |
+
|
build/lib/pycek_public/crystal_violet.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class crystal_violet(cek.cek_labs):
|
| 5 |
+
def setup_lab(self):
|
| 6 |
+
"""
|
| 7 |
+
Define base information for the lab.
|
| 8 |
+
They can be overwrite by the user using the kwargs in the constructor or
|
| 9 |
+
by calling the set_parameters method.
|
| 10 |
+
"""
|
| 11 |
+
self.add_metadata(
|
| 12 |
+
laboratory = 'Crystal Violet Lab',
|
| 13 |
+
columns = ["Time (s)","Absorbance"]
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
self.expt_time = 1000
|
| 17 |
+
self.number_of_values = 501
|
| 18 |
+
self.noise_level = 0.1
|
| 19 |
+
self.precision = 4
|
| 20 |
+
|
| 21 |
+
self.activation_energy = 63e3 # J/mol
|
| 22 |
+
self.prefactor = 5.9e9 # 1/M/s
|
| 23 |
+
|
| 24 |
+
# Order wrt CV and OH
|
| 25 |
+
self.alpha = 1.0
|
| 26 |
+
self.beta = 0.75
|
| 27 |
+
self.conc_to_abs = 160e3 # Absorbivity of CV at 590 nm (L/mol/cm)
|
| 28 |
+
self.stock_solutions = {"cv" : 2.5e-5, "oh" : 0.5} # mol/L
|
| 29 |
+
|
| 30 |
+
self.volumes = {"cv" : 10, "oh" : 10, "h2o" : 10.0} # mL
|
| 31 |
+
|
| 32 |
+
def create_data(self):
|
| 33 |
+
"""
|
| 34 |
+
Generate the data
|
| 35 |
+
"""
|
| 36 |
+
self.set_parameters(
|
| 37 |
+
sample = self.sample,
|
| 38 |
+
number_of_values = self.number_of_values,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
self.add_metadata(**{
|
| 42 |
+
"Temperature (C)" : self.temperature-273.15,
|
| 43 |
+
"Volume of CV (mL)" : self.volumes['cv'],
|
| 44 |
+
"Volume of OH (mL)" : self.volumes['oh'],
|
| 45 |
+
"Volume of H2O (mL)": self.volumes['h2o'],
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
vtot = np.sum( [ x + np.random.normal(0,self.noise_level,1) for x in self.volumes.values() ] )
|
| 49 |
+
initial_concentration_cv = \
|
| 50 |
+
self.stock_solutions["cv"] * self.volumes["cv"] / vtot
|
| 51 |
+
|
| 52 |
+
concetration_oh = \
|
| 53 |
+
self.stock_solutions["oh"] * self.volumes["oh"] / vtot
|
| 54 |
+
|
| 55 |
+
rate_constant = self.prefactor*np.exp(-self.activation_energy/(self.R*(self.temperature)))
|
| 56 |
+
pseudo_rate_constant = rate_constant * np.power(concetration_oh,self.beta)
|
| 57 |
+
|
| 58 |
+
params = {
|
| 59 |
+
"A" : initial_concentration_cv* self.conc_to_abs,
|
| 60 |
+
"k" : pseudo_rate_constant
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
self.data = self.generate_data_from_function(
|
| 64 |
+
lambda x,A,k: A * np.exp(-k * x) ,
|
| 65 |
+
params,
|
| 66 |
+
self.number_of_values,
|
| 67 |
+
xrange = [0, self.expt_time],
|
| 68 |
+
xspacing = 'linear',
|
| 69 |
+
noise_level = self.noise_level,
|
| 70 |
+
positive = True,
|
| 71 |
+
background = 0.1
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
return
|
| 75 |
+
|
build/lib/pycek_public/generate_random_filenames.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from glob import glob
|
| 3 |
+
import secrets
|
| 4 |
+
import string
|
| 5 |
+
|
| 6 |
+
class TempFilenameGenerator:
|
| 7 |
+
"""
|
| 8 |
+
Generates temporary filenames with .pdb extension and increasing indices.
|
| 9 |
+
Supports both sequential (tmp.{index}.pdb) and random (tmp.{random}.pdb) formats.
|
| 10 |
+
"""
|
| 11 |
+
def __init__(self, directory=".", root="data", ext="csv", random_length=12):
|
| 12 |
+
"""
|
| 13 |
+
Initialize the generator with a target directory.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
directory (str): Directory for the filenames (default: current directory)
|
| 17 |
+
root (str): Root name for the temporary files (default: tmp)
|
| 18 |
+
ext (str): File extension (default: pdb)
|
| 19 |
+
random_length (int): Length of random string in random filenames (default: 12)
|
| 20 |
+
|
| 21 |
+
Example:
|
| 22 |
+
generator = TempFilenameGenerator()
|
| 23 |
+
|
| 24 |
+
# Get sequential filename (e.g., "tmp.0.pdb")
|
| 25 |
+
sequential_file = generator.next
|
| 26 |
+
|
| 27 |
+
# Get random filename (e.g., "tmp.j4k3h2l5m9n8.pdb")
|
| 28 |
+
random_file = generator.random
|
| 29 |
+
"""
|
| 30 |
+
self.directory = directory
|
| 31 |
+
self.root = root
|
| 32 |
+
self.ext = ext
|
| 33 |
+
self.random_length = random_length
|
| 34 |
+
self._current_index = self._find_max_index()
|
| 35 |
+
self._current_filename = None
|
| 36 |
+
|
| 37 |
+
def _find_max_index(self):
|
| 38 |
+
"""Find the highest existing index in the directory."""
|
| 39 |
+
pattern = os.path.join(self.directory, f"{self.root}.*.{self.ext}")
|
| 40 |
+
existing_files = glob(pattern)
|
| 41 |
+
|
| 42 |
+
if not existing_files:
|
| 43 |
+
return -1
|
| 44 |
+
|
| 45 |
+
indices = []
|
| 46 |
+
for filename in existing_files:
|
| 47 |
+
try:
|
| 48 |
+
# Extract index from tmp.{index}.pdb
|
| 49 |
+
index = int(os.path.basename(filename).split('.')[-2])
|
| 50 |
+
indices.append(index)
|
| 51 |
+
except (ValueError, IndexError):
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
return max(indices) if indices else -1
|
| 55 |
+
|
| 56 |
+
def _generate_random_string(self):
|
| 57 |
+
"""Generate a cryptographically secure random string."""
|
| 58 |
+
alphabet = string.ascii_letters + string.digits
|
| 59 |
+
return ''.join(secrets.choice(alphabet) for _ in range(self.random_length))
|
| 60 |
+
|
| 61 |
+
def delete_files(self):
|
| 62 |
+
"""Delete all temporary files in the directory."""
|
| 63 |
+
pattern = os.path.join(self.directory, f"{self.root}.*.{self.ext}")
|
| 64 |
+
for filename in glob(pattern):
|
| 65 |
+
os.remove(filename)
|
| 66 |
+
self._current_index = -1
|
| 67 |
+
|
| 68 |
+
def copy_last(self, dest):
|
| 69 |
+
"""Copy the last generated file to a destination."""
|
| 70 |
+
if self._current_filename is None:
|
| 71 |
+
raise ValueError("No files have been generated yet")
|
| 72 |
+
os.system(f"cp {self._current_filename} {dest}")
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def next(self):
|
| 76 |
+
"""Generate the next filename in sequence."""
|
| 77 |
+
self._current_index += 1
|
| 78 |
+
filename = f"{self.root}.{self._current_index}.{self.ext}"
|
| 79 |
+
self._current_filename = os.path.join(self.directory, filename)
|
| 80 |
+
return self._current_filename
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def random(self):
|
| 84 |
+
"""Generate a random filename."""
|
| 85 |
+
random_string = self._generate_random_string()
|
| 86 |
+
filename = f"{self.root}.{random_string}.{self.ext}"
|
| 87 |
+
self._current_filename = os.path.join(self.directory, filename)
|
| 88 |
+
return self._current_filename
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def current_index(self):
|
| 92 |
+
"""Get the current index value."""
|
| 93 |
+
return self._current_index
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def current(self):
|
| 97 |
+
"""Get the current filename."""
|
| 98 |
+
return self._current_filename
|
| 99 |
+
|
| 100 |
+
# import random
|
| 101 |
+
# import string
|
| 102 |
+
# import os
|
| 103 |
+
# from typing import Optional
|
| 104 |
+
|
| 105 |
+
# def generate_random_filename(
|
| 106 |
+
# extension: Optional[str] = 'csv',
|
| 107 |
+
# length: Optional[int] = 10,
|
| 108 |
+
# prefix: Optional[str] = 'data_',
|
| 109 |
+
# directory: Optional[str] = '',
|
| 110 |
+
# existing_check: Optional[bool] = True
|
| 111 |
+
# ) -> str:
|
| 112 |
+
# """
|
| 113 |
+
# Generate a random filename with optional parameters.
|
| 114 |
+
|
| 115 |
+
# Args:
|
| 116 |
+
# extension (str): File extension to append (e.g., '.txt', '.pdf')
|
| 117 |
+
# length (int): Length of the random string (default: 10)
|
| 118 |
+
# prefix (str): Prefix to add before the random string
|
| 119 |
+
# directory (str): Directory path where the file will be created
|
| 120 |
+
# existing_check (bool): Whether to check if filename already exists
|
| 121 |
+
|
| 122 |
+
# Returns:
|
| 123 |
+
# str: Generated filename
|
| 124 |
+
|
| 125 |
+
# Raises:
|
| 126 |
+
# ValueError: If length is less than 1
|
| 127 |
+
# ValueError: If unable to generate unique filename after 100 attempts
|
| 128 |
+
# """
|
| 129 |
+
# if length < 1:
|
| 130 |
+
# raise ValueError("Length must be at least 1")
|
| 131 |
+
|
| 132 |
+
# # Clean up the extension
|
| 133 |
+
# if extension and not extension.startswith('.'):
|
| 134 |
+
# extension = '.' + extension
|
| 135 |
+
|
| 136 |
+
# # Clean up the directory path
|
| 137 |
+
# if directory:
|
| 138 |
+
# directory = os.path.abspath(directory)
|
| 139 |
+
# if not os.path.exists(directory):
|
| 140 |
+
# os.makedirs(directory)
|
| 141 |
+
|
| 142 |
+
# attempts = 0
|
| 143 |
+
# max_attempts = 100
|
| 144 |
+
|
| 145 |
+
# while True:
|
| 146 |
+
# # Generate random string using ASCII letters and digits
|
| 147 |
+
# random_string = ''.join(
|
| 148 |
+
# random.choices(string.ascii_letters + string.digits, k=length)
|
| 149 |
+
# )
|
| 150 |
+
|
| 151 |
+
# # Combine all parts of the filename
|
| 152 |
+
# filename = f"{prefix}{random_string}{extension}"
|
| 153 |
+
|
| 154 |
+
# # Add directory path if specified
|
| 155 |
+
# if directory:
|
| 156 |
+
# filename = os.path.join(directory, filename)
|
| 157 |
+
|
| 158 |
+
# # Check if file exists (if requested)
|
| 159 |
+
# if not existing_check or not os.path.exists(filename):
|
| 160 |
+
# return filename
|
| 161 |
+
|
| 162 |
+
# attempts += 1
|
| 163 |
+
# if attempts >= max_attempts:
|
| 164 |
+
# raise ValueError(
|
| 165 |
+
# f"Unable to generate unique filename after {max_attempts} attempts"
|
| 166 |
+
# )
|
build/lib/pycek_public/logger.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import logging
|
| 3 |
+
import colorama
|
| 4 |
+
from colorama import Fore, Style
|
| 5 |
+
|
| 6 |
+
# Initialize colorama to work on Windows too
|
| 7 |
+
colorama.init()
|
| 8 |
+
|
| 9 |
+
# Define custom VERBOSE level (between DEBUG-10 and INFO-20)
|
| 10 |
+
VERBOSE = 15
|
| 11 |
+
logging.addLevelName(VERBOSE, 'VERBOSE')
|
| 12 |
+
RESULT = 25
|
| 13 |
+
logging.addLevelName(RESULT, 'RESULT')
|
| 14 |
+
|
| 15 |
+
class ColoredFormatter(logging.Formatter):
|
| 16 |
+
"""Custom formatter that adds colors to log messages based on level with fixed width"""
|
| 17 |
+
COLORS = {
|
| 18 |
+
'DEBUG': Fore.CYAN + Style.BRIGHT,
|
| 19 |
+
'VERBOSE': Fore.BLUE + Style.BRIGHT,
|
| 20 |
+
'INFO': Fore.GREEN + Style.BRIGHT,
|
| 21 |
+
'RESULT' : Fore.RESET + Style.BRIGHT,
|
| 22 |
+
'WARNING': Fore.YELLOW + Style.BRIGHT,
|
| 23 |
+
'ERROR': Fore.RED + Style.BRIGHT,
|
| 24 |
+
'CRITICAL': Fore.MAGENTA + Style.BRIGHT
|
| 25 |
+
}
|
| 26 |
+
LEVEL_NAME_WIDTH = 8
|
| 27 |
+
|
| 28 |
+
def format(self, record):
|
| 29 |
+
# Save original levelname and message
|
| 30 |
+
orig_levelname = record.levelname
|
| 31 |
+
orig_msg = record.msg
|
| 32 |
+
|
| 33 |
+
# Determine the color for the level
|
| 34 |
+
color_code = self.COLORS.get(orig_levelname, '')
|
| 35 |
+
|
| 36 |
+
# Calculate padding for levelname
|
| 37 |
+
padding = ' ' * (self.LEVEL_NAME_WIDTH - len(orig_levelname))
|
| 38 |
+
|
| 39 |
+
# Apply color to levelname with padding
|
| 40 |
+
record.levelname = f"{color_code}{orig_levelname}{padding}{Style.RESET_ALL}"
|
| 41 |
+
|
| 42 |
+
# Apply color to the message as well
|
| 43 |
+
record.msg = f"{color_code}{orig_msg}{Style.RESET_ALL}"
|
| 44 |
+
|
| 45 |
+
# Format the message
|
| 46 |
+
result = super().format(record)
|
| 47 |
+
|
| 48 |
+
# Restore original values
|
| 49 |
+
record.levelname = orig_levelname
|
| 50 |
+
record.msg = orig_msg
|
| 51 |
+
return result
|
| 52 |
+
|
| 53 |
+
class ColoredLogger(logging.Logger):
|
| 54 |
+
"""Custom logger class with verbose method"""
|
| 55 |
+
|
| 56 |
+
def verbose(self, msg, *args, **kwargs):
|
| 57 |
+
"""Log at custom VERBOSE level"""
|
| 58 |
+
if self.isEnabledFor(VERBOSE):
|
| 59 |
+
self._log(VERBOSE, msg, args, **kwargs)
|
| 60 |
+
|
| 61 |
+
def result(self, msg, *args, **kwargs):
|
| 62 |
+
"""Log at custom RESULT level"""
|
| 63 |
+
if self.isEnabledFor(RESULT):
|
| 64 |
+
self._log(RESULT, msg, args, **kwargs)
|
| 65 |
+
|
| 66 |
+
# logging.Logger.result = custom_logging
|
| 67 |
+
|
| 68 |
+
def setup_logger(name='colored_logger', level="INFO"):
|
| 69 |
+
"""Set up and return a colored logger instance"""
|
| 70 |
+
|
| 71 |
+
# Register our custom logger class
|
| 72 |
+
logging.setLoggerClass(ColoredLogger)
|
| 73 |
+
|
| 74 |
+
# Create logger
|
| 75 |
+
logger = logging.getLogger(name)
|
| 76 |
+
|
| 77 |
+
# Set logger level
|
| 78 |
+
try:
|
| 79 |
+
logger.setLevel(getattr(logging, level.upper()))
|
| 80 |
+
except AttributeError:
|
| 81 |
+
logger.setLevel(logging.INFO) # Default to INFO if invalid level
|
| 82 |
+
|
| 83 |
+
# Create console handler
|
| 84 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 85 |
+
console_handler.setLevel(level)
|
| 86 |
+
|
| 87 |
+
# Create formatter
|
| 88 |
+
formatter = ColoredFormatter(
|
| 89 |
+
# fmt='%(asctime)s - %(levelname)s - %(message)s',
|
| 90 |
+
# datefmt='%Y-%m-%d %H:%M:%S'
|
| 91 |
+
fmt='%(levelname)8s - %(message)s'
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Add formatter to handler
|
| 95 |
+
console_handler.setFormatter(formatter)
|
| 96 |
+
|
| 97 |
+
# Add handler to logger
|
| 98 |
+
logger.addHandler(console_handler)
|
| 99 |
+
|
| 100 |
+
return logger
|
| 101 |
+
|
| 102 |
+
# Example usage
|
| 103 |
+
if __name__ == '__main__':
|
| 104 |
+
logger = setup_logger()
|
| 105 |
+
|
| 106 |
+
# Test all log levels including new VERBOSE level
|
| 107 |
+
logger.debug("This is a debug message")
|
| 108 |
+
logger.verbose("This is a verbose message") # New verbose level
|
| 109 |
+
logger.info("This is an info message")
|
| 110 |
+
logger.warning("This is a warning message")
|
| 111 |
+
logger.error("This is an error message")
|
| 112 |
+
logger.critical("This is a critical message")
|
build/lib/pycek_public/statistics_lab.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class stats_lab(cek.cek_labs):
|
| 5 |
+
def setup_lab(self):
|
| 6 |
+
"""
|
| 7 |
+
Define base information for the lab
|
| 8 |
+
"""
|
| 9 |
+
self.add_metadata(
|
| 10 |
+
laboratory = 'Basic Statistics Lab',
|
| 11 |
+
columns = ["X","Y"]
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
self.available_samples = [
|
| 15 |
+
'Averages',
|
| 16 |
+
'Propagation of uncertainty',
|
| 17 |
+
'Comparison of averages',
|
| 18 |
+
'Linear fit',
|
| 19 |
+
'Non linear fit',
|
| 20 |
+
'Detection of outliers',
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
self.sample_parameters['Averages'] = {
|
| 24 |
+
"gen_values" : [
|
| 25 |
+
(1.0, 0.1),
|
| 26 |
+
(12., 2.0)
|
| 27 |
+
],
|
| 28 |
+
'exp_values' : (1.0,9.0),
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
self.sample_parameters['Propagation of uncertainty'] = {
|
| 32 |
+
"gen_values" : [
|
| 33 |
+
(15.0, 1.0),
|
| 34 |
+
(133., 2.0)
|
| 35 |
+
],
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
self.sample_parameters['Comparison of averages'] = {
|
| 39 |
+
"gen_values" : [
|
| 40 |
+
(15.0, 1.0),
|
| 41 |
+
(13.2, 2.0)
|
| 42 |
+
],
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
self.sample_parameters['Linear fit'] = {
|
| 46 |
+
"function" : lambda x,m,q: m*x + q,
|
| 47 |
+
"gen_values" : {'m':12.3 , 'q':1.0},
|
| 48 |
+
"xrange" : [0.0 , 10.0]
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
self.sample_parameters['Non linear fit'] = {
|
| 52 |
+
"nval" : 10,
|
| 53 |
+
"function" : lambda x,E0,K0,Kp,V0: E0 + K0 * x / Kp * ( (V0/x)**Kp / (Kp-1)+1) - K0*V0/(Kp-1),
|
| 54 |
+
"gen_values" : {"E0":-634.2, "K0":12.43, "Kp":4.28, "V0":99.11},
|
| 55 |
+
"xrange" : [50 , 140]
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
self.sample_parameters['Detection of outliers'] = {
|
| 59 |
+
"function" : lambda x,m,q: m*x + q,
|
| 60 |
+
"gen_values" : {'m':2.3 , 'q':0.1},
|
| 61 |
+
"xrange" : [10.0 , 20.0],
|
| 62 |
+
"shift" : 2,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
def create_data(self):
|
| 66 |
+
"""
|
| 67 |
+
Generate the data
|
| 68 |
+
"""
|
| 69 |
+
if self.sample is None:
|
| 70 |
+
raise Exception("Sample not defined")
|
| 71 |
+
|
| 72 |
+
prm = self.sample_parameters[ self.sample ]
|
| 73 |
+
|
| 74 |
+
if self.user_specified_file is None:
|
| 75 |
+
output_file = self.filename_gen.random
|
| 76 |
+
else:
|
| 77 |
+
output_file = self.output_file
|
| 78 |
+
|
| 79 |
+
self.set_parameters(
|
| 80 |
+
number_of_values = self.number_of_values,
|
| 81 |
+
output_file = output_file,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
if "noise" in prm:
|
| 85 |
+
self.set_parameters( noise_level = prm["noise"] )
|
| 86 |
+
|
| 87 |
+
self.add_metadata(
|
| 88 |
+
number_of_values = self.number_of_values,
|
| 89 |
+
sample = self.sample,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
if self.sample in ["Averages", 'Propagation of uncertainty', 'Comparison of averages']:
|
| 93 |
+
data = self._generate_normal_random(self.number_of_values, prm['gen_values'])
|
| 94 |
+
|
| 95 |
+
elif self.sample in ["Linear fit"]:
|
| 96 |
+
data = self.generate_data_from_function(
|
| 97 |
+
prm["function"],
|
| 98 |
+
prm['gen_values'],
|
| 99 |
+
self.number_of_values,
|
| 100 |
+
prm['xrange'],
|
| 101 |
+
noise_level = self.noise_level,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
elif self.sample in ["Non linear fit"]:
|
| 105 |
+
data = self.generate_data_from_function(
|
| 106 |
+
prm["function"],
|
| 107 |
+
prm['gen_values'],
|
| 108 |
+
self.number_of_values,
|
| 109 |
+
prm['xrange'],
|
| 110 |
+
noise_level = self.noise_level,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
elif self.sample in ["Detection of outliers"]:
|
| 114 |
+
data = self.generate_data_from_function(
|
| 115 |
+
prm["function"],
|
| 116 |
+
prm['gen_values'],
|
| 117 |
+
self.number_of_values,
|
| 118 |
+
prm['xrange'],
|
| 119 |
+
noise_level = self.noise_level,
|
| 120 |
+
)
|
| 121 |
+
i = np.random.randint(self.number_of_values)
|
| 122 |
+
data[i,1] += prm['shift']
|
| 123 |
+
|
| 124 |
+
return data
|
| 125 |
+
|
build/lib/pycek_public/surface_adsorption.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class surface_adsorption(cek.cek_labs):
|
| 5 |
+
def setup_lab(self):
|
| 6 |
+
"""
|
| 7 |
+
Define base information for the lab.
|
| 8 |
+
They can be overwrite by the user using the kwargs in the constructor or
|
| 9 |
+
by calling the set_parameters method.
|
| 10 |
+
"""
|
| 11 |
+
self.add_metadata(
|
| 12 |
+
laboratory = 'Surface Adsorption Lab',
|
| 13 |
+
columns = ["Dye added (mg)", "Dye in solution (mol/L)"]
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
self.volume = 1 # L
|
| 17 |
+
self.minDye = 500 # mg
|
| 18 |
+
self.maxDye = 10000 # mg
|
| 19 |
+
|
| 20 |
+
self.sample_parameters = {
|
| 21 |
+
"dH" : -19.51e3, # J/mol
|
| 22 |
+
"dS" : -10, # J/mol/K
|
| 23 |
+
"Q" : 0.0001, # monolayer coverage (mol/m^2)
|
| 24 |
+
"molarMass": 584.910641, # g/mol
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
self.number_of_values = 100
|
| 28 |
+
self.noise_level = 1e-6
|
| 29 |
+
self.precision = 10
|
| 30 |
+
|
| 31 |
+
def create_data(self):
|
| 32 |
+
"""
|
| 33 |
+
Generate the data
|
| 34 |
+
"""
|
| 35 |
+
if self.user_specified_file is None:
|
| 36 |
+
output_file = self.filename_gen.random
|
| 37 |
+
else:
|
| 38 |
+
output_file = self.output_file
|
| 39 |
+
|
| 40 |
+
self.set_parameters(
|
| 41 |
+
sample = self.sample,
|
| 42 |
+
number_of_values = self.number_of_values,
|
| 43 |
+
output_file = output_file,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
self.add_metadata(**{
|
| 47 |
+
"Temperature (K)" : self.temperature,
|
| 48 |
+
"Volume (L)" : self.volume,
|
| 49 |
+
"Molar mass (g/mol)" : self.sample_parameters["molarMass"],
|
| 50 |
+
"MinDye (mg)" : self.minDye,
|
| 51 |
+
"MaxDye (mg)" : self.maxDye,
|
| 52 |
+
'Number of values' : self.number_of_values,
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
# Langmuir isotherm equilibrium constant
|
| 56 |
+
# Convert to kJ/mol
|
| 57 |
+
lnK = (-self.sample_parameters["dH"] / (self.temperature) + self.sample_parameters["dS"]) / self.R
|
| 58 |
+
K = np.exp(lnK) # in L/mol
|
| 59 |
+
|
| 60 |
+
conversion_factor = 1000 * self.sample_parameters["molarMass"] * self.volume
|
| 61 |
+
conc_range = np.array([self.minDye, self.maxDye]) / conversion_factor
|
| 62 |
+
|
| 63 |
+
data_array = self.generate_data_from_function(
|
| 64 |
+
lambda x,K,Q: ((x*K - K*Q - 1) + np.sqrt((x*K - K*Q - 1)**2 + 4*x*K) ) / (2*K) ,
|
| 65 |
+
{"K":K , "Q":self.sample_parameters["Q"]},
|
| 66 |
+
self.number_of_values,
|
| 67 |
+
xrange = conc_range,
|
| 68 |
+
xspacing = 'linear',
|
| 69 |
+
noise_level = self.noise_level,
|
| 70 |
+
positive = True,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
data_array[:,0] *= conversion_factor
|
| 74 |
+
# grams of dye added
|
| 75 |
+
# x = np.linspace(self.minDye, self.maxDye, self.number_of_values) / 1000
|
| 76 |
+
# moles = x / self.params["molarMass"] # g
|
| 77 |
+
# initial_concentration = moles / self.volume # mol/L
|
| 78 |
+
# y = self.measure(K, self.params["Q"], initial_concentration)
|
| 79 |
+
|
| 80 |
+
# # Because noise is added to the concentration in solution, but
|
| 81 |
+
# # the concentration on the surface is required in post-processing, which
|
| 82 |
+
# # is very small, we divide by 1000
|
| 83 |
+
|
| 84 |
+
# y = cek.add_noise(y, self.uncertainty/1000)
|
| 85 |
+
|
| 86 |
+
# data_array = np.column_stack((x, y))
|
| 87 |
+
|
| 88 |
+
return data_array
|
| 89 |
+
|
deployment/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12
|
| 2 |
+
COPY --from=ghcr.io/astral-sh/uv:0.4.20 /uv /bin/uv
|
| 3 |
+
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
ENV UV_SYSTEM_PYTHON=1
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
COPY --chown=user ./deployment/requirements.txt requirements.txt
|
| 11 |
+
RUN uv pip install -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy and install the local repo, then remove it
|
| 14 |
+
COPY --chown=user ./ /pycek_public
|
| 15 |
+
RUN uv pip install /pycek_public && rm -rf /pycek
|
| 16 |
+
|
| 17 |
+
COPY --chown=user ./marimo /app
|
| 18 |
+
RUN chown -R user:user /app
|
| 19 |
+
|
| 20 |
+
USER user
|
| 21 |
+
|
| 22 |
+
CMD ["python", "app.py"]
|
deployment/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**Will need to be run in this directory**
|
| 2 |
+
|
| 3 |
+
Run `./build.sh` which will build the docker file as `cek-marimo`
|
| 4 |
+
|
| 5 |
+
The container may then be run as e.g.
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
docker run -p 8000:8000 cek-marimo
|
| 9 |
+
```
|
deployment/build.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
|
| 3 |
+
docker build -t cek-marimo -f Dockerfile ..
|
deployment/requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
colorama
|
| 3 |
+
marimo
|
marimo/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (1.41 kB). View file
|
|
|
marimo/__pycache__/bomb_calorimetry.cpython-311.pyc
ADDED
|
Binary file (6.4 kB). View file
|
|
|
marimo/__pycache__/index.cpython-311.pyc
ADDED
|
Binary file (893 Bytes). View file
|
|
|
marimo/app.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, Callable, Coroutine
|
| 2 |
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 3 |
+
import marimo
|
| 4 |
+
from fastapi import FastAPI, Form, Request, Response
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# Create a marimo asgi app
|
| 8 |
+
server = (
|
| 9 |
+
marimo.create_asgi_app()
|
| 10 |
+
.with_app(path="", root="./index.py")
|
| 11 |
+
.with_app(path="/bc", root="./bomb_calorimetry.py")
|
| 12 |
+
.with_app(path="/cv", root="./crystal_violet.py")
|
| 13 |
+
.with_app(path="/stats", root="./statistics_lab.py")
|
| 14 |
+
.with_app(path="/surface", root="./surface_adsorption.py")
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# Create a FastAPI app
|
| 18 |
+
app = FastAPI()
|
| 19 |
+
|
| 20 |
+
app.mount("/", server.build())
|
| 21 |
+
|
| 22 |
+
# Run the server
|
| 23 |
+
if __name__ == "__main__":
|
| 24 |
+
import uvicorn
|
| 25 |
+
|
| 26 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
marimo/bomb_calorimetry.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import marimo
|
| 2 |
+
|
| 3 |
+
__generated_with = "0.11.0"
|
| 4 |
+
app = marimo.App(width="medium")
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@app.cell
|
| 8 |
+
def _():
|
| 9 |
+
import marimo as mo
|
| 10 |
+
import pycek_public as cek
|
| 11 |
+
lab = cek.bomb_calorimetry(make_plots=True)
|
| 12 |
+
return cek, lab, mo
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.cell
|
| 16 |
+
def _(mo):
|
| 17 |
+
mo.md(
|
| 18 |
+
"""
|
| 19 |
+
#Bomb calorimetry lab
|
| 20 |
+
|
| 21 |
+
This notebook mimics a bomb calorimetry laboratory experiment.
|
| 22 |
+
In each experiment a tablet of reactant is prepared and combusted in the calorimeter.
|
| 23 |
+
During the experiment the temperature inside the calorimeter is monitored.
|
| 24 |
+
|
| 25 |
+
## Obejectives
|
| 26 |
+
1. Calibration of the calorimeter
|
| 27 |
+
2. Calculation of enthalpy of combustion of sucrose
|
| 28 |
+
3. Calculation of enthalpy of combustion of napthalene
|
| 29 |
+
|
| 30 |
+
## Instructions
|
| 31 |
+
1. Type your student ID
|
| 32 |
+
2. Select a sample
|
| 33 |
+
3. Click "Run Experiment"
|
| 34 |
+
4. Perform at least 4 experiments per sample
|
| 35 |
+
___
|
| 36 |
+
"""
|
| 37 |
+
)
|
| 38 |
+
return
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@app.cell
|
| 42 |
+
def _(lab, mo):
|
| 43 |
+
def set_ID(value):
|
| 44 |
+
try:
|
| 45 |
+
student_number = int(value)
|
| 46 |
+
if student_number <= 0:
|
| 47 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 48 |
+
else:
|
| 49 |
+
print(f"Valid Student ID: {student_number}")
|
| 50 |
+
lab.set_student_ID(int(value))
|
| 51 |
+
except ValueError:
|
| 52 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 53 |
+
|
| 54 |
+
student_ID = mo.ui.text(value="", label="Student ID:",on_change=set_ID)
|
| 55 |
+
|
| 56 |
+
def set_fname(value):
|
| 57 |
+
lab._set_filename(value)
|
| 58 |
+
|
| 59 |
+
exp_ID = mo.ui.text(value="Automatic", label="Output file:",
|
| 60 |
+
on_change=set_fname)
|
| 61 |
+
|
| 62 |
+
sample_selector = mo.ui.dropdown(
|
| 63 |
+
options=lab.available_samples, value=None, label="Select sample:"
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
run_button = mo.ui.run_button(label="Run Experiment")
|
| 67 |
+
reset_button = mo.ui.run_button(label="Reset Counter")
|
| 68 |
+
|
| 69 |
+
# Create download button using marimo's download function
|
| 70 |
+
|
| 71 |
+
mo.vstack([student_ID, exp_ID, sample_selector, run_button, reset_button])
|
| 72 |
+
return (
|
| 73 |
+
exp_ID,
|
| 74 |
+
reset_button,
|
| 75 |
+
run_button,
|
| 76 |
+
sample_selector,
|
| 77 |
+
set_ID,
|
| 78 |
+
set_fname,
|
| 79 |
+
student_ID,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@app.cell
|
| 84 |
+
def _(cek, lab, mo, reset_button, run_button, sample_selector, student_ID):
|
| 85 |
+
if reset_button.value:
|
| 86 |
+
lab.ID = 0
|
| 87 |
+
lab._set_filename(None)
|
| 88 |
+
|
| 89 |
+
image = ""
|
| 90 |
+
message = ""
|
| 91 |
+
download_button = ""
|
| 92 |
+
if run_button.value:
|
| 93 |
+
mo.stop(not student_ID.value.isdigit(), mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 94 |
+
mo.stop(sample_selector.value is None, mo.md(f"### No sample selected !!"))
|
| 95 |
+
|
| 96 |
+
lab.set_parameters(sample=sample_selector.value,precision=0.01)
|
| 97 |
+
_ = lab.create_data()
|
| 98 |
+
fname = lab.write_data_to_file()
|
| 99 |
+
|
| 100 |
+
with open(fname, "r") as f:
|
| 101 |
+
file_content = f.read()
|
| 102 |
+
message = f"### Running Experiment\n"
|
| 103 |
+
for k,v in lab.metadata.items():
|
| 104 |
+
message += f"####{k} = {v}\n"
|
| 105 |
+
message += f"#### File created = {fname}\n"
|
| 106 |
+
|
| 107 |
+
download_button = mo.download(
|
| 108 |
+
file_content,
|
| 109 |
+
filename=fname,
|
| 110 |
+
label=f"Download {fname}",
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
plot = cek.plotting()
|
| 114 |
+
data,_,_ = lab.read_data_file(fname)
|
| 115 |
+
plot.quick_plot(data,output=fname.replace(".csv",".png"))
|
| 116 |
+
image = mo.image(fname.replace(".csv",".png"),width=500)
|
| 117 |
+
|
| 118 |
+
mo.vstack([mo.md(message),download_button,image])
|
| 119 |
+
return (
|
| 120 |
+
data,
|
| 121 |
+
download_button,
|
| 122 |
+
f,
|
| 123 |
+
file_content,
|
| 124 |
+
fname,
|
| 125 |
+
image,
|
| 126 |
+
k,
|
| 127 |
+
message,
|
| 128 |
+
plot,
|
| 129 |
+
v,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@app.cell
|
| 134 |
+
def _():
|
| 135 |
+
import numpy as np
|
| 136 |
+
return (np,)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
app.run()
|
marimo/crystal_violet.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import marimo
|
| 2 |
+
|
| 3 |
+
__generated_with = "0.11.0"
|
| 4 |
+
app = marimo.App(width="medium")
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@app.cell
|
| 8 |
+
def _():
|
| 9 |
+
import marimo as mo
|
| 10 |
+
import pycek_public as cek
|
| 11 |
+
lab = cek.crystal_violet(make_plots=True)
|
| 12 |
+
return cek, lab, mo
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.cell
|
| 16 |
+
def _(mo):
|
| 17 |
+
mo.md(
|
| 18 |
+
"""
|
| 19 |
+
# Crystdal Violet Lab
|
| 20 |
+
|
| 21 |
+
This notebook mimics a kinetics laboratory experiment, where a UV-Vis spectrophotometer is used to measure the absorbance as the reaction between crystal violet and hydroxide proceeds.
|
| 22 |
+
The absorbance versus time data can then be used to determine the rate of the reaction with respect to both crystal violet and hydroxide ions.
|
| 23 |
+
|
| 24 |
+
## Objectives
|
| 25 |
+
1. Determine the reaction order with respect to CV
|
| 26 |
+
2. Determine the reaction order with respect to hydroxide
|
| 27 |
+
3. Determine the rate constant for the overall reaction
|
| 28 |
+
4. Determine the activation energy
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
## Instructions
|
| 32 |
+
1. Type your student ID
|
| 33 |
+
2. Select the volumes of the CV solution, the hydroxide solution and DI water to use
|
| 34 |
+
3. Select the temperature of the experiment
|
| 35 |
+
4. Click "Run Experiment"
|
| 36 |
+
5. Perform two sets of at least three experiments each:
|
| 37 |
+
- constant [CV] while the [OH$^-$] is varied
|
| 38 |
+
- constant [OH$^-$] while the [CV] is varied
|
| 39 |
+
6. Obtain another set of data where the temperature is changed and compute the activation energy and pre-exponential factor
|
| 40 |
+
___
|
| 41 |
+
"""
|
| 42 |
+
)
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@app.cell
|
| 47 |
+
def _(lab, mo):
|
| 48 |
+
def set_ID(value):
|
| 49 |
+
try:
|
| 50 |
+
student_number = int(value)
|
| 51 |
+
if student_number <= 0:
|
| 52 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 53 |
+
else:
|
| 54 |
+
print(f"Valid Student ID: {student_number}")
|
| 55 |
+
lab.set_student_ID(int(value))
|
| 56 |
+
except ValueError:
|
| 57 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 58 |
+
|
| 59 |
+
student_ID = mo.ui.text(value="", label="Student ID:",on_change=set_ID)
|
| 60 |
+
|
| 61 |
+
def set_fname(value):
|
| 62 |
+
lab._set_filename(value)
|
| 63 |
+
exp_ID = mo.ui.text(value="Automatic", label="Output file:", on_change=set_fname)
|
| 64 |
+
|
| 65 |
+
cv_volume = mo.ui.number(start=0,stop=100,step=1,value=None,label="Volume of CV solution (mL)")
|
| 66 |
+
oh_volume = mo.ui.number(start=0,stop=100,step=1,value=None,label="Volume of OH solution (mL)")
|
| 67 |
+
h2o_volume = mo.ui.number(start=0,stop=100,step=1,value=None,label="Volume of DI water (mL)")
|
| 68 |
+
temperature = mo.ui.number(start=0,stop=100,step=1,value=25,label="Temperature (C)")
|
| 69 |
+
run_button = mo.ui.run_button(label="Run Experiment")
|
| 70 |
+
reset_button = mo.ui.run_button(label="Reset Counter")
|
| 71 |
+
|
| 72 |
+
# Create download button using marimo's download function
|
| 73 |
+
|
| 74 |
+
mo.vstack([student_ID,
|
| 75 |
+
exp_ID,
|
| 76 |
+
cv_volume,
|
| 77 |
+
oh_volume,
|
| 78 |
+
h2o_volume,
|
| 79 |
+
temperature,
|
| 80 |
+
run_button,
|
| 81 |
+
reset_button])
|
| 82 |
+
return (
|
| 83 |
+
cv_volume,
|
| 84 |
+
exp_ID,
|
| 85 |
+
h2o_volume,
|
| 86 |
+
oh_volume,
|
| 87 |
+
reset_button,
|
| 88 |
+
run_button,
|
| 89 |
+
set_ID,
|
| 90 |
+
set_fname,
|
| 91 |
+
student_ID,
|
| 92 |
+
temperature,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@app.cell
|
| 97 |
+
def _(
|
| 98 |
+
cek,
|
| 99 |
+
cv_volume,
|
| 100 |
+
h2o_volume,
|
| 101 |
+
lab,
|
| 102 |
+
mo,
|
| 103 |
+
oh_volume,
|
| 104 |
+
reset_button,
|
| 105 |
+
run_button,
|
| 106 |
+
student_ID,
|
| 107 |
+
temperature,
|
| 108 |
+
):
|
| 109 |
+
if reset_button.value:
|
| 110 |
+
lab.ID = 0
|
| 111 |
+
lab._set_filename(None)
|
| 112 |
+
|
| 113 |
+
image = ""
|
| 114 |
+
message = ""
|
| 115 |
+
download_button = ""
|
| 116 |
+
if run_button.value:
|
| 117 |
+
mo.stop(not student_ID.value.isdigit(), mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 118 |
+
|
| 119 |
+
cv_vol = cv_volume.value
|
| 120 |
+
oh_vol = oh_volume.value
|
| 121 |
+
h2o_vol = h2o_volume.value
|
| 122 |
+
|
| 123 |
+
lab.set_parameters(
|
| 124 |
+
volumes={'cv': cv_vol, 'oh': oh_vol, 'h2o': h2o_vol},
|
| 125 |
+
temperature=temperature.value+273.15
|
| 126 |
+
)
|
| 127 |
+
_ = lab.create_data()
|
| 128 |
+
fname = lab.write_data_to_file()
|
| 129 |
+
|
| 130 |
+
with open(fname, "r") as f:
|
| 131 |
+
file_content = f.read()
|
| 132 |
+
message = f"### Running Experiment\n"
|
| 133 |
+
for k,v in lab.metadata.items():
|
| 134 |
+
message += f"####{k} = {v}\n"
|
| 135 |
+
message += f"#### File created = {fname}\n"
|
| 136 |
+
|
| 137 |
+
download_button = mo.download(
|
| 138 |
+
file_content,
|
| 139 |
+
filename=fname,
|
| 140 |
+
label=f"Download {fname}",
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
plot = cek.plotting()
|
| 144 |
+
data,_,_ = lab.read_data_file(fname)
|
| 145 |
+
plot.quick_plot(data,output=fname.replace(".csv",".png"))
|
| 146 |
+
image = mo.image(fname.replace(".csv",".png"),width=500)
|
| 147 |
+
|
| 148 |
+
mo.vstack([mo.md(message),download_button,image])
|
| 149 |
+
return (
|
| 150 |
+
cv_vol,
|
| 151 |
+
data,
|
| 152 |
+
download_button,
|
| 153 |
+
f,
|
| 154 |
+
file_content,
|
| 155 |
+
fname,
|
| 156 |
+
h2o_vol,
|
| 157 |
+
image,
|
| 158 |
+
k,
|
| 159 |
+
message,
|
| 160 |
+
oh_vol,
|
| 161 |
+
plot,
|
| 162 |
+
v,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
app.run()
|
marimo/index.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import marimo
|
| 2 |
+
|
| 3 |
+
__generated_with = "0.11.0"
|
| 4 |
+
app = marimo.App(width="medium")
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@app.cell
|
| 8 |
+
def _():
|
| 9 |
+
import marimo as mo
|
| 10 |
+
|
| 11 |
+
@app.cell
|
| 12 |
+
def _(mo):
|
| 13 |
+
mo.md(
|
| 14 |
+
"""
|
| 15 |
+
#Labs
|
| 16 |
+
|
| 17 |
+
1. [Bomb Calorimetry](/bc)
|
| 18 |
+
2. [Crystal Violet](/cv)
|
| 19 |
+
3. [Statistics](/stats)
|
| 20 |
+
4. [Surface Adsorption](/surface)
|
| 21 |
+
"""
|
| 22 |
+
)
|
| 23 |
+
return
|
marimo/statistics_lab.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import marimo
|
| 2 |
+
|
| 3 |
+
__generated_with = "0.11.0"
|
| 4 |
+
app = marimo.App(width="medium")
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@app.cell
|
| 8 |
+
def _():
|
| 9 |
+
import marimo as mo
|
| 10 |
+
import pycek_public as cek
|
| 11 |
+
lab = cek.stats_lab(make_plots=True)
|
| 12 |
+
return cek, lab, mo
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.cell
|
| 16 |
+
def _(mo):
|
| 17 |
+
mo.md(
|
| 18 |
+
"""
|
| 19 |
+
# Statistics lab
|
| 20 |
+
This numerical lab consists a few small tasks, which cover the key statistics topics that were introduced in the previous chapter.
|
| 21 |
+
They are also preparatory for the following labs, where you would have to use the same concepts in more complicated situations.
|
| 22 |
+
In particular, if you are using python, it would be beneficial to solve some of this exercises by creating specific functions that can the be reused (maybe with small modifications) in the following labs.
|
| 23 |
+
|
| 24 |
+
## Tasks
|
| 25 |
+
1. Average and standard error
|
| 26 |
+
2. Propagation of uncertainty
|
| 27 |
+
3. Comparison of averages
|
| 28 |
+
4. Linear Fit
|
| 29 |
+
5. Non linear fit
|
| 30 |
+
6. Outlier detection
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
## Instructions
|
| 34 |
+
1. Type your student ID
|
| 35 |
+
2. Select a task
|
| 36 |
+
3. Click "Run Experiment"
|
| 37 |
+
4. Analysed the data
|
| 38 |
+
___
|
| 39 |
+
"""
|
| 40 |
+
)
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@app.cell
|
| 45 |
+
def _(lab, mo):
|
| 46 |
+
def set_ID(value):
|
| 47 |
+
try:
|
| 48 |
+
student_number = int(value)
|
| 49 |
+
if student_number <= 0:
|
| 50 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 51 |
+
else:
|
| 52 |
+
print(f"Valid Student ID: {student_number}")
|
| 53 |
+
lab.set_student_ID(int(value))
|
| 54 |
+
except ValueError:
|
| 55 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 56 |
+
|
| 57 |
+
student_ID = mo.ui.text(value="", label="Student ID:",on_change=set_ID)
|
| 58 |
+
|
| 59 |
+
def set_fname(value):
|
| 60 |
+
lab._set_filename(value)
|
| 61 |
+
|
| 62 |
+
exp_ID = mo.ui.text(value="Automatic", label="Output file:",
|
| 63 |
+
on_change=set_fname)
|
| 64 |
+
|
| 65 |
+
sample_selector = mo.ui.dropdown(
|
| 66 |
+
options=lab.available_samples, value=None, label="Select task:"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
run_button = mo.ui.run_button(label="Run Experiment")
|
| 70 |
+
reset_button = mo.ui.run_button(label="Reset Counter")
|
| 71 |
+
|
| 72 |
+
# Create download button using marimo's download function
|
| 73 |
+
|
| 74 |
+
mo.vstack([student_ID, exp_ID, sample_selector, run_button, reset_button])
|
| 75 |
+
return (
|
| 76 |
+
exp_ID,
|
| 77 |
+
reset_button,
|
| 78 |
+
run_button,
|
| 79 |
+
sample_selector,
|
| 80 |
+
set_ID,
|
| 81 |
+
set_fname,
|
| 82 |
+
student_ID,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@app.cell
|
| 87 |
+
def _(cek, lab, mo, reset_button, run_button, sample_selector, student_ID):
|
| 88 |
+
if reset_button.value:
|
| 89 |
+
lab.ID = 0
|
| 90 |
+
lab._set_filename(None)
|
| 91 |
+
|
| 92 |
+
image = ""
|
| 93 |
+
message = ""
|
| 94 |
+
download_button = ""
|
| 95 |
+
if run_button.value:
|
| 96 |
+
mo.stop(not student_ID.value.isdigit(), mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 97 |
+
mo.stop(sample_selector.value is None, mo.md(f"### No sample selected !!"))
|
| 98 |
+
|
| 99 |
+
lab.set_parameters(
|
| 100 |
+
number_of_values = 12,
|
| 101 |
+
sample=sample_selector.value
|
| 102 |
+
)
|
| 103 |
+
_ = lab.create_data()
|
| 104 |
+
fname = lab.write_data_to_file()
|
| 105 |
+
|
| 106 |
+
with open(fname, "r") as f:
|
| 107 |
+
file_content = f.read()
|
| 108 |
+
message = f"### Running Experiment\n"
|
| 109 |
+
for k,v in lab.metadata.items():
|
| 110 |
+
message += f"####{k} = {v}\n"
|
| 111 |
+
message += f"#### File created = {fname}\n"
|
| 112 |
+
|
| 113 |
+
download_button = mo.download(
|
| 114 |
+
file_content,
|
| 115 |
+
filename=fname,
|
| 116 |
+
label=f"Download {fname}",
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
plot = cek.plotting()
|
| 120 |
+
data,_,_ = lab.read_data_file(fname)
|
| 121 |
+
plot.quick_plot(data,output=fname.replace(".csv",".png"))
|
| 122 |
+
image = mo.image(fname.replace(".csv",".png"),width=500)
|
| 123 |
+
|
| 124 |
+
mo.vstack([mo.md(message),download_button,image])
|
| 125 |
+
return (
|
| 126 |
+
data,
|
| 127 |
+
download_button,
|
| 128 |
+
f,
|
| 129 |
+
file_content,
|
| 130 |
+
fname,
|
| 131 |
+
image,
|
| 132 |
+
k,
|
| 133 |
+
message,
|
| 134 |
+
plot,
|
| 135 |
+
v,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
app.run()
|
marimo/surface_adsorption.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import marimo
|
| 2 |
+
|
| 3 |
+
__generated_with = "0.11.0"
|
| 4 |
+
app = marimo.App(width="medium")
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@app.cell
|
| 8 |
+
def _():
|
| 9 |
+
import marimo as mo
|
| 10 |
+
import pycek_public as cek
|
| 11 |
+
lab = cek.cek.surface_adsorption(make_plots=True)
|
| 12 |
+
return cek, lab, mo
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.cell
|
| 16 |
+
def _(mo):
|
| 17 |
+
mo.md(
|
| 18 |
+
"""
|
| 19 |
+
# Surface Adsorption Lab
|
| 20 |
+
|
| 21 |
+
In the virtual laboratory below, we will be looking at the adsorption of the dye Acid Blue 158 on chitin in water.
|
| 22 |
+
The simulated experiments mimic different conditions and will be used to determine the enthalpy of adsorption of the dye on the substrate.
|
| 23 |
+
The output file contains the concentration of the dye left in solution, as a function of the amount that was added to the beaker with the kitin powder.
|
| 24 |
+
|
| 25 |
+
## Objectives
|
| 26 |
+
1. Calculation of the Langmuir constant ($K_L$) and the monolayer coverage ($Q$) at different temperatures
|
| 27 |
+
2. Compare the fitted values obtained from fitting both forms of the Langmuir isotherm (linear and non-linear)
|
| 28 |
+
3. alculation of the adsorption enthalpy
|
| 29 |
+
4. Comparison with the provided experimental value
|
| 30 |
+
|
| 31 |
+
## Instructions
|
| 32 |
+
1. Type your student ID
|
| 33 |
+
2. Select the volumes of the CV solution, the hydroxide solution and DI water to use
|
| 34 |
+
3. Select the temperature of the experiment
|
| 35 |
+
4. Click "Run Experiment"
|
| 36 |
+
5. Perform at least 5 experiments at different temperatures
|
| 37 |
+
___
|
| 38 |
+
"""
|
| 39 |
+
)
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@app.cell
|
| 44 |
+
def _(lab, mo):
|
| 45 |
+
def set_ID(value):
|
| 46 |
+
try:
|
| 47 |
+
student_number = int(value)
|
| 48 |
+
if student_number <= 0:
|
| 49 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 50 |
+
else:
|
| 51 |
+
print(f"Valid Student ID: {student_number}")
|
| 52 |
+
lab.set_student_ID(int(value))
|
| 53 |
+
except ValueError:
|
| 54 |
+
print(mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 55 |
+
|
| 56 |
+
student_ID = mo.ui.text(value="", label="Student ID:",on_change=set_ID)
|
| 57 |
+
|
| 58 |
+
def set_fname(value):
|
| 59 |
+
lab._set_filename(value)
|
| 60 |
+
exp_ID = mo.ui.text(value="Automatic", label="Output file:", on_change=set_fname)
|
| 61 |
+
|
| 62 |
+
temperature = mo.ui.number(start=0,stop=100,step=1,value=25,label="Temperature (C)")
|
| 63 |
+
|
| 64 |
+
run_button = mo.ui.run_button(label="Run Experiment")
|
| 65 |
+
reset_button = mo.ui.run_button(label="Reset Counter")
|
| 66 |
+
|
| 67 |
+
# Create download button using marimo's download function
|
| 68 |
+
|
| 69 |
+
mo.vstack([student_ID,
|
| 70 |
+
exp_ID,
|
| 71 |
+
temperature,
|
| 72 |
+
run_button,
|
| 73 |
+
reset_button])
|
| 74 |
+
return (
|
| 75 |
+
exp_ID,
|
| 76 |
+
reset_button,
|
| 77 |
+
run_button,
|
| 78 |
+
set_ID,
|
| 79 |
+
set_fname,
|
| 80 |
+
student_ID,
|
| 81 |
+
temperature,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@app.cell
|
| 86 |
+
def _(cek, lab, mo, reset_button, run_button, student_ID, temperature):
|
| 87 |
+
if reset_button.value:
|
| 88 |
+
lab.ID = 0
|
| 89 |
+
lab._set_filename(None)
|
| 90 |
+
|
| 91 |
+
image = ""
|
| 92 |
+
message = ""
|
| 93 |
+
download_button = ""
|
| 94 |
+
if run_button.value:
|
| 95 |
+
mo.stop(not student_ID.value.isdigit(), mo.md(f"### Invalid Student ID: {student_ID.value}"))
|
| 96 |
+
|
| 97 |
+
lab.set_parameters(
|
| 98 |
+
temperature = temperature.value+273.15
|
| 99 |
+
)
|
| 100 |
+
_ = lab.create_data()
|
| 101 |
+
fname = lab.write_data_to_file()
|
| 102 |
+
|
| 103 |
+
with open(fname, "r") as f:
|
| 104 |
+
file_content = f.read()
|
| 105 |
+
message = f"### Running Experiment\n"
|
| 106 |
+
for k,v in lab.metadata.items():
|
| 107 |
+
message += f"####{k} = {v}\n"
|
| 108 |
+
message += f"#### File created = {fname}\n"
|
| 109 |
+
|
| 110 |
+
download_button = mo.download(
|
| 111 |
+
file_content,
|
| 112 |
+
filename=fname,
|
| 113 |
+
label=f"Download {fname}",
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
plot = cek.plotting()
|
| 117 |
+
data,_,_ = lab.read_data_file(fname)
|
| 118 |
+
plot.quick_plot(data,output=fname.replace(".csv",".png"))
|
| 119 |
+
image = mo.image(fname.replace(".csv",".png"),width=500)
|
| 120 |
+
|
| 121 |
+
mo.vstack([mo.md(message),download_button,image])
|
| 122 |
+
return (
|
| 123 |
+
data,
|
| 124 |
+
download_button,
|
| 125 |
+
f,
|
| 126 |
+
file_content,
|
| 127 |
+
fname,
|
| 128 |
+
image,
|
| 129 |
+
k,
|
| 130 |
+
message,
|
| 131 |
+
plot,
|
| 132 |
+
v,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
app.run()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools >= 61.0", "wheel", "numpy"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[tool.setuptools.packages.find]
|
| 6 |
+
where = ["src"]
|
| 7 |
+
|
| 8 |
+
[project]
|
| 9 |
+
name = "pycek"
|
| 10 |
+
version = "1.0.0"
|
| 11 |
+
requires-python = ">= 3.8"
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "Paolo Raiteri", email = "p.raiteri@curtin.edu.au"},
|
| 14 |
+
]
|
| 15 |
+
description = "read and write coordinates"
|
| 16 |
+
license = {file = "LICENSE"}
|
| 17 |
+
readme = {file = "README.md", content-type = "text/markdown"}
|
| 18 |
+
dependencies = [
|
| 19 |
+
"numpy",
|
| 20 |
+
"scipy",
|
| 21 |
+
"lmfit",
|
| 22 |
+
"matplotlib",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
[project.optional-dependencies]
|
| 26 |
+
dev = [
|
| 27 |
+
"black",
|
| 28 |
+
"mypy",
|
| 29 |
+
"pylint",
|
| 30 |
+
"alive-progress",
|
| 31 |
+
]
|
src/pycek_public.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.2
|
| 2 |
+
Name: pycek_public
|
| 3 |
+
Version: 1.0.0
|
| 4 |
+
Summary: read and write coordinates
|
| 5 |
+
Author-email: Paolo Raiteri <p.raiteri@curtin.edu.au>
|
| 6 |
+
License: MIT License
|
| 7 |
+
|
| 8 |
+
Copyright (c) 2025 Paolo Raiteri
|
| 9 |
+
|
| 10 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 11 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 12 |
+
in the Software without restriction, including without limitation the rights
|
| 13 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 14 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 15 |
+
furnished to do so, subject to the following conditions:
|
| 16 |
+
|
| 17 |
+
The above copyright notice and this permission notice shall be included in all
|
| 18 |
+
copies or substantial portions of the Software.
|
| 19 |
+
|
| 20 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 21 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 22 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 23 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 24 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 25 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 26 |
+
SOFTWARE.
|
| 27 |
+
|
| 28 |
+
Requires-Python: >=3.8
|
| 29 |
+
Description-Content-Type: text/markdown
|
| 30 |
+
License-File: LICENSE
|
| 31 |
+
Requires-Dist: numpy
|
| 32 |
+
Requires-Dist: scipy
|
| 33 |
+
Requires-Dist: lmfit
|
| 34 |
+
Requires-Dist: matplotlib
|
| 35 |
+
Provides-Extra: dev
|
| 36 |
+
Requires-Dist: black; extra == "dev"
|
| 37 |
+
Requires-Dist: mypy; extra == "dev"
|
| 38 |
+
Requires-Dist: pylint; extra == "dev"
|
| 39 |
+
Requires-Dist: alive-progress; extra == "dev"
|
| 40 |
+
|
| 41 |
+
# pycek
|
| 42 |
+
python package for CHEM2000
|
src/pycek_public.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LICENSE
|
| 2 |
+
README.md
|
| 3 |
+
pyproject.toml
|
| 4 |
+
src/pycek_public/__init__.py
|
| 5 |
+
src/pycek_public/bomb_calorimetry.py
|
| 6 |
+
src/pycek_public/cek_labs.py
|
| 7 |
+
src/pycek_public/crystal_violet.py
|
| 8 |
+
src/pycek_public/generate_random_filenames.py
|
| 9 |
+
src/pycek_public/logger.py
|
| 10 |
+
src/pycek_public/plotting.py
|
| 11 |
+
src/pycek_public/statistics_lab.py
|
| 12 |
+
src/pycek_public/surface_adsorption.py
|
| 13 |
+
src/pycek_public.egg-info/PKG-INFO
|
| 14 |
+
src/pycek_public.egg-info/SOURCES.txt
|
| 15 |
+
src/pycek_public.egg-info/dependency_links.txt
|
| 16 |
+
src/pycek_public.egg-info/requires.txt
|
| 17 |
+
src/pycek_public.egg-info/top_level.txt
|
src/pycek_public.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/pycek_public.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy
|
| 2 |
+
scipy
|
| 3 |
+
lmfit
|
| 4 |
+
matplotlib
|
| 5 |
+
|
| 6 |
+
[dev]
|
| 7 |
+
black
|
| 8 |
+
mypy
|
| 9 |
+
pylint
|
| 10 |
+
alive-progress
|
src/pycek_public.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pycek_public
|
src/pycek_public/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .cek_labs import *
|
| 2 |
+
|
| 3 |
+
from .generate_random_filenames import *
|
| 4 |
+
from .logger import *
|
| 5 |
+
|
| 6 |
+
from .statistics_lab import *
|
| 7 |
+
from .bomb_calorimetry import *
|
| 8 |
+
from .crystal_violet import *
|
| 9 |
+
from .surface_adsorption import *
|
| 10 |
+
|
| 11 |
+
from .plotting import *
|
| 12 |
+
|
src/pycek_public/bomb_calorimetry.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pprint as pp
|
| 4 |
+
|
| 5 |
+
class bomb_calorimetry(cek.cek_labs):
|
| 6 |
+
def setup_lab(self):
|
| 7 |
+
"""
|
| 8 |
+
Define base information for the lab
|
| 9 |
+
"""
|
| 10 |
+
self.add_metadata(
|
| 11 |
+
laboratory = 'Bomb Calorimetry',
|
| 12 |
+
columns = ["Time (s)","Temperature (K)"]
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
self.available_samples = ['benzoic', 'sucrose', 'naphthalene']
|
| 16 |
+
|
| 17 |
+
self.ignition_time = 20
|
| 18 |
+
self.relaxation_time = 3
|
| 19 |
+
self.number_of_values = 100
|
| 20 |
+
self.noise_level = 0.1
|
| 21 |
+
|
| 22 |
+
self.slope_before = np.random.uniform(0., self.noise_level) / 3
|
| 23 |
+
self.slope_after = np.random.uniform(0., self.noise_level) / 3
|
| 24 |
+
|
| 25 |
+
self.RT = self.R * self.temperature
|
| 26 |
+
|
| 27 |
+
# calorimeter constant (J/K)
|
| 28 |
+
self.calorimeter_constant = {'value':10135,'std_error':0.0}
|
| 29 |
+
self.sample_parameters["co2"] = {
|
| 30 |
+
"mM" : 44.01,
|
| 31 |
+
"dH" : -393.51e3, # co2 enthapy of formation (J/mol/K)
|
| 32 |
+
}
|
| 33 |
+
self.sample_parameters["h2o"] = {
|
| 34 |
+
"mM" : 18.015,
|
| 35 |
+
"dH" : -285.83e3,# h2o enthapy of formation (J/mol/K)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
self.sample_parameters["benzoic"] = {
|
| 39 |
+
"mM" : 122.123,
|
| 40 |
+
"n1" : 7,
|
| 41 |
+
"n2" : 3,
|
| 42 |
+
"dn" : 7-15/2,
|
| 43 |
+
"dHf" : {'value':-384.8e3,'std_error':0.5e3},
|
| 44 |
+
"dHc" : {'value':-3227.26e3,'std_error':0.2e3},
|
| 45 |
+
}
|
| 46 |
+
self.sample_parameters["sucrose"] = {
|
| 47 |
+
"mM" : 342.3,
|
| 48 |
+
"n1" : 12,
|
| 49 |
+
"n2" : 11,
|
| 50 |
+
"dn" : 0,
|
| 51 |
+
"dHf" : {'value':-2221.2e3,'std_error':0.2e3},
|
| 52 |
+
"dHc" : {'value':-5643.4e3,'std_error':1.8e3},
|
| 53 |
+
}
|
| 54 |
+
self.sample_parameters["naphthalene"] = {
|
| 55 |
+
"mM" : 128.17,
|
| 56 |
+
"n1" : 10,
|
| 57 |
+
"n2" : 4,
|
| 58 |
+
"dn" : 10 - 12,
|
| 59 |
+
"dHf" : {'value':77e3,'std_error':10.0e3},
|
| 60 |
+
"dHc" : {'value':-5160e3,'std_error':20.0e3},
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
def create_data(self):
|
| 64 |
+
"""
|
| 65 |
+
Generate the data
|
| 66 |
+
"""
|
| 67 |
+
if self.sample is None:
|
| 68 |
+
raise Exception("Sample not defined")
|
| 69 |
+
|
| 70 |
+
prm = self.sample_parameters[ self.sample ]
|
| 71 |
+
|
| 72 |
+
self.set_parameters(
|
| 73 |
+
sample = self.sample,
|
| 74 |
+
number_of_values = self.number_of_values,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
self.mass = np.random.normal(1000, 100)
|
| 78 |
+
self.add_metadata(**{
|
| 79 |
+
'Tablet mass (mg)': self.mass,
|
| 80 |
+
"Ignition time (s)" : self.ignition_time,
|
| 81 |
+
"Sample" : self.sample,
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
moles = self.mass / 1000 / prm["mM"]
|
| 85 |
+
|
| 86 |
+
# combustion enthalpy
|
| 87 |
+
# nH{co2} + mH{h2o} - H = DcH
|
| 88 |
+
DcH = prm["n1"] * self.sample_parameters["co2"]["dH"] + \
|
| 89 |
+
prm["n2"] * self.sample_parameters["h2o"]["dH"] - prm["dHf"]["value"]
|
| 90 |
+
|
| 91 |
+
dH = DcH * moles
|
| 92 |
+
dnrt = moles * self.RT * prm["dn"]
|
| 93 |
+
dU = dH - dnrt
|
| 94 |
+
|
| 95 |
+
deltaT = -dU / self.calorimeter_constant['value']
|
| 96 |
+
|
| 97 |
+
x = np.linspace(0, self.number_of_values, self.number_of_values)
|
| 98 |
+
y = np.random.normal(0, self.noise_level, self.number_of_values)
|
| 99 |
+
|
| 100 |
+
dd = 0.
|
| 101 |
+
T = self.temperature
|
| 102 |
+
for i in range(self.number_of_values):
|
| 103 |
+
if i < self.ignition_time:
|
| 104 |
+
T += self.slope_before
|
| 105 |
+
else:
|
| 106 |
+
T += self.slope_after
|
| 107 |
+
dd = deltaT * (1 - np.exp( - (i - self.ignition_time) / self.relaxation_time) )
|
| 108 |
+
y[i] += T + dd
|
| 109 |
+
|
| 110 |
+
self.data = np.column_stack((x,y))
|
| 111 |
+
|
| 112 |
+
return
|
| 113 |
+
|
src/pycek_public/cek_labs.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
|
| 5 |
+
from abc import ABC, abstractmethod
|
| 6 |
+
class cek_labs(ABC):
|
| 7 |
+
def __init__(self, **kwargs):
|
| 8 |
+
self.token = None
|
| 9 |
+
self.student_ID = 123456789
|
| 10 |
+
|
| 11 |
+
self.noise_level = 1
|
| 12 |
+
self.precision = 1
|
| 13 |
+
|
| 14 |
+
self.available_samples = []
|
| 15 |
+
self.sample_parameters = {}
|
| 16 |
+
self.sample = None
|
| 17 |
+
|
| 18 |
+
self.R = 8.314
|
| 19 |
+
self.NA = 6.022e23
|
| 20 |
+
self.temperature = 298
|
| 21 |
+
|
| 22 |
+
self.number_of_values = 100
|
| 23 |
+
self.output_file = None
|
| 24 |
+
self.filename_gen = cek.TempFilenameGenerator()
|
| 25 |
+
|
| 26 |
+
self.metadata = OrderedDict({
|
| 27 |
+
'student_ID' : self.student_ID,
|
| 28 |
+
'number_of_values' : self.number_of_values,
|
| 29 |
+
'output_file' : self.output_file,
|
| 30 |
+
})
|
| 31 |
+
|
| 32 |
+
self.logger_level = "ERROR"
|
| 33 |
+
|
| 34 |
+
# Define some lab specific parameters
|
| 35 |
+
# Can overwrite the defaults
|
| 36 |
+
for k,w in kwargs.items():
|
| 37 |
+
setattr(self, k, w)
|
| 38 |
+
np.random.seed(self.student_ID)
|
| 39 |
+
|
| 40 |
+
self.logger = cek.setup_logger(level=self.logger_level)
|
| 41 |
+
# self.logger.debug("This is a debug message")
|
| 42 |
+
# self.logger.verbose("This is a verbose message") # New verbose level
|
| 43 |
+
# self.logger.info("This is an info message")
|
| 44 |
+
# self.logger.result("This is an result message")
|
| 45 |
+
# self.logger.warning("This is a warning message")
|
| 46 |
+
# self.logger.error("This is an error message")
|
| 47 |
+
# self.logger.critical("This is a critical message")
|
| 48 |
+
# quit()
|
| 49 |
+
|
| 50 |
+
# Lab specific parameters
|
| 51 |
+
self.setup_lab()
|
| 52 |
+
|
| 53 |
+
def __str__(self):
|
| 54 |
+
return f'CHEM2000 Lab: {self.__class__.__name__}'
|
| 55 |
+
|
| 56 |
+
def set_student_ID(self,student_ID):
|
| 57 |
+
if isinstance(student_ID,int):
|
| 58 |
+
self.student_ID = student_ID
|
| 59 |
+
elif isinstance(student_ID,str):
|
| 60 |
+
if student_ID.isdigit():
|
| 61 |
+
self.student_ID = int(student_ID)
|
| 62 |
+
else:
|
| 63 |
+
raise ValueError("student_ID must be an integer")
|
| 64 |
+
else:
|
| 65 |
+
raise ValueError("student_ID must be an integer")
|
| 66 |
+
np.random.seed(self.student_ID)
|
| 67 |
+
self.update_metadata_from_attr()
|
| 68 |
+
self.logger.debug(f"Initial seed = {np.random.get_state()[1][0]}")
|
| 69 |
+
|
| 70 |
+
def set_token(self, token):
|
| 71 |
+
self.token = token
|
| 72 |
+
#print(f"Check: {self._check_token()}")
|
| 73 |
+
|
| 74 |
+
def _check_token(self):
|
| 75 |
+
if self.token != 23745419:
|
| 76 |
+
return True
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
def add_metadata(self, **kwargs):
|
| 80 |
+
for key, value in kwargs.items():
|
| 81 |
+
self.metadata[key] = value
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
def update_metadata_from_attr(self):
|
| 85 |
+
for k in self.metadata:
|
| 86 |
+
try:
|
| 87 |
+
self.metadata[k] = getattr(self, k)
|
| 88 |
+
except:
|
| 89 |
+
pass
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
def set_parameters(self, **kwargs):
|
| 93 |
+
"""
|
| 94 |
+
Set parameters for the lab
|
| 95 |
+
"""
|
| 96 |
+
for k,w in kwargs.items():
|
| 97 |
+
if k == "student_ID":
|
| 98 |
+
self.set_student_ID(w)
|
| 99 |
+
else:
|
| 100 |
+
setattr(self, k, w)
|
| 101 |
+
self.update_metadata_from_attr()
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
def write_metadata(self,f=None):
|
| 105 |
+
"""
|
| 106 |
+
Write metadata to the data file
|
| 107 |
+
"""
|
| 108 |
+
if f is None:
|
| 109 |
+
def dump(s):
|
| 110 |
+
self.logger.info(s)
|
| 111 |
+
else:
|
| 112 |
+
def dump(s):
|
| 113 |
+
with open(f, 'a') as file:
|
| 114 |
+
file.write(f"# {s}\n")
|
| 115 |
+
|
| 116 |
+
for key, value in self.metadata.items():
|
| 117 |
+
string = f"{key}"
|
| 118 |
+
string = string.replace("_"," ")
|
| 119 |
+
string = string[0].upper() + string[1:] + f" = {value}"
|
| 120 |
+
dump(string)
|
| 121 |
+
|
| 122 |
+
def read_metadata(self,f):
|
| 123 |
+
"""
|
| 124 |
+
Read metadata from the data file
|
| 125 |
+
|
| 126 |
+
Return: metadata (dict)
|
| 127 |
+
"""
|
| 128 |
+
metadata = OrderedDict({})
|
| 129 |
+
|
| 130 |
+
hash_lines = []
|
| 131 |
+
with open(f, 'r') as file:
|
| 132 |
+
for line in file:
|
| 133 |
+
if line.strip().startswith('#'):
|
| 134 |
+
hash_lines.append(line.replace('#','').strip())
|
| 135 |
+
|
| 136 |
+
for l in hash_lines:
|
| 137 |
+
if ":" in l:
|
| 138 |
+
key, value = l.split(':')
|
| 139 |
+
elif "=" in l:
|
| 140 |
+
key, value = l.split('=')
|
| 141 |
+
else:
|
| 142 |
+
raise Exception("Unknown separator")
|
| 143 |
+
key = key.replace("#","").strip()
|
| 144 |
+
metadata[key] = value.strip()
|
| 145 |
+
|
| 146 |
+
return metadata
|
| 147 |
+
|
| 148 |
+
def write_data_to_file(self, **kwargs):
|
| 149 |
+
"""
|
| 150 |
+
"""
|
| 151 |
+
if self.output_file is None:
|
| 152 |
+
filename = self.filename_gen.random
|
| 153 |
+
else:
|
| 154 |
+
filename = self.output_file
|
| 155 |
+
self.add_metadata(output_file=filename)
|
| 156 |
+
|
| 157 |
+
with open(filename, 'w') as f:
|
| 158 |
+
# Write the column names
|
| 159 |
+
cols = None
|
| 160 |
+
if "columns" in kwargs:
|
| 161 |
+
cols = kwargs["columns"]
|
| 162 |
+
elif "columns" in self.metadata:
|
| 163 |
+
cols = self.metadata["columns" ]
|
| 164 |
+
if cols is not None:
|
| 165 |
+
f.write(",".join(cols) + "\n")
|
| 166 |
+
|
| 167 |
+
# Convert NumPy array to list if needed
|
| 168 |
+
# if isinstance(self.data, np.ndarray):
|
| 169 |
+
# self.data = self.data.tolist()
|
| 170 |
+
|
| 171 |
+
# Write data
|
| 172 |
+
for row in self.data:
|
| 173 |
+
# Handle multiple columns
|
| 174 |
+
if isinstance(row, (list, tuple, np.ndarray)):
|
| 175 |
+
line = ",".join(map(str, row))
|
| 176 |
+
# Handle single-column case
|
| 177 |
+
else:
|
| 178 |
+
line = str(row)
|
| 179 |
+
f.write(line + "\n")
|
| 180 |
+
|
| 181 |
+
# Write the kwargs as metadata
|
| 182 |
+
self.write_metadata(filename)
|
| 183 |
+
|
| 184 |
+
return filename
|
| 185 |
+
|
| 186 |
+
def read_data_file(self,filename=None):
|
| 187 |
+
if filename is None:
|
| 188 |
+
raise ValueError("Filename is missing")
|
| 189 |
+
|
| 190 |
+
# Read file and separate comments from data
|
| 191 |
+
comments = []
|
| 192 |
+
data_lines = []
|
| 193 |
+
|
| 194 |
+
with open(filename, "r") as f:
|
| 195 |
+
for line in f:
|
| 196 |
+
if line.startswith("#"):
|
| 197 |
+
comments.append(line.strip()) # Store comment lines
|
| 198 |
+
else:
|
| 199 |
+
data_lines.append(line.strip()) # Store data lines
|
| 200 |
+
|
| 201 |
+
# Extract header and data
|
| 202 |
+
header = data_lines[0] # First non-comment line is the header
|
| 203 |
+
data_lines = "\n".join(data_lines[0:]) # Join remaining lines as CSV data
|
| 204 |
+
|
| 205 |
+
# Convert CSV data to NumPy array
|
| 206 |
+
from io import StringIO
|
| 207 |
+
from numpy.lib.recfunctions import structured_to_unstructured
|
| 208 |
+
|
| 209 |
+
data = np.genfromtxt(
|
| 210 |
+
StringIO(data_lines), delimiter=',',
|
| 211 |
+
comments='#', names=True,
|
| 212 |
+
skip_header=0, dtype=None)
|
| 213 |
+
|
| 214 |
+
data_array = structured_to_unstructured(data)
|
| 215 |
+
|
| 216 |
+
metadata = None
|
| 217 |
+
if len(comments) > 0:
|
| 218 |
+
metadata = OrderedDict({})
|
| 219 |
+
for l in comments:
|
| 220 |
+
if ":" in l:
|
| 221 |
+
key, value = l.split(':')
|
| 222 |
+
elif "=" in l:
|
| 223 |
+
key, value = l.split('=')
|
| 224 |
+
else:
|
| 225 |
+
raise Exception("Unknown separator")
|
| 226 |
+
key = key.replace("#","").strip()
|
| 227 |
+
metadata[key.strip()] = value.strip()
|
| 228 |
+
|
| 229 |
+
# Output results
|
| 230 |
+
# print("Comments:")
|
| 231 |
+
# print("\n".join(comments))
|
| 232 |
+
# print("\nExtracted Data:")
|
| 233 |
+
# print(data_array)
|
| 234 |
+
return data_array, header, metadata
|
| 235 |
+
|
| 236 |
+
def process_file(self, filename=None):
|
| 237 |
+
self.read_data(filename)
|
| 238 |
+
result = self.process_data()
|
| 239 |
+
return result
|
| 240 |
+
|
| 241 |
+
def _valid_ID(self,ID):
|
| 242 |
+
if ID in ["23745411"]:
|
| 243 |
+
return True
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
def _round_values(self, values, precision=None):
|
| 247 |
+
if precision is None:
|
| 248 |
+
precision = self.precision
|
| 249 |
+
rounded_values = [round(v, precision) for v in values]
|
| 250 |
+
values = np.array(rounded_values, dtype=float)
|
| 251 |
+
return values
|
| 252 |
+
|
| 253 |
+
def _generate_uniform_random(self, lower, upper, n):
|
| 254 |
+
return self._round_values(np.random.uniform(lower, upper, n))
|
| 255 |
+
|
| 256 |
+
def _generate_normal_random(self,n,prm):
|
| 257 |
+
list_of_1d_arrays = []
|
| 258 |
+
for p in prm:
|
| 259 |
+
values = np.random.normal(p[0], p[1], size=n)
|
| 260 |
+
list_of_1d_arrays.append(self._round_values(values))
|
| 261 |
+
|
| 262 |
+
if len(prm) == 1:
|
| 263 |
+
return np.array(self._round_values(values))
|
| 264 |
+
else:
|
| 265 |
+
return np.column_stack( [*list_of_1d_arrays] )
|
| 266 |
+
|
| 267 |
+
def _generate_noise(self,n,noise_level=None,ntype="normal"):
|
| 268 |
+
if noise_level == None:
|
| 269 |
+
raise ValueError("Missing noise level")
|
| 270 |
+
if noise_level <= 0:
|
| 271 |
+
return np.zeros(n)
|
| 272 |
+
if ntype == "normal":
|
| 273 |
+
return np.random.normal(0, noise_level, size=n)
|
| 274 |
+
|
| 275 |
+
def _generate_data_from_function(self, func, params, nvalues, xrange):
|
| 276 |
+
x = np.sort(self._generate_uniform_random(nvalues,*xrange))
|
| 277 |
+
y = func(x, *params) + self._generate_noise(nvalues)
|
| 278 |
+
y = self._round_values(y)
|
| 279 |
+
return np.column_stack((x,y))
|
| 280 |
+
|
| 281 |
+
import numpy as np
|
| 282 |
+
from typing import Callable, Dict, Optional, Union, Tuple
|
| 283 |
+
|
| 284 |
+
def generate_data_from_function(
|
| 285 |
+
self,
|
| 286 |
+
function: Callable,
|
| 287 |
+
params: Dict,
|
| 288 |
+
nvalues: int,
|
| 289 |
+
xrange: Optional[Tuple[float, float]] = None,
|
| 290 |
+
xspacing: str = 'random',
|
| 291 |
+
noise_level: Optional[float] = None,
|
| 292 |
+
background: Optional[float] = None,
|
| 293 |
+
weights: Optional[bool] = None,
|
| 294 |
+
positive: bool = False
|
| 295 |
+
) -> np.ndarray:
|
| 296 |
+
"""
|
| 297 |
+
Generate synthetic data points from a given function with optional noise and background.
|
| 298 |
+
|
| 299 |
+
Parameters
|
| 300 |
+
----------
|
| 301 |
+
function : callable
|
| 302 |
+
The model function to generate data from. Should accept x values and **kwargs.
|
| 303 |
+
params : dict
|
| 304 |
+
Parameters to pass to the function as keyword arguments.
|
| 305 |
+
nvalues : int
|
| 306 |
+
Number of data points to generate.
|
| 307 |
+
xrange : tuple of float, optional
|
| 308 |
+
Range of x values (min, max). Required if generating data points.
|
| 309 |
+
xspacing : str, default='random'
|
| 310 |
+
Method to space x values. Options:
|
| 311 |
+
- 'linear': Evenly spaced points
|
| 312 |
+
- 'random': Uniformly distributed random points
|
| 313 |
+
noise_level : float, optional
|
| 314 |
+
Standard deviation of Gaussian noise to add to y values.
|
| 315 |
+
background : float, optional
|
| 316 |
+
Constant background level to add to all y values.
|
| 317 |
+
weights : bool, optional
|
| 318 |
+
If True, include weights in output (NOT IMPLEMENTED).
|
| 319 |
+
positive : bool, default=False
|
| 320 |
+
If True, take absolute value of final y values.
|
| 321 |
+
|
| 322 |
+
Returns
|
| 323 |
+
-------
|
| 324 |
+
np.ndarray
|
| 325 |
+
2D array with shape (nvalues, 2) containing (x, y) pairs.
|
| 326 |
+
|
| 327 |
+
Raises
|
| 328 |
+
------
|
| 329 |
+
ValueError
|
| 330 |
+
If xrange is None or invalid xspacing type is provided.
|
| 331 |
+
"""
|
| 332 |
+
# Validate inputs
|
| 333 |
+
if xrange is None:
|
| 334 |
+
raise ValueError("xrange must be provided as (min, max) tuple")
|
| 335 |
+
|
| 336 |
+
if not isinstance(nvalues, int) or nvalues <= 0:
|
| 337 |
+
raise ValueError("nvalues must be a positive integer")
|
| 338 |
+
|
| 339 |
+
# Generate x values
|
| 340 |
+
if xspacing == "linear":
|
| 341 |
+
x = np.linspace(*xrange, nvalues)
|
| 342 |
+
elif xspacing == "random":
|
| 343 |
+
x = np.sort(self._generate_uniform_random(*xrange, nvalues))
|
| 344 |
+
else:
|
| 345 |
+
raise ValueError(f"xspacing must be 'linear' or 'random', got '{xspacing}'")
|
| 346 |
+
|
| 347 |
+
# Generate base y values from function
|
| 348 |
+
y = function(x, **params)
|
| 349 |
+
|
| 350 |
+
# Add optional modifications
|
| 351 |
+
if background is not None:
|
| 352 |
+
y += background
|
| 353 |
+
|
| 354 |
+
if noise_level is not None:
|
| 355 |
+
y += self._generate_noise(nvalues,noise_level)
|
| 356 |
+
|
| 357 |
+
if positive:
|
| 358 |
+
y = np.abs(y)
|
| 359 |
+
|
| 360 |
+
# Note: weights parameter is currently unused
|
| 361 |
+
if weights is not None:
|
| 362 |
+
# TODO: Implement weights handling
|
| 363 |
+
pass
|
| 364 |
+
|
| 365 |
+
y = self._round_values(y)
|
| 366 |
+
|
| 367 |
+
return np.column_stack((x, y))
|
| 368 |
+
|
| 369 |
+
def create_data_file(self):
|
| 370 |
+
data = self.create_data()
|
| 371 |
+
self.write_data_to_file(
|
| 372 |
+
self.metadata['output_file'],
|
| 373 |
+
data, **self.metadata )
|
| 374 |
+
return self.metadata['output_file']
|
| 375 |
+
|
| 376 |
+
def get_data(self):
|
| 377 |
+
return self.data
|
| 378 |
+
def get_metadata(self):
|
| 379 |
+
return self.metadata
|
| 380 |
+
|
| 381 |
+
@abstractmethod
|
| 382 |
+
def setup_lab(self,**kwargs):
|
| 383 |
+
pass
|
| 384 |
+
|
| 385 |
+
@abstractmethod
|
| 386 |
+
def create_data(self):
|
| 387 |
+
pass
|
| 388 |
+
|
src/pycek_public/crystal_violet.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class crystal_violet(cek.cek_labs):
|
| 5 |
+
def setup_lab(self):
|
| 6 |
+
"""
|
| 7 |
+
Define base information for the lab.
|
| 8 |
+
They can be overwrite by the user using the kwargs in the constructor or
|
| 9 |
+
by calling the set_parameters method.
|
| 10 |
+
"""
|
| 11 |
+
self.add_metadata(
|
| 12 |
+
laboratory = 'Crystal Violet Lab',
|
| 13 |
+
columns = ["Time (s)","Absorbance"]
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
self.expt_time = 1000
|
| 17 |
+
self.number_of_values = 501
|
| 18 |
+
self.noise_level = 0.1
|
| 19 |
+
self.precision = 4
|
| 20 |
+
|
| 21 |
+
self.activation_energy = 63e3 # J/mol
|
| 22 |
+
self.prefactor = 5.9e9 # 1/M/s
|
| 23 |
+
|
| 24 |
+
# Order wrt CV and OH
|
| 25 |
+
self.alpha = 1.0
|
| 26 |
+
self.beta = 0.75
|
| 27 |
+
self.conc_to_abs = 160e3 # Absorbivity of CV at 590 nm (L/mol/cm)
|
| 28 |
+
self.stock_solutions = {"cv" : 2.5e-5, "oh" : 0.5} # mol/L
|
| 29 |
+
|
| 30 |
+
self.volumes = {"cv" : 10, "oh" : 10, "h2o" : 10.0} # mL
|
| 31 |
+
|
| 32 |
+
def create_data(self):
|
| 33 |
+
"""
|
| 34 |
+
Generate the data
|
| 35 |
+
"""
|
| 36 |
+
self.set_parameters(
|
| 37 |
+
sample = self.sample,
|
| 38 |
+
number_of_values = self.number_of_values,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
self.add_metadata(**{
|
| 42 |
+
"Temperature (C)" : self.temperature-273.15,
|
| 43 |
+
"Volume of CV (mL)" : self.volumes['cv'],
|
| 44 |
+
"Volume of OH (mL)" : self.volumes['oh'],
|
| 45 |
+
"Volume of H2O (mL)": self.volumes['h2o'],
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
vtot = np.sum( [ x + np.random.normal(0,self.noise_level,1) for x in self.volumes.values() ] )
|
| 49 |
+
initial_concentration_cv = \
|
| 50 |
+
self.stock_solutions["cv"] * self.volumes["cv"] / vtot
|
| 51 |
+
|
| 52 |
+
concetration_oh = \
|
| 53 |
+
self.stock_solutions["oh"] * self.volumes["oh"] / vtot
|
| 54 |
+
|
| 55 |
+
rate_constant = self.prefactor*np.exp(-self.activation_energy/(self.R*(self.temperature)))
|
| 56 |
+
pseudo_rate_constant = rate_constant * np.power(concetration_oh,self.beta)
|
| 57 |
+
|
| 58 |
+
params = {
|
| 59 |
+
"A" : initial_concentration_cv* self.conc_to_abs,
|
| 60 |
+
"k" : pseudo_rate_constant
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
self.data = self.generate_data_from_function(
|
| 64 |
+
lambda x,A,k: A * np.exp(-k * x) ,
|
| 65 |
+
params,
|
| 66 |
+
self.number_of_values,
|
| 67 |
+
xrange = [0, self.expt_time],
|
| 68 |
+
xspacing = 'linear',
|
| 69 |
+
noise_level = self.noise_level,
|
| 70 |
+
positive = True,
|
| 71 |
+
background = 0.1
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
return
|
| 75 |
+
|
src/pycek_public/generate_random_filenames.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from glob import glob
|
| 3 |
+
import secrets
|
| 4 |
+
import string
|
| 5 |
+
|
| 6 |
+
class TempFilenameGenerator:
|
| 7 |
+
"""
|
| 8 |
+
Generates temporary filenames with .pdb extension and increasing indices.
|
| 9 |
+
Supports both sequential (tmp.{index}.pdb) and random (tmp.{random}.pdb) formats.
|
| 10 |
+
"""
|
| 11 |
+
def __init__(self, directory=".", root="data", ext="csv", random_length=12):
|
| 12 |
+
"""
|
| 13 |
+
Initialize the generator with a target directory.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
directory (str): Directory for the filenames (default: current directory)
|
| 17 |
+
root (str): Root name for the temporary files (default: tmp)
|
| 18 |
+
ext (str): File extension (default: pdb)
|
| 19 |
+
random_length (int): Length of random string in random filenames (default: 12)
|
| 20 |
+
|
| 21 |
+
Example:
|
| 22 |
+
generator = TempFilenameGenerator()
|
| 23 |
+
|
| 24 |
+
# Get sequential filename (e.g., "tmp.0.pdb")
|
| 25 |
+
sequential_file = generator.next
|
| 26 |
+
|
| 27 |
+
# Get random filename (e.g., "tmp.j4k3h2l5m9n8.pdb")
|
| 28 |
+
random_file = generator.random
|
| 29 |
+
"""
|
| 30 |
+
self.directory = directory
|
| 31 |
+
self.root = root
|
| 32 |
+
self.ext = ext
|
| 33 |
+
self.random_length = random_length
|
| 34 |
+
self._current_index = self._find_max_index()
|
| 35 |
+
self._current_filename = None
|
| 36 |
+
|
| 37 |
+
def _find_max_index(self):
|
| 38 |
+
"""Find the highest existing index in the directory."""
|
| 39 |
+
pattern = os.path.join(self.directory, f"{self.root}.*.{self.ext}")
|
| 40 |
+
existing_files = glob(pattern)
|
| 41 |
+
|
| 42 |
+
if not existing_files:
|
| 43 |
+
return -1
|
| 44 |
+
|
| 45 |
+
indices = []
|
| 46 |
+
for filename in existing_files:
|
| 47 |
+
try:
|
| 48 |
+
# Extract index from tmp.{index}.pdb
|
| 49 |
+
index = int(os.path.basename(filename).split('.')[-2])
|
| 50 |
+
indices.append(index)
|
| 51 |
+
except (ValueError, IndexError):
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
return max(indices) if indices else -1
|
| 55 |
+
|
| 56 |
+
def _generate_random_string(self):
|
| 57 |
+
"""Generate a cryptographically secure random string."""
|
| 58 |
+
alphabet = string.ascii_letters + string.digits
|
| 59 |
+
return ''.join(secrets.choice(alphabet) for _ in range(self.random_length))
|
| 60 |
+
|
| 61 |
+
def delete_files(self):
|
| 62 |
+
"""Delete all temporary files in the directory."""
|
| 63 |
+
pattern = os.path.join(self.directory, f"{self.root}.*.{self.ext}")
|
| 64 |
+
for filename in glob(pattern):
|
| 65 |
+
os.remove(filename)
|
| 66 |
+
self._current_index = -1
|
| 67 |
+
|
| 68 |
+
def copy_last(self, dest):
|
| 69 |
+
"""Copy the last generated file to a destination."""
|
| 70 |
+
if self._current_filename is None:
|
| 71 |
+
raise ValueError("No files have been generated yet")
|
| 72 |
+
os.system(f"cp {self._current_filename} {dest}")
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def next(self):
|
| 76 |
+
"""Generate the next filename in sequence."""
|
| 77 |
+
self._current_index += 1
|
| 78 |
+
filename = f"{self.root}.{self._current_index}.{self.ext}"
|
| 79 |
+
self._current_filename = os.path.join(self.directory, filename)
|
| 80 |
+
return self._current_filename
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def random(self):
|
| 84 |
+
"""Generate a random filename."""
|
| 85 |
+
random_string = self._generate_random_string()
|
| 86 |
+
filename = f"{self.root}.{random_string}.{self.ext}"
|
| 87 |
+
self._current_filename = os.path.join(self.directory, filename)
|
| 88 |
+
return self._current_filename
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def current_index(self):
|
| 92 |
+
"""Get the current index value."""
|
| 93 |
+
return self._current_index
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def current(self):
|
| 97 |
+
"""Get the current filename."""
|
| 98 |
+
return self._current_filename
|
| 99 |
+
|
| 100 |
+
# import random
|
| 101 |
+
# import string
|
| 102 |
+
# import os
|
| 103 |
+
# from typing import Optional
|
| 104 |
+
|
| 105 |
+
# def generate_random_filename(
|
| 106 |
+
# extension: Optional[str] = 'csv',
|
| 107 |
+
# length: Optional[int] = 10,
|
| 108 |
+
# prefix: Optional[str] = 'data_',
|
| 109 |
+
# directory: Optional[str] = '',
|
| 110 |
+
# existing_check: Optional[bool] = True
|
| 111 |
+
# ) -> str:
|
| 112 |
+
# """
|
| 113 |
+
# Generate a random filename with optional parameters.
|
| 114 |
+
|
| 115 |
+
# Args:
|
| 116 |
+
# extension (str): File extension to append (e.g., '.txt', '.pdf')
|
| 117 |
+
# length (int): Length of the random string (default: 10)
|
| 118 |
+
# prefix (str): Prefix to add before the random string
|
| 119 |
+
# directory (str): Directory path where the file will be created
|
| 120 |
+
# existing_check (bool): Whether to check if filename already exists
|
| 121 |
+
|
| 122 |
+
# Returns:
|
| 123 |
+
# str: Generated filename
|
| 124 |
+
|
| 125 |
+
# Raises:
|
| 126 |
+
# ValueError: If length is less than 1
|
| 127 |
+
# ValueError: If unable to generate unique filename after 100 attempts
|
| 128 |
+
# """
|
| 129 |
+
# if length < 1:
|
| 130 |
+
# raise ValueError("Length must be at least 1")
|
| 131 |
+
|
| 132 |
+
# # Clean up the extension
|
| 133 |
+
# if extension and not extension.startswith('.'):
|
| 134 |
+
# extension = '.' + extension
|
| 135 |
+
|
| 136 |
+
# # Clean up the directory path
|
| 137 |
+
# if directory:
|
| 138 |
+
# directory = os.path.abspath(directory)
|
| 139 |
+
# if not os.path.exists(directory):
|
| 140 |
+
# os.makedirs(directory)
|
| 141 |
+
|
| 142 |
+
# attempts = 0
|
| 143 |
+
# max_attempts = 100
|
| 144 |
+
|
| 145 |
+
# while True:
|
| 146 |
+
# # Generate random string using ASCII letters and digits
|
| 147 |
+
# random_string = ''.join(
|
| 148 |
+
# random.choices(string.ascii_letters + string.digits, k=length)
|
| 149 |
+
# )
|
| 150 |
+
|
| 151 |
+
# # Combine all parts of the filename
|
| 152 |
+
# filename = f"{prefix}{random_string}{extension}"
|
| 153 |
+
|
| 154 |
+
# # Add directory path if specified
|
| 155 |
+
# if directory:
|
| 156 |
+
# filename = os.path.join(directory, filename)
|
| 157 |
+
|
| 158 |
+
# # Check if file exists (if requested)
|
| 159 |
+
# if not existing_check or not os.path.exists(filename):
|
| 160 |
+
# return filename
|
| 161 |
+
|
| 162 |
+
# attempts += 1
|
| 163 |
+
# if attempts >= max_attempts:
|
| 164 |
+
# raise ValueError(
|
| 165 |
+
# f"Unable to generate unique filename after {max_attempts} attempts"
|
| 166 |
+
# )
|
src/pycek_public/logger.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import logging
|
| 3 |
+
import colorama
|
| 4 |
+
from colorama import Fore, Style
|
| 5 |
+
|
| 6 |
+
# Initialize colorama to work on Windows too
|
| 7 |
+
colorama.init()
|
| 8 |
+
|
| 9 |
+
# Define custom VERBOSE level (between DEBUG-10 and INFO-20)
|
| 10 |
+
VERBOSE = 15
|
| 11 |
+
logging.addLevelName(VERBOSE, 'VERBOSE')
|
| 12 |
+
RESULT = 25
|
| 13 |
+
logging.addLevelName(RESULT, 'RESULT')
|
| 14 |
+
|
| 15 |
+
class ColoredFormatter(logging.Formatter):
|
| 16 |
+
"""Custom formatter that adds colors to log messages based on level with fixed width"""
|
| 17 |
+
COLORS = {
|
| 18 |
+
'DEBUG': Fore.CYAN + Style.BRIGHT,
|
| 19 |
+
'VERBOSE': Fore.BLUE + Style.BRIGHT,
|
| 20 |
+
'INFO': Fore.GREEN + Style.BRIGHT,
|
| 21 |
+
'RESULT' : Fore.RESET + Style.BRIGHT,
|
| 22 |
+
'WARNING': Fore.YELLOW + Style.BRIGHT,
|
| 23 |
+
'ERROR': Fore.RED + Style.BRIGHT,
|
| 24 |
+
'CRITICAL': Fore.MAGENTA + Style.BRIGHT
|
| 25 |
+
}
|
| 26 |
+
LEVEL_NAME_WIDTH = 8
|
| 27 |
+
|
| 28 |
+
def format(self, record):
|
| 29 |
+
# Save original levelname and message
|
| 30 |
+
orig_levelname = record.levelname
|
| 31 |
+
orig_msg = record.msg
|
| 32 |
+
|
| 33 |
+
# Determine the color for the level
|
| 34 |
+
color_code = self.COLORS.get(orig_levelname, '')
|
| 35 |
+
|
| 36 |
+
# Calculate padding for levelname
|
| 37 |
+
padding = ' ' * (self.LEVEL_NAME_WIDTH - len(orig_levelname))
|
| 38 |
+
|
| 39 |
+
# Apply color to levelname with padding
|
| 40 |
+
record.levelname = f"{color_code}{orig_levelname}{padding}{Style.RESET_ALL}"
|
| 41 |
+
|
| 42 |
+
# Apply color to the message as well
|
| 43 |
+
record.msg = f"{color_code}{orig_msg}{Style.RESET_ALL}"
|
| 44 |
+
|
| 45 |
+
# Format the message
|
| 46 |
+
result = super().format(record)
|
| 47 |
+
|
| 48 |
+
# Restore original values
|
| 49 |
+
record.levelname = orig_levelname
|
| 50 |
+
record.msg = orig_msg
|
| 51 |
+
return result
|
| 52 |
+
|
| 53 |
+
class ColoredLogger(logging.Logger):
|
| 54 |
+
"""Custom logger class with verbose method"""
|
| 55 |
+
|
| 56 |
+
def verbose(self, msg, *args, **kwargs):
|
| 57 |
+
"""Log at custom VERBOSE level"""
|
| 58 |
+
if self.isEnabledFor(VERBOSE):
|
| 59 |
+
self._log(VERBOSE, msg, args, **kwargs)
|
| 60 |
+
|
| 61 |
+
def result(self, msg, *args, **kwargs):
|
| 62 |
+
"""Log at custom RESULT level"""
|
| 63 |
+
if self.isEnabledFor(RESULT):
|
| 64 |
+
self._log(RESULT, msg, args, **kwargs)
|
| 65 |
+
|
| 66 |
+
# logging.Logger.result = custom_logging
|
| 67 |
+
|
| 68 |
+
def setup_logger(name='colored_logger', level="INFO"):
|
| 69 |
+
"""Set up and return a colored logger instance"""
|
| 70 |
+
|
| 71 |
+
# Register our custom logger class
|
| 72 |
+
logging.setLoggerClass(ColoredLogger)
|
| 73 |
+
|
| 74 |
+
# Create logger
|
| 75 |
+
logger = logging.getLogger(name)
|
| 76 |
+
|
| 77 |
+
# Set logger level
|
| 78 |
+
try:
|
| 79 |
+
logger.setLevel(getattr(logging, level.upper()))
|
| 80 |
+
except AttributeError:
|
| 81 |
+
logger.setLevel(logging.INFO) # Default to INFO if invalid level
|
| 82 |
+
|
| 83 |
+
# Create console handler
|
| 84 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 85 |
+
console_handler.setLevel(level)
|
| 86 |
+
|
| 87 |
+
# Create formatter
|
| 88 |
+
formatter = ColoredFormatter(
|
| 89 |
+
# fmt='%(asctime)s - %(levelname)s - %(message)s',
|
| 90 |
+
# datefmt='%Y-%m-%d %H:%M:%S'
|
| 91 |
+
fmt='%(levelname)8s - %(message)s'
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Add formatter to handler
|
| 95 |
+
console_handler.setFormatter(formatter)
|
| 96 |
+
|
| 97 |
+
# Add handler to logger
|
| 98 |
+
logger.addHandler(console_handler)
|
| 99 |
+
|
| 100 |
+
return logger
|
| 101 |
+
|
| 102 |
+
# Example usage
|
| 103 |
+
if __name__ == '__main__':
|
| 104 |
+
logger = setup_logger()
|
| 105 |
+
|
| 106 |
+
# Test all log levels including new VERBOSE level
|
| 107 |
+
logger.debug("This is a debug message")
|
| 108 |
+
logger.verbose("This is a verbose message") # New verbose level
|
| 109 |
+
logger.info("This is an info message")
|
| 110 |
+
logger.warning("This is a warning message")
|
| 111 |
+
logger.error("This is an error message")
|
| 112 |
+
logger.critical("This is a critical message")
|
src/pycek_public/plotting.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
import numpy as np
|
| 3 |
+
from scipy import stats
|
| 4 |
+
|
| 5 |
+
class plotting():
|
| 6 |
+
def quick_plot(self, scatter=None, line=None, columns=["X","Y"], output=None, hline=None):
|
| 7 |
+
"""
|
| 8 |
+
Plot the data along with the best fit line and its associated confidence band.
|
| 9 |
+
|
| 10 |
+
Parameters:
|
| 11 |
+
data (list): List of data to plot
|
| 12 |
+
columns (list): Axes labels (default is "X","Y")
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
None: Displays a matplotlib plot with data, fit line, and confidence band
|
| 16 |
+
|
| 17 |
+
Example:
|
| 18 |
+
>>> quick_plot(x_data, y_data)
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
if scatter is None and line is None:
|
| 22 |
+
raise ValueError("Either scatter or line should be provided")
|
| 23 |
+
|
| 24 |
+
if not isinstance(scatter,list):
|
| 25 |
+
scatter = [scatter]
|
| 26 |
+
if not isinstance(line,list):
|
| 27 |
+
line = [line]
|
| 28 |
+
|
| 29 |
+
# Plot the observed data points as a scatter plot
|
| 30 |
+
for ds in scatter:
|
| 31 |
+
plt.scatter(ds[:,0],ds[:,1], color='blue', label='Data')
|
| 32 |
+
|
| 33 |
+
# Add labels and title to the plot
|
| 34 |
+
plt.xlabel(columns[0])
|
| 35 |
+
plt.ylabel(columns[1])
|
| 36 |
+
|
| 37 |
+
if hline is not None:
|
| 38 |
+
plt.axhline(hline, color='black', linestyle='--')
|
| 39 |
+
|
| 40 |
+
# Display the legend
|
| 41 |
+
plt.legend()
|
| 42 |
+
|
| 43 |
+
# Show the plot
|
| 44 |
+
if output is None:
|
| 45 |
+
plt.show()
|
| 46 |
+
else:
|
| 47 |
+
plt.savefig(output)
|
| 48 |
+
plt.close()
|
| 49 |
+
|
src/pycek_public/statistics_lab.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class stats_lab(cek.cek_labs):
|
| 5 |
+
def setup_lab(self):
|
| 6 |
+
"""
|
| 7 |
+
Define base information for the lab
|
| 8 |
+
"""
|
| 9 |
+
self.add_metadata(
|
| 10 |
+
laboratory = 'Basic Statistics Lab',
|
| 11 |
+
columns = ["X","Y"]
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
self.available_samples = [
|
| 15 |
+
'Averages',
|
| 16 |
+
'Propagation of uncertainty',
|
| 17 |
+
'Comparison of averages',
|
| 18 |
+
'Linear fit',
|
| 19 |
+
'Non linear fit',
|
| 20 |
+
'Detection of outliers',
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
self.sample_parameters['Averages'] = {
|
| 24 |
+
"gen_values" : [
|
| 25 |
+
(1.0, 0.1),
|
| 26 |
+
(12., 2.0)
|
| 27 |
+
],
|
| 28 |
+
'exp_values' : (1.0,9.0),
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
self.sample_parameters['Propagation of uncertainty'] = {
|
| 32 |
+
"gen_values" : [
|
| 33 |
+
(15.0, 1.0),
|
| 34 |
+
(133., 2.0)
|
| 35 |
+
],
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
self.sample_parameters['Comparison of averages'] = {
|
| 39 |
+
"gen_values" : [
|
| 40 |
+
(15.0, 1.0),
|
| 41 |
+
(13.2, 2.0)
|
| 42 |
+
],
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
self.sample_parameters['Linear fit'] = {
|
| 46 |
+
"function" : lambda x,m,q: m*x + q,
|
| 47 |
+
"gen_values" : {'m':12.3 , 'q':1.0},
|
| 48 |
+
"xrange" : [0.0 , 10.0]
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
self.sample_parameters['Non linear fit'] = {
|
| 52 |
+
"nval" : 10,
|
| 53 |
+
"function" : lambda x,E0,K0,Kp,V0: E0 + K0 * x / Kp * ( (V0/x)**Kp / (Kp-1)+1) - K0*V0/(Kp-1),
|
| 54 |
+
"gen_values" : {"E0":-634.2, "K0":12.43, "Kp":4.28, "V0":99.11},
|
| 55 |
+
"xrange" : [50 , 140]
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
self.sample_parameters['Detection of outliers'] = {
|
| 59 |
+
"function" : lambda x,m,q: m*x + q,
|
| 60 |
+
"gen_values" : {'m':2.3 , 'q':0.1},
|
| 61 |
+
"xrange" : [10.0 , 20.0],
|
| 62 |
+
"shift" : 2,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
def create_data(self):
|
| 66 |
+
"""
|
| 67 |
+
Generate the data
|
| 68 |
+
"""
|
| 69 |
+
if self.sample is None:
|
| 70 |
+
raise Exception("Sample not defined")
|
| 71 |
+
|
| 72 |
+
prm = self.sample_parameters[ self.sample ]
|
| 73 |
+
|
| 74 |
+
self.set_parameters(
|
| 75 |
+
number_of_values = self.number_of_values,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
if "noise" in prm:
|
| 79 |
+
self.set_parameters( noise_level = prm["noise"] )
|
| 80 |
+
|
| 81 |
+
self.add_metadata(
|
| 82 |
+
number_of_values = self.number_of_values,
|
| 83 |
+
sample = self.sample,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
if self.sample in ["Averages", 'Propagation of uncertainty', 'Comparison of averages']:
|
| 87 |
+
data = self._generate_normal_random(self.number_of_values, prm['gen_values'])
|
| 88 |
+
|
| 89 |
+
elif self.sample in ["Linear fit"]:
|
| 90 |
+
data = self.generate_data_from_function(
|
| 91 |
+
prm["function"],
|
| 92 |
+
prm['gen_values'],
|
| 93 |
+
self.number_of_values,
|
| 94 |
+
prm['xrange'],
|
| 95 |
+
noise_level = self.noise_level,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
elif self.sample in ["Non linear fit"]:
|
| 99 |
+
data = self.generate_data_from_function(
|
| 100 |
+
prm["function"],
|
| 101 |
+
prm['gen_values'],
|
| 102 |
+
self.number_of_values,
|
| 103 |
+
prm['xrange'],
|
| 104 |
+
noise_level = self.noise_level,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
elif self.sample in ["Detection of outliers"]:
|
| 108 |
+
data = self.generate_data_from_function(
|
| 109 |
+
prm["function"],
|
| 110 |
+
prm['gen_values'],
|
| 111 |
+
self.number_of_values,
|
| 112 |
+
prm['xrange'],
|
| 113 |
+
noise_level = self.noise_level,
|
| 114 |
+
)
|
| 115 |
+
i = np.random.randint(self.number_of_values)
|
| 116 |
+
data[i,1] += prm['shift']
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
self.data = data
|
| 120 |
+
return data
|
| 121 |
+
|
src/pycek_public/surface_adsorption.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycek_public as cek
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class surface_adsorption(cek.cek_labs):
|
| 5 |
+
def setup_lab(self):
|
| 6 |
+
"""
|
| 7 |
+
Define base information for the lab.
|
| 8 |
+
They can be overwrite by the user using the kwargs in the constructor or
|
| 9 |
+
by calling the set_parameters method.
|
| 10 |
+
"""
|
| 11 |
+
self.add_metadata(
|
| 12 |
+
laboratory = 'Surface Adsorption Lab',
|
| 13 |
+
columns = ["Dye added (mg)", "Dye in solution (mol/L)"]
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
self.volume = 1 # L
|
| 17 |
+
self.minDye = 500 # mg
|
| 18 |
+
self.maxDye = 10000 # mg
|
| 19 |
+
|
| 20 |
+
self.sample_parameters = {
|
| 21 |
+
"dH" : -19.51e3, # J/mol
|
| 22 |
+
"dS" : -10, # J/mol/K
|
| 23 |
+
"Q" : 0.0001, # monolayer coverage (mol/m^2)
|
| 24 |
+
"molarMass": 584.910641, # g/mol
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
self.number_of_values = 100
|
| 28 |
+
self.noise_level = 1e-6
|
| 29 |
+
self.precision = 10
|
| 30 |
+
|
| 31 |
+
def create_data(self):
|
| 32 |
+
"""
|
| 33 |
+
Generate the data
|
| 34 |
+
"""
|
| 35 |
+
self.set_parameters(
|
| 36 |
+
sample = self.sample,
|
| 37 |
+
number_of_values = self.number_of_values,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
self.add_metadata(**{
|
| 41 |
+
"Temperature (K)" : self.temperature,
|
| 42 |
+
"Volume (L)" : self.volume,
|
| 43 |
+
"Molar mass (g/mol)" : self.sample_parameters["molarMass"],
|
| 44 |
+
"MinDye (mg)" : self.minDye,
|
| 45 |
+
"MaxDye (mg)" : self.maxDye,
|
| 46 |
+
'Number of values' : self.number_of_values,
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
# Langmuir isotherm equilibrium constant
|
| 50 |
+
# Convert to kJ/mol
|
| 51 |
+
lnK = (-self.sample_parameters["dH"] / (self.temperature) + self.sample_parameters["dS"]) / self.R
|
| 52 |
+
K = np.exp(lnK) # in L/mol
|
| 53 |
+
|
| 54 |
+
conversion_factor = 1000 * self.sample_parameters["molarMass"] * self.volume
|
| 55 |
+
conc_range = np.array([self.minDye, self.maxDye]) / conversion_factor
|
| 56 |
+
|
| 57 |
+
self.data = self.generate_data_from_function(
|
| 58 |
+
lambda x,K,Q: ((x*K - K*Q - 1) + np.sqrt((x*K - K*Q - 1)**2 + 4*x*K) ) / (2*K) ,
|
| 59 |
+
{"K":K , "Q":self.sample_parameters["Q"]},
|
| 60 |
+
self.number_of_values,
|
| 61 |
+
xrange = conc_range,
|
| 62 |
+
xspacing = 'linear',
|
| 63 |
+
noise_level = self.noise_level,
|
| 64 |
+
positive = True,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
self.data[:,0] *= conversion_factor
|
| 68 |
+
# grams of dye added
|
| 69 |
+
# x = np.linspace(self.minDye, self.maxDye, self.number_of_values) / 1000
|
| 70 |
+
# moles = x / self.params["molarMass"] # g
|
| 71 |
+
# initial_concentration = moles / self.volume # mol/L
|
| 72 |
+
# y = self.measure(K, self.params["Q"], initial_concentration)
|
| 73 |
+
|
| 74 |
+
# # Because noise is added to the concentration in solution, but
|
| 75 |
+
# # the concentration on the surface is required in post-processing, which
|
| 76 |
+
# # is very small, we divide by 1000
|
| 77 |
+
|
| 78 |
+
# y = cek.add_noise(y, self.uncertainty/1000)
|
| 79 |
+
|
| 80 |
+
# data_array = np.column_stack((x, y))
|
| 81 |
+
|
| 82 |
+
return self.data
|
| 83 |
+
|