Skip to content

Commit

Permalink
BD facs diva support (#46)
Browse files Browse the repository at this point in the history
BD facs diva devices only have channel_name_s set for some but not all channels. If not present for all channels fcsparser fallbacks to the channel_names_n.

This pull request includes two main adaptations:

- Using channel_names_n as a fallback for channels without a value in channel_names_s
- Puts both $PnN and $PnS as columns into the metadata dataframe

All adaptations have corresponding unit tests
  • Loading branch information
CaRniFeXeR authored Apr 26, 2023
1 parent 6ca7e70 commit 2c8502a
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 39 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
**/*.pyc
.cache
/venv
/build
/.vscode
/dist
/build
/dist
versioneer.py
/fcsparser.egg-info
setup.cfg
39 changes: 22 additions & 17 deletions fcsparser/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,22 +375,24 @@ def read_text(self, file_handle):
else:
self.channel_numbers = range(1, pars + 1) # Channel numbers start from 1


# Extract parameter names
try:
names_n = tuple([text["$P{0}N".format(i)] for i in self.channel_numbers])
except KeyError:
names_n = []

try:
names_s = tuple([text["$P{0}S".format(i)] for i in self.channel_numbers])
except KeyError:
names_s = []

self.channel_names_s = names_s
self.channel_names_n = names_n

channel_names_n = []
channel_names_s = []
for channel_number in self.channel_numbers:
n_key = f'$P{channel_number}N'
s_key = f'$P{channel_number}S'
name_n = text.get(n_key, "")
name_s = text.get(s_key, "")

channel_names_n.append(name_n)
channel_names_s.append(name_s if name_s != "" else name_n)

self.channel_names_n = tuple(channel_names_n)
self.channel_names_s = tuple(channel_names_s)

# Convert some of the fields into integer values
keys_encoding_bits = ["$P{0}B".format(i) for i in self.channel_numbers]
keys_encoding_bits = [f"$P{channel_number}B" for channel_number in self.channel_numbers]

add_keys_to_convert_to_int = ["$NEXTDATA", "$PAR", "$TOT"]

Expand Down Expand Up @@ -642,9 +644,12 @@ def reformat_meta(self):
if "$PnE" in column_names:
df["$PnE"] = df["$PnE"].apply(lambda x: x.split(","))

df.index.name = "Channel Number"
meta["_channels_"] = df
meta["_channel_names_"] = self.get_channel_names()
if self._channel_naming not in df.columns.names:
df[self._channel_naming] = self.get_channel_names()

df.index.name = 'Channel Number'
meta['_channels_'] = df
meta['_channel_names_'] = self.get_channel_names()

@property
def dataframe(self):
Expand Down
Binary file not shown.
61 changes: 41 additions & 20 deletions fcsparser/tests/test_fcs_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,24 @@
BASE_PATH = os.path.join(HERE, 'data', 'FlowCytometers')

# Used for checking data segments in fcs files generated by different machines.
FILE_IDENTIFIER_TO_PATH = {'mq fcs 2.0': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS2.0',
'EY_2013-07-19_PBS_FCS_2.0_Custom_Without_Add_Well_A1.001.fcs'),
'mq fcs 3.0': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS3.0',
'FCS3.0_Custom_Compatible.fcs'),
'mq fcs 3.1': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS3.1',
'EY_2013-07-19_PBS_FCS_3.1_Well_A1.001.fcs'),
'LSR II fcs 3.0': os.path.join(BASE_PATH, 'HTS_BD_LSR-II',
'HTS_BD_LSR_II_Mixed_Specimen_001_D6_D06.fcs'),
'Fortessa fcs 3.0': os.path.join(BASE_PATH, 'Fortessa',
'FCS_3.0_Fortessa_PBS_Specimen_001_A1_A01.fcs'),
'large fake fcs': os.path.join(BASE_PATH, 'fake_large_fcs',
'fake_large_fcs.fcs'),
'cyflow cube 8': os.path.join(BASE_PATH, 'cyflow_cube_8',
'cyflow_cube_8.fcs'),
'fake bitmask error': os.path.join(BASE_PATH, 'fake_bitmask_error',
'fcs1_cleaned.lmd'),
'Cytek xP5': os.path.join(BASE_PATH, 'Cytek_xP5', 'Cytek_xP5.fcs'),
'guava muse': os.path.join(BASE_PATH, 'GuavaMuse',
'Guava Muse.fcs'), }
FILE_IDENTIFIER_TO_PATH = {
'mq fcs 2.0': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS2.0',
'EY_2013-07-19_PBS_FCS_2.0_Custom_Without_Add_Well_A1.001.fcs'),
'mq fcs 3.0': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS3.0',
'FCS3.0_Custom_Compatible.fcs'),
'mq fcs 3.1': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS3.1',
'EY_2013-07-19_PBS_FCS_3.1_Well_A1.001.fcs'),
'LSR II fcs 3.0': os.path.join(BASE_PATH, 'HTS_BD_LSR-II',
'HTS_BD_LSR_II_Mixed_Specimen_001_D6_D06.fcs'),
'Fortessa fcs 3.0': os.path.join(BASE_PATH, 'Fortessa',
'FCS_3.0_Fortessa_PBS_Specimen_001_A1_A01.fcs'),
'large fake fcs': os.path.join(BASE_PATH, 'fake_large_fcs', 'fake_large_fcs.fcs'),
'cyflow cube 8': os.path.join(BASE_PATH, 'cyflow_cube_8', 'cyflow_cube_8.fcs'),
'fake bitmask error': os.path.join(BASE_PATH, 'fake_bitmask_error', 'fcs1_cleaned.lmd'),
'Cytek xP5': os.path.join(BASE_PATH, 'Cytek_xP5', 'Cytek_xP5.fcs'),
'FACS Diva': os.path.join(BASE_PATH, 'FACS_Diva', 'facs_diva_test.fcs'),
'guava muse': os.path.join(BASE_PATH, 'GuavaMuse','Guava Muse.fcs')
}

# The group of files below is used for checking behavior other than reading data.
ADDITIONAL_FILE_MAPPING = {'duplicate_names': os.path.join(BASE_PATH, 'MiltenyiBiotec', 'FCS3.1',
Expand Down Expand Up @@ -349,7 +348,29 @@ def test_channel_naming_manual(self):

self.assertListEqual(channel_names, pns_names)
self.assertListEqual(list(data.columns.values), pns_names)


def test_channel_namming_FACS_Diva(self):
"""Check that channel names correspond to manual setting."""
path = FILE_IDENTIFIER_TO_PATH['FACS Diva']
meta = parse_fcs(path, meta_data_only=True,
reformat_meta=True, channel_naming='$PnN')
channel_names = list(meta['_channel_names_'])
channel_meta = meta['_channels_']
pnn_names = ['Time', 'FSC-A', 'FSC-W', 'SSC-A', 'FITC-A', 'PE-A', 'PerCP-A',
'PE-Cy7-A', 'PacificBlue-A', 'APC-A', 'Alexa700-A', 'APC-Cy7-A']
self.assertListEqual(channel_names, pnn_names)
self.assertListEqual(list(channel_meta["$PnN"]), pnn_names)

meta = parse_fcs(path, meta_data_only=True,
reformat_meta=True, channel_naming='$PnS')
channel_names = list(meta['_channel_names_'])
channel_meta = meta['_channels_']
pns_names = ['Time', 'FSC-A', 'FSC-W', 'SSC-A', 'CD20', 'CD10', 'CD45',
'CD34', 'Syto 41', 'CD19', 'CD38', 'APC-Cy7-A']
self.assertListEqual(channel_names, pns_names)
self.assertListEqual(list(channel_meta["$PnS"]), pns_names)


def test_speed_of_reading_fcs_files(self):
"""Test the speed of loading a FCS files"""
file_path = FILE_IDENTIFIER_TO_PATH['mq fcs 3.1']
Expand Down

0 comments on commit 2c8502a

Please sign in to comment.