Skip to content

Commit

Permalink
Merge branch 'v1.1.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
castillohair committed Jan 8, 2017
2 parents ca52afa + 6b6849e commit 9696171
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 40 deletions.
2 changes: 1 addition & 1 deletion FlowCal/__init__.py
Expand Up @@ -6,7 +6,7 @@
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
__version__ = '1.1.0'
__version__ = '1.1.1'

import io
import excel_ui
Expand Down
63 changes: 48 additions & 15 deletions FlowCal/excel_ui.py
Expand Up @@ -215,8 +215,9 @@ def process_beads_table(beads_table,
base_dir=".",
verbose=False,
plot=False,
plot_dir=".",
full_output=False):
plot_dir=None,
full_output=False,
get_transform_fxn_kwargs={}):
"""
Process calibration bead samples, as specified by an input table.
Expand Down Expand Up @@ -259,11 +260,15 @@ def process_beads_table(beads_table,
sample, and each beads sample.
plot_dir : str, optional
Directory relative to `base_dir` into which plots are saved. If
`plot` is False, this parameter is ignored.
`plot` is False, this parameter is ignored. If ``plot==True`` and
``plot_dir is None``, plot without saving.
full_output : bool, optional
Flag indicating whether to include an additional output, containing
intermediate results from the generation of the MEF transformation
functions.
get_transform_fxn_kwargs : dict, optional
Additional parameters passed directly to internal
``mef.get_transform_fxn()`` function call.
Returns
-------
Expand Down Expand Up @@ -298,7 +303,8 @@ def process_beads_table(beads_table,
print("="*len(msg))

# Check that plotting directory exist, create otherwise
if plot and not os.path.exists(os.path.join(base_dir, plot_dir)):
if plot and plot_dir is not None \
and not os.path.exists(os.path.join(base_dir, plot_dir)):
os.makedirs(os.path.join(base_dir, plot_dir))

# Extract header and channel names for which MEF values are specified.
Expand Down Expand Up @@ -416,9 +422,13 @@ def process_beads_table(beads_table,
# Histogram plot parameters
hist_params = {'xscale': 'logicle'}
# Plot
figname = os.path.join(base_dir,
plot_dir,
"density_hist_{}.png".format(beads_id))
if plot_dir is not None:
figname = os.path.join(
base_dir,
plot_dir,
"density_hist_{}.png".format(beads_id))
else:
figname = None
plt.figure(figsize=(6,4))
FlowCal.plot.density_and_hist(
beads_sample,
Expand Down Expand Up @@ -477,8 +487,10 @@ def process_beads_table(beads_table,
verbose=False,
plot=plot,
plot_filename=beads_id,
plot_dir=os.path.join(base_dir, plot_dir),
full_output=full_output)
plot_dir=os.path.join(base_dir, plot_dir) \
if plot_dir is not None else None,
full_output=full_output,
**get_transform_fxn_kwargs)

if full_output:
mef_transform_fxn = mef_output.transform_fxn
Expand Down Expand Up @@ -520,7 +532,7 @@ def process_samples_table(samples_table,
base_dir=".",
verbose=False,
plot=False,
plot_dir="."):
plot_dir=None):
"""
Process flow cytometry samples, as specified by an input table.
Expand Down Expand Up @@ -574,7 +586,8 @@ def process_samples_table(samples_table,
sample, and each beads sample.
plot_dir : str, optional
Directory relative to `base_dir` into which plots are saved. If
`plot` is False, this parameter is ignored.
`plot` is False, this parameter is ignored. If ``plot==True`` and
``plot_dir is None``, plot without saving.
Returns
-------
Expand All @@ -597,7 +610,8 @@ def process_samples_table(samples_table,
print("="*len(msg))

# Check that plotting directory exist, create otherwise
if plot and not os.path.exists(os.path.join(base_dir, plot_dir)):
if plot and plot_dir is not None \
and not os.path.exists(os.path.join(base_dir, plot_dir)):
os.makedirs(os.path.join(base_dir, plot_dir))

# Extract header and channel names for which units are specified.
Expand Down Expand Up @@ -801,9 +815,13 @@ def process_samples_table(samples_table,
hist_params.append(param)

# Plot
figname = os.path.join(base_dir,
plot_dir,
"{}.png".format(sample_id))
if plot_dir is not None:
figname = os.path.join(
base_dir,
plot_dir,
"{}.png".format(sample_id))
else:
figname = None
FlowCal.plot.density_and_hist(
sample,
sample_gated,
Expand Down Expand Up @@ -862,6 +880,10 @@ def add_beads_stats(beads_table, beads_samples, mef_outputs=None):
``mef_outputs[i]`` should correspond to ``beads_table.iloc[i]``.
"""
# The index name is not preserved if beads_table is empty.
# Save the index name for later
beads_table_index_name = beads_table.index.name

# Add per-row info
notes = []
n_events = []
Expand Down Expand Up @@ -953,6 +975,9 @@ def add_beads_stats(beads_table, beads_samples, mef_outputs=None):
channel + ' Beads Params. Values',
params_str)

# Restore index name if table is empty
if len(beads_table) == 0:
beads_table.index.name = beads_table_index_name

def add_samples_stats(samples_table, samples):
"""
Expand Down Expand Up @@ -999,6 +1024,10 @@ def add_samples_stats(samples_table, samples):
and a warning message will be written to the "Analysis Notes" field.
"""
# The index name is not preserved if samples_table is empty.
# Save the index name for later
samples_table_index_name = samples_table.index.name

# Add per-row info
notes = []
n_events = []
Expand Down Expand Up @@ -1114,6 +1143,10 @@ def add_samples_stats(samples_table, samples):
channel + ' Geom. CV',
FlowCal.stats.gcv(sample_positive, channel))

# Restore index name if table is empty
if len(samples_table) == 0:
samples_table.index.name = samples_table_index_name

def generate_histograms_table(samples_table, samples, max_bins=1024):
"""
Generate a table of histograms as a DataFrame.
Expand Down
12 changes: 11 additions & 1 deletion FlowCal/gate.py
Expand Up @@ -39,6 +39,7 @@ def start_end(data, num_start=250, num_end=100, full_output=False):
the number of parameters (aka channels).
num_start, num_end : int, optional
Number of events to gate out from beginning and end of `data`.
Ignored if less than 0.
full_output : bool, optional
Flag specifying to return additional outputs. If true, the outputs
are given as a namedtuple.
Expand All @@ -58,13 +59,22 @@ def start_end(data, num_start=250, num_end=100, full_output=False):
number of events in `data`.
"""

if num_start < 0:
num_start = 0
if num_end < 0:
num_end = 0

if data.shape[0] < (num_start + num_end):
raise ValueError('Number of events to discard greater than total' +
' number of events.')

mask = np.ones(shape=data.shape[0],dtype=bool)
mask[:num_start] = False
mask[-num_end:] = False
if num_end > 0:
# catch the edge case where `num_end=0` causes mask[-num_end:] to mask
# off all events
mask[-num_end:] = False
gated_data = data[mask]

if full_output:
Expand Down
57 changes: 41 additions & 16 deletions FlowCal/io.py
Expand Up @@ -1512,25 +1512,50 @@ def __new__(cls, infile):
resolution = tuple(resolution)

# Detector voltage: Stored in the keyword parameter $PnV for channel n.
# The CellQuest Pro software saves the detector voltage in keyword
# parameters BD$WORD13, BD$WORD14, BD$WORD15... for channels 1, 2,
# 3...
if 'CREATOR' in fcs_file.text and \
'CellQuest Pro' in fcs_file.text.get('CREATOR'):
detector_voltage = [fcs_file.text.get('BD$WORD{}'.format(12 + i))
for i in range(1, num_channels + 1)]
else:
detector_voltage = [fcs_file.text.get('$P{}V'.format(i))
for i in range(1, num_channels + 1)]
detector_voltage = [float(dvi) if dvi is not None else None
for dvi in detector_voltage]
detector_voltage = []
for i in range(1, num_channels + 1):
channel_detector_voltage = fcs_file.text.get('$P{}V'.format(i))

# The CellQuest Pro software saves the detector voltage in keyword
# parameters BD$WORD13, BD$WORD14, BD$WORD15... for channels 1, 2,
# 3...
if channel_detector_voltage is None and 'CREATOR' in fcs_file.text \
and 'CellQuest Pro' in fcs_file.text.get('CREATOR'):
channel_detector_voltage = fcs_file.text.get('BD$WORD{}' \
.format(12+i))

# Attempt to cast extracted value to float
# The FCS3.1 standard restricts $PnV to be a floating-point value
# only. Any value that cannot be casted will be replaced with None.
if channel_detector_voltage is not None:
try:
channel_detector_voltage = float(channel_detector_voltage)
except ValueError:
channel_detector_voltage = None
detector_voltage.append(channel_detector_voltage)
detector_voltage = tuple(detector_voltage)

# Amplifier gain: Stored in the keyword parameter $PnG for channel n.
amplifier_gain = [fcs_file.text.get('$P{}G'.format(i))
for i in range(1, num_channels + 1)]
amplifier_gain = [float(agi) if agi is not None else None
for agi in amplifier_gain]
amplifier_gain = []
for i in range(1, num_channels + 1):
channel_amp_gain = fcs_file.text.get('$P{}G'.format(i))

# The FlowJo Collector's Edition version 7.5.110.7 software saves
# the amplifier gain in keyword parameters CytekP01G, CytekP02G,
# CytekP03G, ... for channels 1, 2, 3, ...
if channel_amp_gain is None and 'CREATOR' in fcs_file.text and \
'FlowJoCollectorsEdition' in fcs_file.text.get('CREATOR'):
channel_amp_gain = fcs_file.text.get('CytekP{:02d}G'.format(i))

# Attempt to cast extracted value to float
# The FCS3.1 standard restricts $PnG to be a floating-point value
# only. Any value that cannot be casted will be replaced with None.
if channel_amp_gain is not None:
try:
channel_amp_gain = float(channel_amp_gain)
except ValueError:
channel_amp_gain = None
amplifier_gain.append(channel_amp_gain)
amplifier_gain = tuple(amplifier_gain)

# Get data from fcs_file object, and change writeable flag.
Expand Down
1 change: 0 additions & 1 deletion FlowCal/plot.py
Expand Up @@ -1330,7 +1330,6 @@ def scatter3d(data_list,
marker='o',
alpha=0.1,
color=color[i],
c=color[i],
**kwargs)

# Remove tick labels
Expand Down
38 changes: 38 additions & 0 deletions test/test_gate.py
Expand Up @@ -55,6 +55,44 @@ def test_mask(self):
np.array([0,0,1,1,1,1,1,0,0,0], dtype=bool)
)

def test_0_end_gated_data_1(self):
np.testing.assert_array_equal(
FlowCal.gate.start_end(self.d, num_start=2, num_end=0),
np.array([
[3, 9, 4],
[4, 10, 5],
[5, 1, 6],
[6, 2, 7],
[7, 3, 8],
[8, 4, 9],
[9, 5, 10],
[10, 6, 1],
])
)

def test_0_end_gated_data_2(self):
np.testing.assert_array_equal(
FlowCal.gate.start_end(
self.d, num_start=2, num_end=0, full_output=True).gated_data,
np.array([
[3, 9, 4],
[4, 10, 5],
[5, 1, 6],
[6, 2, 7],
[7, 3, 8],
[8, 4, 9],
[9, 5, 10],
[10, 6, 1],
])
)

def test_0_end_mask(self):
np.testing.assert_array_equal(
FlowCal.gate.start_end(
self.d, num_start=2, num_end=0, full_output=True).mask,
np.array([0,0,1,1,1,1,1,1,1,1], dtype=bool)
)

def test_error(self):
with self.assertRaises(ValueError):
FlowCal.gate.start_end(self.d, num_start=5, num_end=7)
Expand Down
12 changes: 6 additions & 6 deletions test/test_io.py
Expand Up @@ -350,7 +350,7 @@ class TestFCSAttributesAmplifierGain(unittest.TestCase):
- Data001.fcs: [None, None, None, None, None, None]
- Data002.fcs: [None, None, None, None, None, None, None, None,
None]
- Data003.fcs: [None, None, None, None, None, None, None, None]
- Data003.fcs: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
- Data004.fcs: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 0.01]
Expand All @@ -368,7 +368,7 @@ def test_attribute(self):
self.assertEqual(self.d[1].amplifier_gain(),
[None, None, None, None, None, None, None, None, None])
self.assertEqual(self.d[2].amplifier_gain(),
[None, None, None, None, None, None, None, None])
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
self.assertEqual(self.d[3].amplifier_gain(),
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 0.01])
Expand All @@ -380,7 +380,7 @@ def test_attribute_single(self):
"""
self.assertEqual(self.d[0].amplifier_gain('FSC-H'), None)
self.assertEqual(self.d[1].amplifier_gain('FITC-A'), None)
self.assertEqual(self.d[2].amplifier_gain('SSC'), None)
self.assertEqual(self.d[2].amplifier_gain('SSC'), 1.0)
self.assertEqual(self.d[3].amplifier_gain('GFP-A'), 1.0)

def test_attribute_many(self):
Expand All @@ -399,7 +399,7 @@ def test_attribute_many(self):
self.assertEqual(self.d[2].amplifier_gain(['FSC',
'SSC',
'FL1']),
[None, None, None])
[1.0, 1.0, 1.0])
self.assertEqual(self.d[3].amplifier_gain(['FSC PMT-A',
'FSC PMT-H',
'FSC PMT-W']),
Expand All @@ -412,7 +412,7 @@ def test_slice_single_str(self):
"""
self.assertEqual(self.d[0][:, 'FSC-H'].amplifier_gain(), [None])
self.assertEqual(self.d[1][:, 'FITC-A'].amplifier_gain(), [None])
self.assertEqual(self.d[2][:, 'SSC'].amplifier_gain(), [None])
self.assertEqual(self.d[2][:, 'SSC'].amplifier_gain(), [1.0])
self.assertEqual(self.d[3][:, 'GFP-A'].amplifier_gain(), [1.0])

def test_slice_many_str(self):
Expand All @@ -431,7 +431,7 @@ def test_slice_many_str(self):
self.assertEqual(self.d[2][:, ['FSC',
'SSC',
'FL1']].amplifier_gain(),
[None, None, None])
[1.0, 1.0, 1.0])
self.assertEqual(self.d[3][:, ['FSC PMT-A',
'FSC PMT-H',
'Time']].amplifier_gain(),
Expand Down

0 comments on commit 9696171

Please sign in to comment.