praiteri commited on
Commit
843a502
·
0 Parent(s):

first commit

Browse files
Files changed (39) hide show
  1. .gitattributes +3 -0
  2. LICENSE +21 -0
  3. README.md +2 -0
  4. build/lib/pycek_public/__init__.py +10 -0
  5. build/lib/pycek_public/bomb_calorimetry.py +113 -0
  6. build/lib/pycek_public/cek_labs.py +388 -0
  7. build/lib/pycek_public/crystal_violet.py +75 -0
  8. build/lib/pycek_public/generate_random_filenames.py +166 -0
  9. build/lib/pycek_public/logger.py +112 -0
  10. build/lib/pycek_public/statistics_lab.py +125 -0
  11. build/lib/pycek_public/surface_adsorption.py +89 -0
  12. deployment/Dockerfile +22 -0
  13. deployment/README.md +9 -0
  14. deployment/build.sh +3 -0
  15. deployment/requirements.txt +3 -0
  16. marimo/__pycache__/app.cpython-311.pyc +0 -0
  17. marimo/__pycache__/bomb_calorimetry.cpython-311.pyc +0 -0
  18. marimo/__pycache__/index.cpython-311.pyc +0 -0
  19. marimo/app.py +26 -0
  20. marimo/bomb_calorimetry.py +140 -0
  21. marimo/crystal_violet.py +167 -0
  22. marimo/index.py +23 -0
  23. marimo/statistics_lab.py +140 -0
  24. marimo/surface_adsorption.py +137 -0
  25. pyproject.toml +31 -0
  26. src/pycek_public.egg-info/PKG-INFO +42 -0
  27. src/pycek_public.egg-info/SOURCES.txt +17 -0
  28. src/pycek_public.egg-info/dependency_links.txt +1 -0
  29. src/pycek_public.egg-info/requires.txt +10 -0
  30. src/pycek_public.egg-info/top_level.txt +1 -0
  31. src/pycek_public/__init__.py +12 -0
  32. src/pycek_public/bomb_calorimetry.py +113 -0
  33. src/pycek_public/cek_labs.py +388 -0
  34. src/pycek_public/crystal_violet.py +75 -0
  35. src/pycek_public/generate_random_filenames.py +166 -0
  36. src/pycek_public/logger.py +112 -0
  37. src/pycek_public/plotting.py +49 -0
  38. src/pycek_public/statistics_lab.py +121 -0
  39. 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
+