peterspackman commited on
Commit
5cc02e3
·
1 Parent(s): a28dfd2

remove build directory...

Browse files
Dockerfile CHANGED
@@ -5,10 +5,12 @@ ENV PATH="/home/user/.local/bin:$PATH"
5
 
6
  ENV UV_SYSTEM_PYTHON=1
7
  WORKDIR /app
 
 
8
 
9
  # Copy and install the local repo, then remove it
10
- RUN uv pip install git+https://github.com/praiteri/pycek_public
11
- RUN uv pip install marimo fastapi colorama
12
  COPY --chown=user ./marimo /app
13
  RUN chown -R user:user /app
14
  USER user
 
5
 
6
  ENV UV_SYSTEM_PYTHON=1
7
  WORKDIR /app
8
+ COPY --chown=user ./deployment/requirements.txt requirements.txt
9
+ RUN uv pip install -r requirements.txt
10
 
11
  # Copy and install the local repo, then remove it
12
+ COPY --chown=user ./ /pycek_public
13
+ RUN uv pip install /pycek_public && rm -rf /pycek_public
14
  COPY --chown=user ./marimo /app
15
  RUN chown -R user:user /app
16
  USER user
build/lib/pycek_public/__init__.py DELETED
@@ -1,10 +0,0 @@
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 DELETED
@@ -1,113 +0,0 @@
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 DELETED
@@ -1,388 +0,0 @@
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 DELETED
@@ -1,75 +0,0 @@
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 DELETED
@@ -1,166 +0,0 @@
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 DELETED
@@ -1,112 +0,0 @@
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 DELETED
@@ -1,125 +0,0 @@
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 DELETED
@@ -1,89 +0,0 @@
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
-