This package contains the CalCFU and Plate class for automatically calculating CFU counts under the NCIMS 2400 standards.
Project description
CalCFU
Table of Contents
Overview
These Python scripts calculate CFU counts for plating methods outlined in the NCIMS 2400 using two custom classes.
Plate
for storing plate characteristics.CalCFU
for the calculator logic.
While the calculation can be performed easily in most cases, this script allows for bulk-automated calculations where any dilution and number of plates can be used.
The code below outlines the entire process and references the NCIMS 2400s.
Getting Started
pip install calcfu
Plate
Plates are set up via the Plate
dataclass.
from calcfu import Plate
# 1 PAC plate with a 10^-2 dilution of a weighed sample yielding a total count of 234.
plates_1 = Plate(plate_type="PAC", count=234, dilution=-2, weighed=True, num_plts=1)
# 1 PAC plate with a 10^-3 dilution of a weighed sample yielding a total count of 53.
plates_2 = Plate(plate_type="PAC", count=53, dilution=-3, weighed=True, num_plts=1)
Fields
Each instance of the dataclass is created with five arguments which are set as fields.
Arguments:
plate_type
[ str ]- Plate type.
count
[ int ]- Raw plate counts.
dilution
[ str ]- Dilution used to plate.
weighed
[ bool ]- Sample was weighed or not.
num_plts
[ int ]- Number of plates for each dilution.
- By default, this is set to 1.
@dataclass(frozen=True, order=True)
class Plate(CalcConfig):
plate_type: str
count: int
dilution: int
weighed: bool
num_plts: int = 1
Class Variables
When an instance of the Plate
or CalCFU
class is created, it inherits from the CalcConfig
class which stores
all necessary configuration variables for the calculator.
-
PLATE_RANGES
[ dict ]- Acceptable counts for each plate type.
-
WEIGHED_UNITS
[ dict ]- Units for if weighed or not.
-
VALID_DILUTIONS
[ tuple ]- Acceptable dilutions for each plate type.
-
INPUT_VALIDATORS
[ dict ]- A dictionary of anonymous functions used to validate input arguments.
@dataclass(frozen=True, order=True)
class Plate(CalcConfig):
...
@dataclass(frozen=True, order=True)
class CalCFU(CalcConfig):
...
class CalcConfig:
# Logging/File Path Variables
...
VALID_DILUTIONS = (0, -1, -2, -3, -4)
PLATE_RANGES = {
"SPC": (25, 250),
"PAC": (25, 250),
"RAC": (25, 250),
"CPC": (1, 154),
"HSCC": (1, 154),
"PCC": (1, 154),
"YM": (),
"RYM": ()}
WEIGHED_UNITS = {True: " / g", False: " / mL"}
INPUT_VALIDATORS = {
# count must be an integer and greater than 0
"plate_type": lambda plate_type: plate_type in CalcConfig.PLATE_RANGES,
"count": lambda count: isinstance(count, int) and count > 0,
# dilution must be in valid dilutions
"dilution": lambda dilution: dilution in CalcConfig.VALID_DILUTIONS,
"weighed": lambda weighed: isinstance(weighed, bool),
# num_plts must be an integer and greater than 0
"num_plts": lambda num_plts: isinstance(num_plts, int) and num_plts > 0,
# plates must all be an instance of the Plate dataclass and must be all the same plate_type
"plates": lambda plates, plt_cls: all(isinstance(plate, plt_cls) and plate.plate_type == plates[0].plate_type
for plate in plates),
"all_weighed": lambda plates: all(plates[0].weighed == plate.weighed for plate in plates)}
Field Validation
Arguments/fields are validated via a __post_init__
method where each key is checked
against conditions in self.INPUT_VALIDATORS
# post init dunder method for validation
def __post_init__(self):
for key, value in asdict(self).items():
assert self.INPUT_VALIDATORS[key](value), \
"Invalid value. Check calc_config.py."
Properties
Properties are also defined to allow for read-only calculation of attributes from the input arguments.
@property
def cnt_range(self):
# self.cnt_range[0] is min, self.cnt_range[1] is max
return self.PLATE_RANGES.get(self.plate_type, None)
@property
def in_between(self):
if self.cnt_range[0] <= self.count <= self.cnt_range[1]:
return True
else:
return False
@property
def sign(self):
if 0 <= self.count < self.cnt_range[0]:
return "<"
elif self.count > self.cnt_range[1]:
return ">"
else:
return ""
@property
def _bounds_abs_diff(self):
# Dict of bounds and their abs difference between the number of colonies.
return {bound: abs(self.count - bound) for bound in self.cnt_range}
@property
def hbound_abs_diff(self):
return abs(self.count - self.cnt_range[1])
@property
def closest_bound(self):
# return closest bound based on min abs diff between count and bound
return min(self._bounds_abs_diff, key=self._bounds_abs_diff.get)
cnt_range
[ tuple: 2 ]- Countable colony numbers for a plate type.
in_between
[ bool ]- If
count
within countable range.
- If
sign
[ str ]- Sign for reported count if all
count
values are outside the acceptable range. - Used in
CalCFU._calc_no_dil_valid
.
- Sign for reported count if all
_bounds_abs_diff
[ dict ]- Absolute differences of
count
and low and highcnt_ranges
.
- Absolute differences of
hbound_abs_diff
[ int ]- Absolute difference of
count
and high ofcnt_range
.
- Absolute difference of
closest_bound
[ int ]- Closest count in
cnt_range
tocount
. - Based on minimum absolute difference between
count
andcnt_range
s. The smaller the difference, the closer thecount
is to a bound.
- Closest count in
CalCFU
The calculator is contained in the CalCFU
dataclass.
Using the previously created Plate
instances, a CalCFU
instance is created.
from calcfu import CalCFU
# Setup calculator with two PAC plates that contain a weighed sample.
calc = CalCFU(plates=[plates_1, plates_2])
Fields
Each instance of CountCalculator is initialized with a list of the plates to be calculated:
Arguments:
plates
[ list ]- Contains Plate instances.
- Validated via
__post_init__
method.
plate_ids
[ list ]- Optional
- Contains list of plate IDs.
- Used to identify incorrect plates in error message.
@dataclass(frozen=True, order=True)
class CalCFU(CalcConfig):
plates: List
plate_ids: Optional[List] = None
Properties
@property
def valid_plates(self):
return [plate for plate in self.plates if plate.in_between]
@property
def reported_units(self):
# grab first plate and use plate type. should be all the same
return f"{self.plates[0].plate_type}{self.WEIGHED_UNITS.get(self.plates[0].weighed)}"
valid_plates
[ list ]- Plates that have acceptable counts for their plate type.
reported_units
[ str ]- Units based on plate type and if weighed.
- Estimated letter added in
self.calculate()
Methods
Two methods are available for use with the CountCalculator instance:
calculate
bank_round
calculate(self)
This method is the "meat-and-potatoes" of the script. It calculates the reported/adjusted count based on the plates given.
Optional arguments:
round_to
[ int ]- Digit to round to. Default is 1.
- Relative to leftmost digit (0). Python is 0 indexed.
- ex. Round to 1: 2(5),666
- ex. Round to 3: 25,6(6)6
- Digit to round to. Default is 1.
report_count
[ bool ]- Option to return reported count or unrounded, unlabeled adjusted count.
First, each plate is checked to see if its count is in between the accepted count range. Based on the number of valid plates, a different method is used to calculate the result.
def calculate(self, round_to=2, report_count=True):
valid_plates = self.valid_plates
# assign empty str to sign var. will be default unless no plate valid
sign = ""
# track if estimated i.e. no plate is valid.
estimated = False
if len(valid_plates) == 0:
sign, adj_count = self._calc_no_dil_valid()
estimated = True
elif len(valid_plates) == 1:
# only one plate is valid so multiple by reciprocal of dil.
valid_plate = valid_plates[0]
adj_count = valid_plate.count * (10 ** abs(valid_plate.dilution))
else:
adj_count = self._calc_multi_dil_valid()
if report_count:
units = f"{('' if not estimated else 'e')}{self.reported_units}"
# add sign, thousands separator, and units
return f"{sign}{'{:,}'.format(self.bank_round(adj_count, round_to))} {units}"
else:
return adj_count
_calc_no_dil_valid(self, report_count)
This function runs when no plates have valid counts.
Arguments:
report_count
[ bool ]- Same as
calculate
.
- Same as
Procedure:
- Reduce the
self.plates
down to one plate by checking adjacent plates'hbound_abs_diff
.- The plate with the smallest difference is closest to the highest acceptable count bound.
Ex. |267 - 250| = 17 and |275 - 250| = 25
17 < 25 so 267 is closer to 250 than 275.
- Set throwaway variable
value
tocount
orclosest_bound
based on if reported count needed. - Finally, return
sign
and multiply the closest bound by the reciprocal of the dilution.
def _calc_no_dil_valid(self, report_count):
# Use reduce to reduce plates to a single plate:
# plate with the lowest absolute difference between the hbound and value
closest_to_hbound = reduce(lambda p1, p2: min(p1, p2, key=lambda x: x.hbound_abs_diff), self.plates)
# if reporting, use closest bound; otherwise, use count.
value = closest_to_hbound.closest_bound if report_count else closest_to_hbound.count
return closest_to_hbound.sign, value * (10 ** abs(closest_to_hbound.dilution))
_calc_multi_dil_valid(self)
This method runs if multiple plates have valid counts.
Variables:
main_dil
[ int ]- The lowest dilution of the
valid_plates
.
- The lowest dilution of the
dil_weights
[ list ]- The weights each dilution/plate contributes to the total count.
div_factor
[ int ]- The sum of
dil_weights
. Part of the denominator of the weighted averaged.
- The sum of
Procedure:
- First, sum counts from all valid plates (
plates_1
andplates_2
).1 - If all plates are the same dilution, set
div_factor
to the total number of valid plates.- Each plate has equal weight in
div_factor
. - NCIMS 2400a.16.l.1 | NCIMS 2400a-4.17.e
- Each plate has equal weight in
- Otherwise, we will take a weighted average taking into account how each dilution contributes to the
div_factor
.2 - Each dilution will have a weight of how much it contributes to the total count (via the
div_factor
)- If the plate dilution is the
main_dil
, set the dilution's weight to 1.- This value is the
main_dil
's weight towards the total count. - The least diluted plate contributes the largest number of colonies to the overall count. It will always be 1 and serves to normalize the effect of the other dilutions.
- NCIMS 2400a.16.l.1 | NCIMS 2400a-4.17.e
- This value is the
- If it is not, subtract the absolute value of
main_dil
by the absolute value ofplate.dilution
.- By raising 10 to the power of
abs_diff_dil
, the plate dilution's weight - relative tomain_dil
- is calculated.
- By raising 10 to the power of
- If the plate dilution is the
- Each dilution weight is then multiplied by the number of plates used for that dilution.
- The sum of all dilution weights in
dil_weights
is the division weight,div_factor
. - Dividing the
total
by the product ofdiv_factor
andmain_dil
yields the adjusted count.3
Figure 1. Sum of counts from all valid plates (Step 2) |
Figure 2. Weighted average formula. (Step 3) |
Figure 3. Adjusted count formula. (Step 7) |
def _calc_multi_dil_valid(self):
valid_plates = self.valid_plates
total = sum(plate.count for plate in valid_plates)
main_dil = max(plate.dilution for plate in valid_plates)
# If all plates have the same dilution.
if all(plate.dilution == valid_plates[0].dilution for plate in valid_plates):
# each plates is equally weighed because all the same dil
div_factor = sum(1 * plate.num_plts for plate in valid_plates)
else:
dil_weights = []
for plate in valid_plates:
if plate.dilution == main_dil:
dil_weights.append(1 * plate.num_plts)
else:
# calculate dil weight relative to main_dil
abs_diff_dil = abs(main_dil) - abs(plate.dilution)
dil_weights.append((10 ** abs_diff_dil) * plate.num_plts)
div_factor = sum(dil_weights)
return int(total / (div_factor * (10 ** main_dil)))
Once a value is returned...
bank_round(value, place_from_left)
This method rounds values using banker's rounding. String manipulation was used rather than working with floats to avoid rounding errors.
Arguments:
value
[ int ]- Value to be rounded.
place_from_left
[ int ]- Digit to round to. Leftmost digit is 1 (NOT 0).
- See
calculate()
for examples.
Variables:
value_len
[ int ]- Len of string value.
str_abbr_value
[ str ]- Abbreviated value as string.
- Sliced to only allow 1 digit before rounded digit.
- Python rounding behavior changes based on digits after.
- NCIMS 2400a.19.c.1.a-b | NCIMS 2400a-4.19.d.1.a-b
str_padded_value
[ str ]- Zero-padded value as string.
adj_value
[ int ]- Abbreviated, padded value as integer.
adj_place_from_left
- Adjusted index for base python
round()
. Needs to be ndigits from decimal point. Ex. round(2(1)5., -1) -> 220
- Adjusted index for base python
final_val
[ int ]- Rounded value.
@staticmethod
def bank_round(value, place_from_left):
if isinstance(value, int) and isinstance(place_from_left, int):
# Length of unrounded value.
value_len = len(str(value))
# remove digits that would alter rounding only allowing 1 digit before desired place
str_abbr_value = str(value)[0:place_from_left + 1]
# pad with 0's equal to number of removed digits
str_padded_value = str_abbr_value + ("0" * (value_len - len(str_abbr_value)))
adj_value = int(str_padded_value)
# reindex place_from_left for round function.
# place_from_left = 2 for 2(5)553. to round, needs to be -3 so subtract length by place and multiply by -1.
adj_place_from_left = -1 * (value_len - place_from_left)
final_val = round(adj_value, adj_place_from_left)
return final_val
else:
raise ValueError("Invalid value or place (Not an integer).")
Example:
result = bank_round(value=24553, place_from_left=2)
- Find the length of the value as a string.
value_len=5
- Abbreviate value as string.
str_abbr_value="245"
- Pad value as string out to original length.
str_padded_value="24500"
- Convert padded value back to a number.
adj_value=24500
- Reindex
place_from_left
for theround
function.adj_place_from_left=-3
- Round
adj_value
withplace_from_left
, the new index.result=24000
References
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file calcfu-1.0.1.tar.gz
.
File metadata
- Download URL: calcfu-1.0.1.tar.gz
- Upload date:
- Size: 14.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.7
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4aa7bf346000a2ee3f9ea29fe9459393a03ff64611dc3468f61d8afc74c425ef |
|
MD5 | 59c06ce14153aaaee6be188678078a03 |
|
BLAKE2b-256 | f7afd4db6a5ec89891001bc9f24c5ccde594381d6da6d4ecfa104f04846d21e3 |
File details
Details for the file calcfu-1.0.1-py3-none-any.whl
.
File metadata
- Download URL: calcfu-1.0.1-py3-none-any.whl
- Upload date:
- Size: 13.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.7
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 719a427ed0bfe821de3af3fc84cb0df9652a22712ae8c9da9b8e65a166e8c126 |
|
MD5 | 7edda44161139078a1b6e292ed2877dd |
|
BLAKE2b-256 | 46d05b4d5d1800da18c7efbfb37da2cc0790b261b52588a516e31898ed810b73 |