From d8d1926637da57779b33e7c213ccafb6420348c6 Mon Sep 17 00:00:00 2001 From: maik Date: Tue, 30 Apr 2024 09:54:56 +0200 Subject: [PATCH 1/5] add autologicle normalization function --- pytometry/tools/_normalization.py | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/pytometry/tools/_normalization.py b/pytometry/tools/_normalization.py index 1579a1e..d046069 100644 --- a/pytometry/tools/_normalization.py +++ b/pytometry/tools/_normalization.py @@ -554,3 +554,81 @@ def _log_root(b: float, w: float) -> float: x_hi = d return d + +def autoLgcl(adata, channels, m=4.5, q=0.05): + """ + Automatically apply a logicle transformation to specified channels in an AnnData object. + + Code adapated from the `Cytofkit` package (Chen et al. 2016). + + This function processes multiple channels within an AnnData object by applying a logicle transformation to each one. + + Parameters: + - adata (AnnData): The AnnData object containing the data to be transformed. + - channels (list of str): A list of channel names to be logicle transformed. + - m (float, optional): The upper limit for the transformation parameter 'm'. Defaults to 4.5. + - q (float, optional): The quantile to determine the lower threshold for the transformation. Defaults to 0.05. + + Returns: + - dict: A dictionary with channel names as keys and dictionaries containing logicle transformation parameters as values. + """ + if not isinstance(adata, AnnData): + raise TypeError("adata has to be an object of class 'AnnData'") + if not channels: + raise ValueError("Please specify the channels to be logicle transformed") + indx = [channel in adata.var_names for channel in channels] + if not all(indx): + missing_channels = [channels[i] for i in range(len(channels)) if not indx[i]] + raise ValueError(f"Channels {missing_channels} were not found in the adata object.") + + trans = [logicleTransform(channel, adata, m, q) for channel in channels] + return dict(zip(channels, trans)) + +def logicleTransform(channel, adata, m, q): + """ + Helper function to apply a logicle transformation to a single channel in an AnnData object. + + This is an internal helper function used by `autoLgcl` to transform the data of a specified channel using the logicle method. + + + Parameters: + - channel (str): The name of the channel to be transformed. + - adata (AnnData): The AnnData object containing the data for the specified channel. + - m (float): The upper limit for the transformation parameter 'm'. + - q (float): The quantile to determine the lower threshold for the transformation. + + Returns: + - dict: A dictionary with details of the logicle transformation parameters and results. + + Note: + - If the computed parameter 'w' is NaN or exceeds 2, it resets to a default value of 0.1, and 't' and 'm' are set to default values of 4000 and 4.5, respectively. + """ + data = adata.X[:, adata.var_names == channel].flatten() + w = 0 + t = np.max(data) + ndata = data[data < 0] + nThres = np.quantile(ndata, 0.25) - 1.5 * np.subtract(*np.percentile(ndata, [75, 25])) + ndata = ndata[ndata >= nThres] + transId = f"{channel}_autolgclTransform" + + if len(ndata): + r = np.finfo(float).eps + np.quantile(ndata, q) + if 10**m * abs(r) <= t: + w = 0 + else: + w = (m - np.log10(t/abs(r)))/2 + if np.isnan(w) or w > 2: + print(f"autoLgcl failed for channel: {channel}; using default logicle transformation") + w = 0.1 + t = 4000 + m = 4.5 + + return { + "channel": channel, + "transformation": "logicle", + "transformationId": transId, + "w": w, + "t": t, + "m": m, + "a": 0 + } \ No newline at end of file From f78a419e3e4ca71e74efbc46c4b45de76370c41d Mon Sep 17 00:00:00 2001 From: maik Date: Tue, 30 Apr 2024 11:29:47 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20add=20usage=20example=20to=20au?= =?UTF-8?q?tologicle=20normalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytometry/tools/_normalization.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytometry/tools/_normalization.py b/pytometry/tools/_normalization.py index d046069..585fcc3 100644 --- a/pytometry/tools/_normalization.py +++ b/pytometry/tools/_normalization.py @@ -1,6 +1,7 @@ import numpy as np from anndata import AnnData from scipy import interpolate +from flowutils import transforms def normalize_arcsinh(adata: AnnData, cofactor=5, inplace: bool = True): @@ -571,6 +572,12 @@ def autoLgcl(adata, channels, m=4.5, q=0.05): Returns: - dict: A dictionary with channel names as keys and dictionaries containing logicle transformation parameters as values. + + Usage: + params = autoLgcl(adata, channels=list(adata.var_names)) + for channel in adata.var_names: + channel_idx = np.where(adata.var_names == channel)[0][0] + adata.X[:, channel_idx] = transforms.logicle(adata.X[:, channel_idx], channel_indices=[channel_idx], **params[channel]) """ if not isinstance(adata, AnnData): raise TypeError("adata has to be an object of class 'AnnData'") From c4cd238bc62317cb1d4f1c092e97086b86d48651 Mon Sep 17 00:00:00 2001 From: buettnerm Date: Fri, 3 May 2024 14:15:27 -0700 Subject: [PATCH 3/5] :lipstick: Run Precommit --- docs/tutorials/quickstart.ipynb | 12 ++- pytometry/tools/_normalization.py | 123 ++++++++++++++++++------------ 2 files changed, 87 insertions(+), 48 deletions(-) diff --git a/docs/tutorials/quickstart.ipynb b/docs/tutorials/quickstart.ipynb index 3f0e933..183f797 100644 --- a/docs/tutorials/quickstart.ipynb +++ b/docs/tutorials/quickstart.ipynb @@ -157,6 +157,16 @@ "adata_logicle = pm.tl.normalize_logicle(adata, inplace=False)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "f528f69a", + "metadata": {}, + "outputs": [], + "source": [ + "adata_autologicle = pm.tl.normalize_autologicle(adata, inplace=False)" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -193,7 +203,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.9.18" }, "vscode": { "interpreter": { diff --git a/pytometry/tools/_normalization.py b/pytometry/tools/_normalization.py index 585fcc3..9afc4d5 100644 --- a/pytometry/tools/_normalization.py +++ b/pytometry/tools/_normalization.py @@ -1,7 +1,7 @@ import numpy as np from anndata import AnnData -from scipy import interpolate from flowutils import transforms +from scipy import interpolate def normalize_arcsinh(adata: AnnData, cofactor=5, inplace: bool = True): @@ -556,80 +556,109 @@ def _log_root(b: float, w: float) -> float: return d -def autoLgcl(adata, channels, m=4.5, q=0.05): - """ - Automatically apply a logicle transformation to specified channels in an AnnData object. - Code adapated from the `Cytofkit` package (Chen et al. 2016). +def normalize_autoLgcl(adata, channels, m=4.5, q=0.05, inplace=True): + """Autologicle transformation. - This function processes multiple channels within an AnnData object by applying a logicle transformation to each one. + Automatically apply a logicle transformation to specified channels in an AnnData + object. + Code adapated from the `Cytofkit` package (Chen et al. 2016). + This function processes multiple channels within an AnnData object by applying a + logicle transformation to each one. - Parameters: - - adata (AnnData): The AnnData object containing the data to be transformed. - - channels (list of str): A list of channel names to be logicle transformed. - - m (float, optional): The upper limit for the transformation parameter 'm'. Defaults to 4.5. - - q (float, optional): The quantile to determine the lower threshold for the transformation. Defaults to 0.05. + Args: + adata (AnnData): The AnnData object containing the data to be transformed. + channels (list of str): A list of channel names to be logicle transformed. + m (float, optional): The upper limit for the transformation parameter 'm'. + Defaults to 4.5. + q (float, optional): The quantile to determine the lower threshold for the + transformation. Defaults to 0.05. Returns: - - dict: A dictionary with channel names as keys and dictionaries containing logicle transformation parameters as values. + dict: A dictionary with channel names as keys and dictionaries containing + logicle transformation parameters as values. Usage: - params = autoLgcl(adata, channels=list(adata.var_names)) - for channel in adata.var_names: - channel_idx = np.where(adata.var_names == channel)[0][0] - adata.X[:, channel_idx] = transforms.logicle(adata.X[:, channel_idx], channel_indices=[channel_idx], **params[channel]) + params = pm.tl._autoLgcl_params(adata, channels=list(adata.var_names)) + for channel in adata.var_names: + channel_idx = np.where(adata.var_names == channel)[0][0] + adata.X[:, channel_idx] = transforms.logicle(adata.X[:, channel_idx], + channel_indices=[channel_idx], + **params[channel]) """ + adata = adata if inplace else adata.copy() + # check inputs if not isinstance(adata, AnnData): - raise TypeError("adata has to be an object of class 'AnnData'") - if not channels: - raise ValueError("Please specify the channels to be logicle transformed") - indx = [channel in adata.var_names for channel in channels] - if not all(indx): - missing_channels = [channels[i] for i in range(len(channels)) if not indx[i]] - raise ValueError(f"Channels {missing_channels} were not found in the adata object.") - - trans = [logicleTransform(channel, adata, m, q) for channel in channels] - return dict(zip(channels, trans)) - -def logicleTransform(channel, adata, m, q): - """ - Helper function to apply a logicle transformation to a single channel in an AnnData object. + raise TypeError("adata has to be an object of class 'AnnData'") + if not channels: + raise ValueError("Please specify the channels to be logicle transformed") + indx = [channel in adata.var_names for channel in channels] + if not all(indx): + missing_channels = [channels[i] for i in range(len(channels)) if not indx[i]] + raise ValueError( + f"Channels {missing_channels} were not found in the adata object." + ) + # Get params for logicle transformation + trans = [_logicleTransform(channel, adata, m, q) for channel in channels] + # Create parameter list for the autologicle transformation + params = dict(zip(channels, trans)) + for channel in adata.var_names: + channel_idx = np.where(adata.var_names == channel)[0][0] + adata.X[:, channel_idx] = transforms.logicle( + adata.X[:, channel_idx], channel_indices=[channel_idx], **params[channel] + ) + return None if inplace else adata + - This is an internal helper function used by `autoLgcl` to transform the data of a specified channel using the logicle method. - +def _logicleTransform(channel, adata, m, q): + """Helper function for logicle transform. - Parameters: - - channel (str): The name of the channel to be transformed. - - adata (AnnData): The AnnData object containing the data for the specified channel. - - m (float): The upper limit for the transformation parameter 'm'. - - q (float): The quantile to determine the lower threshold for the transformation. + Helper function to apply a logicle transformation to a single channel in + an AnnData object. + This is an internal helper function used by `autoLgcl` to transform the data + of a specified channel using the logicle method. + + Args: + channel (str): The name of the channel to be transformed. + adata (AnnData): The AnnData object containing the data for the + specified channel. + m (float): The upper limit for the transformation parameter 'm'. + q (float): The quantile to determine the lower threshold for the + transformation. Returns: - - dict: A dictionary with details of the logicle transformation parameters and results. + dict: A dictionary with details of the logicle transformation parameters + and results. Note: - - If the computed parameter 'w' is NaN or exceeds 2, it resets to a default value of 0.1, and 't' and 'm' are set to default values of 4000 and 4.5, respectively. + If the computed parameter 'w' is NaN or exceeds 2, it resets to a default value + of 0.1, and 't' and 'm' are set to default values of 4000 and 4.5, respectively. """ - data = adata.X[:, adata.var_names == channel].flatten() + data = adata.X[:, adata.var_names == channel].flatten() w = 0 t = np.max(data) ndata = data[data < 0] - nThres = np.quantile(ndata, 0.25) - 1.5 * np.subtract(*np.percentile(ndata, [75, 25])) + nThres = np.quantile(ndata, 0.25) - 1.5 * np.subtract( + *np.percentile(ndata, [75, 25]) + ) ndata = ndata[ndata >= nThres] transId = f"{channel}_autolgclTransform" - + if len(ndata): r = np.finfo(float).eps + np.quantile(ndata, q) if 10**m * abs(r) <= t: w = 0 else: - w = (m - np.log10(t/abs(r)))/2 + w = (m - np.log10(t / abs(r))) / 2 if np.isnan(w) or w > 2: - print(f"autoLgcl failed for channel: {channel}; using default logicle transformation") + print( + f"autoLgcl failed for channel: {channel}; using default logicle" + " transformation" + ) w = 0.1 t = 4000 m = 4.5 - + return { "channel": channel, "transformation": "logicle", @@ -637,5 +666,5 @@ def logicleTransform(channel, adata, m, q): "w": w, "t": t, "m": m, - "a": 0 - } \ No newline at end of file + "a": 0, + } From 4a6325bda57e5ecac352440f7e56fdbef71c98cc Mon Sep 17 00:00:00 2001 From: buettnerm Date: Fri, 3 May 2024 14:24:42 -0700 Subject: [PATCH 4/5] :memo: update docs --- docs/tutorials/quickstart.ipynb | 2 +- pyproject.toml | 1 + pytometry/__init__.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/quickstart.ipynb b/docs/tutorials/quickstart.ipynb index 183f797..9ca4e47 100644 --- a/docs/tutorials/quickstart.ipynb +++ b/docs/tutorials/quickstart.ipynb @@ -164,7 +164,7 @@ "metadata": {}, "outputs": [], "source": [ - "adata_autologicle = pm.tl.normalize_autologicle(adata, inplace=False)" + "adata_autologicle = pm.tl.normalize_autoLgcl(adata, inplace=False)" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 1c2d992..8620d21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "seaborn", "matplotlib", "readfcs >=1.1.0", + "flowutils", "datashader", "consensusclustering", "minisom" diff --git a/pytometry/__init__.py b/pytometry/__init__.py index 6cf1ad3..c17bcb4 100644 --- a/pytometry/__init__.py +++ b/pytometry/__init__.py @@ -37,6 +37,7 @@ tools.normalize_arcsinh tools.normalize_logicle tools.normalize_biExp + tools.normalize_autoLgcl Plotting (`pl`) =============== @@ -50,7 +51,7 @@ """ -__version__ = "0.1.4" # denote a pre-release for 0.1.0 with 0.1a1 +__version__ = "0.1.5" # denote a pre-release for 0.1.0 with 0.1a1 from . import plotting as pl from . import preprocessing as pp From 33edf286cde052239d00260e75627497ccd29d61 Mon Sep 17 00:00:00 2001 From: buettnerm Date: Fri, 3 May 2024 14:59:50 -0700 Subject: [PATCH 5/5] :bug: Modify params --- docs/tutorials/quickstart.ipynb | 2 +- pytometry/__init__.py | 2 +- pytometry/tools/__init__.py | 7 ++++- pytometry/tools/_normalization.py | 45 +++++++++++++++++-------------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/tutorials/quickstart.ipynb b/docs/tutorials/quickstart.ipynb index 9ca4e47..183f797 100644 --- a/docs/tutorials/quickstart.ipynb +++ b/docs/tutorials/quickstart.ipynb @@ -164,7 +164,7 @@ "metadata": {}, "outputs": [], "source": [ - "adata_autologicle = pm.tl.normalize_autoLgcl(adata, inplace=False)" + "adata_autologicle = pm.tl.normalize_autologicle(adata, inplace=False)" ] }, { diff --git a/pytometry/__init__.py b/pytometry/__init__.py index c17bcb4..66d423d 100644 --- a/pytometry/__init__.py +++ b/pytometry/__init__.py @@ -37,7 +37,7 @@ tools.normalize_arcsinh tools.normalize_logicle tools.normalize_biExp - tools.normalize_autoLgcl + tools.normalize_autologicle Plotting (`pl`) =============== diff --git a/pytometry/tools/__init__.py b/pytometry/tools/__init__.py index 630e66f..0f5c455 100644 --- a/pytometry/tools/__init__.py +++ b/pytometry/tools/__init__.py @@ -1,2 +1,7 @@ -from ._normalization import normalize_arcsinh, normalize_biExp, normalize_logicle +from ._normalization import ( + normalize_arcsinh, + normalize_autologicle, + normalize_biExp, + normalize_logicle, +) from .clustering._flowsom import flowsom_clustering, meta_clustering, som_clustering diff --git a/pytometry/tools/_normalization.py b/pytometry/tools/_normalization.py index 9afc4d5..a6808af 100644 --- a/pytometry/tools/_normalization.py +++ b/pytometry/tools/_normalization.py @@ -557,7 +557,7 @@ def _log_root(b: float, w: float) -> float: return d -def normalize_autoLgcl(adata, channels, m=4.5, q=0.05, inplace=True): +def normalize_autologicle(adata, channels=None, m=4.5, q=0.05, inplace=True): """Autologicle transformation. Automatically apply a logicle transformation to specified channels in an AnnData @@ -590,22 +590,28 @@ def normalize_autoLgcl(adata, channels, m=4.5, q=0.05, inplace=True): # check inputs if not isinstance(adata, AnnData): raise TypeError("adata has to be an object of class 'AnnData'") - if not channels: - raise ValueError("Please specify the channels to be logicle transformed") - indx = [channel in adata.var_names for channel in channels] - if not all(indx): - missing_channels = [channels[i] for i in range(len(channels)) if not indx[i]] - raise ValueError( - f"Channels {missing_channels} were not found in the adata object." - ) - # Get params for logicle transformation - trans = [_logicleTransform(channel, adata, m, q) for channel in channels] - # Create parameter list for the autologicle transformation - params = dict(zip(channels, trans)) - for channel in adata.var_names: + if channels is None: + channels = adata.var_names + else: + # Turn string into a list + if isinstance(channels, str): + channels = [channels] + raise ValueError("channels have to be in list format.") + # Check if all channel names are valid + indx = [channel in adata.var_names for channel in channels] + if not all(indx): + missing_channels = [ + channels[i] for i in range(len(channels)) if not indx[i] + ] + raise ValueError( + f"Channels {missing_channels} were not found in the adata object." + ) + # Perform autologicle transformation on all specified channels + for channel in channels: channel_idx = np.where(adata.var_names == channel)[0][0] + params = _logicleTransform(channel, adata, m, q) adata.X[:, channel_idx] = transforms.logicle( - adata.X[:, channel_idx], channel_indices=[channel_idx], **params[channel] + adata.X[:, channel_idx], channel_indices=[channel_idx], **params ) return None if inplace else adata @@ -634,7 +640,7 @@ def _logicleTransform(channel, adata, m, q): If the computed parameter 'w' is NaN or exceeds 2, it resets to a default value of 0.1, and 't' and 'm' are set to default values of 4000 and 4.5, respectively. """ - data = adata.X[:, adata.var_names == channel].flatten() + data = adata.X[:, adata.var_names == channel].flatten().copy() w = 0 t = np.max(data) ndata = data[data < 0] @@ -642,7 +648,7 @@ def _logicleTransform(channel, adata, m, q): *np.percentile(ndata, [75, 25]) ) ndata = ndata[ndata >= nThres] - transId = f"{channel}_autolgclTransform" + # transId = f"{channel}_autolgclTransform" if len(ndata): r = np.finfo(float).eps + np.quantile(ndata, q) @@ -660,9 +666,8 @@ def _logicleTransform(channel, adata, m, q): m = 4.5 return { - "channel": channel, - "transformation": "logicle", - "transformationId": transId, + # "transformation": "logicle", + # "transformationId": transId, "w": w, "t": t, "m": m,