1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of SENAITE.CORE. |
4
|
|
|
# |
5
|
|
|
# SENAITE.CORE is free software: you can redistribute it and/or modify it under |
6
|
|
|
# the terms of the GNU General Public License as published by the Free Software |
7
|
|
|
# Foundation, version 2. |
8
|
|
|
# |
9
|
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT |
10
|
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
11
|
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
12
|
|
|
# details. |
13
|
|
|
# |
14
|
|
|
# You should have received a copy of the GNU General Public License along with |
15
|
|
|
# this program; if not, write to the Free Software Foundation, Inc., 51 |
16
|
|
|
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
17
|
|
|
# |
18
|
|
|
# Copyright 2018-2021 by it's authors. |
19
|
|
|
# Some rights reserved, see README and LICENSE. |
20
|
|
|
|
21
|
|
|
import math |
22
|
|
|
|
23
|
|
|
from bika.lims import api |
24
|
|
|
from bika.lims.interfaces import IAnalysisService |
25
|
|
|
from bika.lims.interfaces import IBaseAnalysis |
26
|
|
|
from bika.lims.interfaces import IReferenceSample |
27
|
|
|
from bika.lims.interfaces.analysis import IRequestAnalysis |
28
|
|
|
from bika.lims.utils import formatDecimalMark |
29
|
|
|
|
30
|
|
|
|
31
|
|
|
def create_analysis(context, source, **kwargs): |
32
|
|
|
"""Create a new Analysis. The source can be an Analysis Service or |
33
|
|
|
an existing Analysis, and all possible field values will be set to the |
34
|
|
|
values found in the source object. |
35
|
|
|
:param context: The analysis will be created inside this object. |
36
|
|
|
:param source: The schema of this object will be used to populate analysis. |
37
|
|
|
:param kwargs: The values of any keys which match schema fieldnames will |
38
|
|
|
be inserted into the corresponding fields in the new analysis. |
39
|
|
|
:returns: Analysis object that was created |
40
|
|
|
:rtype: Analysis |
41
|
|
|
""" |
42
|
|
|
# Ensure we have an object as source |
43
|
|
|
source = api.get_object(source) |
44
|
|
|
if not IBaseAnalysis.providedBy(source): |
45
|
|
|
raise ValueError("Type not supported: {}".format(repr(type(source)))) |
46
|
|
|
|
47
|
|
|
# compute the id of the new analysis if necessary |
48
|
|
|
analysis_id = kwargs.get("id") |
49
|
|
|
if not analysis_id: |
50
|
|
|
keyword = source.getKeyword() |
51
|
|
|
analysis_id = generate_analysis_id(context, keyword) |
52
|
|
|
|
53
|
|
|
# get the service to be assigned to the analysis |
54
|
|
|
service = source |
55
|
|
|
if not IAnalysisService.providedBy(source): |
56
|
|
|
service = source.getAnalysisService() |
57
|
|
|
|
58
|
|
|
# use "Analysis" as portal_type unless explicitly set |
59
|
|
|
portal_type = kwargs.pop("portal_type", "Analysis") |
60
|
|
|
|
61
|
|
|
# initialize interims with those from the service if not explicitly set |
62
|
|
|
interim_fields = kwargs.pop("InterimFields", service.getInterimFields()) |
63
|
|
|
|
64
|
|
|
# do not copy these fields from source |
65
|
|
|
skip_fields = [ |
66
|
|
|
"Hidden", |
67
|
|
|
"Attachment", |
68
|
|
|
"Result", |
69
|
|
|
"ResultCaptureDate", |
70
|
|
|
"Worksheet" |
71
|
|
|
] |
72
|
|
|
|
73
|
|
|
kwargs.update({ |
74
|
|
|
"container": context, |
75
|
|
|
"portal_type": portal_type, |
76
|
|
|
"skip": skip_fields, |
77
|
|
|
"id": analysis_id, |
78
|
|
|
"AnalysisService": service, |
79
|
|
|
"InterimFields": interim_fields, |
80
|
|
|
}) |
81
|
|
|
return api.copy_object(source, **kwargs) |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
def get_significant_digits(numeric_value): |
85
|
|
|
""" |
86
|
|
|
Returns the precision for a given floatable value. |
87
|
|
|
If value is None or not floatable, returns None. |
88
|
|
|
Will return positive values if the result is below 1 and will |
89
|
|
|
return 0 values if the result is above or equal to 1. |
90
|
|
|
:param numeric_value: the value to get the precision from |
91
|
|
|
:returns: the numeric_value's precision |
92
|
|
|
Examples: |
93
|
|
|
numeric_value Returns |
94
|
|
|
0 0 |
95
|
|
|
0.22 1 |
96
|
|
|
1.34 0 |
97
|
|
|
0.0021 3 |
98
|
|
|
0.013 2 |
99
|
|
|
2 0 |
100
|
|
|
22 0 |
101
|
|
|
""" |
102
|
|
|
try: |
103
|
|
|
numeric_value = float(numeric_value) |
104
|
|
|
except (TypeError, ValueError): |
105
|
|
|
return None |
106
|
|
|
if numeric_value == 0: |
107
|
|
|
return 0 |
108
|
|
|
significant_digit = int(math.floor(math.log10(abs(numeric_value)))) |
109
|
|
|
return 0 if significant_digit > 0 else abs(significant_digit) |
110
|
|
|
|
111
|
|
|
|
112
|
|
|
def _format_decimal_or_sci(result, precision, threshold, sciformat): |
113
|
|
|
# Current result's precision is above the threshold? |
114
|
|
|
sig_digits = get_significant_digits(result) |
115
|
|
|
|
116
|
|
|
# Note that if result < 1, sig_digits > 0. Otherwise, sig_digits = 0 |
117
|
|
|
# Eg: |
118
|
|
|
# result = 0.2 -> sig_digit = 1 |
119
|
|
|
# 0.002 -> sig_digit = 3 |
120
|
|
|
# 0 -> sig_digit = 0 |
121
|
|
|
# 2 -> sig_digit = 0 |
122
|
|
|
# See get_significant_digits signature for further details! |
123
|
|
|
# |
124
|
|
|
# Also note if threshold is negative, the result will always be expressed |
125
|
|
|
# in scientific notation: |
126
|
|
|
# Eg. |
127
|
|
|
# result=12345, threshold=-3, sig_digit=0 -> 1.2345e4 = 1.2345·10⁴ |
128
|
|
|
# |
129
|
|
|
# So, if sig_digits is > 0, the power must be expressed in negative |
130
|
|
|
# Eg. |
131
|
|
|
# result=0.0012345, threshold=3, sig_digit=3 -> 1.2345e-3=1.2345·10-³ |
132
|
|
|
sci = sig_digits >= threshold and abs( |
133
|
|
|
threshold) > 0 and sig_digits <= precision |
134
|
|
|
sign = '-' if sig_digits > 0 else '' |
135
|
|
|
if sig_digits == 0 and abs(threshold) > 0 and abs(int(float(result))) > 0: |
136
|
|
|
# Number >= 1, need to check if the number of non-decimal |
137
|
|
|
# positions is above the threshold |
138
|
|
|
sig_digits = int(math.log(abs(float(result)), 10)) if abs( |
139
|
|
|
float(result)) >= 10 else 0 |
140
|
|
|
sci = sig_digits >= abs(threshold) |
141
|
|
|
|
142
|
|
|
formatted = '' |
143
|
|
|
if sci: |
144
|
|
|
# First, cut the extra decimals according to the precision |
145
|
|
|
prec = precision if precision and precision > 0 else 0 |
146
|
|
|
nresult = str("%%.%sf" % prec) % api.to_float(result, 0) |
147
|
|
|
|
148
|
|
|
if sign: |
149
|
|
|
# 0.0012345 -> 1.2345 |
150
|
|
|
res = float(nresult) * (10 ** sig_digits) |
151
|
|
|
else: |
152
|
|
|
# Non-decimal positions |
153
|
|
|
# 123.45 -> 1.2345 |
154
|
|
|
res = float(nresult) / (10 ** sig_digits) |
155
|
|
|
res = int(res) if res.is_integer() else res |
156
|
|
|
|
157
|
|
|
# Scientific notation |
158
|
|
|
if sciformat == 2: |
159
|
|
|
# ax10^b or ax10^-b |
160
|
|
|
formatted = "%s%s%s%s" % (res, "x10^", sign, sig_digits) |
161
|
|
|
elif sciformat == 3: |
162
|
|
|
# ax10<super>b</super> or ax10<super>-b</super> |
163
|
|
|
formatted = "%s%s%s%s%s" % ( |
164
|
|
|
res, "x10<sup>", sign, sig_digits, "</sup>") |
165
|
|
|
elif sciformat == 4: |
166
|
|
|
# ax10^b or ax10^-b |
167
|
|
|
formatted = "%s%s%s%s" % (res, "·10^", sign, sig_digits) |
168
|
|
|
elif sciformat == 5: |
169
|
|
|
# ax10<super>b</super> or ax10<super>-b</super> |
170
|
|
|
formatted = "%s%s%s%s%s" % ( |
171
|
|
|
res, "·10<sup>", sign, sig_digits, "</sup>") |
172
|
|
|
else: |
173
|
|
|
# Default format: aE^+b |
174
|
|
|
sig_digits = "%02d" % sig_digits |
175
|
|
|
formatted = "%s%s%s%s" % (res, "e", sign, sig_digits) |
176
|
|
|
else: |
177
|
|
|
# Decimal notation |
178
|
|
|
prec = precision if precision and precision > 0 else 0 |
179
|
|
|
formatted = str("%%.%sf" % prec) % api.to_float(result, 0) |
180
|
|
|
if float(formatted) == 0 and '-' in formatted: |
181
|
|
|
# We don't want things like '-0.00' |
182
|
|
|
formatted = formatted.replace('-', '') |
183
|
|
|
return formatted |
184
|
|
|
|
185
|
|
|
|
186
|
|
|
def format_uncertainty(analysis, decimalmark=".", sciformat=1): |
187
|
|
|
"""Return formatted uncertainty value |
188
|
|
|
|
189
|
|
|
If the "Calculate precision from uncertainties" is enabled in |
190
|
|
|
the Analysis service, and |
191
|
|
|
|
192
|
|
|
a) If the non-decimal number of digits of the result is above |
193
|
|
|
the service's ExponentialFormatPrecision, the uncertainty will |
194
|
|
|
be formatted in scientific notation. The uncertainty exponential |
195
|
|
|
value used will be the same as the one used for the result. The |
196
|
|
|
uncertainty will be rounded according to the same precision as |
197
|
|
|
the result. |
198
|
|
|
|
199
|
|
|
Example: |
200
|
|
|
Given an Analysis with an uncertainty of 37 for a range of |
201
|
|
|
results between 30000 and 40000, with an |
202
|
|
|
ExponentialFormatPrecision equal to 4 and a result of 32092, |
203
|
|
|
this method will return 0.004E+04 |
204
|
|
|
|
205
|
|
|
b) If the number of digits of the integer part of the result is |
206
|
|
|
below the ExponentialFormatPrecision, the uncertainty will be |
207
|
|
|
formatted as decimal notation and the uncertainty will be |
208
|
|
|
rounded one position after reaching the last 0 (precision |
209
|
|
|
calculated according to the uncertainty value). |
210
|
|
|
|
211
|
|
|
Example: |
212
|
|
|
Given an Analysis with an uncertainty of 0.22 for a range of |
213
|
|
|
results between 1 and 10 with an ExponentialFormatPrecision |
214
|
|
|
equal to 4 and a result of 5.234, this method will return 0.2 |
215
|
|
|
|
216
|
|
|
If the "Calculate precision from Uncertainties" is disabled in the |
217
|
|
|
analysis service, the same rules described above applies, but the |
218
|
|
|
precision used for rounding the uncertainty is not calculated from |
219
|
|
|
the uncertainty neither the result. The fixed length precision is |
220
|
|
|
used instead. |
221
|
|
|
|
222
|
|
|
If the result is not floatable or no uncertainty defined, returns |
223
|
|
|
an empty string. |
224
|
|
|
|
225
|
|
|
The default decimal mark '.' will be replaced by the decimalmark |
226
|
|
|
specified. |
227
|
|
|
|
228
|
|
|
:param analysis: the analysis from which the uncertainty, precision |
229
|
|
|
and other additional info have to be retrieved |
230
|
|
|
:param decimalmark: decimal mark to use. By default '.' |
231
|
|
|
:param sciformat: 1. The sci notation has to be formatted as aE^+b |
232
|
|
|
2. The sci notation has to be formatted as ax10^b |
233
|
|
|
3. As 2, but with super html entity for exp |
234
|
|
|
4. The sci notation has to be formatted as a·10^b |
235
|
|
|
5. As 4, but with super html entity for exp |
236
|
|
|
By default 1 |
237
|
|
|
:returns: the formatted uncertainty |
238
|
|
|
""" |
239
|
|
|
try: |
240
|
|
|
result = float(analysis.getResult()) |
241
|
|
|
except (ValueError, TypeError): |
242
|
|
|
pass |
243
|
|
|
|
244
|
|
|
uncertainty = analysis.getUncertainty() |
245
|
|
|
|
246
|
|
|
if not uncertainty: |
247
|
|
|
return "" |
248
|
|
|
|
249
|
|
|
# always convert exponential notation to decimal |
250
|
|
|
if "e" in uncertainty.lower(): |
251
|
|
|
uncertainty = api.float_to_string(float(uncertainty)) |
252
|
|
|
|
253
|
|
|
precision = -1 |
254
|
|
|
# always get full precision of the uncertainty if user entered manually |
255
|
|
|
# => avoids rounding and cut-off |
256
|
|
|
allow_manual = analysis.getAllowManualUncertainty() |
257
|
|
|
manual_value = analysis.getField("Uncertainty").get(analysis) |
258
|
|
|
if allow_manual and manual_value: |
259
|
|
|
precision = uncertainty[::-1].find(".") |
260
|
|
|
|
261
|
|
|
if precision == -1: |
262
|
|
|
precision = analysis.getPrecision(result) |
263
|
|
|
|
264
|
|
|
# Scientific notation? |
265
|
|
|
# Get the default precision for scientific notation |
266
|
|
|
threshold = analysis.getExponentialFormatPrecision() |
267
|
|
|
formatted = _format_decimal_or_sci( |
268
|
|
|
uncertainty, precision, threshold, sciformat) |
269
|
|
|
|
270
|
|
|
# strip off trailing zeros and the orphane dot, |
271
|
|
|
# e.g.: 1.000000 -> 1 |
272
|
|
|
if "." in formatted: |
273
|
|
|
formatted = formatted.rstrip("0").rstrip(".") |
274
|
|
|
|
275
|
|
|
return formatDecimalMark(formatted, decimalmark) |
276
|
|
|
|
277
|
|
|
|
278
|
|
|
def format_numeric_result(analysis, result, decimalmark='.', sciformat=1): |
279
|
|
|
""" |
280
|
|
|
Returns the formatted number part of a results value. This is |
281
|
|
|
responsible for deciding the precision, and notation of numeric |
282
|
|
|
values in accordance to the uncertainty. If a non-numeric |
283
|
|
|
result value is given, the value will be returned unchanged. |
284
|
|
|
|
285
|
|
|
The following rules apply: |
286
|
|
|
|
287
|
|
|
If the "Calculate precision from uncertainties" is enabled in |
288
|
|
|
the Analysis service, and |
289
|
|
|
|
290
|
|
|
a) If the non-decimal number of digits of the result is above |
291
|
|
|
the service's ExponentialFormatPrecision, the result will |
292
|
|
|
be formatted in scientific notation. |
293
|
|
|
|
294
|
|
|
Example: |
295
|
|
|
Given an Analysis with an uncertainty of 37 for a range of |
296
|
|
|
results between 30000 and 40000, with an |
297
|
|
|
ExponentialFormatPrecision equal to 4 and a result of 32092, |
298
|
|
|
this method will return 3.2092E+04 |
299
|
|
|
|
300
|
|
|
b) If the number of digits of the integer part of the result is |
301
|
|
|
below the ExponentialFormatPrecision, the result will be |
302
|
|
|
formatted as decimal notation and the resulta will be rounded |
303
|
|
|
in accordance to the precision (calculated from the uncertainty) |
304
|
|
|
|
305
|
|
|
Example: |
306
|
|
|
Given an Analysis with an uncertainty of 0.22 for a range of |
307
|
|
|
results between 1 and 10 with an ExponentialFormatPrecision |
308
|
|
|
equal to 4 and a result of 5.234, this method will return 5.2 |
309
|
|
|
|
310
|
|
|
If the "Calculate precision from Uncertainties" is disabled in the |
311
|
|
|
analysis service, the same rules described above applies, but the |
312
|
|
|
precision used for rounding the result is not calculated from |
313
|
|
|
the uncertainty. The fixed length precision is used instead. |
314
|
|
|
|
315
|
|
|
For further details, visit |
316
|
|
|
https://jira.bikalabs.com/browse/LIMS-1334 |
317
|
|
|
|
318
|
|
|
The default decimal mark '.' will be replaced by the decimalmark |
319
|
|
|
specified. |
320
|
|
|
|
321
|
|
|
:param analysis: the analysis from which the uncertainty, precision |
322
|
|
|
and other additional info have to be retrieved |
323
|
|
|
:param result: result to be formatted. |
324
|
|
|
:param decimalmark: decimal mark to use. By default '.' |
325
|
|
|
:param sciformat: 1. The sci notation has to be formatted as aE^+b |
326
|
|
|
2. The sci notation has to be formatted as ax10^b |
327
|
|
|
3. As 2, but with super html entity for exp |
328
|
|
|
4. The sci notation has to be formatted as a·10^b |
329
|
|
|
5. As 4, but with super html entity for exp |
330
|
|
|
By default 1 |
331
|
|
|
:result: should be a string to preserve the decimal precision. |
332
|
|
|
:returns: the formatted result as string |
333
|
|
|
""" |
334
|
|
|
try: |
335
|
|
|
result = float(result) |
336
|
|
|
except ValueError: |
337
|
|
|
return result |
338
|
|
|
|
339
|
|
|
# continuing with 'nan' result will cause formatting to fail. |
340
|
|
|
if math.isnan(result): |
341
|
|
|
return result |
342
|
|
|
|
343
|
|
|
# Scientific notation? |
344
|
|
|
# Get the default precision for scientific notation |
345
|
|
|
threshold = analysis.getExponentialFormatPrecision() |
346
|
|
|
precision = analysis.getPrecision(result) |
347
|
|
|
formatted = _format_decimal_or_sci(result, precision, threshold, sciformat) |
348
|
|
|
return formatDecimalMark(formatted, decimalmark) |
349
|
|
|
|
350
|
|
|
|
351
|
|
|
def create_retest(analysis, **kwargs): |
352
|
|
|
"""Creates a retest of the given analysis |
353
|
|
|
""" |
354
|
|
|
if not IRequestAnalysis.providedBy(analysis): |
355
|
|
|
raise ValueError("Type not supported: {}".format(repr(type(analysis)))) |
356
|
|
|
|
357
|
|
|
# Create a copy of the original analysis |
358
|
|
|
parent = api.get_parent(analysis) |
359
|
|
|
kwargs.update({ |
360
|
|
|
"portal_type": api.get_portal_type(analysis), |
361
|
|
|
"RetestOf": analysis, |
362
|
|
|
}) |
363
|
|
|
retest = create_analysis(parent, analysis, **kwargs) |
364
|
|
|
|
365
|
|
|
# Add the retest to the same worksheet, if any |
366
|
|
|
worksheet = analysis.getWorksheet() |
367
|
|
|
if worksheet: |
368
|
|
|
worksheet.addAnalysis(retest) |
369
|
|
|
|
370
|
|
|
return retest |
371
|
|
|
|
372
|
|
|
|
373
|
|
|
def create_duplicate(analysis, **kwargs): |
374
|
|
|
"""Creates a duplicate of the given analysis |
375
|
|
|
""" |
376
|
|
|
if not IRequestAnalysis.providedBy(analysis): |
377
|
|
|
raise ValueError("Type not supported: {}".format(repr(type(analysis)))) |
378
|
|
|
|
379
|
|
|
worksheet = analysis.getWorksheet() |
380
|
|
|
if not worksheet: |
381
|
|
|
raise ValueError("Cannot create a duplicate without worksheet") |
382
|
|
|
|
383
|
|
|
sample_id = analysis.getRequestID() |
384
|
|
|
kwargs.update({ |
385
|
|
|
"portal_type": "DuplicateAnalysis", |
386
|
|
|
"Analysis": analysis, |
387
|
|
|
"Worksheet": worksheet, |
388
|
|
|
"ReferenceAnalysesGroupID": "{}-D".format(sample_id), |
389
|
|
|
}) |
390
|
|
|
|
391
|
|
|
return create_analysis(worksheet, analysis, **kwargs) |
392
|
|
|
|
393
|
|
|
|
394
|
|
|
def create_reference_analysis(reference_sample, source, **kwargs): |
395
|
|
|
"""Creates a reference analysis inside the referencesample |
396
|
|
|
""" |
397
|
|
|
ref = api.get_object(reference_sample) |
398
|
|
|
if not IReferenceSample.providedBy(ref): |
399
|
|
|
raise ValueError("Type not supported: {}".format(repr(type(ref)))) |
400
|
|
|
|
401
|
|
|
# Set the type of the reference analysis |
402
|
|
|
ref_type = "b" if ref.getBlank() else "c" |
403
|
|
|
kwargs.update({ |
404
|
|
|
"portal_type": "ReferenceAnalysis", |
405
|
|
|
"ReferenceType": ref_type, |
406
|
|
|
}) |
407
|
|
|
return create_analysis(ref, source, **kwargs) |
408
|
|
|
|
409
|
|
|
|
410
|
|
|
def generate_analysis_id(instance, keyword): |
411
|
|
|
"""Generates a new analysis ID |
412
|
|
|
""" |
413
|
|
|
count = 1 |
414
|
|
|
new_id = keyword |
415
|
|
|
while new_id in instance.objectIds(): |
416
|
|
|
new_id = "{}-{}".format(keyword, count) |
417
|
|
|
count += 1 |
418
|
|
|
return new_id |
419
|
|
|
|