Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add channel_labels #323

Merged
merged 6 commits into from
Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 57 additions & 0 deletions FlowCal/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ def __repr__(self):
'amplification_type',
'detector_voltage',
'amplifier_gain',
'channel_labels',
'range',
'resolution'])

Expand Down Expand Up @@ -1015,6 +1016,8 @@ class FCSData(np.ndarray):
Get the detector voltage used for the specified channel(s).
amplifier_gain
Get the amplifier gain used for the specified channel(s).
channel_labels
Get the label of the specified channel(s).
range
Get the range of the specified channel(s).
resolution
Expand Down Expand Up @@ -1331,6 +1334,42 @@ def amplifier_gain(self, channels=None):
else:
return self._amplifier_gain[channels]

def channel_labels(self, channels=None):
"""
Get the label of the specified channel(s).

The label for channel "n" is extracted from the $PnS
parameter, if available.

Parameters
----------
channels : int, str, list of int, list of str
Channel(s) for which to get the label. If None,
return a list with the label of all channels, in the
order of ``FCSData.channels``.

Return
------
str or list of str
The label of the specified channel(s). If no
information about the label is found for a channel,
return None.

"""
# Check default
if channels is None:
channels = self._channels

# Get numerical indices of channels
channels = self._name_to_index(channels)

# Get the label of the specified channels
if hasattr(channels, '__iter__') \
and not isinstance(channels, six.string_types):
return [self._channel_labels[ch] for ch in channels]
else:
return self._channel_labels[channels]

def range(self, channels=None):
"""
Get the range of the specified channel(s).
Expand Down Expand Up @@ -1708,6 +1747,13 @@ def __new__(cls, infile):
amplifier_gain.append(channel_amp_gain)
amplifier_gain = tuple(amplifier_gain)

# Channel label: Stored in the keyword parameter $PnS for channel n.
channel_labels = []
for i in range(1, num_channels + 1):
channel_label = fcs_file.text.get('$P{}S'.format(i), None)
channel_labels.append(channel_label)
channel_labels = tuple(channel_labels)

# Get data from fcs_file object, and change writeable flag.
data = fcs_file.data
data.flags.writeable = True
Expand All @@ -1729,6 +1775,7 @@ def __new__(cls, infile):
obj._amplification_type = amplification_type
obj._detector_voltage = detector_voltage
obj._amplifier_gain = amplifier_gain
obj._channel_labels = channel_labels
obj._range = data_range
obj._resolution = resolution

Expand Down Expand Up @@ -1771,6 +1818,8 @@ def __array_finalize__(self, obj):
self._detector_voltage = copy.deepcopy(obj._detector_voltage)
if hasattr(obj, '_amplifier_gain'):
self._amplifier_gain = copy.deepcopy(obj._amplifier_gain)
if hasattr(obj, '_channel_labels'):
self._channel_labels = copy.deepcopy(obj._channel_labels)
if hasattr(obj, '_range'):
self._range = copy.deepcopy(obj._range)
if hasattr(obj, '_resolution'):
Expand Down Expand Up @@ -1824,6 +1873,7 @@ def __reduce__(self):
amplification_type = self._amplification_type,
detector_voltage = self._detector_voltage,
amplifier_gain = self._amplifier_gain,
channel_labels = self._channel_labels,
range = self._range,
resolution = self._resolution)

Expand Down Expand Up @@ -1885,6 +1935,7 @@ def __setstate__(self, state):
self._amplification_type = fcsdata_state.amplification_type
self._detector_voltage = fcsdata_state.detector_voltage
self._amplifier_gain = fcsdata_state.amplifier_gain
self._channel_labels = fcsdata_state.channel_labels
self._range = fcsdata_state.range
self._resolution = fcsdata_state.resolution

Expand Down Expand Up @@ -2105,6 +2156,8 @@ def __getitem__(self, key):
[new_arr._detector_voltage[kc] for kc in key_channel])
new_arr._amplifier_gain = tuple(
[new_arr._amplifier_gain[kc] for kc in key_channel])
new_arr._channel_labels = tuple(
[new_arr._channel_labels[kc] for kc in key_channel])
new_arr._range = \
[new_arr._range[kc] for kc in key_channel]
new_arr._resolution = tuple(\
Expand All @@ -2117,6 +2170,8 @@ def __getitem__(self, key):
new_arr._detector_voltage[key_channel]
new_arr._amplifier_gain = \
new_arr._amplifier_gain[key_channel]
new_arr._channel_labels = \
new_arr._channel_labels[key_channel]
new_arr._range = \
new_arr._range[key_channel]
new_arr._resolution = \
Expand All @@ -2129,6 +2184,8 @@ def __getitem__(self, key):
tuple([new_arr._detector_voltage[key_channel]])
new_arr._amplifier_gain = \
tuple([new_arr._amplifier_gain[key_channel]])
new_arr._channel_labels = \
tuple([new_arr._channel_labels[key_channel]])
new_arr._range = \
[new_arr._range[key_channel]]
new_arr._resolution = \
Expand Down
Binary file added test/Data005.fcs
Binary file not shown.
162 changes: 158 additions & 4 deletions test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
- Data003.fcs: FCS 3.0 from FlowJo Collectors Edition 7.5 /
BD FACScan Flow Cytometer
- Data004.fcs: FCS 3.0 including floating-point data
- Data005.fcs: FCS 3.0 including $PnS value
"""
filenames = ['test/Data001.fcs',
'test/Data002.fcs',
'test/Data003.fcs',
'test/Data004.fcs',
'test/Data005.fcs',
]

class TestFCSDataLoading(unittest.TestCase):
Expand Down Expand Up @@ -2301,6 +2303,146 @@ def test_nondefault_nbins_many_5(self):
scale='linear'),
[bins1, np.arange(262145) - 0.5, bins2])

class TestFCSAttributesChannelLabels(unittest.TestCase):
"""
Test correct extraction, functioning, and slicing of channel label.

We have previously looked at the contents of the $PnS attribute for
the test files and identified the correct label(marker):
- Data001.fcs: [None, None, None, None, None, None]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last element of this list should be 'Time (204.80 sec.)'. This is what you have later in the test_attribute() function.

- Data002.fcs: [None, None, None, None, None, None, None, None, None]
- Data003.fcs: [None, None, None, None, None, None, None, None]
- Data004.fcs: [None, None, None, None, None, None, None,
None, None, None, None, None, None, None]
- Data005.fcs: ['209Bi_CD11b', '140Ce', '142Ce', 'Center', '161Dy_CD56',
'162Dy_gdTCR', '163Dy_CRTH2', '164Dy_CLEC12A', '166Er_CD25',
'167Er_CCR7', '168Er_CD3', '170Er_CD38', '151Eu_CD123',
'153Eu_PD-1', 'Event_length', '155Gd_CD27', '156Gd_CCR5',
'157Gd', '158Gd_CD117', '160Gd_CD14', '165Ho_CCR6', '127I',
'113In_CD57', '115In_CD11c', '191Ir', '193Ir', '175Lu', '176Lu',
'93Nb', '142Nd_CD19', '143Nd_CD45RA', '144Nd_CD141', '145Nd_CD4',
'146Nd_CD8', '148Nd_CD16', '150Nd_CD1c', 'Offset', '192Os',
'206Pb', '102Pd', '104Pd', '105Pd', '106Pd', '108Pd', '110Pd',
'141Pr_CD33', '194Pt', '195Pt', '196Pt', '198Pt', 'Residual',
'103Rh', '147Sm_CD20', '149Sm_CD127', '152Sm_CD66b', '154Sm_CD86',
'181Ta', '159Tb_CD24', '169Tm_CX3CR1', '131Xe', '89Y_CD45',
'171Yb_CD161', '172Yb_CD209', '173Yb_CXCR3', '174Yb_HLADR',
'176Yb_CCR4', None, None, 'Time'])

"""
def setUp(self):
self.d = [FlowCal.io.FCSData(filenames[i]) for i in range(5)]


def test_attribute(self):
"""
Testing correct reporting of channel_labels.

"""
self.assertListEqual(
self.d[0].channel_labels(),
[None, None, None, None, None, 'Time (204.80 sec.)'])
self.assertListEqual(
self.d[1].channel_labels(),
[None, None, None, None, None, None, None, None, None])
self.assertListEqual(
self.d[2].channel_labels(),
[None, None, None, None, None, None, None, None])
self.assertListEqual(
self.d[3].channel_labels(),
[None, None, None, None, None, None, None,
None, None, None, None, None, None, None])
self.assertListEqual(
self.d[4].channel_labels(),
['209Bi_CD11b', '140Ce', '142Ce', 'Center', '161Dy_CD56',
'162Dy_gdTCR', '163Dy_CRTH2', '164Dy_CLEC12A', '166Er_CD25',
'167Er_CCR7', '168Er_CD3', '170Er_CD38', '151Eu_CD123',
'153Eu_PD-1', 'Event_length', '155Gd_CD27', '156Gd_CCR5',
'157Gd', '158Gd_CD117', '160Gd_CD14', '165Ho_CCR6', '127I',
'113In_CD57', '115In_CD11c', '191Ir', '193Ir', '175Lu', '176Lu',
'93Nb', '142Nd_CD19', '143Nd_CD45RA', '144Nd_CD141', '145Nd_CD4',
'146Nd_CD8', '148Nd_CD16', '150Nd_CD1c', 'Offset', '192Os',
'206Pb', '102Pd', '104Pd', '105Pd', '106Pd', '108Pd', '110Pd',
'141Pr_CD33', '194Pt', '195Pt', '196Pt', '198Pt', 'Residual',
'103Rh', '147Sm_CD20', '149Sm_CD127', '152Sm_CD66b', '154Sm_CD86',
'181Ta', '159Tb_CD24', '169Tm_CX3CR1', '131Xe', '89Y_CD45',
'171Yb_CD161', '172Yb_CD209', '173Yb_CXCR3', '174Yb_HLADR',
'176Yb_CCR4', None, None, 'Time'])

def test_attribute_single(self):
"""
Testing correct reporting of label_channels for a single channel.

"""
self.assertEqual(self.d[0].channel_labels('FSC-H'), None)
self.assertEqual(self.d[1].channel_labels('FITC-A'), None)
self.assertEqual(self.d[2].channel_labels('SSC'), None)
self.assertEqual(self.d[3].channel_labels('GFP-A'), None)
self.assertEqual(self.d[4].channel_labels('Bi209Di'), '209Bi_CD11b')

def test_attribute_many(self):
"""
Testing correct reporting of channel_labels for many channels.

"""
self.assertListEqual(
self.d[0].channel_labels(['SSC-H', 'FL2-H', 'FL3-H']),
[None, None, None])
self.assertListEqual(
self.d[1].channel_labels(['FITC-A', 'PE-A', 'PE-Cy7-A']),
[None, None, None])
self.assertListEqual(
self.d[2].channel_labels(['FSC', 'SSC', 'TIME']),
[None, None, None])
self.assertListEqual(
self.d[3].channel_labels(['FSC PMT-A', 'FSC PMT-H', 'FSC PMT-W']),
[None, None, None])
self.assertListEqual(
self.d[4].channel_labels(['Bi209Di', 'Ce140Di', 'Ce142Di']),
['209Bi_CD11b', '140Ce', '142Ce'])

def test_slice_single_str(self):
"""
Testing correct reporting of channel_labels after slicing.

"""
self.assertListEqual(
self.d[0][:, 'FSC-H'].channel_labels(),
[None,])
self.assertListEqual(
self.d[1][:, 'FITC-A'].channel_labels(),
[None,])
self.assertListEqual(
self.d[2][:, 'SSC'].channel_labels(),
[None,])
self.assertListEqual(
self.d[3][:, 'GFP-A'].channel_labels(),
[None,])
self.assertListEqual(
self.d[4][:, 'Bi209Di'].channel_labels(),
['209Bi_CD11b',])

def test_slice_many_str(self):
"""
Testing correct reporting of channel_labels after slicing.

"""
self.assertListEqual(
self.d[0][:, ['SSC-H', 'FL2-H', 'FL3-H']].channel_labels(),
[None, None, None])
self.assertListEqual(
self.d[1][:, ['FITC-A', 'PE-A', 'PE-Cy7-A']].channel_labels(),
[None, None, None])
self.assertListEqual(
self.d[2][:, ['FSC', 'SSC', 'TIME']].channel_labels(),
[None, None, None])
self.assertListEqual(
self.d[3][:,['FSC PMT-A', 'FSC PMT-H', 'FSC PMT-W']].channel_labels(),
[None, None, None])
self.assertListEqual(
self.d[4][:,['Bi209Di', 'Ce140Di', 'Ce142Di']].channel_labels(),
['209Bi_CD11b', '140Ce', '142Ce'])

class TestFCSAttributes(unittest.TestCase):
def setUp(self):
self.d = FlowCal.io.FCSData(filenames[0])
Expand Down Expand Up @@ -2789,10 +2931,21 @@ def test_pickle_unpickle(self):
loaded_attrs = set(dir(test_fcs))
self.assertEqual(attrs, loaded_attrs)
# Check contents of attrs affected by pickling
pickle_sensitive_attrs = ['_resolution', '_range', '_amplifier_gain',
'_detector_voltage', '_amplification_type', '_channels',
'_acquisition_end_time', '_acquisition_start_time', '_time_step',
'_data_type', '_analysis', '_text', '_infile']
pickle_sensitive_attrs = [
'_resolution',
'_range',
'_amplifier_gain',
'_channel_labels',
'_detector_voltage',
'_amplification_type',
'_channels',
'_acquisition_end_time',
'_acquisition_start_time',
'_time_step',
'_data_type',
'_analysis',
'_text',
'_infile']
# Also test computed (property) attrs
computed_attrs = [
'infile',
Expand All @@ -2810,6 +2963,7 @@ def test_pickle_unpickle(self):
'amplification_type',
'detector_voltage',
'amplifier_gain',
'channel_labels',
'range',
'resolution',
'hist_bins',
Expand Down