chromaquant 0.3.1__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. chromaquant/__init__.py +9 -2
  2. chromaquant/data/__init__.py +14 -0
  3. chromaquant/data/breakdown.py +430 -0
  4. chromaquant/data/dataset.py +195 -0
  5. chromaquant/data/table.py +412 -0
  6. chromaquant/data/value.py +215 -0
  7. chromaquant/formula/__init__.py +13 -0
  8. chromaquant/formula/base_formulas.py +168 -0
  9. chromaquant/formula/formula.py +507 -0
  10. chromaquant/import_local_packages.py +55 -0
  11. chromaquant/logging_and_handling.py +76 -0
  12. chromaquant/match/__init__.py +13 -0
  13. chromaquant/match/match.py +184 -0
  14. chromaquant/match/match_config.py +296 -0
  15. chromaquant/match/match_tools.py +154 -0
  16. chromaquant/{Handle → results}/__init__.py +2 -2
  17. chromaquant/results/reporting_tools.py +190 -0
  18. chromaquant/results/results.py +250 -0
  19. chromaquant/utils/__init__.py +14 -0
  20. chromaquant/utils/categories.py +127 -0
  21. chromaquant/utils/chemical_formulas.py +104 -0
  22. chromaquant/utils/dataframe_processing.py +222 -0
  23. chromaquant/utils/file_tools.py +100 -0
  24. chromaquant/utils/formula_tools.py +119 -0
  25. chromaquant-0.5.0.dist-info/METADATA +61 -0
  26. chromaquant-0.5.0.dist-info/RECORD +29 -0
  27. {chromaquant-0.3.1.dist-info → chromaquant-0.5.0.dist-info}/WHEEL +1 -1
  28. {chromaquant-0.3.1.dist-info → chromaquant-0.5.0.dist-info}/licenses/LICENSE.txt +1 -1
  29. chromaquant-0.5.0.dist-info/licenses/LICENSES_bundled.txt +251 -0
  30. chromaquant/Handle/handleDirectories.py +0 -89
  31. chromaquant/Manual/HydroUI.py +0 -418
  32. chromaquant/Manual/QuantUPP.py +0 -373
  33. chromaquant/Manual/Quantification.py +0 -1305
  34. chromaquant/Manual/__init__.py +0 -10
  35. chromaquant/Manual/duplicateMatch.py +0 -211
  36. chromaquant/Manual/fpm_match.py +0 -798
  37. chromaquant/Manual/label-type.py +0 -179
  38. chromaquant/Match/AutoFpmMatch.py +0 -1133
  39. chromaquant/Match/__init__.py +0 -12
  40. chromaquant/Quant/AutoQuantification.py +0 -1329
  41. chromaquant/Quant/__init__.py +0 -12
  42. chromaquant/__main__.py +0 -493
  43. chromaquant/properties.json +0 -4
  44. chromaquant-0.3.1.dist-info/METADATA +0 -189
  45. chromaquant-0.3.1.dist-info/RECORD +0 -22
  46. chromaquant-0.3.1.dist-info/entry_points.txt +0 -2
  47. chromaquant-0.3.1.dist-info/licenses/LICENSES_bundled.txt +0 -1035
chromaquant/__init__.py CHANGED
@@ -2,9 +2,16 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  """
5
- ChromaQuant package initialization
5
+ chromaquant.utils package initialization
6
6
 
7
7
  Julia Hancock
8
8
  Created 10-20-2024
9
9
 
10
- """
10
+ """
11
+
12
+ from .results import Results
13
+ from .formula import Formula
14
+ from .match import MatchConfig
15
+ from .data import Table
16
+ from .data import Value
17
+ from .data import Breakdown
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ ChromaQuant.data module initialization
6
+
7
+ Julia Hancock
8
+ Created 10-19-2024
9
+
10
+ """
11
+
12
+ from .table import Table
13
+ from .value import Value
14
+ from .breakdown import Breakdown
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ COPYRIGHT STATEMENT:
5
+
6
+ ChromaQuant – A quantification software for complex gas chromatographic data
7
+
8
+ Copyright (c) 2026, by Julia Hancock
9
+ Affiliation: Dr. Julie Elaine Rorrer
10
+ URL: https://www.rorrerlab.com/
11
+
12
+ License: BSD 3-Clause License
13
+
14
+ ---
15
+
16
+ CLASS DEFINITION FOR BREAKDOWNS
17
+
18
+ Julia Hancock
19
+ Started 1-28-2026
20
+
21
+ """
22
+
23
+ import logging
24
+ from openpyxl.utils import get_column_letter
25
+ from pandas import DataFrame
26
+ from typing import Any, TYPE_CHECKING
27
+ if TYPE_CHECKING:
28
+ from ..results import Results
29
+ from .dataset import DataSet
30
+ from .table import Table
31
+ from ..logging_and_handling import setup_logger, setup_error_logging
32
+
33
+ """ LOGGING AND HANDLING """
34
+
35
+ # Create a logger
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Format the logger
39
+ logger = setup_logger(logger)
40
+
41
+ # Get an error logging decorator
42
+ error_logging = setup_error_logging(logger)
43
+
44
+ """ CLASS"""
45
+
46
+
47
+ # Class definition for Breakdown
48
+ class Breakdown(DataSet):
49
+
50
+ # Initialize
51
+ def __init__(self,
52
+ start_cell: str = '',
53
+ sheet: str = '',
54
+ conditional_aggregate: str = 'SUMIFS',
55
+ header: str = '',
56
+ results: Results = None):
57
+
58
+ # Run DataSet initialization
59
+ super().__init__(data=DataFrame(),
60
+ start_cell=start_cell,
61
+ sheet=sheet,
62
+ header=header,
63
+ results=results)
64
+
65
+ # Create attributes
66
+ self._data = DataFrame()
67
+ self.allowed_conditional_aggregates = ['SUMIFS',
68
+ 'COUNTIFS',
69
+ 'AVERAGEIFS',
70
+ 'MINIFS',
71
+ 'MAXIFS']
72
+ self._breakdown_cache = {}
73
+ self.length = len(self._data)
74
+
75
+ # If the conditional aggregate is not equal to an expected value...
76
+ if conditional_aggregate not in self.allowed_conditional_aggregates:
77
+ # Raise an error
78
+ raise ValueError(('Conditional aggregate not recognized: '
79
+ f'{conditional_aggregate}'))
80
+ # Otherwise, assign to an attribute
81
+ else:
82
+ self.conditional_aggregate = conditional_aggregate
83
+
84
+ """ PROPERTIES """
85
+ # Data properties - GETTER ONLY
86
+ # Getter
87
+ @property
88
+ def data(self) -> Any:
89
+ return self._data
90
+
91
+ # Setter
92
+ @data.setter
93
+ def data(self, value: DataFrame | None):
94
+ self._data = value
95
+
96
+ # Deleter
97
+ @data.deleter
98
+ def data(self):
99
+ del self._data
100
+ self._data = DataFrame()
101
+
102
+ # Sheet properties
103
+ # Getter
104
+ @property
105
+ def sheet(self) -> str:
106
+ return self._sheet
107
+
108
+ # Setter
109
+ @sheet.setter
110
+ def sheet(self, value: str):
111
+ if value == '':
112
+ raise ValueError('Breakdown sheet cannot be an empty string.')
113
+ self._sheet = value
114
+ if self._mediator is not None:
115
+ self._mediator.update_datasets()
116
+
117
+ # Deleter
118
+ @sheet.deleter
119
+ def sheet(self):
120
+ del self._sheet
121
+
122
+ # Start cell properties
123
+ # Getter
124
+ @property
125
+ def start_cell(self) -> str:
126
+ return self._start_cell
127
+
128
+ # Setter
129
+ @start_cell.setter
130
+ def start_cell(self, value: str):
131
+ # Try...
132
+ try:
133
+ # Get the cell's absolute indices
134
+ self.start_column, self.start_row = self.get_cell_indices(value)
135
+ # Set the starting cell
136
+ self._start_cell = value
137
+ # If an exception occurs...
138
+ except Exception as e:
139
+ raise ValueError(f'Passed start cell is not valid: {e}')
140
+ if self._mediator is not None:
141
+ self._mediator.update_datasets()
142
+
143
+ # Deleter
144
+ @start_cell.deleter
145
+ def start_cell(self):
146
+ del self._start_cell
147
+ del self.start_row, self.start_column
148
+
149
+ """ METHODS """
150
+ # Method to create a conditional aggregate formula based on criteria
151
+ def create_conditional_aggregate_formula(self,
152
+ table: Table,
153
+ criteria: dict[str, str],
154
+ summarize_column: str = ''
155
+ ):
156
+
157
+ # Create a blank formula template
158
+ formula_template = ''
159
+
160
+ # If the conditional aggregate is COUNTIFS...
161
+ if self.conditional_aggregate == 'COUNTIFS':
162
+
163
+ # Create a COUNTIFS-specific inner formula template
164
+ formula_template = ''
165
+
166
+ # Otherwise, if summarize column is not empty...
167
+ elif summarize_column != '':
168
+ # Create an inner formula template
169
+ formula_template = f'{table.reference[summarize_column]['range']}'
170
+
171
+ # Otherwise, raise an error
172
+ else:
173
+ raise ValueError(('Argument summarize_column cannot be blank for'
174
+ 'conditional aggregates besides COUNTIFS'))
175
+
176
+ # For every key-value pair in criteria...
177
+ for cell_or_range in criteria:
178
+
179
+ # Add criteria range to formula_template
180
+ formula_template += f', {cell_or_range}'
181
+ # Add criteria to formula_template
182
+ formula_template += f', {criteria[cell_or_range]}'
183
+
184
+ # Get the final formula by wrapping in the passed conditional aggregate
185
+ formula = f'={self.conditional_aggregate}({formula_template})'
186
+
187
+ return formula
188
+
189
+ # Method to create a one-dimensional breakdown
190
+ def create_1D(self,
191
+ table: Table,
192
+ group_by_column: str,
193
+ summarize_column: str,
194
+ groups_to_summarize: list[str] = None):
195
+
196
+ # If groups_to_summarize is not none...
197
+ if groups_to_summarize is not None:
198
+ # Set unique_groups to a shallow copy of groups_to_summarize
199
+ unique_groups = groups_to_summarize[:]
200
+
201
+ # Otherwise...
202
+ else:
203
+ # Get a list of unique values in Table in the group by column
204
+ # NOTE: IGNORES NONE
205
+ unique_groups = [group for group in
206
+ table.data[group_by_column].unique()
207
+ if group is not None]
208
+
209
+ # Sort the list of unique groups
210
+ unique_groups.sort()
211
+
212
+ # Create an empty 1D DataFrame using the unique groups
213
+ self._data = DataFrame({group: [None] for group in unique_groups})
214
+
215
+ # Create an index for the current header cell
216
+ header_cell_index = 0
217
+
218
+ # Get the start cell's absolute indices
219
+ absolute_start_col, absolute_start_row = \
220
+ self.get_cell_indices(self.start_cell)
221
+
222
+ # For every group...
223
+ for group in unique_groups:
224
+
225
+ # Get the current header's column and row
226
+ start_col = \
227
+ get_column_letter(absolute_start_col + 1 + header_cell_index)
228
+ start_row = \
229
+ absolute_start_row + 1
230
+
231
+ # Get the current header cell
232
+ header_cell = f'{start_col}${start_row}'
233
+
234
+ # Define the criteria dictionary
235
+ criteria = {table.reference[group_by_column]['range']: header_cell}
236
+
237
+ # Get a formula string
238
+ formula_string = \
239
+ self.create_conditional_aggregate_formula(table,
240
+ criteria,
241
+ summarize_column
242
+ )
243
+
244
+ # Add the formula string to the current group entry
245
+ self._data[group] = formula_string
246
+
247
+ # Iterate the header cell index
248
+ header_cell_index += 1
249
+
250
+ # Set the data length
251
+ self.length = len(self._data)
252
+
253
+ # Set the breakdown cache
254
+ self._breakdown_cache = {'function': self.create_1D,
255
+ 'arguments': {'table':
256
+ table,
257
+ 'group_by_column':
258
+ group_by_column,
259
+ 'summarize_column':
260
+ summarize_column,
261
+ 'groups_to_summarize':
262
+ groups_to_summarize}}
263
+
264
+ return None
265
+
266
+ # Method to create a two-dimensional breakdown
267
+ def create_2D(self,
268
+ table: Table,
269
+ group_by_col_1: str,
270
+ group_by_col_2: str,
271
+ summarize_column: str,
272
+ groups_to_summarize: dict[str, list[str]] = None):
273
+
274
+ # Create an empty dictionary to contain lists of unique groups
275
+ unique_groups = {group_by_col_1: [], group_by_col_2: []}
276
+
277
+ # If groups_to_summarize is not none...
278
+ if groups_to_summarize is not None:
279
+
280
+ # Get a list of booleans based on whether each key
281
+ # in groups_to_summarize matches a group_by_col
282
+ check_groups_list = [True if group_col in
283
+ [group_by_col_1, group_by_col_2]
284
+ else False for group_col in
285
+ groups_to_summarize]
286
+
287
+ # If groups to summarize is longer than two or contains a string
288
+ # that is not equal to either column string...
289
+ if len(groups_to_summarize) > 2 or not all(check_groups_list):
290
+
291
+ # Raise an error
292
+ raise ValueError('Unexpected keys in groups_to_summarize.')
293
+
294
+ # Otherwise, pass
295
+ else:
296
+ pass
297
+
298
+ # Create an empty dictionary if groups_to_summarize is None
299
+ groups_to_summarize = \
300
+ groups_to_summarize if groups_to_summarize is not None else {}
301
+
302
+ # For every entry in unique_groups...
303
+ for group_col in unique_groups:
304
+
305
+ # If the group column is present in groups_to_summarize...
306
+ if group_col in groups_to_summarize:
307
+
308
+ # Set unique_groups[group_col] to a shallow
309
+ # copy of groups_to_summarize entry
310
+ unique_groups[group_col] = groups_to_summarize[group_col][:]
311
+
312
+ # Otherwise...
313
+ else:
314
+ # Set unique_groups[group_col] to a list of unique values
315
+ # in the group by column
316
+ unique_groups[group_col] = [group for group in
317
+ table.data[group_col].unique()
318
+ if group is not None]
319
+
320
+ # Sort the list
321
+ unique_groups[group_col].sort()
322
+
323
+ # Create a new column for group_2 (row headers)
324
+ self._data[group_by_col_2] = \
325
+ [group for group in unique_groups[group_by_col_2]]
326
+
327
+ # Get the start cell's absolute indices
328
+ absolute_start_col, absolute_start_row = \
329
+ self.get_cell_indices(self.start_cell)
330
+
331
+ # Create column index to track current column
332
+ column_index = 0
333
+
334
+ # For every group in unique_groups group_by_1 (treat as columns)...
335
+ for group_1 in unique_groups[group_by_col_1]:
336
+
337
+ # Get the current column header's column and row
338
+ column_start_col = \
339
+ get_column_letter(absolute_start_col + 2 +
340
+ column_index)
341
+ column_start_row = \
342
+ absolute_start_row + 2
343
+
344
+ # Get the current column header cell
345
+ column_header_cell = f'{column_start_col}${column_start_row}'
346
+
347
+ # Create row index to track current row
348
+ row_index = 0
349
+
350
+ # For every group in unique_groups group_by_2 (treat as rows)...
351
+ for group_2 in unique_groups[group_by_col_2]:
352
+
353
+ # Get the current row header's column and row
354
+ row_start_col = \
355
+ get_column_letter(absolute_start_col + 1)
356
+ row_start_row = \
357
+ absolute_start_row + 3 + row_index
358
+
359
+ # Get the current row header cell
360
+ row_header_cell = f'${row_start_col}{row_start_row}'
361
+
362
+ # Define the criteria dictionary
363
+ criteria = {table.reference[group_by_col_1]['range']:
364
+ column_header_cell,
365
+ table.reference[group_by_col_2]['range']:
366
+ row_header_cell}
367
+
368
+ # Get a formula string
369
+ formula_string = \
370
+ self.create_conditional_aggregate_formula(table,
371
+ criteria,
372
+ summarize_column
373
+ )
374
+
375
+ # Add the formula string to the current group entry
376
+ self._data.at[row_index, group_1] = formula_string
377
+
378
+ # Iterate the row cell index
379
+ row_index += 1
380
+
381
+ # Iterate the header cell index
382
+ column_index += 1
383
+
384
+ # Set the data length
385
+ self.length = len(self._data)
386
+
387
+ # Set the breakdown cache
388
+ self._breakdown_cache = {'function': self.create_2D,
389
+ 'arguments': {'table':
390
+ table,
391
+ 'group_by_col_1':
392
+ group_by_col_1,
393
+ 'group_by_col_2':
394
+ group_by_col_2,
395
+ 'summarize_column':
396
+ summarize_column,
397
+ 'groups_to_summarize':
398
+ groups_to_summarize}}
399
+
400
+ return None
401
+
402
+ def merge_1D(self,
403
+ breakdown_list):
404
+
405
+ return None
406
+
407
+ def merge_2D(self,
408
+ breakdown_list):
409
+
410
+ return None
411
+
412
+ """ STATIC METHODS """
413
+
414
+ # Base conditional aggregate method
415
+ @staticmethod
416
+ def _wrap_conditional_aggregate(inner, outer):
417
+
418
+ # If the inner starts with an equals sign...
419
+ if inner[0] == '=':
420
+ # Remove the equals sign
421
+ inner = inner[1:]
422
+
423
+ # Otherwise, pass
424
+ else:
425
+ pass
426
+
427
+ # Get the formula string
428
+ formula_string = f"={outer}({inner})"
429
+
430
+ return formula_string
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ COPYRIGHT STATEMENT:
5
+
6
+ ChromaQuant – A quantification software for complex gas chromatographic data
7
+
8
+ Copyright (c) 2026, by Julia Hancock
9
+ Affiliation: Dr. Julie Elaine Rorrer
10
+ URL: https://www.rorrerlab.com/
11
+
12
+ License: BSD 3-Clause License
13
+
14
+ ---
15
+
16
+ CLASS DEFINITION FOR DATASETS
17
+
18
+ Julia Hancock
19
+ Started 1-12-2026
20
+
21
+ """
22
+
23
+ import logging
24
+ from openpyxl.utils.cell import coordinate_from_string, \
25
+ column_index_from_string
26
+ from typing import Any, TYPE_CHECKING
27
+ if TYPE_CHECKING:
28
+ from ..results import Results
29
+ import uuid
30
+ from ..logging_and_handling import setup_logger, setup_error_logging
31
+
32
+ """ LOGGING AND HANDLING """
33
+
34
+ # Create a logger
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # Format the logger
38
+ logger = setup_logger(logger)
39
+
40
+ # Get an error logging decorator
41
+ error_logging = setup_error_logging(logger)
42
+
43
+ """ CLASS """
44
+
45
+
46
+ # Define the DataSet class
47
+ class DataSet:
48
+
49
+ # Initialize
50
+ def __init__(self,
51
+ data: Any = float('nan'),
52
+ start_cell: str = '',
53
+ sheet: str = '',
54
+ type: str = 'DataSet',
55
+ header: str = '',
56
+ results: Results = None):
57
+
58
+ # Initialize instance attributes
59
+ self.type = type
60
+ self._start_cell = start_cell if start_cell != '' else '$A$1'
61
+ self._sheet = sheet if sheet != '' else 'Sheet1'
62
+ self._header = header
63
+ self._reference: dict[str, Any] = {}
64
+ self.start_column, self.start_row = \
65
+ self.get_cell_indices(self._start_cell)
66
+
67
+ # Initialize data attribute
68
+ self._data = data
69
+
70
+ # Create unique ID for the instance
71
+ self.id = str(uuid.uuid4())
72
+
73
+ # Add a mediator attribute
74
+ self._mediator = results
75
+
76
+ # Define the object representation by including its data
77
+ def __repr__(self):
78
+ return f"Contents of chromaquant.{self.type} Object:\n{self._data}"
79
+
80
+ """ PROPERTIES """
81
+ # Define the reference property, ONLY DEFINE GETTER
82
+ @property
83
+ def reference(self) -> dict[str, Any]:
84
+ return self._reference
85
+
86
+ # Data properties
87
+ # Getter
88
+ @property
89
+ def data(self) -> Any:
90
+ return self._data
91
+
92
+ # Setter
93
+ @data.setter
94
+ def data(self, value: Any) -> None:
95
+ self._data = value
96
+
97
+ # Deleter
98
+ @data.deleter
99
+ def data(self) -> None:
100
+ del self._data
101
+
102
+ # Sheet properties
103
+ # Getter
104
+ @property
105
+ def sheet(self) -> str:
106
+ return self._sheet
107
+
108
+ # Setter
109
+ @sheet.setter
110
+ def sheet(self, value: str):
111
+ if value == '':
112
+ raise ValueError('Table sheet cannot be an empty string.')
113
+ self._sheet = value
114
+ if self._mediator is not None:
115
+ self._mediator.update_datasets()
116
+
117
+ # Deleter
118
+ @sheet.deleter
119
+ def sheet(self):
120
+ del self._sheet
121
+
122
+ # Start cell properties
123
+ # Getter
124
+ @property
125
+ def start_cell(self) -> str:
126
+ return self._start_cell
127
+
128
+ # Setter
129
+ @start_cell.setter
130
+ def start_cell(self, value: str):
131
+ try:
132
+ # Get the cell's absolute indices
133
+ self.start_column, self.start_row = self.get_cell_indices(value)
134
+ # Set the starting cell
135
+ self._start_cell = value
136
+ except Exception as e:
137
+ raise ValueError(f'Passed start cell is not valid: {e}')
138
+ if self._mediator is not None:
139
+ self._mediator.update_datasets()
140
+
141
+ # Deleter
142
+ @start_cell.deleter
143
+ def start_cell(self):
144
+ del self._start_cell
145
+ del self.start_row, self.start_column
146
+
147
+ # Header properties
148
+ # Getter
149
+ @property
150
+ def header(self) -> str:
151
+ return self._header
152
+
153
+ # Setter
154
+ @header.setter
155
+ def header(self, value: str):
156
+ self._header = value
157
+ if self._mediator is not None:
158
+ self._mediator.update_datasets()
159
+
160
+ # Deleter
161
+ @header.deleter
162
+ def header(self):
163
+ del self._header
164
+ if self._mediator is not None:
165
+ self._mediator.update_datasets()
166
+
167
+ # Mediator properties
168
+ # Getter
169
+ @property
170
+ def mediator(self) -> str:
171
+ return self._mediator
172
+
173
+ # Setter
174
+ @mediator.setter
175
+ def mediator(self, mediator: Results):
176
+ self._mediator = mediator
177
+
178
+ """ STATIC METHODS """
179
+
180
+ # Static method to get the absolute indices of a cell
181
+ @staticmethod
182
+ def get_cell_indices(cell: str) -> tuple[int, int]:
183
+
184
+ # Split the coordinate
185
+ column_index, row_index = \
186
+ coordinate_from_string(cell)
187
+
188
+ # Get the absolute row by subtracting one
189
+ row_index = row_index - 1
190
+
191
+ # Get the column index from the string, adjusting to get absolute
192
+ column_index = \
193
+ column_index_from_string(column_index) - 1
194
+
195
+ return column_index, row_index