diff --git a/.gitignore b/.gitignore index 470e279..b4e2ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ **/*.pyc .cache /venv -/build /.vscode -/dist \ No newline at end of file +/build +/dist +versioneer.py +/fcsparser.egg-info +setup.cfg diff --git a/fcsparser/api.py b/fcsparser/api.py index 55694ad..fbee7d3 100644 --- a/fcsparser/api.py +++ b/fcsparser/api.py @@ -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"] @@ -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): diff --git a/fcsparser/tests/data/FlowCytometers/FACS_Diva/facs_diva_test.fcs b/fcsparser/tests/data/FlowCytometers/FACS_Diva/facs_diva_test.fcs new file mode 100644 index 0000000..3c1ff54 Binary files /dev/null and b/fcsparser/tests/data/FlowCytometers/FACS_Diva/facs_diva_test.fcs differ diff --git a/fcsparser/tests/test_fcs_reader.py b/fcsparser/tests/test_fcs_reader.py index 8720118..d821bd3 100755 --- a/fcsparser/tests/test_fcs_reader.py +++ b/fcsparser/tests/test_fcs_reader.py @@ -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', @@ -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']